gren-lang / gren-lang/core

`remainderBy 0` puts `NaN` in an `Int`, while its sibling `modBy 0` kills the program

Open
#141 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug
Dominant language
JavaScript
Stars
48
Forks
14
Avg merge
4h 14m
Merged PRs (30d)
1

Description

Repository: gren-lang/core
Found against: gren 0.6.6, gren-lang/core 7.4.2, node 22

Summary

Three functions in Math divide, and a zero divisor gets three different
answers:

expression result kind of answer
7 // 0 0 a number
Math.remainderBy 0 7 NaN not an Int
Math.modBy 0 7 program stops a runtime exception, sometimes silent

remainderBy's type is Int -> Int -> Int, and it returns a value that is not an integer.
The NaN then propagates: it compares unequal to
everything including itself, it survives String.fromInt as the text "NaN",
and it corrupts any Int accumulator it reaches.

Reproduction

One program asks both functions, in order:

module Main exposing (main)

import Init
import Math
import Node
import Stream
import Task exposing (Task)


zero : Int
zero =
    String.count "ab" - String.count "ab"


main : Node.SimpleProgram a
main =
    Node.defineSimpleProgram
        (\env -> Node.endSimpleProgram (emit env))


emit : Node.Environment -> Task Never {}
emit env =
    Stream.writeLineAsBytes
        ("remainderBy 0 7 = " ++ String.fromInt (Math.remainderBy zero 7))
        env.stdout
        |> Task.andThen
            (\out ->
                Stream.writeLineAsBytes
                    ("modBy 0 7 = " ++ String.fromInt (Math.modBy zero 7))
                    out
            )
        |> Task.map (\_ -> {})
        |> Task.onError (\_ -> Task.succeed {})
$ gren make Main --output=app
$ node app
remainderBy 0 7 = NaN
$ echo $?
0

The first line is the bug this report is about: an Int-typed expression
printed as NaN. Bind it — r = Math.remainderBy zero 7 — and it keeps
answering as a float: r == 0 is False, r == r is False, and toFloat r
is NaN.

The second line never prints. stderr is empty — zero bytes — and the exit
status is 0, so the program that died looks exactly like a program that
finished.

The modBy half is quieter than it looks

Put the modBy 0 in the program's first effect instead — the same program,
with emit shortened to a single write:

emit : Node.Environment -> Task Never {}
emit env =
    Stream.writeLineAsBytes
        ("modBy 0 7 = " ++ String.fromInt (Math.modBy zero 7))
        env.stdout
        |> Task.map (\_ -> {})
        |> Task.onError (\_ -> Task.succeed {})

and it crashes, with an error message, but exits with 0:

$ node app
Error: Cannot perform mod 0. Division by zero error.
    at _Debug_crash (app:1049:13)
    …
    at _Scheduler_step (app:295:23)
    at _Platform_dispatchEffects (app:1384:5)
$ echo $?
0

The difference is where the crash lands, and it has nothing to do with modBy.
In the program's first synchronous effect the exception is still inside the
try/catch that Generate/Node.hs wraps around the program, which prints it
without setting a status — that is gren-lang/compiler#385. One write later that
catch is gone, and core's own stream kernel takes the exception instead,
relabels it a stream cancellation and stops the scheduler; that is a separate
defect, filed as https://github.com/gren-lang/core/issues/142

Either way the status is 0. However, depending on where modBy 0 is
reached from, the crash may be quiet, or obvious.

Cause

core/src/Gren/Kernel/Math.js:

var _Math_remainderBy = F2(function (b, a) {
  return a % b;
});
var _Math_modBy = F2(function (modulus, x) {
  var answer = x % modulus;
  return modulus === 0
    ? __Debug_crash(11)
    : …
});

modBy guards the zero divisor (and crashes)
remainderBy does not.
// is unguarded too but survives by accident: _Basics_idiv is Math.trunc(a / b), which is
Infinity, and the code generator's inline form appends | 0, which turns
that into 0.

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

The behavior is implemented in core/src/Gren/Kernel/Math.js; start by comparing _Math_remainderBy with _Math_modBy and locate the existing Math test entry point. Reproduce the supplied Gren program, establish the intended zero-divisor behavior for remainderBy, and add regression coverage showing that an Int result does not become NaN.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript
Domain
backend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.