Semantic Diff

#9291 Print null checks with the precedence of ===

1 commit

32116b50cb88b30e39f284e2abfcb4cb1d027441..pr-9291

b2d750baa · mununki · Nov 10, 2026, 14:29 UTC

Print null checks with the precedence of ===

Why

Nullable.isNullable(e), and any other %is_nullable, compiles to the JS IR node Is_null_or_undefined e, which Js_dump prints as e == null. It printed e at level 1, below the precedence of ==, so an operand that binds more loosely lost its parentheses and the check applied to part of it: Nullable.isNullable(c ? a : b) compiled to c ? a : b == null, which JavaScript reads as c ? a : (b == null). The same happened to ||, && and bitwise operands, and an object in an arrow function's body gave a syntax error. It also put the whole check in parentheses wherever it was not at the top level, which the output of Stdlib_Nullable shows. Both now follow Js_op_util.op_prec EqEqEq, as x === null does: parentheses when the level is above that of ==, and the operand at its left level.

modifiedJS operand of a null check

let pick = (c, a: Nullable.t<int>, b: Nullable.t<int>) => Nullable.isNullable(c ? a : b)let sum = (a: int, b: int) => Nullable.isNullable(Nullable.make(a + b))
js
Before
After
function pick(c, a, b) {function pick(c, a, b) { return c ? a : b == null; return ( c ? a : b ) == null;}} function sum(a, b) {function sum(a, b) { return a + b | 0 == null; return (a + b | 0) == null;}}

modifiedJS null check as an operand

let equal = (a, b, eq) => Option.equal(Nullable.toOption(a), Nullable.toOption(b), eq)
js
Before
After
function equal(a, b, eq) {function equal(a, b, eq) { return Stdlib_Option.equal((a == null) ? undefined : Primitive_option.some(a), (b == null) ? undefined : Primitive_option.some(b), eq); return Stdlib_Option.equal(a == null ? undefined : Primitive_option.some(a), b == null ? undefined : Primitive_option.some(b), eq);}}

Review

  • Is the precedence of === right for ==? Both are equality operators with the same precedence and left associativity, so a context that takes a === b without parentheses takes a == null too: arguments, let initializers, ?:, ||, &&, bitwise operators and the left side of ===. The right side of === and the operand of ! still get parentheses, as in y === (x == null) and !(x == null).
  • Which checked-in outputs change? Only Stdlib_Nullable, Stdlib_TempTests and optional_external_arg_once_test, where the test of ?: loses its parentheses, and Primitive_option, where x == null as an operand of || does. No checked-in output had an operand that lost them.
  • Why does the conditional break over lines? Cond prints its own parentheses that way when it needs them, as elsewhere in the output.
  • What else prints an operand below its precedence? In prints both operands at level 0 (#9222), Typeof puts itself in no parentheses, and Fun ignores the level. They are separate fixes.