Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/float-literal-equality-simplify.fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Fix the JavaScript simplifier treating equal float literals, or an int and a float literal of the same value, as different constants, which turned `x == 2.5 && x == 2.5` into `false`.
29 changes: 29 additions & 0 deletions compiler/core/js_analyzer.ml
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,35 @@ let no_side_effect_statement st =
true
with _ -> false

(* The value of a number literal as JavaScript sees it, so that [1] and
[1.0] are the same number. [None] for a bigint, which is never [===] to
a number. *)
let number_value (n : J.number) : float option =
match n with
| Int {i} -> Some (Int32.to_float i)
| Float {f} -> float_of_string_opt f
| BigInt _ -> None

let eq_literal (x : J.expression_desc) (y : J.expression_desc) : bool option =
match (x, y) with
| ( Number (BigInt {positive = p0; value = v0}),
Number (BigInt {positive = p1; value = v1}) ) ->
Some (p0 = p1 && v0 = v1)
| Number (BigInt _), Number _ | Number _, Number (BigInt _) -> Some false
| Number n0, Number n1 -> (
match (number_value n0, number_value n1) with
(* Float [=] is IEEE equality like [===]: NaN differs from itself, and
[0.] equals [-0.] *)
| Some f0, Some f1 -> Some (f0 = f1)
| _ -> None)
| Bool b0, Bool b1 -> Some (b0 = b1)
| Str s0, Str s1 -> Some (s0 = s1)
| Null, Null | Undefined _, Undefined _ -> Some true
| ( (Bool _ | Number _ | Str _ | Null | Undefined _),
(Bool _ | Number _ | Str _ | Null | Undefined _) ) ->
Some false
| _ -> None

(* TODO: generate [fold2]
This make sense, for example:
{[
Expand Down
4 changes: 4 additions & 0 deletions compiler/core/js_analyzer.mli
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ val no_side_effect_statement : J.statement -> bool

val eq_expression : J.expression -> J.expression -> bool

val eq_literal : J.expression_desc -> J.expression_desc -> bool option
(** Whether two literals are the same value under JavaScript [===], for
example [1] and [1.0]; [None] when either is not a literal. *)

val eq_block : J.block -> J.block -> bool

val rev_flatten_seq : J.expression -> J.block
Expand Down
43 changes: 26 additions & 17 deletions compiler/core/js_exp_make.ml
Original file line number Diff line number Diff line change
Expand Up @@ -1161,32 +1161,41 @@ let rec simplify_and_ ~n (e1 : t) (e2 : t) : t option =
| ( Bin
( EqEqEq,
{expression_desc = Var ia},
{expression_desc = Bool _ | Null | Undefined _ | Number _ | Str _}
),
{
expression_desc =
(Bool _ | Null | Undefined _ | Number _ | Str _) as v1;
} ),
Bin
( EqEqEq,
{expression_desc = Var ib},
{expression_desc = Bool _ | Null | Undefined _ | Number _ | Str _}
) )
when Js_op_util.same_vident ia ib ->
(* Note: case x = y is handled above *)
Some false_
{
expression_desc =
(Bool _ | Null | Undefined _ | Number _ | Str _) as v2;
} ) )
when Js_op_util.same_vident ia ib -> (
match Js_analyzer.eq_literal v1 v2 with
| Some true -> Some e1
| Some false -> Some false_
| None -> None)
| ( Bin
( ((EqEqEq | NotEqEq) as op1),
{expression_desc = Var ia},
({
expression_desc = Bool _ | Null | Undefined _ | Number _ | Str _;
} as v1) ),
{
expression_desc =
(Bool _ | Null | Undefined _ | Number _ | Str _) as v1;
} ),
Bin
( ((EqEqEq | NotEqEq) as op2),
{expression_desc = Var ib},
({
expression_desc = Bool _ | Null | Undefined _ | Number _ | Str _;
} as v2) ) )
when Js_op_util.same_vident ia ib && op1 != op2 ->
if Js_analyzer.eq_expression v1 v2 then Some false_
else if op1 = EqEqEq then Some e1
else Some e2
{
expression_desc =
(Bool _ | Null | Undefined _ | Number _ | Str _) as v2;
} ) )
when Js_op_util.same_vident ia ib && op1 != op2 -> (
match Js_analyzer.eq_literal v1 v2 with
| Some true -> Some false_
| Some false -> if op1 = EqEqEq then Some e1 else Some e2
| None -> None)
| _ -> None
in
res
Expand Down
77 changes: 77 additions & 0 deletions tests/tests/src/number_literal_simplify_test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Generated by ReScript, PLEASE EDIT WITH CARE

import * as Mocha from "mocha";
import * as Test_utils from "./test_utils.mjs";

function float_eq_eq(x) {
return x === 2.5;
}

function float_neq_eq(x) {
return false;
}

function float_eq_neq(x) {
return false;
}

function float_eq_other(x) {
return false;
}

function float_neq_other(x) {
return x === 3.5;
}

