Skip to content

Editorial: Refactor IsLooselyEqual for better symmetry - #3918

Open
gibson042 wants to merge 9 commits into
tc39:mainfrom
gibson042:2026-07-islooselyequal-symmetry
Open

Editorial: Refactor IsLooselyEqual for better symmetry#3918
gibson042 wants to merge 9 commits into
tc39:mainfrom
gibson042:2026-07-islooselyequal-symmetry

Conversation

@gibson042

Copy link
Copy Markdown
Member

...taking advantage of type mismatch after the first step

Ref #3911 (comment)

Comment thread spec.html Outdated
1. If _x_ is either a Number or a String and _y_ is either a Number or a String, then
1. Return IsStrictlyEqual(! ToNumber(__x_), ! ToNumber(_y_)).
1. If _x_ is either a BigInt or a String and _y_ is either a BigInt or a String, then
1. Return IsStrictlyEqual(! ToBigIntOrUndefined(_x_), ! ToBigIntOrUndefined(_y_)).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is pretty confusing imo - you're calling "to bigint or undefined" when the only possible outcome is "to bigint"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it really is bigint or undefined here—either x is a bigint and y is a string or x is a string and y is a bigint... whichever is a bigint is preserved, but the string becomes either a bigint or undefined (the former in e.g. 0n == "0", the latter in e.g. 0n == "0.0").

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the if condition allows for both being strings or both being bigints, but i guess if the types were the same it'd have returned already.

i still find this confusing, and i suspect inlining the logic would be much easier to read/understand.

@gibson042 gibson042 Jul 16, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the if condition allows for both being strings or both being bigints, but i guess if the types were the same it'd have returned already.

Yes, this is the key insight being leveraged here.

i still find this confusing, and i suspect inlining the logic would be much easier to read/understand.

I'm assuming that would entail splitting the steps, e.g.

-        1. If _x_ is either a BigInt or a String and _y_ is either a BigInt or a String, then
-          1. Return IsStrictlyEqual(ToBigIntOrUndefined(_x_), ToBigIntOrUndefined(_y_)).
+        1. If _x_ is a BigInt and _y_ is a String, return IsStrictlyEqual(_x_, ToBigIntOrUndefined(_y_)).
+        1. If _x_ is a String and _y_ is a BigInt, return IsStrictlyEqual(ToBigIntOrUndefined(_x_), _y_).

...and presumably also for the preceding Number vs. String case and its use of ToNumber?

But how are those different from the BigInt vs. Number case and its use of ℝ? The particular ToNumber calls map {number, string} → number and ℝ calls map {bigint, [finite] number} → mathematical value—is it just the {bigint, string} → {bigint, undefined} output of ToBigIntOrUndefined?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm suggesting that "tobigint or undefined" is inherently confusing, because it's using undefined as a sentinel value.

I'd probably prefer to see these steps be something verbose that converts the string to a bigint, and then compares the two.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a boolean from a predicate isn't a special sentinel value, nor is an exception a value at all, let alone a sentinel.

That's a fair point about it being fallible - what would the steps look like in that case?

@gibson042 gibson042 Jul 17, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fundamentally, it all comes down to ParseText output being a polymorphic union over {Parse Node, a List of errors}, which StringToBigInt (and therefore transitively also this proposed ToBigIntOrUndefined) maps into a union over {BigInt, undefined}. You can call that a sentinel value, but the types are disjoint, which I would argue is more accurately described as an option/maybe type. There's no getting around some kind of polymorphism for a fallible operation, the only question is what form it takes, and it's not uncommon for us to use undefined like this.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, but the purpose here is "since one's a string and one's a bigint, coerce the string one to a bigint, and then compare the resulting two bigints", and it feels like unnecessary indirection to compare a string to undefined. You could replace the undefined with any non-bigint type and it'd work the same, i believe, which suggests to me that there's a cleaner way to express it.

Maybe something like:

1. If _x_ is a bigint and _y_ is a string, or _x_ is a string and _y_ is a bigint, then
  1. if _x_ is a string, then
    1. set _str_ to _x_.
    1. set _big_ to _y_.
  1. else,
    1. set _str_ to _y_.
    1. set _big_ to _x_.
  1. Set _num_ to StringToBigInt(_x_).
  1. if _num_ is *false*, return *false*.
  1. return IsLooselyEqual(_num_, _big_).

