diff --git a/CHANGELOG.md b/CHANGELOG.md index 89b882e5d3..d509093664 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added support for configuring pins and width for sdmmc on ESP32 - Added support for map comprehensions - Added USB CDC port drivers for ESP32, RP2, and STM32 platforms +- Added support for integer parts-per-second time unit, with `badarg` raised on int64 overflow ### Changed - Updated network type db() to dbm() to reflect the actual representation of the type diff --git a/libs/estdlib/src/erlang.erl b/libs/estdlib/src/erlang.erl index 760063cec6..afccd88818 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 b3bb7f24b2..1a406f8db2 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -1851,41 +1851,132 @@ 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) { + // AtomVM exposes the Erlang `native` time unit as nanoseconds on all platforms. + *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. +static bool nanoseconds_to_parts_per_second( + avm_int64_t nanoseconds, avm_int64_t parts_per_second, bool round_up, avm_int64_t *parts) +{ + if (UNLIKELY( + nanoseconds < 0 || nanoseconds >= INT64_C(1000000000) || parts_per_second <= 0)) { + return false; + } - } else if (unit == MILLISECOND_ATOM) { - return make_maybe_boxed_int64(ctx, ((int64_t) ts.tv_sec) * 1000UL + ts.tv_nsec / 1000000UL); + 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 == MICROSECOND_ATOM) { - return make_maybe_boxed_int64(ctx, ((int64_t) ts.tv_sec) * 1000000UL + ts.tv_nsec / 1000UL); + if (round_up && (remainder_product % INT64_C(1000000000)) != 0) { + fractional_low += 1; + } - } 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); + if (UNLIKELY(fractional_high > INT64_MAX - fractional_low)) { + return false; + } - } else { - RAISE_ERROR(BADARG_ATOM); + *parts = fractional_high + fractional_low; + return true; +} + +// 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) +{ + if (UNLIKELY( + parts_per_second <= 0 || ts->tv_nsec < 0 || ts->tv_nsec >= INT64_C(1000000000))) { + return false; } + + 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; @@ -1893,28 +1984,52 @@ term nif_erlang_system_time_1(Context *ctx, int argc, term argv[]) unit = argv[0]; } - struct timespec ts; - sys_time(&ts); + return make_time_in_unit(ctx, unit, sys_monotonic_time); +} - if (unit == SECOND_ATOM) { - return make_maybe_boxed_int64(ctx, ts.tv_sec); +term nif_erlang_system_time_1(Context *ctx, int argc, term argv[]) +{ + term unit; + if (argc == 0) { + unit = NATIVE_ATOM; + } else { + unit = argv[0]; + } - } else if (unit == MILLISECOND_ATOM) { - return make_maybe_boxed_int64(ctx, ((int64_t) ts.tv_sec) * 1000UL + ts.tv_nsec / 1000000UL); + return make_time_in_unit(ctx, unit, sys_time); +} - } else if (unit == MICROSECOND_ATOM) { - return make_maybe_boxed_int64(ctx, ((int64_t) ts.tv_sec) * 1000000UL + ts.tv_nsec / 1000UL); +static bool int64_to_time_t_checked(avm_int64_t seconds, time_t *out) +{ + if (((time_t) -1) > (time_t) 0) { + if (seconds < 0) { + 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); + time_t time_seconds = (time_t) (uint64_t) seconds; + if ((uint64_t) time_seconds != (uint64_t) seconds) { + return false; + } + *out = time_seconds; + return true; + } - } else { - RAISE_ERROR(BADARG_ATOM); + time_t time_seconds = (time_t) seconds; + if ((avm_int64_t) time_seconds != seconds) { + return false; } + *out = time_seconds; + return true; } static term build_datetime_from_tm(Context *ctx, struct tm *broken_down_time) { + avm_int64_t year = (avm_int64_t) broken_down_time->tm_year + 1900; + // Avoid passing a negative value to term_from_int11(), which left-shifts its signed argument. + if (UNLIKELY(year < 0 || year > INT16_MAX)) { + RAISE_ERROR(BADARG_ATOM); + } + // 4 = size of date/time tuple, 3 size of date time tuple if (UNLIKELY(memory_ensure_free_opt(ctx, 3 + 4 + 4, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { RAISE_ERROR(OUT_OF_MEMORY_ATOM); @@ -1923,7 +2038,7 @@ static term build_datetime_from_tm(Context *ctx, struct tm *broken_down_time) term time_tuple = term_alloc_tuple(3, &ctx->heap); term date_time_tuple = term_alloc_tuple(2, &ctx->heap); - term_put_tuple_element(date_tuple, 0, term_from_int11(1900 + broken_down_time->tm_year)); + term_put_tuple_element(date_tuple, 0, term_from_int11((int16_t) year)); term_put_tuple_element(date_tuple, 1, term_from_int11(broken_down_time->tm_mon + 1)); term_put_tuple_element(date_tuple, 2, term_from_int11(broken_down_time->tm_mday)); @@ -1946,7 +2061,12 @@ term nif_erlang_universaltime_0(Context *ctx, int argc, term argv[]) sys_time(&ts); struct tm broken_down_time; - return build_datetime_from_tm(ctx, gmtime_r(&ts.tv_sec, &broken_down_time)); + struct tm *universal_time = gmtime_r(&ts.tv_sec, &broken_down_time); + if (UNLIKELY(universal_time == NULL)) { + RAISE_ERROR(BADARG_ATOM); + } + + return build_datetime_from_tm(ctx, universal_time); } // setenv leaks the prior "TZ=value" string on overwrite (unbounded on @@ -2095,35 +2215,35 @@ 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; + avm_int64_t parts_per_second; + if (UNLIKELY(!time_unit_to_parts_per_second(argv[1], &parts_per_second))) { + RAISE_ERROR(BADARG_ATOM); + } - } 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); + // 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; + avm_int64_t seconds = quotient - (remainder < 0); - } else { + time_t time_seconds; + if (UNLIKELY(!int64_to_time_t_checked(seconds, &time_seconds))) { RAISE_ERROR(BADARG_ATOM); } struct tm broken_down_time; - return build_datetime_from_tm(ctx, gmtime_r(&ts.tv_sec, &broken_down_time)); + struct tm *universal_time = gmtime_r(&time_seconds, &broken_down_time); + if (UNLIKELY(universal_time == NULL)) { + RAISE_ERROR(BADARG_ATOM); + } + + return build_datetime_from_tm(ctx, universal_time); } static term nif_os_getenv_1(Context *ctx, int argc, term argv[]) diff --git a/tests/erlang_tests/test_monotonic_time.erl b/tests/erlang_tests/test_monotonic_time.erl index 52c1a390c1..4baee714b7 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,50 @@ test_native_monotonic_time() -> Na2 = erlang:monotonic_time(native), true = is_integer(Na2 - Na1) andalso (Na2 - Na1) >= 0, ok. + +test_integer_time_unit() -> + S = erlang:monotonic_time(second), + S1 = erlang:monotonic_time(1), + true = abs(S1 - S) =< 1, + + Ms = erlang:monotonic_time(millisecond), + Ms1 = erlang:monotonic_time(1000), + true = abs(Ms1 - Ms) =< 1, + + Us = erlang:monotonic_time(microsecond), + Us1 = erlang:monotonic_time(1000000), + true = abs(Us1 - Us) =< 1000, + + Ns = erlang:monotonic_time(nanosecond), + Ns1 = erlang:monotonic_time(1000000000), + true = abs(Ns1 - Ns) =< 1000000, + + 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..d0b9dd08b5 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,18 @@ 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, 59}} = calendar:system_time_to_universal_time( + -1000, 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(), @@ -163,5 +181,108 @@ test_nanosecond_universal_time() -> 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 = + case erlang:system_info(machine) of + "ATOM" -> + {{1970, 1, 1}, {0, 0, 1}} = calendar:system_time_to_universal_time( + 1000000000, native + ), + ok; + _ -> + ok + end, + ok. + +test_integer_time_unit() -> + S = erlang:system_time(second), + S1 = erlang:system_time(1), + true = abs(S1 - S) =< 1, + + Ms = erlang:system_time(millisecond), + Ms1 = erlang:system_time(1000), + true = abs(Ms1 - Ms) =< 1, + + Us = erlang:system_time(microsecond), + Us1 = erlang:system_time(1000000), + true = abs(Us1 - Us) =< 1000, + + Ns = erlang:system_time(nanosecond), + Ns1 = erlang:system_time(1000000000), + true = abs(Ns1 - Ns) =< 1000000, + + 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() -> + {{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), + + {{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), + + {{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), + + {{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), + {{1969, 12, 31}, {23, 59, 59}} = calendar:system_time_to_universal_time(-256, 256), + {{1969, 12, 31}, {23, 59, 58}} = calendar:system_time_to_universal_time(-257, 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, 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 = expect( + fun() -> calendar:system_time_to_universal_time(0, not_a_time_unit) end, badarg + ), + ok = test_atomvm_integer_unit_bounds(), + ok. + +test_atomvm_integer_unit_bounds() -> + case erlang:system_info(machine) of + "ATOM" -> + ok = expect( + fun() -> calendar:system_time_to_universal_time(9223372036854775807, 1) end, + badarg + ), + ok = expect( + fun() -> calendar:system_time_to_universal_time(-9223372036854775808, 1) end, + badarg + ), + ok; + _ -> + ok + end. + +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.