Skip to content

Commit fe55a5f

Browse files
committed
Refactor time-unit conversion into shared helpers
Extract time_unit_to_parts_per_second, nanoseconds_to_parts_per_second, timespec_to_parts_per_second, and make_time_in_unit helpers to eliminate code duplication between monotonic_time and system_time BIFs. The new timespec_to_parts_per_second correctly handles negative timestamps with non-zero tv_nsec by using an adjusted-seconds approach that avoids rejecting valid results whose intermediate sec*pps would overflow but whose final floored value fits in int64. Calendar function now reuses time_unit_to_parts_per_second for unit dispatch. Add tests for non-power-of-10 integer units (256, 48000), boundary floor-division cases, and additional badarg coverage. Signed-off-by: Peter M <petermm@gmail.com>
1 parent 1850de9 commit fe55a5f

3 files changed

Lines changed: 158 additions & 96 deletions

File tree

src/libAtomVM/nifs.c

Lines changed: 110 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1851,114 +1851,142 @@ term nif_erlang_make_ref_0(Context *ctx, int argc, term argv[])
18511851
return term_from_ref_ticks(ref_ticks, &ctx->heap);
18521852
}
18531853

1854-
term nif_erlang_monotonic_time_1(Context *ctx, int argc, term argv[])
1854+
static bool time_unit_to_parts_per_second(term unit, avm_int64_t *parts_per_second)
18551855
{
1856-
UNUSED(ctx);
1857-
1858-
term unit;
1859-
if (argc == 0) {
1860-
unit = NATIVE_ATOM;
1856+
if (unit == SECOND_ATOM) {
1857+
*parts_per_second = 1;
1858+
} else if (unit == MILLISECOND_ATOM) {
1859+
*parts_per_second = 1000;
1860+
} else if (unit == MICROSECOND_ATOM) {
1861+
*parts_per_second = INT64_C(1000000);
1862+
} else if (unit == NANOSECOND_ATOM || unit == NATIVE_ATOM) {
1863+
*parts_per_second = INT64_C(1000000000);
1864+
} else if (term_is_int64(unit)) {
1865+
*parts_per_second = term_maybe_unbox_int64(unit);
1866+
if (UNLIKELY(*parts_per_second <= 0)) {
1867+
return false;
1868+
}
18611869
} else {
1862-
unit = argv[0];
1870+
return false;
18631871
}
18641872

1865-
struct timespec ts;
1866-
sys_monotonic_time(&ts);
1873+
return true;
1874+
}
18671875

1868-
if (unit == SECOND_ATOM) {
1869-
return make_maybe_boxed_int64(ctx, ts.tv_sec);
1876+
// Convert nanoseconds to parts using: parts = nanoseconds * pps / 1e9
1877+
// Splits into high/low to avoid intermediate overflow.
1878+
// Caller must ensure 0 <= nanoseconds < 1e9 and pps > 0.
1879+
static bool nanoseconds_to_parts_per_second(
1880+
avm_int64_t nanoseconds, avm_int64_t parts_per_second, bool round_up, avm_int64_t *parts)
1881+
{
1882+
avm_int64_t quotient = parts_per_second / INT64_C(1000000000);
1883+
avm_int64_t remainder = parts_per_second % INT64_C(1000000000);
1884+
avm_int64_t fractional_high = nanoseconds * quotient;
1885+
avm_int64_t remainder_product = nanoseconds * remainder;
1886+
avm_int64_t fractional_low = remainder_product / INT64_C(1000000000);
18701887

1871-
} else if (unit == MILLISECOND_ATOM) {
1872-
return make_maybe_boxed_int64(ctx, ((int64_t) ts.tv_sec) * 1000UL + ts.tv_nsec / 1000000UL);
1888+
if (round_up && (remainder_product % INT64_C(1000000000)) != 0) {
1889+
fractional_low += 1;
1890+
}
18731891

1874-
} else if (unit == MICROSECOND_ATOM) {
1875-
return make_maybe_boxed_int64(ctx, ((int64_t) ts.tv_sec) * 1000000UL + ts.tv_nsec / 1000UL);
1892+
if (UNLIKELY(fractional_high > INT64_MAX - fractional_low)) {
1893+
return false;
1894+
}
18761895

1877-
} else if (unit == NANOSECOND_ATOM || unit == NATIVE_ATOM) {
1878-
return make_maybe_boxed_int64(ctx, ((int64_t) ts.tv_sec) * INT64_C(1000000000) + ts.tv_nsec);
1896+
*parts = fractional_high + fractional_low;
1897+
return true;
1898+
}
18791899

1880-
} else if (term_is_int64(unit)) {
1881-
avm_int64_t parts_per_second = term_maybe_unbox_int64(unit);
1882-
if (UNLIKELY(parts_per_second <= 0)) {
1883-
RAISE_ERROR(BADARG_ATOM);
1884-
}
1900+
// Convert a normalized timespec (0 <= tv_nsec < 1e9) to integer parts.
1901+
// Uses floor semantics for negative timestamps with non-zero tv_nsec.
1902+
static bool timespec_to_parts_per_second(
1903+
const struct timespec *ts, avm_int64_t parts_per_second, avm_int64_t *parts)
1904+
{
1905+
avm_int64_t seconds = (avm_int64_t) ts->tv_sec;
1906+
avm_int64_t fractional_part;
1907+
1908+
if (ts->tv_nsec == 0 || seconds >= 0) {
18851909
if (UNLIKELY(
1886-
((ts.tv_sec > 0) && ((avm_int64_t) ts.tv_sec > INT64_MAX / parts_per_second))
1887-
|| ((ts.tv_sec < 0) && ((avm_int64_t) ts.tv_sec < INT64_MIN / parts_per_second)))) {
1888-
RAISE_ERROR(BADARG_ATOM);
1910+
((seconds > 0) && (seconds > INT64_MAX / parts_per_second))
1911+
|| ((seconds < 0) && (seconds < INT64_MIN / parts_per_second)))) {
1912+
return false;
18891913
}
1890-
avm_int64_t second_part = (avm_int64_t) ts.tv_sec * parts_per_second;
1891-
avm_int64_t quotient = parts_per_second / INT64_C(1000000000);
1892-
avm_int64_t remainder = parts_per_second % INT64_C(1000000000);
1893-
avm_int64_t fractional_high = (avm_int64_t) ts.tv_nsec * quotient;
1894-
avm_int64_t fractional_low = ((avm_int64_t) ts.tv_nsec * remainder) / INT64_C(1000000000);
1895-
if (UNLIKELY(fractional_high > INT64_MAX - fractional_low)) {
1896-
RAISE_ERROR(BADARG_ATOM);
1914+
1915+
if (UNLIKELY(!nanoseconds_to_parts_per_second(
1916+
(avm_int64_t) ts->tv_nsec, parts_per_second, false, &fractional_part))) {
1917+
return false;
18971918
}
1898-
avm_int64_t fractional_part = fractional_high + fractional_low;
1919+
1920+
avm_int64_t second_part = seconds * parts_per_second;
18991921
if (UNLIKELY(second_part > INT64_MAX - fractional_part)) {
1900-
RAISE_ERROR(BADARG_ATOM);
1922+
return false;
19011923
}
1902-
return make_maybe_boxed_int64(ctx, second_part + fractional_part);
19031924

1904-
} else {
1905-
RAISE_ERROR(BADARG_ATOM);
1925+
*parts = second_part + fractional_part;
1926+
return true;
1927+
}
1928+
1929+
// Preserve floor semantics for normalized negative timespecs such as {-2, 999999999}.
1930+
avm_int64_t adjusted_seconds = seconds + 1;
1931+
if (UNLIKELY(adjusted_seconds < INT64_MIN / parts_per_second)) {
1932+
return false;
1933+
}
1934+
1935+
if (UNLIKELY(!nanoseconds_to_parts_per_second(
1936+
INT64_C(1000000000) - (avm_int64_t) ts->tv_nsec, parts_per_second, true,
1937+
&fractional_part))) {
1938+
return false;
19061939
}
1940+
1941+
avm_int64_t second_part = adjusted_seconds * parts_per_second;
1942+
if (UNLIKELY(second_part < INT64_MIN + fractional_part)) {
1943+
return false;
1944+
}
1945+
1946+
*parts = second_part - fractional_part;
1947+
return true;
19071948
}
19081949

1909-
term nif_erlang_system_time_1(Context *ctx, int argc, term argv[])
1950+
static term make_time_in_unit(Context *ctx, term unit, void (*time_fun)(struct timespec *))
19101951
{
1911-
UNUSED(ctx);
1952+
avm_int64_t parts_per_second;
1953+
if (UNLIKELY(!time_unit_to_parts_per_second(unit, &parts_per_second))) {
1954+
RAISE_ERROR(BADARG_ATOM);
1955+
}
19121956

1957+
struct timespec ts;
1958+
time_fun(&ts);
1959+
1960+
avm_int64_t value;
1961+
if (UNLIKELY(!timespec_to_parts_per_second(&ts, parts_per_second, &value))) {
1962+
RAISE_ERROR(BADARG_ATOM);
1963+
}
1964+
1965+
return make_maybe_boxed_int64(ctx, value);
1966+
}
1967+
1968+
term nif_erlang_monotonic_time_1(Context *ctx, int argc, term argv[])
1969+
{
19131970
term unit;
19141971
if (argc == 0) {
19151972
unit = NATIVE_ATOM;
19161973
} else {
19171974
unit = argv[0];
19181975
}
19191976

1920-
struct timespec ts;
1921-
sys_time(&ts);
1922-
1923-
if (unit == SECOND_ATOM) {
1924-
return make_maybe_boxed_int64(ctx, ts.tv_sec);
1925-
1926-
} else if (unit == MILLISECOND_ATOM) {
1927-
return make_maybe_boxed_int64(ctx, ((int64_t) ts.tv_sec) * 1000UL + ts.tv_nsec / 1000000UL);
1928-
1929-
} else if (unit == MICROSECOND_ATOM) {
1930-
return make_maybe_boxed_int64(ctx, ((int64_t) ts.tv_sec) * 1000000UL + ts.tv_nsec / 1000UL);
1931-
1932-
} else if (unit == NANOSECOND_ATOM || unit == NATIVE_ATOM) {
1933-
return make_maybe_boxed_int64(ctx, ((int64_t) ts.tv_sec) * INT64_C(1000000000) + ts.tv_nsec);
1934-
1935-
} else if (term_is_int64(unit)) {
1936-
avm_int64_t parts_per_second = term_maybe_unbox_int64(unit);
1937-
if (UNLIKELY(parts_per_second <= 0)) {
1938-
RAISE_ERROR(BADARG_ATOM);
1939-
}
1940-
if (UNLIKELY(
1941-
((ts.tv_sec > 0) && ((avm_int64_t) ts.tv_sec > INT64_MAX / parts_per_second))
1942-
|| ((ts.tv_sec < 0) && ((avm_int64_t) ts.tv_sec < INT64_MIN / parts_per_second)))) {
1943-
RAISE_ERROR(BADARG_ATOM);
1944-
}
1945-
avm_int64_t second_part = (avm_int64_t) ts.tv_sec * parts_per_second;
1946-
avm_int64_t quotient = parts_per_second / INT64_C(1000000000);
1947-
avm_int64_t remainder = parts_per_second % INT64_C(1000000000);
1948-
avm_int64_t fractional_high = (avm_int64_t) ts.tv_nsec * quotient;
1949-
avm_int64_t fractional_low = ((avm_int64_t) ts.tv_nsec * remainder) / INT64_C(1000000000);
1950-
if (UNLIKELY(fractional_high > INT64_MAX - fractional_low)) {
1951-
RAISE_ERROR(BADARG_ATOM);
1952-
}
1953-
avm_int64_t fractional_part = fractional_high + fractional_low;
1954-
if (UNLIKELY(second_part > INT64_MAX - fractional_part)) {
1955-
RAISE_ERROR(BADARG_ATOM);
1956-
}
1957-
return make_maybe_boxed_int64(ctx, second_part + fractional_part);
1977+
return make_time_in_unit(ctx, unit, sys_monotonic_time);
1978+
}
19581979

1980+
term nif_erlang_system_time_1(Context *ctx, int argc, term argv[])
1981+
{
1982+
term unit;
1983+
if (argc == 0) {
1984+
unit = NATIVE_ATOM;
19591985
} else {
1960-
RAISE_ERROR(BADARG_ATOM);
1986+
unit = argv[0];
19611987
}
1988+
1989+
return make_time_in_unit(ctx, unit, sys_time);
19621990
}
19631991

19641992
static term build_datetime_from_tm(Context *ctx, struct tm *broken_down_time)
@@ -2150,27 +2178,14 @@ term nif_calendar_system_time_to_universal_time_2(Context *ctx, int argc, term a
21502178
}
21512179
avm_int64_t value = term_maybe_unbox_int64(argv[0]);
21522180

2153-
avm_int64_t divisor;
2154-
if (argv[1] == SECOND_ATOM) {
2155-
divisor = 1;
2156-
} else if (argv[1] == MILLISECOND_ATOM) {
2157-
divisor = 1000;
2158-
} else if (argv[1] == MICROSECOND_ATOM) {
2159-
divisor = INT64_C(1000000);
2160-
} else if (argv[1] == NANOSECOND_ATOM || argv[1] == NATIVE_ATOM) {
2161-
divisor = INT64_C(1000000000);
2162-
} else if (term_is_int64(argv[1])) {
2163-
divisor = term_maybe_unbox_int64(argv[1]);
2164-
if (UNLIKELY(divisor <= 0)) {
2165-
RAISE_ERROR(BADARG_ATOM);
2166-
}
2167-
} else {
2181+
avm_int64_t parts_per_second;
2182+
if (UNLIKELY(!time_unit_to_parts_per_second(argv[1], &parts_per_second))) {
21682183
RAISE_ERROR(BADARG_ATOM);
21692184
}
21702185

21712186
// Floor division: round negative fractional seconds toward negative infinity
2172-
avm_int64_t quotient = value / divisor;
2173-
avm_int64_t remainder = value % divisor;
2187+
avm_int64_t quotient = value / parts_per_second;
2188+
avm_int64_t remainder = value % parts_per_second;
21742189
struct timespec ts = {
21752190
.tv_sec = (time_t) (quotient - (remainder < 0)),
21762191
.tv_nsec = 0

tests/erlang_tests/test_monotonic_time.erl

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ start() ->
3939

4040
ok = test_native_monotonic_time(),
4141
ok = test_integer_time_unit(),
42+
ok = test_non_power_of_10_integer_time_unit(),
4243
ok = test_bad_integer_time_unit(),
4344

4445
1.
@@ -97,7 +98,23 @@ test_integer_time_unit() ->
9798

9899
ok.
99100

101+
test_non_power_of_10_integer_time_unit() ->
102+
ok = test_integer_time_unit_monotonicity(256),
103+
ok = test_integer_time_unit_monotonicity(48000),
104+
ok.
105+
100106
test_bad_integer_time_unit() ->
101107
ok = expect(fun() -> erlang:monotonic_time(0) end, badarg),
102108
ok = expect(fun() -> erlang:monotonic_time(-1) end, badarg),
103109
ok.
110+
111+
test_integer_time_unit_monotonicity(PartsPerSecond) ->
112+
T1 = erlang:monotonic_time(PartsPerSecond),
113+
receive
114+
after 1 -> ok
115+
end,
116+
T2 = erlang:monotonic_time(PartsPerSecond),
117+
true = is_integer(T1),
118+
true = is_integer(T2),
119+
true = T2 >= T1,
120+
ok.

tests/erlang_tests/test_system_time.erl

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ start() ->
3535
ok = test_time_unit_ratios(),
3636

3737
ok = test_integer_time_unit(),
38+
ok = test_non_power_of_10_integer_time_unit(),
3839
ok = test_bad_integer_time_unit(),
3940

4041
ok = expect(fun() -> erlang:system_time(not_a_time_unit) end, badarg),
@@ -137,7 +138,9 @@ test_system_time_to_universal_time() ->
137138
{{1969, 12, 31}, {23, 59, 59}} = calendar:system_time_to_universal_time(-1, nanosecond),
138139
{{1969, 12, 31}, {23, 59, 59}} = calendar:system_time_to_universal_time(-1, native),
139140

140-
ok = expect(fun() -> calendar:system_time_to_universal_time(not_an_integer, second) end, badarg),
141+
ok = expect(
142+
fun() -> calendar:system_time_to_universal_time(not_an_integer, second) end, badarg
143+
),
141144

142145
ok = test_nanosecond_universal_time(),
143146
ok = test_native_universal_time(),
@@ -207,6 +210,11 @@ test_integer_time_unit() ->
207210

208211
ok.
209212

213+
test_non_power_of_10_integer_time_unit() ->
214+
ok = test_integer_parts_per_second_ratio(256),
215+
ok = test_integer_parts_per_second_ratio(48000),
216+
ok.
217+
210218
test_integer_unit_universal_time() ->
211219
%% integer 1 = seconds
212220
{{1970, 1, 1}, {0, 0, 0}} = calendar:system_time_to_universal_time(0, 1),
@@ -218,12 +226,21 @@ test_integer_unit_universal_time() ->
218226
{{1970, 1, 1}, {0, 0, 1}} = calendar:system_time_to_universal_time(1000, 1000),
219227
{{1970, 1, 1}, {0, 0, 1}} = calendar:system_time_to_universal_time(1001, 1000),
220228
{{1969, 12, 31}, {23, 59, 59}} = calendar:system_time_to_universal_time(-1, 1000),
229+
{{1969, 12, 31}, {23, 59, 58}} = calendar:system_time_to_universal_time(-1001, 1000),
221230

222231
%% integer 1000000 = microseconds
223232
{{1970, 1, 1}, {0, 0, 0}} = calendar:system_time_to_universal_time(0, 1000000),
224233
{{1970, 1, 1}, {0, 0, 1}} = calendar:system_time_to_universal_time(1000000, 1000000),
225234
{{1969, 12, 31}, {23, 59, 59}} = calendar:system_time_to_universal_time(-1, 1000000),
226235

236+
%% integer 256 and 48000 = arbitrary parts per second
237+
{{1970, 1, 1}, {0, 0, 0}} = calendar:system_time_to_universal_time(255, 256),
238+
{{1970, 1, 1}, {0, 0, 1}} = calendar:system_time_to_universal_time(256, 256),
239+
{{1969, 12, 31}, {23, 59, 59}} = calendar:system_time_to_universal_time(-1, 256),
240+
{{1969, 12, 31}, {23, 59, 59}} = calendar:system_time_to_universal_time(-255, 256),
241+
{{1970, 1, 1}, {0, 0, 1}} = calendar:system_time_to_universal_time(48000, 48000),
242+
{{1969, 12, 31}, {23, 59, 59}} = calendar:system_time_to_universal_time(-1, 48000),
243+
227244
ok.
228245

229246
test_bad_integer_time_unit() ->
@@ -232,5 +249,18 @@ test_bad_integer_time_unit() ->
232249
ok.
233250

234251
test_bad_integer_unit_universal_time() ->
252+
ok = expect(
253+
fun() -> calendar:system_time_to_universal_time(not_an_integer, second) end, badarg
254+
),
255+
ok = expect(fun() -> calendar:system_time_to_universal_time(not_an_integer, 1000) end, badarg),
235256
ok = expect(fun() -> calendar:system_time_to_universal_time(0, 0) end, badarg),
257+
ok = expect(fun() -> calendar:system_time_to_universal_time(0, -1) end, badarg),
258+
ok.
259+
260+
test_integer_parts_per_second_ratio(PartsPerSecond) ->
261+
Seconds = erlang:system_time(second),
262+
Parts = erlang:system_time(PartsPerSecond),
263+
true = is_integer(Parts) andalso Parts > 0,
264+
true = Parts >= Seconds * PartsPerSecond,
265+
true = Parts < Seconds * PartsPerSecond + (PartsPerSecond * 2),
236266
ok.

0 commit comments

Comments
 (0)