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)