From 69c72906fef07a45f9aa1e8702a42a80d262204f Mon Sep 17 00:00:00 2001 From: "carpentry-heartbeat[bot]" Date: Wed, 19 Aug 2026 13:35:59 +0200 Subject: [PATCH] Reduce Datetime.now's nanoseconds without narrowing through a C long MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `now` derived its nanoseconds field with (Long.to-int (mod (Uint64.to-long t) 1000000000l)) where `t` is the `Uint64` from `System.nanotime`. `Uint64.to-long` is `return (long)x;` in carp_stdint.h, and C `long` is 32 bits on every ILP32 target (32-bit Linux, armhf) and on Windows x64, so the modulo ran on the sign-extended low 32 bits of a 64-bit count. Measured on a 32-bit armhf host, where sizeof(long) is 4: System.nanotime = 1787138980662882507 Uint64.to-long = 715796683 ; == nanotime mod 2^32 Datetime.now ns = 715797220 correct answer = 662882507 The field is a function of the truncated low 32 bits, so it wraps discontinuously about every 4.29 s of wall clock — and once that low word's top bit is set the cast sign-extends and C's `%` returns a negative result, so the field can come back negative outright. A sample taken while verifying this reported -272749194. Every runner in the org's CI is 64-bit, which is why it survived. The reduction now stays in `Uint64` throughout — written as `(- t (* (/ t n) n))`, since core registers no `Uint64.mod` — and only the result, which is below one billion, is narrowed. It lives in `Datetime.subsecond-nanos` so it can be pinned by assertions everywhere, not only on the targets where the truncation is observable. Two of the six vectors have teeth on 64-bit hosts too: at 2^63 and at `Uint64.MAX` the old `(long)` cast preserves the bits but yields a negative `Long`, and the old expression then hands back a negative nanoseconds field. A seventh assertion brackets `Datetime.now` between two `System.nanotime` samples so the call site is pinned as well, standing down on the rare sample pair that straddles a second boundary. `Uint64.to-long` was the only place in time.carp where a genuinely 64-bit value is narrowed; the remaining `Long` conversions all carry timezone offsets, which never exceed a day. --- docs/Datetime.html | 23 +++++++++++++++++++++++ test/time.carp | 46 ++++++++++++++++++++++++++++++++++++++++++++++ time.carp | 11 ++++++++++- 3 files changed, 79 insertions(+), 1 deletion(-) diff --git a/docs/Datetime.html b/docs/Datetime.html index 3824225..e8fd667 100644 --- a/docs/Datetime.html +++ b/docs/Datetime.html @@ -1200,6 +1200,29 @@

+
+ +

+ subsecond-nanos +

+
+
+ defn +
+

+ (Fn [Uint64] Int) +

+
+                        (subsecond-nanos t)
+                    
+

+

returns the sub-second part of the nanosecond count t, +that is, t modulo one billion.

+

The reduction is exact over the whole Uint64 range, including counts wider +than a C long.

+ +

+

diff --git a/test/time.carp b/test/time.carp index b176ba5..1964a50 100644 --- a/test/time.carp +++ b/test/time.carp @@ -17,6 +17,11 @@ ; render a Datetime as "YYYY-MM-DD HH:MM:SS" for whole-instant comparisons. (defn stamp [dt] (fmt "%s %s" &(Datetime.isoformat dt) &(Datetime.isotime dt))) +; parts stay under 2^31: a wider Long literal truncates where C long is 32 bits. +(defn nanos [secs ns] + (let [billion (Uint64.from-long 1000000000l)] + (+ (* (Uint64.from-long secs) billion) (Uint64.from-long ns)))) + ; n UTF-8 continuation bytes, which decode to no character at all. (defn conts [n] (String.from-bytes &(Array.replicate n &128b))) @@ -669,6 +674,47 @@ &(at-tz 2100 1 1 19 0 0 @&Timezone.utc)) "equal-instant? stays timezone-aware past 2038") + ; --- sub-second reduction of a 64-bit clock --- + (assert-equal test + 999999999 + (Datetime.subsecond-nanos (Uint64.from-long 999999999l)) + "subsecond-nanos leaves a count below one second alone") + (assert-equal test + 500000000 + (Datetime.subsecond-nanos (nanos 1l 500000000l)) + "subsecond-nanos drops whole seconds") + (assert-equal test + 294967296 + (Datetime.subsecond-nanos + (Uint64.bit-shift-left (Uint64.from-long 1l) (Uint64.from-long 32l))) + "subsecond-nanos reduces 2^32") + (assert-equal test + 662882507 + (Datetime.subsecond-nanos (nanos 1787138980l 662882507l)) + "subsecond-nanos reduces a realistic monotonic-clock reading") + (assert-equal test + 854775808 + (Datetime.subsecond-nanos + (Uint64.bit-shift-left (Uint64.from-long 1l) (Uint64.from-long 63l))) + "subsecond-nanos reduces 2^63") + (assert-equal test + 709551615 + (Datetime.subsecond-nanos Uint64.MAX) + "subsecond-nanos reduces the largest representable count") + + ; with no second boundary between the samples, now's nanos sit between them. + (let-do [billion (Uint64.from-long 1000000000l) + a (System.nanotime) + dt (Datetime.now) + b (System.nanotime) + lo (Datetime.subsecond-nanos a) + hi (Datetime.subsecond-nanos b) + ns (Maybe.from @(Datetime.nanoseconds &dt) -1) + spans-a-second? (or (> (- b a) billion) (> lo hi))] + (assert-true test + (or spans-a-second? (and (<= lo ns) (<= ns hi))) + "now reads its nanoseconds off the full-width clock")) + (assert-equal test &@"00:00:30" &(Datetime.isotime diff --git a/time.carp b/time.carp index e189aa9..d01f965 100644 --- a/time.carp +++ b/time.carp @@ -459,6 +459,15 @@ The ISO format is of the form `HH:MM:SS`. The date value is not represented.") (Maybe.from @(minutes dt) 0) (Maybe.from @(seconds dt) 0))) + (doc subsecond-nanos "returns the sub-second part of the nanosecond count `t`, +that is, `t` modulo one billion. + +The reduction is exact over the whole `Uint64` range, including counts wider +than a C `long`.") + (defn subsecond-nanos [t] + (let [billion (Uint64.from-long 1000000000l)] + (Long.to-int (Uint64.to-long (- t (* (/ t billion) billion)))))) + (doc now "returns the `Datetime` representing the current time. All of the time information is obtained from the operating system directly.") @@ -471,7 +480,7 @@ All of the time information is obtained from the operating system directly.") (Maybe.Just @(TM.tm_hour ct)) (Maybe.Just @(TM.tm_min ct)) (Maybe.Just @(TM.tm_sec ct)) - (Maybe.Just (Long.to-int (mod (Uint64.to-long t) 1000000000l))) + (Maybe.Just (subsecond-nanos t)) (Maybe.Just (Timezone.init @(TM.tm_zone ct) @(TM.tm_gmtoff ct)