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.