then it's very explicit and doesn't need a new sentinel-returning AO (obv with some kind of spec ternary it'd be much cleaner)

@gibson042 gibson042 Jul 17, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those steps accomplish the necessary behavior, but IMO would actually reduce comprehensibility of IsLooselyEqual, which has many branches for [order-independent] special type pairs.

  • SameType → IsStrictlyEqual
  • (undefined, null) → true
  • [normative-optional] (document.all, undefined | null) → true
  • (false | true, *) → IsLooselyEqual(ToNumber(false | true), *)
  • (Object, String | Number | BigInt | Symbol) → IsLooselyEqual(ToPrimitive(Object), String | Number | BigInt | Symbol)
  • (String, Number) → IsStrictlyEqual(ToNumber(String), Number)
  • (String, BigInt) → IsStrictlyEqual(StringToBigInt(String), BigInt)
  • (BigInt, Number) → ℝ(BigInt) = ℝ(Number)

I think there's even another possible approach that replaces recursion with binding reassignments:

1. If _x_ is either *undefined* or *null*, or _y_ is either *undefined* or *null*, then
  1. [normative-optional] If the host is a web browser or otherwise supports The [[IsHTMLDDA]] Internal Slot, then
    1. If _x_ is an Object and _x_ has an [[IsHTMLDDA]] internal slot, set _x_ to *undefined*.
    1. If _y_ is an Object and _y_ has an [[IsHTMLDDA]] internal slot, set _y_ to *undefined*.
  1. If _x_ is either *undefined* or *null* and _y_ is either *undefined* or *null*, return *true*.
  1. Return *false*.
1. If _x_ is an Object and _y_ is not an Object, set _x_ to ? ToPrimitive(_x_).
1. If _y_ is an Object and _x_ is not an Object, set _y_ to ? ToPrimitive(_y_).
1. If SameType(_x_, _y_) is *true*, return IsStrictlyEqual(_x_, _y_).
1. If _x_ is a Boolean, set _x_ to ! ToNumber(_x_).
1. If _y_ is a Boolean, set _y_ to ! ToNumber(_y_).
1. NOTE: Either _x_ and _y_ are both Numbers, or their types are different.
1. If _x_ is either a Number or a String and _y_ is either a Number or a String, then
  1. Return IsStrictlyEqual(! ToNumber(_x_), ! ToNumber(_y_)).
1. If _x_ is either a BigInt or a String and _y_ is either a BigInt or a String, then
  1. Return IsStrictlyEqual(ToBigIntOrUndefined(_x_), ToBigIntOrUndefined(_y_)).
1. If _x_ is either a BigInt or a finite Number and _y_ is either a BigInt or a finite Number, then
  1. If ℝ(_x_) = ℝ(_y_), return *true*.
1. Return *false*.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that seems like the winner so far

Comment thread spec.html Outdated
Comment on lines +5514 to +5520
1. Assert: _primitive_ is not an Object.
1. If _primitive_ is a BigInt, return _primitive_.
1. If _primitive_ is one of *undefined*, *null*, a Number, or a Symbol, return *undefined*.
1. If _primitive_ is *false*, return *0*<sub>ℤ</sub>.
1. If _primitive_ is *true*, return *1*<sub>ℤ</sub>.
1. Assert: _primitive_ is a String.
1. Return StringToBigInt(_primitive_).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i normally love more small AOs but in this case i think inlining the table directly into ToBigInt would be clearer.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had the same thought.

@gibson042
gibson042 force-pushed the 2026-07-islooselyequal-symmetry branch from ed41146 to 63925ff Compare July 16, 2026 22:57
Comment thread spec.html Outdated
<emu-alg>
1. Assert: _primitive_ is not an Object.
1. If _primitive_ is a BigInt, return _primitive_.
1. If _primitive_ is one of *undefined*, *null*, a Number, or a Symbol, return *undefined*.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This case is poorly covered by Editorial Conventions: Comparisons. We could avoid it by splitting into two steps, but even if so it'd be worth establishing a convention.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, it says (modulo some elision):

when comparing for equality against multiple options,
consolidate is using one of for more than two options (e.g., _x_ is one of _option1_, _option2_, or _option3_)

so that would seem to cover this step, and agree with what you've got.

On the other hand, there's a bullet under Phrasing Conventions that begins "to create an untagged union of types". Under those rules, you'd say:

Suggested change
1. If _primitive_ is one of *undefined*, *null*, a Number, or a Symbol, return *undefined*.
1. If _primitive_ is either a Number, a Symbol, *undefined*, or *null*, return *undefined*.

The question is whether this context is "creating an untagged union of types". I'm not sure what was intended when it was added to Editorial Conventions, but it's a boiled-down version of #2972 (comment) and following, which tried to be fairly general in its application, explicitly including forms such as if _X_ is <disjunction>.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I decided that this is "an untagged union of types" given current conventions, and opened #3937 to drive more clarity.

@gibson042
gibson042 force-pushed the 2026-07-islooselyequal-symmetry branch from 63925ff to 974e715 Compare July 16, 2026 23:06
@gibson042 gibson042 added the request preview ask the bot to trigger a PR preview label Jul 17, 2026
@github-actions github-actions Bot removed the request preview ask the bot to trigger a PR preview label Jul 17, 2026
@github-actions

Copy link
Copy Markdown

The rendered spec preview for this PR is available as a single page at https://tc39.es/ecma262/pr/3918 and as multiple pages at https://tc39.es/ecma262/pr/3918/multipage .

Comment thread spec.html Outdated
<dd>It converts _primitive_ to a BigInt value, or to *undefined* if such conversion is not supported.</dd>
</dl>
<emu-alg>
1. Assert: _primitive_ is not an Object.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of this assertion, maybe _primitive_ should be declared as an ECMAScript language value, but not an Object, or just a primitive value.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't have any current uses of that pattern AFAICT, but I like it. As of the latest push here, "primitive value" is now a <dfn>ed term.

@gibson042
gibson042 force-pushed the 2026-07-islooselyequal-symmetry branch from ef195a0 to 9b1dc12 Compare August 4, 2026 18:27
Comment thread spec.html Outdated
<emu-clause id="sec-primitive-value">
<h1>primitive value</h1>
<p>member of one of the types Undefined, Null, Boolean, Number, BigInt, Symbol, or String as defined in clause <emu-xref href="#sec-ecmascript-data-types-and-values"></emu-xref></p>
<p>a <dfn variants="primitive values">primitive value</dfn> is a member of one of the types Undefined, Null, Boolean, Number, BigInt, Symbol, or String as defined in clause <emu-xref href="#sec-ecmascript-data-types-and-values"></emu-xref></p>

@jmdyck jmdyck Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This goes against the 'Terms and Definitions' convention (which I'm guessing was inherited from Ecma) that each term & its defining text should have the same part of speech. In this case, they should both be noun phrases, but you've changed it to a sentence.

Besides that, #1278 is going to happen eventually, so it's probably best not to add to T&D. Instead, just leave this as is, and instead add the <dfn> to the corresponding sentence in 4.3.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This goes against the 'Terms and Definitions' convention (which I'm guessing was inherited from Ecma) that each term & its defining text should have the same part of speech. In this case, they should both be noun phrases, but you've changed it to a sentence.

Just like "implementation-approximated" and "implementation-defined" a few terms up.

Besides that, #1278 is going to happen eventually, so it's probably best not to add to T&D. Instead, just leave this as is, and instead add the <dfn> to the corresponding sentence in 4.3.

4.3 explicitly states "This overview is not part of the standard proper", which makes it seem like a bad home for such a <dfn>. I guess it eventually belongs in ECMAScript Language Types.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just like "implementation-approximated" and "implementation-defined" a few terms up.

Yeah, those (and "host-defined") were added in PR #1951. Looks like I didn't comment on the wording anomaly then.

4.3 explicitly states "This overview is not part of the standard proper", which makes it seem like a bad home for such a .

Hm, didn't notice that. And yeah, there are no <dfn>s currently in 4.3. So agreed, bad suggestion. Should we explicitly mark 4.3 as "informative"?

I guess it eventually belongs in ECMAScript Language Types.

Yup.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just moved it now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants