Skip to content

Commit 261b6ef

Browse files
committed
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 <petermm@gmail.com>
1 parent 078004b commit 261b6ef

6 files changed

Lines changed: 174 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2323
- Added Thumb-2 support to armv6m JIT backend, optimizing code for ARMv7-M and later cores
2424
- Added support for `binary:split/2,3` list patterns and `trim` / `trim_all` options
2525
- Added `timer:send_after/2`, `timer:send_after/3` and `timer:apply_after/4`
26+
- Added support for integer parts-per-second timeunit
2627

2728
### Changed
2829
- ~10% binary size reduction by rewriting module loading logic

libs/estdlib/src/erlang.erl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@
189189
-type atom_encoding() :: latin1 | utf8 | unicode.
190190

191191
-type mem_type() :: binary.
192-
-type time_unit() :: second | millisecond | microsecond | nanosecond | native.
192+
-type time_unit() :: second | millisecond | microsecond | nanosecond | native | pos_integer().
193193
-type timestamp() :: {
194194
MegaSecs :: non_neg_integer(), Secs :: non_neg_integer(), MicroSecs :: non_neg_integer
195195
}.

libs/exavmlib/lib/System.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ defmodule System do
2626
| :millisecond
2727
| :microsecond
2828
| :nanosecond
29+
| pos_integer()
2930

