From 821baec88d94e305f66dad82e774253d6af3135e Mon Sep 17 00:00:00 2001 From: Peter M Date: Mon, 23 Mar 2026 20:35:00 +0100 Subject: [PATCH] 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 | 1 + 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, 174 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ebf733c9c5..7fb2a7eeac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added DWARF debug information support for JIT-compiled code - Added I2C and SPI APIs to rp2 platform - Added `code:get_object_code/1` +- Added support for integer parts-per-second timeunit ### Changed - ~10% binary size reduction by rewriting module loading logic diff --git a/libs/estdlib/src/erlang.erl b/libs/estdlib/src/erlang.erl index 8a65b158ee..49f2a8c0b9 100644 --- a/libs/estdlib/src/erlang.erl +++ b/libs/estdlib/src/erlang.erl @@ -187,7 +187,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 6f413d0d9c..4c5702bc6d 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -1860,6 +1860,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); } @@ -1891,6 +1915,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); } @@ -2004,7 +2052,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) { @@ -2023,6 +2070,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.