Skip to content

Support integer parts-per-second time unit#2227

Draft
petermm wants to merge 1 commit into
atomvm:release-0.7from
petermm:feature/integer-time-unit
Draft

Support integer parts-per-second time unit#2227
petermm wants to merge 1 commit into
atomvm:release-0.7from
petermm:feature/integer-time-unit

Conversation

@petermm

@petermm petermm commented Mar 23, 2026

Copy link
Copy Markdown
Contributor

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.

Used in elixir DynamicSupervisor - completes the timeunit support.

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.

These changes are made under both the "Apache 2.0" and the "GNU Lesser General
Public License 2.1 or later" license terms (dual license).

SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later

@petermm petermm force-pushed the feature/integer-time-unit branch 2 times, most recently from dad612d to 1f640b2 Compare March 24, 2026 12:54
@petermm petermm force-pushed the feature/integer-time-unit branch 5 times, most recently from 0303a59 to 4dff77a Compare April 8, 2026 13:22
@petermm petermm force-pushed the feature/integer-time-unit branch from 4dff77a to b5006b8 Compare May 28, 2026 18:21
@petermm petermm force-pushed the feature/integer-time-unit branch from b5006b8 to fe55a5f Compare June 6, 2026 02:56

@bettio bettio left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tests are not working on the BEAM, let's fix this first.

log from run-tests:

datetime:OK
Raised {badmatch,fail}, stacktrace:
[{test_system_time,test_bad_integer_unit_universal_time,0,
                   [{file,"/__w/AtomVM/AtomVM/tests/erlang_tests/test_system_time.erl"},
                    {line,259}]},
 {test_system_time,start,0,
                   [{file,"/__w/AtomVM/AtomVM/tests/erlang_tests/test_system_time.erl"},
                    {line,45}]},
 {erl_eval,do_apply,7,[{file,"erl_eval.erl"},{line,915}]},
 {erl_eval,try_clauses,10,[{file,"erl_eval.erl"},{line,1233}]},
 {erl_eval,expr,6,[{file,"erl_eval.erl"},{line,663}]},
 {erl_eval,exprs,6,[{file,"erl_eval.erl"},{line,271}]},
 {init,start_it,1,[]},
 {init,start_em,1,[]}]
test_system_time:
test_system_time:FAILED

Comment thread tests/erlang_tests/test_system_time.erl Outdated
Comment thread tests/erlang_tests/test_monotonic_time.erl Outdated
Comment thread CHANGELOG.md Outdated
@petermm

petermm commented Jun 8, 2026

Copy link
Copy Markdown
Contributor Author

thank you, shall blame the CI blunder on it being backed up;-)

let me know if a squash is needed.

@petermm petermm force-pushed the feature/integer-time-unit branch from f713183 to c817ad6 Compare June 8, 2026 17:43
@petermm

petermm commented Jun 8, 2026

Copy link
Copy Markdown
Contributor Author

AMP, all fixed:

PR Review — Integer parts-per-second time units & calendar hardening

Reviewed commits (oldest → newest):

Commit Summary
be75107 Support integer parts-per-second time unit
9374c25 Fix calendar:system_time_to_universal_time/2 input validation and negative rounding
efd8ca9 Refactor time-unit conversion into shared helpers
6ef0b66 Harden calendar time conversion bounds
c817ad6 Keep integer time unit tests BEAM-compatible

Scope: adds positive integer ("parts per second") time units to erlang:monotonic_time/1,
erlang:system_time/1, and calendar:system_time_to_universal_time/2; extracts shared
conversion helpers; and hardens overflow / range handling.

Verdict

Solid work. The overflow math and floor-division semantics are correct under the documented
preconditions, the refactor removes real duplication, and the test coverage is good. There is
one real (edge-case) bug — a signed-left-shift UB path for negative years — plus a few
small hardening and test-portability suggestions below.


Findings

1. (Bug, low-frequency / UB) Negative year can reach term_from_int11() signed left shift

build_datetime_from_tm() now guards the year against int16_t truncation, but still permits
negative years down to INT16_MIN:

avm_int64_t year = (avm_int64_t) broken_down_time->tm_year + 1900;
if (UNLIKELY(year < INT16_MIN || year > INT16_MAX)) {
    RAISE_ERROR(BADARG_ATOM);
}
...
term_put_tuple_element(date_tuple, 0, term_from_int11((int16_t) year));

term_from_int11() is return (value << 4) | TERM_INTEGER_TAG;. Left-shifting a negative
signed value is undefined behavior in C (pre-C23). A sufficiently negative time_t fed to
gmtime_r() on a 64-bit-time_t platform (reachable via the calendar path with a large
negative timestamp) can yield tm_year + 1900 < 0, which passes the current guard and then
shifts a negative int16_t.