3031
@doc """
3132
Returns the current monotonic time in the `:native` time unit.

src/libAtomVM/nifs.c

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1867,6 +1867,30 @@ term nif_erlang_monotonic_time_1(Context *ctx, int argc, term argv[])
18671867
} else if (unit == NANOSECOND_ATOM || unit == NATIVE_ATOM) {
18681868
return make_maybe_boxed_int64(ctx, ((int64_t) ts.tv_sec) * INT64_C(1000000000) + ts.tv_nsec);
18691869

1870+
} else if (term_is_int64(unit)) {
1871+
avm_int64_t parts_per_second = term_maybe_unbox_int64(unit);
1872+
if (UNLIKELY(parts_per_second <= 0)) {
1873+
RAISE_ERROR(BADARG_ATOM);
1874+
}
1875+
if (UNLIKELY(
1876+
((ts.tv_sec > 0) && ((avm_int64_t) ts.tv_sec > INT64_MAX / parts_per_second))
1877+
|| ((ts.tv_sec < 0) && ((avm_int64_t) ts.tv_sec < INT64_MIN / parts_per_second)))) {
1878+
RAISE_ERROR(BADARG_ATOM);
1879+
}
1880+
avm_int64_t second_part = (avm_int64_t) ts.tv_sec * parts_per_second;
1881+
avm_int64_t quotient = parts_per_second / INT64_C(1000000000);
1882+
avm_int64_t remainder = parts_per_second % INT64_C(1000000000);
1883+
avm_int64_t fractional_high = (avm_int64_t) ts.tv_nsec * quotient;
1884+
avm_int64_t fractional_low = ((avm_int64_t) ts.tv_nsec * remainder) / INT64_C(1000000000);
1885+
if (UNLIKELY(fractional_high > INT64_MAX - fractional_low)) {
1886+
RAISE_ERROR(BADARG_ATOM);
1887+
}
1888+
avm_int64_t fractional_part = fractional_high + fractional_low;
1889+
if (UNLIKELY(second_part > INT64_MAX - fractional_part)) {
1890+
RAISE_ERROR(BADARG_ATOM);
1891+
}
1892+
return make_maybe_boxed_int64(ctx, second_part + fractional_part);
1893+
18701894
} else {
18711895
RAISE_ERROR(BADARG_ATOM);
18721896
}
@@ -1898,6 +1922,30 @@ term nif_erlang_system_time_1(Context *ctx, int argc, term argv[])
18981922
} else if (unit == NANOSECOND_ATOM || unit == NATIVE_ATOM) {
18991923
return make_maybe_boxed_int64(ctx, ((int64_t) ts.tv_sec) * INT64_C(1000000000) + ts.tv_nsec);
19001924

1925+
} else if (term_is_int64(unit)) {
1926+
avm_int64_t parts_per_second = term_maybe_unbox_int64(unit);
1927+
if (UNLIKELY(parts_per_second <= 0)) {
1928+
RAISE_ERROR(BADARG_ATOM);
1929+
}
1930+
if (UNLIKELY(
1931+
((ts.tv_sec > 0) && ((avm_int64_t) ts.tv_sec > INT64_MAX / parts_per_second))
1932+
|| ((ts.tv_sec < 0) && ((avm_int64_t) ts.tv_sec < INT64_MIN / parts_per_second)))) {
1933+
RAISE_ERROR(BADARG_ATOM);
1934+
}
1935+
avm_int64_t second_part = (avm_int64_t) ts.tv_sec * parts_per_second;
1936+
avm_int64_t quotient = parts_per_second / INT64_C(1000000000);
1937+
avm_int64_t remainder = parts_per_second % INT64_C(1000000000);
1938+
avm_int64_t fractional_high = (avm_int64_t) ts.tv_nsec * quotient;
1939+
avm_int64_t fractional_low = ((avm_int64_t) ts.tv_nsec * remainder) / INT64_C(1000000000);
1940+
if (UNLIKELY(fractional_high > INT64_MAX - fractional_low)) {
1941+
RAISE_ERROR(BADARG_ATOM);
1942+
}
1943+
avm_int64_t fractional_part = fractional_high + fractional_low;
1944+
if (UNLIKELY(second_part > INT64_MAX - fractional_part)) {
1945+
RAISE_ERROR(BADARG_ATOM);
1946+
}
1947+
return make_maybe_boxed_int64(ctx, second_part + fractional_part);
1948+
19011949
} else {
19021950
RAISE_ERROR(BADARG_ATOM);
19031951
}
@@ -2011,7 +2059,6 @@ term nif_calendar_system_time_to_universal_time_2(Context *ctx, int argc, term a
20112059
UNUSED(argc);
20122060

20132061
struct timespec ts;
2014-
20152062
avm_int64_t value = term_maybe_unbox_int64(argv[0]);
20162063

20172064
if (argv[1] == SECOND_ATOM) {
@@ -2030,6 +2077,20 @@ term nif_calendar_system_time_to_universal_time_2(Context *ctx, int argc, term a
20302077
ts.tv_sec = (time_t) (value / INT64_C(1000000000));
20312078
ts.tv_nsec = value % INT64_C(1000000000);
20322079

2080+
} else if (term_is_int64(argv[1])) {
2081+
avm_int64_t parts_per_second = term_maybe_unbox_int64(argv[1]);
2082+
if (UNLIKELY(parts_per_second <= 0)) {
2083+
RAISE_ERROR(BADARG_ATOM);
2084+
}
2085+
if (UNLIKELY(!term_is_int64(argv[0]))) {
2086+
RAISE_ERROR(BADARG_ATOM);
2087+
}
2088+
ts.tv_sec = (time_t) (value / parts_per_second);
2089+
if ((value % parts_per_second) < 0) {
2090+
ts.tv_sec -= 1;
2091+
}
2092+
ts.tv_nsec = 0;
2093+
20332094
} else {
20342095
RAISE_ERROR(BADARG_ATOM);
20352096
}

tests/erlang_tests/test_monotonic_time.erl

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ start() ->
3838
true = is_integer(N2 - N1) andalso (N2 - N1) >= 0,
3939

4040
ok = test_native_monotonic_time(),
41+
ok = test_integer_time_unit(),
42+
ok = test_bad_integer_time_unit(),
4143

4244
1.
4345

@@ -46,6 +48,15 @@ test_diff(X) when is_integer(X) andalso X >= 0 ->
4648
test_diff(X) when X < 0 ->
4749
0.
4850

51+
expect(F, Expect) ->
52+
try
53+
F(),
54+
fail
55+
catch
56+
_:E when E == Expect ->
57+
ok
58+
end.
59+
4960
test_native_monotonic_time() ->
5061
Na1 = erlang:monotonic_time(native),
5162
receive
@@ -54,3 +65,39 @@ test_native_monotonic_time() ->
5465
Na2 = erlang:monotonic_time(native),
5566
true = is_integer(Na2 - Na1) andalso (Na2 - Na1) >= 0,
5667
ok.
68+
69+
test_integer_time_unit() ->
70+
%% integer 1 = parts per second, equivalent to second
71+
S = erlang:monotonic_time(second),
72+
S1 = erlang:monotonic_time(1),
73+
true = abs(S1 - S) =< 1,
74+
75+
%% integer 1000 = parts per second, equivalent to millisecond
76+
Ms = erlang:monotonic_time(millisecond),
77+
Ms1 = erlang:monotonic_time(1000),
78+
true = abs(Ms1 - Ms) =< 1,
79+
80+
%% integer 1000000 = parts per second, equivalent to microsecond
81+
Us = erlang:monotonic_time(microsecond),
82+
Us1 = erlang:monotonic_time(1000000),
83+
true = abs(Us1 - Us) =< 1000,
84+
85+
%% integer 1000000000 = parts per second, equivalent to nanosecond
86+
Ns = erlang:monotonic_time(nanosecond),
87+
Ns1 = erlang:monotonic_time(1000000000),
88+
true = abs(Ns1 - Ns) =< 1000000,
89+
90+
%% verify monotonicity with integer unit
91+
T1 = erlang:monotonic_time(1000),
92+
receive
93+
after 1 -> ok
94+
end,
95+
T2 = erlang:monotonic_time(1000),
96+
true = T2 >= T1,
97+
98+
ok.
99+
100+
test_bad_integer_time_unit() ->
101+
ok = expect(fun() -> erlang:monotonic_time(0) end, badarg),
102+
ok = expect(fun() -> erlang:monotonic_time(-1) end, badarg),
103+
ok.

tests/erlang_tests/test_system_time.erl

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,14 @@ start() ->
3434
ok = test_os_system_time(),
3535
ok = test_time_unit_ratios(),
3636

37+
ok = test_integer_time_unit(),
38+
ok = test_bad_integer_time_unit(),
39+
3740
ok = expect(fun() -> erlang:system_time(not_a_time_unit) end, badarg),
3841

3942
ok = test_system_time_to_universal_time(),
43+
ok = test_integer_unit_universal_time(),
44+
ok = test_bad_integer_unit_universal_time(),
4045

4146
0.
4247

@@ -165,3 +170,60 @@ test_native_universal_time() ->
165170
{{1970, 1, 1}, {0, 0, 0}} = calendar:system_time_to_universal_time(0, native),
166171
{{1970, 1, 1}, {0, 0, 1}} = calendar:system_time_to_universal_time(1000000000, native),
167172
ok.
173+
174+
test_integer_time_unit() ->
175+
%% integer 1 = parts per second, equivalent to second
176+
S = erlang:system_time(second),
177+
S1 = erlang:system_time(1),
178+
true = abs(S1 - S) =< 1,
179+
180+
%% integer 1000 = parts per second, equivalent to millisecond
181+
Ms = erlang:system_time(millisecond),
182+
Ms1 = erlang:system_time(1000),
183+
true = abs(Ms1 - Ms) =< 1,
184+
185+
%% integer 1000000 = parts per second, equivalent to microsecond
186+
Us = erlang:system_time(microsecond),
187+
Us1 = erlang:system_time(1000000),
188+
true = abs(Us1 - Us) =< 1000,
189+
190+
%% integer 1000000000 = parts per second, equivalent to nanosecond
191+
Ns = erlang:system_time(nanosecond),
192+
Ns1 = erlang:system_time(1000000000),
193+
true = abs(Ns1 - Ns) =< 1000000,
194+
195+
%% verify values are positive
196+
true = S1 > 0,
197+
true = Ms1 > 0,
198+
true = Us1 > 0,
199+
true = Ns1 > 0,
200+
201+
ok.
202+
203+
test_integer_unit_universal_time() ->
204+
%% integer 1 = seconds
205+
{{1970, 1, 1}, {0, 0, 0}} = calendar:system_time_to_universal_time(0, 1),
206+
{{1970, 1, 1}, {0, 0, 1}} = calendar:system_time_to_universal_time(1, 1),
207+
{{2023, 7, 8}, {20, 19, 39}} = calendar:system_time_to_universal_time(1688847579, 1),
208+
209+
%% integer 1000 = milliseconds
210+
{{1970, 1, 1}, {0, 0, 0}} = calendar:system_time_to_universal_time(0, 1000),
211+
{{1970, 1, 1}, {0, 0, 1}} = calendar:system_time_to_universal_time(1000, 1000),
212+
{{1970, 1, 1}, {0, 0, 1}} = calendar:system_time_to_universal_time(1001, 1000),
213+
{{1969, 12, 31}, {23, 59, 59}} = calendar:system_time_to_universal_time(-1, 1000),
214+
215+
%% integer 1000000 = microseconds
216+
{{1970, 1, 1}, {0, 0, 0}} = calendar:system_time_to_universal_time(0, 1000000),
217+
{{1970, 1, 1}, {0, 0, 1}} = calendar:system_time_to_universal_time(1000000, 1000000),
218+
{{1969, 12, 31}, {23, 59, 59}} = calendar:system_time_to_universal_time(-1, 1000000),
219+
220+
ok.
221+
222+
test_bad_integer_time_unit() ->
223+
ok = expect(fun() -> erlang:system_time(0) end, badarg),
224+
ok = expect(fun() -> erlang:system_time(-1) end, badarg),
225+
ok.
226+
227+
test_bad_integer_unit_universal_time() ->
228+
ok = expect(fun() -> calendar:system_time_to_universal_time(0, 0) end, badarg),
229+
ok.

0 commit comments

Comments
 (0)