From 1145315bcc63a05d07bf67de252cd349e51125ca Mon Sep 17 00:00:00 2001 From: Peter M Date: Mon, 23 Mar 2026 20:35:00 +0100 Subject: [PATCH 1/3] Support integer parts-per-second time unit Add support for positive integer time units ("parts per second") in erlang:monotonic_time/1, erlang:system_time/1, and calendar:system_time_to_universal_time/2. Restrict integer-unit handling to int64 inputs in the affected NIF paths. Use checked int64 decomposition for monotonic/system time conversion to avoid signed overflow in intermediate arithmetic. For calendar integer units, floor negative fractional values to whole seconds before converting to UTC. Add focused Erlang tests for integer-unit parity, badarg on non-positive integer units, and negative fractional calendar conversion for integer units. Signed-off-by: Peter M --- CHANGELOG.md | 4 ++ libs/estdlib/src/erlang.erl | 2 +- libs/exavmlib/lib/System.ex | 1 + src/libAtomVM/nifs.c | 63 +++++++++++++++++++++- tests/erlang_tests/test_monotonic_time.erl | 47 ++++++++++++++++ tests/erlang_tests/test_system_time.erl | 62 +++++++++++++++++++++ 6 files changed, 177 insertions(+), 2 deletions(-) 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..7cef9e3cd1 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -1867,6 +1867,30 @@ term nif_erlang_monotonic_time_1(Context *ctx, int argc, term argv[]) } 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); + } else if (term_is_int64(unit)) { + avm_int64_t parts_per_second = term_maybe_unbox_int64(unit); + if (UNLIKELY(parts_per_second <= 0)) { + RAISE_ERROR(BADARG_ATOM); + } + if (UNLIKELY( + ((ts.tv_sec > 0) && ((avm_int64_t) ts.tv_sec > INT64_MAX / parts_per_second)) + || ((ts.tv_sec < 0) && ((avm_int64_t) ts.tv_sec < INT64_MIN / parts_per_second)))) { + RAISE_ERROR(BADARG_ATOM); + } + avm_int64_t second_part = (avm_int64_t) ts.tv_sec * parts_per_second; + 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 = (avm_int64_t) ts.tv_nsec * quotient; + avm_int64_t fractional_low = ((avm_int64_t) ts.tv_nsec * remainder) / INT64_C(1000000000); + if (UNLIKELY(fractional_high > INT64_MAX - fractional_low)) { + RAISE_ERROR(BADARG_ATOM); + } + avm_int64_t fractional_part = fractional_high + fractional_low; + if (UNLIKELY(second_part > INT64_MAX - fractional_part)) { + RAISE_ERROR(BADARG_ATOM); + } + return make_maybe_boxed_int64(ctx, second_part + fractional_part); + } else { RAISE_ERROR(BADARG_ATOM); } @@ -1898,6 +1922,30 @@ term nif_erlang_system_time_1(Context *ctx, int argc, term argv[]) } 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); + } else if (term_is_int64(unit)) { + avm_int64_t parts_per_second = term_maybe_unbox_int64(unit); + if (UNLIKELY(parts_per_second <= 0)) { + RAISE_ERROR(BADARG_ATOM); + } + if (UNLIKELY( + ((ts.tv_sec > 0) && ((avm_int64_t) ts.tv_sec > INT64_MAX / parts_per_second)) + || ((ts.tv_sec < 0) && ((avm_int64_t) ts.tv_sec < INT64_MIN / parts_per_second)))) { + RAISE_ERROR(BADARG_ATOM); + } + avm_int64_t second_part = (avm_int64_t) ts.tv_sec * parts_per_second; + 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 = (avm_int64_t) ts.tv_nsec * quotient; + avm_int64_t fractional_low = ((avm_int64_t) ts.tv_nsec * remainder) / INT64_C(1000000000); + if (UNLIKELY(fractional_high > INT64_MAX - fractional_low)) { + RAISE_ERROR(BADARG_ATOM); + } + avm_int64_t fractional_part = fractional_high + fractional_low; + if (UNLIKELY(second_part > INT64_MAX - fractional_part)) { + RAISE_ERROR(BADARG_ATOM); + } + return make_maybe_boxed_int64(ctx, second_part + fractional_part); + } else { RAISE_ERROR(BADARG_ATOM); } @@ -2011,7 +2059,6 @@ term nif_calendar_system_time_to_universal_time_2(Context *ctx, int argc, term a UNUSED(argc); struct timespec ts; - avm_int64_t value = term_maybe_unbox_int64(argv[0]); if (argv[1] == SECOND_ATOM) { @@ -2030,6 +2077,20 @@ term nif_calendar_system_time_to_universal_time_2(Context *ctx, int argc, term a ts.tv_sec = (time_t) (value / INT64_C(1000000000)); ts.tv_nsec = value % INT64_C(1000000000); + } else if (term_is_int64(argv[1])) { + avm_int64_t parts_per_second = term_maybe_unbox_int64(argv[1]); + if (UNLIKELY(parts_per_second <= 0)) { + RAISE_ERROR(BADARG_ATOM); + } + if (UNLIKELY(!term_is_int64(argv[0]))) { + RAISE_ERROR(BADARG_ATOM); + } + ts.tv_sec = (time_t) (value / parts_per_second); + if ((value % parts_per_second) < 0) { + ts.tv_sec -= 1; + } + ts.tv_nsec = 0; + } else { RAISE_ERROR(BADARG_ATOM); } diff --git a/tests/erlang_tests/test_monotonic_time.erl b/tests/erlang_tests/test_monotonic_time.erl index 52c1a390c1..fd5dd9a054 100644 --- a/tests/erlang_tests/test_monotonic_time.erl +++ b/tests/erlang_tests/test_monotonic_time.erl @@ -38,6 +38,8 @@ start() -> true = is_integer(N2 - N1) andalso (N2 - N1) >= 0, ok = test_native_monotonic_time(), + ok = test_integer_time_unit(), + ok = test_bad_integer_time_unit(), 1. @@ -46,6 +48,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 +65,39 @@ 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_bad_integer_time_unit() -> + ok = expect(fun() -> erlang:monotonic_time(0) end, badarg), + ok = expect(fun() -> erlang:monotonic_time(-1) end, badarg), + ok. diff --git a/tests/erlang_tests/test_system_time.erl b/tests/erlang_tests/test_system_time.erl index 423c7aed6f..8b635cd0fe 100644 --- a/tests/erlang_tests/test_system_time.erl +++ b/tests/erlang_tests/test_system_time.erl @@ -34,9 +34,14 @@ start() -> ok = test_os_system_time(), ok = test_time_unit_ratios(), + ok = test_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. @@ -165,3 +170,60 @@ 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_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), + + %% 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), + + 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(0, 0) end, badarg), + ok. From 7637c1dcd1f7f320fd4eb9e1741fabeb39fd8222 Mon Sep 17 00:00:00 2001 From: Peter M Date: Mon, 6 Apr 2026 16:19:27 +0200 Subject: [PATCH 2/3] Fix calendar:system_time_to_universal_time/2 input validation and negative rounding Validate argv[0] is an integer before calling term_maybe_unbox_int64 to prevent interpreting arbitrary term data as an int64. Use floor division instead of C truncation for all time-unit branches so negative sub-second values round toward negative infinity, matching OTP semantics. For example calendar:system_time_to_universal_time(-1, millisecond) now correctly returns {{1969,12,31},{23,59,59}}. Add tests for negative calendar conversions and non-integer input. Signed-off-by: Peter M --- src/libAtomVM/nifs.c | 43 +++++++++++-------------- tests/erlang_tests/test_system_time.erl | 7 ++++ 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index 7cef9e3cd1..df9129b9c7 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -2055,46 +2055,39 @@ 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]); + avm_int64_t divisor; if (argv[1] == SECOND_ATOM) { - ts.tv_sec = (time_t) value; - ts.tv_nsec = 0; - + divisor = 1; } else if (argv[1] == MILLISECOND_ATOM) { - ts.tv_sec = (time_t) (value / 1000); - ts.tv_nsec = (value % 1000) * 1000000; - + divisor = 1000; } else if (argv[1] == MICROSECOND_ATOM) { - ts.tv_sec = (time_t) (value / 1000000); - ts.tv_nsec = (value % 1000000) * 1000; - + divisor = INT64_C(1000000); } 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); - + divisor = INT64_C(1000000000); } else if (term_is_int64(argv[1])) { - avm_int64_t parts_per_second = term_maybe_unbox_int64(argv[1]); - if (UNLIKELY(parts_per_second <= 0)) { - RAISE_ERROR(BADARG_ATOM); - } - if (UNLIKELY(!term_is_int64(argv[0]))) { + divisor = term_maybe_unbox_int64(argv[1]); + if (UNLIKELY(divisor <= 0)) { RAISE_ERROR(BADARG_ATOM); } - ts.tv_sec = (time_t) (value / parts_per_second); - if ((value % parts_per_second) < 0) { - ts.tv_sec -= 1; - } - ts.tv_nsec = 0; - } else { RAISE_ERROR(BADARG_ATOM); } + // Floor division: round negative fractional seconds toward negative infinity + avm_int64_t quotient = value / divisor; + avm_int64_t remainder = value % divisor; + 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_system_time.erl b/tests/erlang_tests/test_system_time.erl index 8b635cd0fe..820913efdd 100644 --- a/tests/erlang_tests/test_system_time.erl +++ b/tests/erlang_tests/test_system_time.erl @@ -131,6 +131,13 @@ 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(), From 4dff77ad89c82f3e04df0446a3c5a6041cc6f5d0 Mon Sep 17 00:00:00 2001 From: Peter M Date: Mon, 6 Apr 2026 16:22:02 +0200 Subject: [PATCH 3/3] Refactor time-unit conversion into shared helpers Extract time_unit_to_parts_per_second, nanoseconds_to_parts_per_second, timespec_to_parts_per_second, and make_time_in_unit helpers to eliminate code duplication between monotonic_time and system_time BIFs. The new timespec_to_parts_per_second correctly handles negative timestamps with non-zero tv_nsec by using an adjusted-seconds approach that avoids rejecting valid results whose intermediate sec*pps would overflow but whose final floored value fits in int64. Calendar function now reuses time_unit_to_parts_per_second for unit dispatch. Add tests for non-power-of-10 integer units (256, 48000), boundary floor-division cases, and additional badarg coverage. Signed-off-by: Peter M --- src/libAtomVM/nifs.c | 205 +++++++++++---------- tests/erlang_tests/test_monotonic_time.erl | 17 ++ tests/erlang_tests/test_system_time.erl | 32 +++- 3 files changed, 158 insertions(+), 96 deletions(-) diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index df9129b9c7..ed0a1d5b67 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -1841,65 +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 if (term_is_int64(unit)) { - avm_int64_t parts_per_second = term_maybe_unbox_int64(unit); - if (UNLIKELY(parts_per_second <= 0)) { - 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( - ((ts.tv_sec > 0) && ((avm_int64_t) ts.tv_sec > INT64_MAX / parts_per_second)) - || ((ts.tv_sec < 0) && ((avm_int64_t) ts.tv_sec < INT64_MIN / parts_per_second)))) { - RAISE_ERROR(BADARG_ATOM); + ((seconds > 0) && (seconds > INT64_MAX / parts_per_second)) + || ((seconds < 0) && (seconds < INT64_MIN / parts_per_second)))) { + return false; } - avm_int64_t second_part = (avm_int64_t) ts.tv_sec * parts_per_second; - 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 = (avm_int64_t) ts.tv_nsec * quotient; - avm_int64_t fractional_low = ((avm_int64_t) ts.tv_nsec * remainder) / INT64_C(1000000000); - if (UNLIKELY(fractional_high > INT64_MAX - fractional_low)) { - RAISE_ERROR(BADARG_ATOM); + + if (UNLIKELY(!nanoseconds_to_parts_per_second( + (avm_int64_t) ts->tv_nsec, parts_per_second, false, &fractional_part))) { + return false; } - avm_int64_t fractional_part = fractional_high + fractional_low; + + avm_int64_t second_part = seconds * parts_per_second; if (UNLIKELY(second_part > INT64_MAX - fractional_part)) { - RAISE_ERROR(BADARG_ATOM); + return false; } - return make_maybe_boxed_int64(ctx, second_part + fractional_part); - } else { - RAISE_ERROR(BADARG_ATOM); + *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; @@ -1907,48 +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); - - } else if (term_is_int64(unit)) { - avm_int64_t parts_per_second = term_maybe_unbox_int64(unit); - if (UNLIKELY(parts_per_second <= 0)) { - RAISE_ERROR(BADARG_ATOM); - } - if (UNLIKELY( - ((ts.tv_sec > 0) && ((avm_int64_t) ts.tv_sec > INT64_MAX / parts_per_second)) - || ((ts.tv_sec < 0) && ((avm_int64_t) ts.tv_sec < INT64_MIN / parts_per_second)))) { - RAISE_ERROR(BADARG_ATOM); - } - avm_int64_t second_part = (avm_int64_t) ts.tv_sec * parts_per_second; - 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 = (avm_int64_t) ts.tv_nsec * quotient; - avm_int64_t fractional_low = ((avm_int64_t) ts.tv_nsec * remainder) / INT64_C(1000000000); - if (UNLIKELY(fractional_high > INT64_MAX - fractional_low)) { - RAISE_ERROR(BADARG_ATOM); - } - avm_int64_t fractional_part = fractional_high + fractional_low; - if (UNLIKELY(second_part > INT64_MAX - fractional_part)) { - RAISE_ERROR(BADARG_ATOM); - } - return make_maybe_boxed_int64(ctx, second_part + fractional_part); + 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) @@ -2062,27 +2090,14 @@ term nif_calendar_system_time_to_universal_time_2(Context *ctx, int argc, term a } avm_int64_t value = term_maybe_unbox_int64(argv[0]); - avm_int64_t divisor; - if (argv[1] == SECOND_ATOM) { - divisor = 1; - } else if (argv[1] == MILLISECOND_ATOM) { - divisor = 1000; - } else if (argv[1] == MICROSECOND_ATOM) { - divisor = INT64_C(1000000); - } else if (argv[1] == NANOSECOND_ATOM || argv[1] == NATIVE_ATOM) { - divisor = INT64_C(1000000000); - } else if (term_is_int64(argv[1])) { - divisor = term_maybe_unbox_int64(argv[1]); - if (UNLIKELY(divisor <= 0)) { - RAISE_ERROR(BADARG_ATOM); - } - } 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 / divisor; - avm_int64_t remainder = value % divisor; + 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 diff --git a/tests/erlang_tests/test_monotonic_time.erl b/tests/erlang_tests/test_monotonic_time.erl index fd5dd9a054..980b0cd91f 100644 --- a/tests/erlang_tests/test_monotonic_time.erl +++ b/tests/erlang_tests/test_monotonic_time.erl @@ -39,6 +39,7 @@ start() -> 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. @@ -97,7 +98,23 @@ test_integer_time_unit() -> 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 820913efdd..0c864d9f52 100644 --- a/tests/erlang_tests/test_system_time.erl +++ b/tests/erlang_tests/test_system_time.erl @@ -35,6 +35,7 @@ start() -> 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), @@ -137,7 +138,9 @@ test_system_time_to_universal_time() -> {{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 = expect( + fun() -> calendar:system_time_to_universal_time(not_an_integer, second) end, badarg + ), ok = test_nanosecond_universal_time(), ok = test_native_universal_time(), @@ -207,6 +210,11 @@ test_integer_time_unit() -> 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), @@ -218,12 +226,21 @@ test_integer_unit_universal_time() -> {{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() -> @@ -232,5 +249,18 @@ test_bad_integer_time_unit() -> 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.