openlang

Same Int, two targets, two answers

Found this while running the same Gleam module on both targets, and it's a runtime fact more than a language one.

import gleam/int
import gleam/io

pub fn main() {
  let id = 9_007_199_254_740_993
  io.println(int.to_string(id))
}

gleam run prints 9007199254740993. gleam run --target javascript prints 9007199254740992. On the Erlang target an Int is a BEAM integer and grows as far as memory lets it. On JavaScript the compiler maps Int to a plain JS number, a double, so anything past 2^53 gets rounded to a neighbour. Same type in the source, same code, and the two runtimes quietly disagree. No error on either side.

The real value came from a JSON payload, the literal is just the smallest repro. How are people handling ints that have to mean the same thing on both targets?

The conversation continues 3

Follow the reasoning
  1. Permalink

    The literal is the easy case to spot. Multiply two ints that are each well inside the safe range and the product can cross 2^53 at run time. The JS target rounds it to the nearest double and returns. The Erlang target allocates a bigger integer and returns. Neither branch reports anything, so the only thing that notices is a test that runs on JavaScript. If the package claims both targets, gleam test --target javascript belongs next to the default run, and the test needs a value past the limit, not a small one.

  2. Permalink

    For the per-target part, Gleam lets one function have an external for one target and a Gleam body for the rest. The body runs wherever no external matches.

    @external(javascript, "./int_ffi.mjs", "is_safe")
    pub fn is_safe(_n: Int) -> Bool {
      True
    }
    export function is_safe(n) {
      return Number.isSafeInteger(n);
    }

    On Erlang the body answers True, which is right there, since nothing is lost. On JavaScript Number.isSafeInteger gets the real number. It also catches a value that was already rounded, because a value past the limit never rounds back down into the safe range. The question is about the runtime, so it makes sense that the answer lives in the runtime's own file.

  3. Permalink

    That worked, and it's behind an opaque type now so nobody can skip the check. pub opaque type Id with a from_int that returns Error(Nil) when is_safe says no. The JSON decoder for ids refuses on JavaScript instead of handing out the rounded neighbour, and Erlang never takes the error branch, which is exactly right.

Other sides of the same question

  1. Why would this CPS sum overflow only in Debug?

    Could someone check my reading of this? A continuation-passing sum, written as an exercise. dotnet run -c Release prints 1000…

    Replies 5fsharp
  2. CTFE happens where the declaration asks for it

    Had D's CTFE pictured wrong for a while. I assumed that once a function is fit for compile time evaluation, the compiler woul…

    Replies 4d
  3. Braces around expr decide what the bytecode compiler sees

    Unbraced expr is the one Tcl habit I'd ban if I could! It looks like a style nit and it isn't. The braced line prints 0. The…

    Replies 16tcl