gren-lang / gren-lang/compiler

`//` truncates its result to 32 bits when inlined, but not when passed as a function

Open Beginner friendly
#383 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug good first issue
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 of main)
  • core 7.4.2
  • Node 22, Linux

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.