Calendar years are non-negative; tighten the lower bound:

 static term build_datetime_from_tm(Context *ctx, struct tm *broken_down_time)
 {
     avm_int64_t year = (avm_int64_t) broken_down_time->tm_year + 1900;
-    if (UNLIKELY(year < INT16_MIN || year > INT16_MAX)) {
+    // term_from_int11() left-shifts its signed argument; never pass a negative
+    // value. Calendar years are non-negative.
+    if (UNLIKELY(year < 0 || year > INT16_MAX)) {
         RAISE_ERROR(BADARG_ATOM);
     }

2. (Hardening) Make helper preconditions executable

nanoseconds_to_parts_per_second() and timespec_to_parts_per_second() are overflow-safe
only under their documented preconditions (0 <= nanoseconds < 1e9, pps > 0, normalized
tv_nsec). Today every caller satisfies these, but the helpers are file-scoped and easy to
misuse later. Cheap insurance:

 static bool nanoseconds_to_parts_per_second(
     avm_int64_t nanoseconds, avm_int64_t parts_per_second, bool round_up, avm_int64_t *parts)
 {
+    if (UNLIKELY(nanoseconds < 0 || nanoseconds >= INT64_C(1000000000) || parts_per_second <= 0)) {
+        return false;
+    }
+
     avm_int64_t quotient = parts_per_second / INT64_C(1000000000);
 static bool timespec_to_parts_per_second(
     const struct timespec *ts, avm_int64_t parts_per_second, avm_int64_t *parts)
 {
+    if (UNLIKELY(parts_per_second <= 0 || ts->tv_nsec < 0 || ts->tv_nsec >= INT64_C(1000000000))) {
+        return false;
+    }
+
     avm_int64_t seconds = (avm_int64_t) ts->tv_sec;

3. (Test portability) Pre-existing native == nanosecond assertion is not BEAM-guaranteed

c817ad6 correctly gated the new int64-overflow assertion behind the "ATOM" machine check.
But test_native_universal_time/0 (pre-existing, not introduced by these commits) still hard-codes:

{{1970, 1, 1}, {0, 0, 1}} = calendar:system_time_to_universal_time(1000000000, native),

OTP does not guarantee native is nanosecond (it is implementation-defined). AtomVM fixes
it to 1e9, which is fine — but the assertion is only portable on AtomVM. For consistency with
the gating approach taken in this PR, consider:

 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 = case erlang:system_info(machine) of
+        "ATOM" ->
+            {{1970, 1, 1}, {0, 0, 1}} =
+                calendar:system_time_to_universal_time(1000000000, native),
+            ok;
+        _ ->
+            ok
+    end,
     ok.

(Optional / outside the strict scope of these 5 commits, since it predates them.)

4. (Test coverage) Add exact-negative-multiple calendar cases

Existing negative tests exercise non-zero remainders (-1001, -257). Add exact multiples to
lock down that remainder == 0 does not over-floor:

     {{1969, 12, 31}, {23, 59, 59}} = calendar:system_time_to_universal_time(-1, millisecond),
+    {{1969, 12, 31}, {23, 59, 59}} = calendar:system_time_to_universal_time(-1000, millisecond),
     {{1969, 12, 31}, {23, 59, 58}} = calendar:system_time_to_universal_time(-1001, millisecond),
     {{1969, 12, 31}, {23, 59, 59}} = calendar:system_time_to_universal_time(-255, 256),
+    {{1969, 12, 31}, {23, 59, 59}} = calendar:system_time_to_universal_time(-256, 256),
     {{1969, 12, 31}, {23, 59, 58}} = calendar:system_time_to_universal_time(-257, 256),

5. (Nit) CHANGELOG wording

-- Added support for integer parts-per-second timeunit, with `badarg` raised on int64 overflow
+- Added support for integer parts-per-second time unit, with `badarg` raised on int64 overflow

Things verified correct (no change needed)

  • Overflow safety of nanoseconds_to_parts_per_second for pps up to INT64_MAX:
    quotient <= 9223372036, nanoseconds * quotient < INT64_MAX,
    nanoseconds * remainder < 1e18, and the final addition is overflow-checked.
  • Floor semantics for negative timestamps in timespec_to_parts_per_second via the
    adjusted_seconds = seconds + 1 / round-up transform — correct for normalized negatives
    such as {-2, 999999999}.
  • Calendar floor division seconds = quotient - (remainder < 0) is exact floor for a
    positive divisor, and the only underflow-scary case (value == INT64_MIN, pps == 1) has
    remainder == 0, so no extra decrement occurs.
  • int64_to_time_t_checked handles both signed and unsigned time_t and round-trips the
    cast to detect range loss (practically correct on standard two's-complement 32/64-bit targets).
  • nanoseconds_to_parts_per_second negative branch is only entered with
    tv_nsec != 0 && seconds < 0, so 1e9 - tv_nsec ∈ (0, 1e9) — preconditions hold.
  • Input validation added to nif_calendar_system_time_to_universal_time_2 (term_is_int64
    before unboxing) and gmtime_r/localtime_r NULL handling are good defensive fixes.

Suggested priority

  1. Finding 1 — fix before merge (UB, even if rare).
  2. Findings 2 & 4 — nice hardening / coverage, low cost.
  3. Findings 3 & 5 — optional polish.

@bettio bettio left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a couple of minor fixes. Feel free to cleanup PR history and push it for merge.

Comment thread src/libAtomVM/nifs.c Outdated
Comment thread src/libAtomVM/nifs.c Outdated
Comment thread src/libAtomVM/nifs.c Outdated
@petermm petermm force-pushed the feature/integer-time-unit branch from 4c19344 to df30d10 Compare June 9, 2026 12:04
@petermm petermm marked this pull request as draft June 9, 2026 12:20
@petermm petermm force-pushed the feature/integer-time-unit branch 4 times, most recently from b52198f to 69a2fdd Compare June 9, 2026 22:22
…bounds

- Add support for integer parts-per-second time units (e.g. 1_000_000 for microseconds)

- Refactor time-unit conversion into shared helper functions

- Harden calendar and integer time unit conversion bounds

- Fix input validation and negative rounding in calendar:system_time_to_universal_time/2

- Ensure BEAM compatibility for monotonic_time and system_time tests

- Use AVM_INT range for calendar year and add year boundary tests

Signed-off-by: Peter M <petermm@gmail.com>
@petermm petermm force-pushed the feature/integer-time-unit branch from 69a2fdd to b751fb0 Compare June 9, 2026 22:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants