diff --git a/CHANGELOG.md b/CHANGELOG.md index 35e48e7564..fb9322a103 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.7.0-beta.0] - Unreleased +### Added + +- Added support for integer parts-per-second timeunit, with `badarg` raised on int64 overflow + ## [0.7.0-alpha.1] - 2026-04-06 ### Added diff --git a/libs/estdlib/src/erlang.erl b/libs/estdlib/src/erlang.erl index 07f00469f2..b0f92e771b 100644 --- a/libs/estdlib/src/erlang.erl +++ b/libs/estdlib/src/erlang.erl @@ -189,7 +189,7 @@ -type atom_encoding() :: latin1 | utf8 | unicode. -type mem_type() :: binary. --type time_unit() :: second | millisecond | microsecond | nanosecond | native. +-type time_unit() :: second | millisecond | microsecond | nanosecond | native | pos_integer(). -type timestamp() :: { MegaSecs :: non_neg_integer(), Secs :: non_neg_integer(), MicroSecs :: non_neg_integer }. diff --git a/libs/exavmlib/lib/System.ex b/libs/exavmlib/lib/System.ex index 2e819d3f23..1395ba1734 100644 --- a/libs/exavmlib/lib/System.ex +++ b/libs/exavmlib/lib/System.ex @@ -26,6 +26,7 @@ defmodule System do | :millisecond | :microsecond | :nanosecond + | pos_integer() @doc """ Returns the current monotonic time in the `:native` time unit. diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index 3c0722972b..ed0a1d5b67 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -1841,41 +1841,122 @@ term nif_erlang_make_ref_0(Context *ctx, int argc, term argv[]) return term_from_ref_ticks(ref_ticks, &ctx->heap); } -term nif_erlang_monotonic_time_1(Context *ctx, int argc, term argv[]) +static bool time_unit_to_parts_per_second(term unit, avm_int64_t *parts_per_second) { - UNUSED(ctx); - - term unit; - if (argc == 0) { - unit = NATIVE_ATOM; + if (unit == SECOND_ATOM) { + *parts_per_second = 1; + } else if (unit == MILLISECOND_ATOM) { + *parts_per_second = 1000; + } else if (unit == MICROSECOND_ATOM) { + *parts_per_second = INT64_C(1000000); + } else if (unit == NANOSECOND_ATOM || unit == NATIVE_ATOM) { + *parts_per_second = INT64_C(1000000000); + } else if (term_is_int64(unit)) { + *parts_per_second = term_maybe_unbox_int64(unit); + if (UNLIKELY(*parts_per_second <= 0)) { + return false; + } } else { - unit = argv[0]; + return false; } - struct timespec ts; - sys_monotonic_time(&ts); + return true; +} - if (unit == SECOND_ATOM) { - return make_maybe_boxed_int64(ctx, ts.tv_sec); +// Convert nanoseconds to parts using: parts = nanoseconds * pps / 1e9 +// Splits into high/low to avoid intermediate overflow. +// Caller must ensure 0 <= nanoseconds < 1e9 and pps > 0. +static bool nanoseconds_to_parts_per_second( + avm_int64_t nanoseconds, avm_int64_t parts_per_second, bool round_up, avm_int64_t *parts) +{ + avm_int64_t quotient = parts_per_second / INT64_C(1000000000); + avm_int64_t remainder = parts_per_second % INT64_C(1000000000); + avm_int64_t fractional_high = nanoseconds * quotient; + avm_int64_t remainder_product = nanoseconds * remainder; + avm_int64_t fractional_low = remainder_product / INT64_C(1000000000); - } else if (unit == MILLISECOND_ATOM) { - return make_maybe_boxed_int64(ctx, ((int64_t) ts.tv_sec) * 1000UL + ts.tv_nsec / 1000000UL); + if (round_up && (remainder_product % INT64_C(1000000000)) != 0) { + fractional_low += 1; + } - } else if (unit == MICROSECOND_ATOM) { - return make_maybe_boxed_int64(ctx, ((int64_t) ts.tv_sec) * 1000000UL + ts.tv_nsec / 1000UL); + if (UNLIKELY(fractional_high > INT64_MAX - fractional_low)) { + return false; + } - } else if (unit == NANOSECOND_ATOM || unit == NATIVE_ATOM) { - return make_maybe_boxed_int64(ctx, ((int64_t) ts.tv_sec) * INT64_C(1000000000) + ts.tv_nsec); + *parts = fractional_high + fractional_low; + return true; +} - } else { - RAISE_ERROR(BADARG_ATOM); +// Convert a normalized timespec (0 <= tv_nsec < 1e9) to integer parts. +// Uses floor semantics for negative timestamps with non-zero tv_nsec. +static bool timespec_to_parts_per_second( + const struct timespec *ts, avm_int64_t parts_per_second, avm_int64_t *parts) +{ + avm_int64_t seconds = (avm_int64_t) ts->tv_sec; + avm_int64_t fractional_part; + + if (ts->tv_nsec == 0 || seconds >= 0) { + if (UNLIKELY( + ((seconds > 0) && (seconds > INT64_MAX / parts_per_second)) + || ((seconds < 0) && (seconds < INT64_MIN / parts_per_second)))) { + return false; + } + + if (UNLIKELY(!nanoseconds_to_parts_per_second( + (avm_int64_t) ts->tv_nsec, parts_per_second, false, &fractional_part))) { + return false; + } + + avm_int64_t second_part = seconds * parts_per_second; + if (UNLIKELY(second_part > INT64_MAX - fractional_part)) { + return false; + } + + *parts = second_part + fractional_part; + return true; + } + + // Preserve floor semantics for normalized negative timespecs such as {-2, 999999999}. + avm_int64_t adjusted_seconds = seconds + 1; + if (UNLIKELY(adjusted_seconds < INT64_MIN / parts_per_second)) { + return false; + } + + if (UNLIKELY(!nanoseconds_to_parts_per_second( + INT64_C(1000000000) - (avm_int64_t) ts->tv_nsec, parts_per_second, true, + &fractional_part))) { + return false; } + + avm_int64_t second_part = adjusted_seconds * parts_per_second; + if (UNLIKELY(second_part < INT64_MIN + fractional_part)) { + return false; + } + + *parts = second_part - fractional_part; + return true; } -term nif_erlang_system_time_1(Context *ctx, int argc, term argv[]) +static term make_time_in_unit(Context *ctx, term unit, void (*time_fun)(struct timespec *)) { - UNUSED(ctx); + avm_int64_t parts_per_second; + if (UNLIKELY(!time_unit_to_parts_per_second(unit, &parts_per_second))) { + RAISE_ERROR(BADARG_ATOM); + } + + struct timespec ts; + time_fun(&ts); + + avm_int64_t value; + if (UNLIKELY(!timespec_to_parts_per_second(&ts, parts_per_second, &value))) { + RAISE_ERROR(BADARG_ATOM); + } + return make_maybe_boxed_int64(ctx, value); +} + +term nif_erlang_monotonic_time_1(Context *ctx, int argc, term argv[]) +{ term unit; if (argc == 0) { unit = NATIVE_ATOM; @@ -1883,24 +1964,19 @@ term nif_erlang_system_time_1(Context *ctx, int argc, term argv[]) unit = argv[0]; } - struct timespec ts; - sys_time(&ts); - - if (unit == SECOND_ATOM) { - return make_maybe_boxed_int64(ctx, ts.tv_sec); - - } else if (unit == MILLISECOND_ATOM) { - return make_maybe_boxed_int64(ctx, ((int64_t) ts.tv_sec) * 1000UL + ts.tv_nsec / 1000000UL); - - } else if (unit == MICROSECOND_ATOM) { - return make_maybe_boxed_int64(ctx, ((int64_t) ts.tv_sec) * 1000000UL + ts.tv_nsec / 1000UL); - - } else if (unit == NANOSECOND_ATOM || unit == NATIVE_ATOM) { - return make_maybe_boxed_int64(ctx, ((int64_t) ts.tv_sec) * INT64_C(1000000000) + ts.tv_nsec); + return make_time_in_unit(ctx, unit, sys_monotonic_time); +} +term nif_erlang_system_time_1(Context *ctx, int argc, term argv[]) +{ + term unit; + if (argc == 0) { + unit = NATIVE_ATOM; } else { - RAISE_ERROR(BADARG_ATOM); + unit = argv[0]; } + + return make_time_in_unit(ctx, unit, sys_time); } static term build_datetime_from_tm(Context *ctx, struct tm *broken_down_time) @@ -2007,33 +2083,26 @@ term nif_erlang_timestamp_0(Context *ctx, int argc, term argv[]) term nif_calendar_system_time_to_universal_time_2(Context *ctx, int argc, term argv[]) { - UNUSED(ctx); UNUSED(argc); - struct timespec ts; - + if (UNLIKELY(!term_is_int64(argv[0]))) { + RAISE_ERROR(BADARG_ATOM); + } avm_int64_t value = term_maybe_unbox_int64(argv[0]); - if (argv[1] == SECOND_ATOM) { - ts.tv_sec = (time_t) value; - ts.tv_nsec = 0; - - } else if (argv[1] == MILLISECOND_ATOM) { - ts.tv_sec = (time_t) (value / 1000); - ts.tv_nsec = (value % 1000) * 1000000; - - } else if (argv[1] == MICROSECOND_ATOM) { - ts.tv_sec = (time_t) (value / 1000000); - ts.tv_nsec = (value % 1000000) * 1000; - - } else if (argv[1] == NANOSECOND_ATOM || argv[1] == NATIVE_ATOM) { - ts.tv_sec = (time_t) (value / INT64_C(1000000000)); - ts.tv_nsec = value % INT64_C(1000000000); - - } else { + avm_int64_t parts_per_second; + if (UNLIKELY(!time_unit_to_parts_per_second(argv[1], &parts_per_second))) { RAISE_ERROR(BADARG_ATOM); } + // Floor division: round negative fractional seconds toward negative infinity + avm_int64_t quotient = value / parts_per_second; + avm_int64_t remainder = value % parts_per_second; + struct timespec ts = { + .tv_sec = (time_t) (quotient - (remainder < 0)), + .tv_nsec = 0 + }; + struct tm broken_down_time; return build_datetime_from_tm(ctx, gmtime_r(&ts.tv_sec, &broken_down_time)); } diff --git a/tests/erlang_tests/test_monotonic_time.erl b/tests/erlang_tests/test_monotonic_time.erl index 52c1a390c1..980b0cd91f 100644 --- a/tests/erlang_tests/test_monotonic_time.erl +++ b/tests/erlang_tests/test_monotonic_time.erl @@ -38,6 +38,9 @@ start() -> true = is_integer(N2 - N1) andalso (N2 - N1) >= 0, ok = test_native_monotonic_time(), + ok = test_integer_time_unit(), + ok = test_non_power_of_10_integer_time_unit(), + ok = test_bad_integer_time_unit(), 1. @@ -46,6 +49,15 @@ test_diff(X) when is_integer(X) andalso X >= 0 -> test_diff(X) when X < 0 -> 0. +expect(F, Expect) -> + try + F(), + fail + catch + _:E when E == Expect -> + ok + end. + test_native_monotonic_time() -> Na1 = erlang:monotonic_time(native), receive @@ -54,3 +66,55 @@ test_native_monotonic_time() -> Na2 = erlang:monotonic_time(native), true = is_integer(Na2 - Na1) andalso (Na2 - Na1) >= 0, ok. + +test_integer_time_unit() -> + %% integer 1 = parts per second, equivalent to second + S = erlang:monotonic_time(second), + S1 = erlang:monotonic_time(1), + true = abs(S1 - S) =< 1, + + %% integer 1000 = parts per second, equivalent to millisecond + Ms = erlang:monotonic_time(millisecond), + Ms1 = erlang:monotonic_time(1000), + true = abs(Ms1 - Ms) =< 1, + + %% integer 1000000 = parts per second, equivalent to microsecond + Us = erlang:monotonic_time(microsecond), + Us1 = erlang:monotonic_time(1000000), + true = abs(Us1 - Us) =< 1000, + + %% integer 1000000000 = parts per second, equivalent to nanosecond + Ns = erlang:monotonic_time(nanosecond), + Ns1 = erlang:monotonic_time(1000000000), + true = abs(Ns1 - Ns) =< 1000000, + + %% verify monotonicity with integer unit + T1 = erlang:monotonic_time(1000), + receive + after 1 -> ok + end, + T2 = erlang:monotonic_time(1000), + true = T2 >= T1, + + ok. + +test_non_power_of_10_integer_time_unit() -> + ok = test_integer_time_unit_monotonicity(256), + ok = test_integer_time_unit_monotonicity(48000), + ok. + +test_bad_integer_time_unit() -> + ok = expect(fun() -> erlang:monotonic_time(0) end, badarg), + ok = expect(fun() -> erlang:monotonic_time(-1) end, badarg), + ok. + +test_integer_time_unit_monotonicity(PartsPerSecond) -> + T1 = erlang:monotonic_time(PartsPerSecond), + receive + after 1 -> ok + end, + T2 = erlang:monotonic_time(PartsPerSecond), + true = is_integer(T1), + true = is_integer(T2), + true = T2 >= T1, + ok. diff --git a/tests/erlang_tests/test_system_time.erl b/tests/erlang_tests/test_system_time.erl index 423c7aed6f..0c864d9f52 100644 --- a/tests/erlang_tests/test_system_time.erl +++ b/tests/erlang_tests/test_system_time.erl @@ -34,9 +34,15 @@ start() -> ok = test_os_system_time(), ok = test_time_unit_ratios(), + ok = test_integer_time_unit(), + ok = test_non_power_of_10_integer_time_unit(), + ok = test_bad_integer_time_unit(), + ok = expect(fun() -> erlang:system_time(not_a_time_unit) end, badarg), ok = test_system_time_to_universal_time(), + ok = test_integer_unit_universal_time(), + ok = test_bad_integer_unit_universal_time(), 0. @@ -126,6 +132,15 @@ test_system_time_to_universal_time() -> {{2023, 7, 8}, {20, 19, 39}} = calendar:system_time_to_universal_time(1688847579, second), {{1969, 12, 31}, {23, 59, 59}} = calendar:system_time_to_universal_time(-1, second), + {{1969, 12, 31}, {23, 59, 59}} = calendar:system_time_to_universal_time(-1, millisecond), + {{1969, 12, 31}, {23, 59, 58}} = calendar:system_time_to_universal_time(-1001, millisecond), + {{1969, 12, 31}, {23, 59, 59}} = calendar:system_time_to_universal_time(-1, microsecond), + {{1969, 12, 31}, {23, 59, 59}} = calendar:system_time_to_universal_time(-1, nanosecond), + {{1969, 12, 31}, {23, 59, 59}} = calendar:system_time_to_universal_time(-1, native), + + ok = expect( + fun() -> calendar:system_time_to_universal_time(not_an_integer, second) end, badarg + ), ok = test_nanosecond_universal_time(), ok = test_native_universal_time(), @@ -165,3 +180,87 @@ test_native_universal_time() -> {{1970, 1, 1}, {0, 0, 0}} = calendar:system_time_to_universal_time(0, native), {{1970, 1, 1}, {0, 0, 1}} = calendar:system_time_to_universal_time(1000000000, native), ok. + +test_integer_time_unit() -> + %% integer 1 = parts per second, equivalent to second + S = erlang:system_time(second), + S1 = erlang:system_time(1), + true = abs(S1 - S) =< 1, + + %% integer 1000 = parts per second, equivalent to millisecond + Ms = erlang:system_time(millisecond), + Ms1 = erlang:system_time(1000), + true = abs(Ms1 - Ms) =< 1, + + %% integer 1000000 = parts per second, equivalent to microsecond + Us = erlang:system_time(microsecond), + Us1 = erlang:system_time(1000000), + true = abs(Us1 - Us) =< 1000, + + %% integer 1000000000 = parts per second, equivalent to nanosecond + Ns = erlang:system_time(nanosecond), + Ns1 = erlang:system_time(1000000000), + true = abs(Ns1 - Ns) =< 1000000, + + %% verify values are positive + true = S1 > 0, + true = Ms1 > 0, + true = Us1 > 0, + true = Ns1 > 0, + + ok. + +test_non_power_of_10_integer_time_unit() -> + ok = test_integer_parts_per_second_ratio(256), + ok = test_integer_parts_per_second_ratio(48000), + ok. + +test_integer_unit_universal_time() -> + %% integer 1 = seconds + {{1970, 1, 1}, {0, 0, 0}} = calendar:system_time_to_universal_time(0, 1), + {{1970, 1, 1}, {0, 0, 1}} = calendar:system_time_to_universal_time(1, 1), + {{2023, 7, 8}, {20, 19, 39}} = calendar:system_time_to_universal_time(1688847579, 1), + + %% integer 1000 = milliseconds + {{1970, 1, 1}, {0, 0, 0}} = calendar:system_time_to_universal_time(0, 1000), + {{1970, 1, 1}, {0, 0, 1}} = calendar:system_time_to_universal_time(1000, 1000), + {{1970, 1, 1}, {0, 0, 1}} = calendar:system_time_to_universal_time(1001, 1000), + {{1969, 12, 31}, {23, 59, 59}} = calendar:system_time_to_universal_time(-1, 1000), + {{1969, 12, 31}, {23, 59, 58}} = calendar:system_time_to_universal_time(-1001, 1000), + + %% integer 1000000 = microseconds + {{1970, 1, 1}, {0, 0, 0}} = calendar:system_time_to_universal_time(0, 1000000), + {{1970, 1, 1}, {0, 0, 1}} = calendar:system_time_to_universal_time(1000000, 1000000), + {{1969, 12, 31}, {23, 59, 59}} = calendar:system_time_to_universal_time(-1, 1000000), + + %% integer 256 and 48000 = arbitrary parts per second + {{1970, 1, 1}, {0, 0, 0}} = calendar:system_time_to_universal_time(255, 256), + {{1970, 1, 1}, {0, 0, 1}} = calendar:system_time_to_universal_time(256, 256), + {{1969, 12, 31}, {23, 59, 59}} = calendar:system_time_to_universal_time(-1, 256), + {{1969, 12, 31}, {23, 59, 59}} = calendar:system_time_to_universal_time(-255, 256), + {{1970, 1, 1}, {0, 0, 1}} = calendar:system_time_to_universal_time(48000, 48000), + {{1969, 12, 31}, {23, 59, 59}} = calendar:system_time_to_universal_time(-1, 48000), + + ok. + +test_bad_integer_time_unit() -> + ok = expect(fun() -> erlang:system_time(0) end, badarg), + ok = expect(fun() -> erlang:system_time(-1) end, badarg), + ok. + +test_bad_integer_unit_universal_time() -> + ok = expect( + fun() -> calendar:system_time_to_universal_time(not_an_integer, second) end, badarg + ), + ok = expect(fun() -> calendar:system_time_to_universal_time(not_an_integer, 1000) end, badarg), + ok = expect(fun() -> calendar:system_time_to_universal_time(0, 0) end, badarg), + ok = expect(fun() -> calendar:system_time_to_universal_time(0, -1) end, badarg), + ok. + +test_integer_parts_per_second_ratio(PartsPerSecond) -> + Seconds = erlang:system_time(second), + Parts = erlang:system_time(PartsPerSecond), + true = is_integer(Parts) andalso Parts > 0, + true = Parts >= Seconds * PartsPerSecond, + true = Parts < Seconds * PartsPerSecond + (PartsPerSecond * 2), + ok.