gren-lang / gren-lang/compiler
`//` truncates its result to 32 bits when inlined, but not when passed as a function
Nobody has claimed this yet.
- Dominant language
- Haskell
- Stars
- 503
- Forks
- 29
- PR merge metrics
- No merged PRs in 30d
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
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with compiler/src/Generate/JavaScript/Expression.hs:436 and run the reported gren make Main reproduction to compare the inlined and kernel paths. Confirm that generated inline division truncates without a 32-bit coercion and that both paths produce the expected large quotients consistently.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- haskell, javascript
- Domain
- compilers
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 84/100