gren-lang / gren-lang/core

`compare` on floats is not an order, and a NaN key makes `Dict` lose entries it still lists

Open
#139 0 comments 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

_Utils_cmp answers GT for every comparison involving NaN, in both
directions and against itself. So compare on Float is neither antisymmetric
nor consistent with ==:

nan = 0 / 0

nan == nan          --> False
compare nan nan     --> GT      -- but == said they are not equal
compare 1.0 nan     --> GT
compare nan 1.0     --> GT      -- and so did the other direction

Dict and Set are ordered structures built on compare alone. An
intransitive compare does not raise there — it silently puts a key where
lookups will not find it, and takes existing keys down with it.

Reproduction

module Case exposing (results)

import Dict


nan : Float
nan =
    0 / 0


d0 : Dict.Dict Float String
d0 =
    Dict.empty
        |> Dict.set 1.0 "one"
        |> Dict.set 2.0 "two"
        |> Dict.set 3.0 "three"


d1 : Dict.Dict Float String
d1 =
    Dict.set nan "nan" d0
expression result expected
Dict.count d1 4 4
Dict.keys d1 1,2,3,NaN 1,2,3,NaN
Dict.get 1.0 d1 Just "one" Just "one"
Dict.get 2.0 d1 Just "two" Just "two"
Dict.get 3.0 d1 Nothing Just "three"
Dict.get nan d1 Nothing Just "nan"

3.0 was inserted before the NaN, is still in keys, is still counted, and can
no longer be retrieved. Nothing raises and nothing warns. The dictionary
reports a size that does not match the number of keys you can reach.

Cause

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

function _Utils_cmp(x, y) {
  if (typeof x !== "object") {
    return x === y ? /*EQ*/ 0 : x < y ? /*LT*/ -1 : /*GT*/ 1;
  }
  ...

For NaN, x === y is false and x < y is false, so the expression falls
through to GT — whichever operand the NaN is, and even when both are. The
final else is standing in for "greater than" when what actually happened is
"unordered", a case IEEE 754 has and this encoding does not.

Dict.set then walks the red-black tree comparing against the NaN node, where
every branch decision is GT regardless of the key being searched for, so a
whole subtree becomes unreachable.

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 by reproducing the Float and Dict examples, then inspect _Utils_cmp in core/src/Gren/Kernel/Utils.js. Trace how its comparison result drives Dict tree lookups; done means NaN comparisons have consistent behavior and inserted keys remain retrievable with the expected count and keys.

Written by the indexing model from the issue text.

Assessment

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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.