Repository navigation
// truncates its result to 32 bits when inlined, but not when passed as a function #383
Description
Summary
The // operator gives two different answers for the same operands depending
on whether the compiler inlined it:
281474976710656 // 2 --> 0
apply (//) 281474976710656 2 --> 140737488355328 -- correct
(apply f a b = f a b, so the second forces the kernel function to run.)
The inlined form is generated as (a / b) | 0, which truncates the result
to 32 bits. The kernel function _Basics_idiv uses Math.trunc, which is
correct at any magnitude. The two have disagreed since Math.trunc was
introduced in core in 2023; the code generator was not updated to match.
This is not only a precision limit; it is the same expression producing two
values depending on syntax.
Reproduction
module Main exposing (main)
import Array
import Math
import Node
import Stream
import Task
main : Node.SimpleProgram a
main =
Node.defineSimpleProgram <| \env ->
Node.endSimpleProgram
(Stream.writeLineAsBytes (String.join "\n" lines) env.stdout
|> Task.onError (\_ -> Task.succeed env.stdout)
|> Task.map (\_ -> {})
)
{-| Applied through a local variable, so the call cannot be inlined and the
kernel function is what runs.
-}
apply : (Int -> Int -> Int) -> Int -> Int -> Int
apply f a b =
f a b
lines : Array String
lines =
[ "inlined 2^48 // 2 = " ++ String.fromInt (281474976710656 // 2)
, "kernel apply (//) 2^48 2 = " ++ String.fromInt (apply (//) 281474976710656 2)
, "inlined 2^31 // 1 = " ++ String.fromInt (2147483648 // 1)
, "kernel apply (//) 2^31 1 = " ++ String.fromInt (apply (//) 2147483648 1)
, "partial map ((//) 2^48) = " ++ String.fromInt (Maybe.withDefault 0 (Array.first (Array.map ((//) 281474976710656) [ 2 ])))
, "Math.truncate (2^48 / 2) = " ++ String.fromInt (Math.truncate (281474976710656 / 2))
]
Actual output
inlined 2^48 // 2 = 0
kernel apply (//) 2^48 2 = 140737488355328
inlined 2^31 // 1 = -2147483648
kernel apply (//) 2^31 1 = 2147483648
partial map ((//) 2^48) = 140737488355328
Math.truncate (2^48 / 2) = 140737488355328
Actual output
inlined 2^48 // 2 = 0
kernel apply (//) 2^48 2 = 140737488355328
inlined 2^31 // 1 = -2147483648
kernel apply (//) 2^31 1 = 2147483648
partial map ((//) 2^48) = 140737488355328
Math.truncate (2^48 / 2) = 140737488355328
Expected output
Every line should read 140737488355328 (or 2147483648 for the 2^31
cases). Int is a double, and both operands and results here are well inside
the exactly-representable range.
Generated JavaScript
From gren make Main, the two paths are visible side by side:
// inlined
String.fromInt((281474976710656 / 2) | 0)
// as a function value
String.fromInt(apply(Basics$idiv, 281474976710656, 2))Cause
compiler/src/Generate/JavaScript/Expression.hs:436:
"idiv" -> JS.Infix JS.OpBitwiseOr (JS.Infix JS.OpDiv left right) (JS.Int 0)while core's src/Gren/Kernel/Basics.js has:
var _Basics_idiv = F2(function (a, b) {
return Math.trunc(a / b);
});The kernel was changed from | 0 to Math.trunc in core commit 47b5b2d
("Use Math.trunc instead of | 0 to truncate numbers", 2023-07-18). The
inlined code path still emits the old form.
Impact
It is the quotient that must fit in 32 bits, not the operands, so the rule
is not about how large the numbers are. A division is incorrect when its
result lands outside signed 32-bit range:
281474976710656 // 16777216 --> 16777216 -- correct: the answer is 2^24
281474976710656 // 131072 --> -2147483648 -- wrong: the answer is 2^31
2147483648 // 1 --> -2147483648 -- wrong
4294967296 // 2 --> -2147483648 -- wrong
4294967296 // 4 --> 1073741824 -- correct
Dividing by one is the case worth calling out: n // 1 is wrong for every n
at or above 2^31.
Note that the neighbouring operations are consistent between the two paths:
Math.remainderBy inlines to %, which matches its kernel exactly, and
Math.modBy is not inlined at all. idiv appears to be the only one that
disagrees with its kernel.
Environment
- Gren 0.6.6
- compiler at
0.6.6-2-gfd5410c3(present at the tip ofmain) - core 7.4.2
- Node 22, Linux
Activity
gilramir commentedon Sep 2, 2026
Oh, this is also a Year 2038 issue!
I hit this again today, going through another route, which was to convert milliseconds to seconds:
Time.posixToMillisgives milliseconds and a POSIX timestamp is seconds, sothe conversion is:
The quotient is about 1.79e9 today, comfortably inside signed 32-bit range, so
this is correct, until 2038-01-19T03:14:08Z, the first
second whose value does not fit. Then the program starts
reporting a negative timestamp about 68 years in the past.
Reproduction
Actual output
Before the 2038 rollover it's ok, but at rollover, it's wrong (negative)