Skip to content

Commit dad612d

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 8ec15f2 commit dad612d

File tree

5 files changed

+173
-2
lines changed

5 files changed

+173
-2
lines changed

libs/estdlib/src/erlang.erl

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

194194
-type mem_type() :: binary.
195-
-type time_unit() :: second | millisecond | microsecond | nanosecond | native.
195+
-type time_unit() :: second | millisecond | microsecond | nanosecond | native | pos_integer().
196196
-type timestamp() :: {
197197
MegaSecs :: non_neg_integer(), Secs :: non_neg_integer(), MicroSecs :: non_neg_integer
198198
}.

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
@@ -1841,6 +1841,30 @@ term nif_erlang_monotonic_time_1(Context *ctx, int argc, term argv[])
18411841
} else if (unit == NANOSECOND_ATOM || unit == NATIVE_ATOM) {
18421842
return make_maybe_boxed_int64(ctx, ((int64_t) ts.tv_sec) * INT64_C(1000000000) + ts.tv_nsec);
18431843

1844+
} else if (term_is_int64(unit)) {
1845+
avm_int64_t parts_per_second = term_maybe_unbox_int64(unit);
1846+
if (UNLIKELY(parts_per_second <= 0)) {
1847+
RAISE_ERROR(BADARG_ATOM);
1848+
}
1849+
if (UNLIKELY(
1850+
((ts.tv_sec > 0) && ((avm_int64_t) ts.tv_sec > INT64_MAX / parts_per_second))
1851+
|| ((ts.tv_sec < 0) && ((avm_int64_t) ts.tv_sec < INT64_MIN / parts_per_second)))) {
1852+
RAISE_ERROR(BADARG_ATOM);
1853+
}
1854+
avm_int64_t second_part = (avm_int64_t) ts.tv_sec * parts_per_second;
1855+
avm_int64_t quotient = parts_per_second / INT64_C(1000000000);
1856+
avm_int64_t remainder = parts_per_second % INT64_C(1000000000);
1857+
avm_int64_t fractional_high = (avm_int64_t) ts.tv_nsec * quotient;
1858+
avm_int64_t fractional_low = ((avm_int64_t) ts.tv_nsec * remainder) / INT64_C(1000000000);
1859+
if (UNLIKELY(fractional_high > INT64_MAX - fractional_low)) {
1860+
RAISE_ERROR(BADARG_ATOM);
1861+
}
1862+
avm_int64_t fractional_part = fractional_high + fractional_low;
1863+
if (UNLIKELY(second_part > INT64_MAX - fractional_part)) {
1864+
RAISE_ERROR(BADARG_ATOM);
1865+
}
1866+
return make_maybe_boxed_int64(ctx, second_part + fractional_part);
1867+
18441868
} else {
18451869
RAISE_ERROR(BADARG_ATOM);
18461870
}
@@ -1872,6 +1896,30 @@ term nif_erlang_system_time_1(Context *ctx, int argc, term argv[])
18721896
} else if (unit == NANOSECOND_ATOM || unit == NATIVE_ATOM) {
18731897
return make_maybe_boxed_int64(ctx, ((int64_t) ts.tv_sec) * INT64_C(1000000000) + ts.tv_nsec);
18741898

1899+
} else if (term_is_int64(unit)) {
1900+
avm_int64_t parts_per_second = term_maybe_unbox_int64(unit);
1901+
if (UNLIKELY(parts_per_second <= 0)) {
1902+
RAISE_ERROR(BADARG_ATOM);
1903+
}
1904+
if (UNLIKELY(
1905+
((ts.tv_sec > 0) && ((avm_int64_t) ts.tv_sec > INT64_MAX / parts_per_second))
1906+
|| ((ts.tv_sec < 0) && ((avm_int64_t) ts.tv_sec < INT64_MIN / parts_per_second)))) {
1907+
RAISE_ERROR(BADARG_ATOM);
1908+
}
1909+
avm_int64_t second_part = (avm_int64_t) ts.tv_sec * parts_per_second;
1910+
avm_int64_t quotient = parts_per_second / INT64_C(1000000000);
1911+
avm_int64_t remainder = parts_per_second % INT64_C(1000000000);
1912+
avm_int64_t fractional_high = (avm_int64_t) ts.tv_nsec * quotient;
1913+
avm_int64_t fractional_low = ((avm_int64_t) ts.tv_nsec * remainder) / INT64_C(1000000000);
1914+
if (UNLIKELY(fractional_high > INT64_MAX - fractional_low)) {
1915+
RAISE_ERROR(BADARG_ATOM);
1916+
}
1917+
avm_int64_t fractional_part = fractional_high + fractional_low;
1918+
if (UNLIKELY(second_part > INT64_MAX - fractional_part)) {
1919+
RAISE_ERROR(BADARG_ATOM);
1920+
}
1921+
return make_maybe_boxed_int64(ctx, second_part + fractional_part);
1922+
18751923
} else {
18761924
RAISE_ERROR(BADARG_ATOM);
18771925
}
@@ -1985,7 +2033,6 @@ term nif_calendar_system_time_to_universal_time_2(Context *ctx, int argc, term a
19852033
UNUSED(argc);
19862034

19872035
struct timespec ts;
1988-
19892036
avm_int64_t value = term_maybe_unbox_int64(argv[0]);
19902037

19912038
if (argv[1] == SECOND_ATOM) {
@@ -2004,6 +2051,20 @@ term nif_calendar_system_time_to_universal_time_2(Context *ctx, int argc, term a
20042051
ts.tv_sec = (time_t) (value / INT64_C(1000000000));
20052052
ts.tv_nsec = value % INT64_C(1000000000);
20062053

2054+
} else if (term_is_int64(argv[1])) {
2055+
avm_int64_t parts_per_second = term_maybe_unbox_int64(argv[1]);
2056+
if (UNLIKELY(parts_per_second <= 0)) {
2057+
RAISE_ERROR(BADARG_ATOM);
2058+
}
2059+
if (UNLIKELY(!term_is_int64(argv[0]))) {
2060+
RAISE_ERROR(BADARG_ATOM);
2061+
}
2062+
ts.tv_sec = (time_t) (value / parts_per_second);
2063+
if ((value % parts_per_second) < 0) {
2064+
ts.tv_sec -= 1;
2065+
}
2066+
ts.tv_nsec = 0;
2067+
20072068
} else {
20082069
RAISE_ERROR(BADARG_ATOM);
20092070
}

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)