Skip to content

Commit 0303a59

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 0ba4205 commit 0303a59

4 files changed

Lines changed: 159 additions & 97 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2525
- Added Thumb-2 support to armv6m JIT backend, optimizing code for ARMv7-M and later cores
2626
- Added support for `binary:split/2,3` list patterns and `trim` / `trim_all` options
2727
- Added `timer:send_after/2`, `timer:send_after/3` and `timer:apply_after/4`
28-
- Added support for integer parts-per-second timeunit
28+
- Added support for integer parts-per-second timeunit, with `badarg` raised on int64 overflow
2929

3030
### Changed
3131
- ~10% binary size reduction by rewriting module loading logic

src/libAtomVM/nifs.c

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

1844-
term nif_erlang_monotonic_time_1(Context *ctx, int argc, term argv[])
1844+
static bool time_unit_to_parts_per_second(term unit, avm_int64_t *parts_per_second)
18451845
{
1846-
UNUSED(ctx);
1847-
1848-
term unit;
1849-
if (argc == 0) {
1850-
unit = NATIVE_ATOM;
1846+
if (unit == SECOND_ATOM) {
1847+
*parts_per_second = 1;
1848+
} else if (unit == MILLISECOND_ATOM) {
1849+
*parts_per_second = 1000;
1850+
} else if (unit == MICROSECOND_ATOM) {
1851+
*parts_per_second = INT64_C(1000000);
1852+
} else if (unit == NANOSECOND_ATOM || unit == NATIVE_ATOM) {
1853+
*parts_per_second = INT64_C(1000000000);
1854+
} else if (term_is_int64(unit)) {
1855+
*parts_per_second = term_maybe_unbox_int64(unit);
1856+
if (UNLIKELY(*parts_per_second <= 0)) {
1857+
return false;
1858+
}
18511859
} else {
1852-
unit = argv[0];
1860+
return false;
18531861
}
18541862

1855-
struct timespec ts;
1856-
sys_monotonic_time(&ts);
1863+
return true;
1864+
}
18571865

1858-
if (unit == SECOND_ATOM) {
1859-
return make_maybe_boxed_int64(ctx, ts.tv_sec);
1866+
// Convert nanoseconds to parts using: parts = nanoseconds * pps / 1e9
1867+
// Splits into high/low to avoid intermediate overflow.
1868+
// Caller must ensure 0 <= nanoseconds < 1e9 and pps > 0.
1869+
static bool nanoseconds_to_parts_per_second(
1870+
avm_int64_t nanoseconds, avm_int64_t parts_per_second, bool round_up, avm_int64_t *parts)
1871+
{
1872+
avm_int64_t quotient = parts_per_second / INT64_C(1000000000);
1873+
avm_int64_t remainder = parts_per_second % INT64_C(1000000000);
1874+
avm_int64_t fractional_high = nanoseconds * quotient;
1875+
avm_int64_t remainder_product = nanoseconds * remainder;
1876+
avm_int64_t fractional_low = remainder_product / INT64_C(1000000000);
18601877

1861-
} else if (unit == MILLISECOND_ATOM) {
1862-
return make_maybe_boxed_int64(ctx, ((int64_t) ts.tv_sec) * 1000UL + ts.tv_nsec / 1000000UL);
1878+
if (round_up && (remainder_product % INT64_C(1000000000)) != 0) {
1879+
fractional_low += 1;
1880+
}
18631881

1864-
} else if (unit == MICROSECOND_ATOM) {
1865-
return make_maybe_boxed_int64(ctx, ((int64_t) ts.tv_sec) * 1000000UL + ts.tv_nsec / 1000UL);
1882+
if (UNLIKELY(fractional_high > INT64_MAX - fractional_low)) {
1883+
return false;
1884+
}
18661885

1867-
} else if (unit == NANOSECOND_ATOM || unit == NATIVE_ATOM) {
1868-
return make_maybe_boxed_int64(ctx, ((int64_t) ts.tv_sec) * INT64_C(1000000000) + ts.tv_nsec);
1886+
*parts = fractional_high + fractional_low;
1887+
return true;
1888+
}
18691889

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-
}
1890+
// Convert a normalized timespec (0 <= tv_nsec < 1e9) to integer parts.
1891+
// Uses floor semantics for negative timestamps with non-zero tv_nsec.
1892+
static bool timespec_to_parts_per_second(
1893+
const struct timespec *ts, avm_int64_t parts_per_second, avm_int64_t *parts)
1894+
{
1895+
avm_int64_t seconds = (avm_int64_t) ts->tv_sec;
1896+
avm_int64_t fractional_part;
1897+
1898+
if (ts->tv_nsec == 0 || seconds >= 0) {
18751899
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);
1900+
((seconds > 0) && (seconds > INT64_MAX / parts_per_second))
1901+
|| ((seconds < 0) && (seconds < INT64_MIN / parts_per_second)))) {
1902+
return false;
18791903
}
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);
1904+
1905+
if (UNLIKELY(!nanoseconds_to_parts_per_second(
1906+
(avm_int64_t) ts->tv_nsec, parts_per_second, false, &fractional_part))) {
1907+
return false;
18871908
}
1888-
avm_int64_t fractional_part = fractional_high + fractional_low;
1909+
1910+
avm_int64_t second_part = seconds * parts_per_second;
18891911
if (UNLIKELY(second_part > INT64_MAX - fractional_part)) {
1890-
RAISE_ERROR(BADARG_ATOM);
1912+
return false;
18911913
}
1892-
return make_maybe_boxed_int64(ctx, second_part + fractional_part);
18931914