function float_or(x) {
if (x !== 2.5) {
return true;
} else {
return x !== 2.5;
}
}

function int_tag_shadowed(v) {
return 6;
}

function float_tag_shadowed(v) {
return 6;
}

let opaque = (x => x);

Mocha.describe("Number_literal_simplify_test", () => {
Mocha.test("equal float literals", () => {
Test_utils.eq("File \"number_literal_simplify_test.res\", line 37, characters 7-14", true, float_eq_eq(opaque(2.5)));
Test_utils.eq("File \"number_literal_simplify_test.res\", line 38, characters 7-14", false, float_eq_eq(opaque(3.5)));
Test_utils.eq("File \"number_literal_simplify_test.res\", line 39, characters 7-14", false, float_neq_eq(opaque(2.5)));
Test_utils.eq("File \"number_literal_simplify_test.res\", line 40, characters 7-14", false, float_eq_neq(opaque(2.5)));
Test_utils.eq("File \"number_literal_simplify_test.res\", line 41, characters 7-14", true, float_or(opaque(3.5)));
Test_utils.eq("File \"number_literal_simplify_test.res\", line 42, characters 7-14", false, float_or(opaque(2.5)));
});
Mocha.test("different float literals", () => {
Test_utils.eq("File \"number_literal_simplify_test.res\", line 45, characters 7-14", false, float_eq_other(opaque(2.5)));
Test_utils.eq("File \"number_literal_simplify_test.res\", line 46, characters 7-14", true, float_neq_other(opaque(3.5)));
Test_utils.eq("File \"number_literal_simplify_test.res\", line 47, characters 7-14", false, float_neq_other(opaque(2.5)));
});
Mocha.test("literal tag equal to a payload literal", () => {
Test_utils.eq("File \"number_literal_simplify_test.res\", line 50, characters 7-14", 6, int_tag_shadowed(opaque(1)));
Test_utils.eq("File \"number_literal_simplify_test.res\", line 51, characters 7-14", 6, int_tag_shadowed(opaque(2.0)));
Test_utils.eq("File \"number_literal_simplify_test.res\", line 52, characters 7-14", 6, float_tag_shadowed(opaque(2.5)));
Test_utils.eq("File \"number_literal_simplify_test.res\", line 53, characters 7-14", 6, float_tag_shadowed(opaque(1.0)));
});
});

export {
float_eq_eq,
float_neq_eq,
float_eq_neq,
float_eq_other,
float_neq_other,
float_or,
int_tag_shadowed,
float_tag_shadowed,
opaque,
}
/* Not a pure module */
55 changes: 55 additions & 0 deletions tests/tests/src/number_literal_simplify_test.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// `x === c1 && x === c2` and `x !== c1 && x === c2` are simplified by
// comparing c1 and c2 as JavaScript values: equal float literals are equal,
// and so are 1 and 1.0.

let float_eq_eq = (x: float) => x == 2.5 && x == 2.5
let float_neq_eq = (x: float) => x != 2.5 && x == 2.5
let float_eq_neq = (x: float) => x == 2.5 && x != 2.5
let float_eq_other = (x: float) => x == 2.5 && x == 3.5
let float_neq_other = (x: float) => x != 2.5 && x == 3.5
let float_or = (x: float) => x != 2.5 || x != 2.5

@unboxed type int_tag = | @as(1) L1 | N(float) | S(string)
// The payload case is unused (warning 11): 1 always matches as L1.
@warning("-11")
let int_tag_shadowed = (v: int_tag) =>
switch v {
| N(1.0) => 4
| _ => 6
}

@unboxed type float_tag = | @as(2.5) L | M(float) | P(string)
@warning("-11")
let float_tag_shadowed = (v: float_tag) =>
switch v {
| M(2.5) => 4
| _ => 6
}

// Keeps the arguments unknown, so the calls are not folded to constants.
let opaque: 'a => 'a = %raw(`x => x`)

open Mocha
open Test_utils

describe(__MODULE__, () => {
test("equal float literals", () => {
eq(__LOC__, true, float_eq_eq(opaque(2.5)))
eq(__LOC__, false, float_eq_eq(opaque(3.5)))
eq(__LOC__, false, float_neq_eq(opaque(2.5)))
eq(__LOC__, false, float_eq_neq(opaque(2.5)))
eq(__LOC__, true, float_or(opaque(3.5)))
eq(__LOC__, false, float_or(opaque(2.5)))
})
test("different float literals", () => {
eq(__LOC__, false, float_eq_other(opaque(2.5)))
eq(__LOC__, true, float_neq_other(opaque(3.5)))
eq(__LOC__, false, float_neq_other(opaque(2.5)))
})
test("literal tag equal to a payload literal", () => {
eq(__LOC__, 6, int_tag_shadowed(opaque(L1)))
eq(__LOC__, 6, int_tag_shadowed(opaque(N(2.0))))
eq(__LOC__, 6, float_tag_shadowed(opaque(L)))
eq(__LOC__, 6, float_tag_shadowed(opaque(M(1.0))))
})
})
Loading