1894-
} else {
1895-
RAISE_ERROR(BADARG_ATOM);
1915+
*parts = second_part + fractional_part;
1916+
return true;
1917+
}
1918+
1919+
// Preserve floor semantics for normalized negative timespecs such as {-2, 999999999}.
1920+
avm_int64_t adjusted_seconds = seconds + 1;
1921+
if (UNLIKELY(adjusted_seconds < INT64_MIN / parts_per_second)) {
1922+
return false;
1923+
}
1924+
1925+
if (UNLIKELY(!nanoseconds_to_parts_per_second(
1926+
INT64_C(1000000000) - (avm_int64_t) ts->tv_nsec, parts_per_second, true,
1927+
&fractional_part))) {
1928+
return false;
18961929
}
1930+
1931+
avm_int64_t second_part = adjusted_seconds * parts_per_second;
1932+
if (UNLIKELY(second_part < INT64_MIN + fractional_part)) {
1933+
return false;
1934+
}
1935+
1936+
*parts = second_part - fractional_part;
1937+
return true;
18971938
}
18981939

1899-
term nif_erlang_system_time_1(Context *ctx, int argc, term argv[])
1940+
static term make_time_in_unit(Context *ctx, term unit, void (*time_fun)(struct timespec *))
19001941
{
1901-
UNUSED(ctx);
1942+
avm_int64_t parts_per_second;
1943+
if (UNLIKELY(!time_unit_to_parts_per_second(unit, &parts_per_second))) {
1944+
RAISE_ERROR(BADARG_ATOM);
1945+
}
19021946

1947+
struct timespec ts;
1948+
time_fun(&ts);
1949+
1950+
avm_int64_t value;
1951+
if (UNLIKELY(!timespec_to_parts_per_second(&ts, parts_per_second, &value))) {
1952+
RAISE_ERROR(BADARG_ATOM);
1953+
}
1954+
1955+
return make_maybe_boxed_int64(ctx, value);
1956+
}
1957+
1958+
term nif_erlang_monotonic_time_1(Context *ctx, int argc, term argv[])
1959+
{
19031960
term unit;
19041961
if (argc == 0) {
19051962
unit = NATIVE_ATOM;
19061963
} else {
19071964
unit = argv[0];
19081965
}
19091966

1910-
struct timespec ts;
1911-
sys_time(&ts);
1912-
1913-
if (unit == SECOND_ATOM) {
1914-
return make_maybe_boxed_int64(ctx, ts.tv_sec);
1915-
1916-
} else if (unit == MILLISECOND_ATOM) {
1917-
return make_maybe_boxed_int64(ctx, ((int64_t) ts.tv_sec) * 1000UL + ts.tv_nsec / 1000000UL);
1918-
1919-
} else if (unit == MICROSECOND_ATOM) {
1920-
return make_maybe_boxed_int64(ctx, ((int64_t) ts.tv_sec) * 1000000UL + ts.tv_nsec / 1000UL);
1921-
1922-
} else if (unit == NANOSECOND_ATOM || unit == NATIVE_ATOM) {
1923-
return make_maybe_boxed_int64(ctx, ((int64_t) ts.tv_sec) * INT64_C(1000000000) + ts.tv_nsec);
1924-
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);
1967+
return make_time_in_unit(ctx, unit, sys_monotonic_time);
1968+
}
19481969

1970+
term nif_erlang_system_time_1(Context *ctx, int argc, term argv[])
1971+
{
1972+
term unit;
1973+
if (argc == 0) {
1974+
unit = NATIVE_ATOM;
19491975
} else {
1950-
RAISE_ERROR(BADARG_ATOM);
1976+
unit = argv[0];
19511977
}
1978+
1979+
return make_time_in_unit(ctx, unit, sys_time);
19521980
}
19531981

19541982
static term build_datetime_from_tm(Context *ctx, struct tm *broken_down_time)
@@ -2062,27 +2090,14 @@ term nif_calendar_system_time_to_universal_time_2(Context *ctx, int argc, term a
20622090
}
20632091
avm_int64_t value = term_maybe_unbox_int64(argv[0]);
20642092

2065-
avm_int64_t divisor;
2066-
if (argv[1] == SECOND_ATOM) {
2067-
divisor = 1;
2068-
} else if (argv[1] == MILLISECOND_ATOM) {
2069-
divisor = 1000;
2070-
} else if (argv[1] == MICROSECOND_ATOM) {
2071-
divisor = INT64_C(1000000);
2072-
} else if (argv[1] == NANOSECOND_ATOM || argv[1] == NATIVE_ATOM) {
2073-
divisor = INT64_C(1000000000);
2074-
} else if (term_is_int64(argv[1])) {
2075-
divisor = term_maybe_unbox_int64(argv[1]);
2076-
if (UNLIKELY(divisor <= 0)) {
2077-
RAISE_ERROR(BADARG_ATOM);
2078-
}
2079-
} else {
2093+
avm_int64_t parts_per_second;
2094+
if (UNLIKELY(!time_unit_to_parts_per_second(argv[1], &parts_per_second))) {
20802095
RAISE_ERROR(BADARG_ATOM);
20812096
}
20822097

20832098
// Floor division: round negative fractional seconds toward negative infinity
2084-
avm_int64_t quotient = value / divisor;
2085-
avm_int64_t remainder = value % divisor;
2099+
avm_int64_t quotient = value / parts_per_second;
2100+
avm_int64_t remainder = value % parts_per_second;
20862101
struct timespec ts = {
20872102
.tv_sec = (time_t) (quotient - (remainder < 0)),
20882103
.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)