Add static loose equality (#364)

This commit is contained in:
Chris Tate
2026-09-20 20:46:52 -05:00
committed by GitHub
parent 69977b9fd0
commit f2c81630ef
28 changed files with 674 additions and 149 deletions
+1 -1
View File
@@ -8,7 +8,7 @@ These are rejected at compile time with an `SC` code, a code frame, and usually
**Language edges**
- Loose `==`/`!=` comparisons compile between two numbers, two strings, or two booleans, and for the `x == null`/`x != null` nullish idiom. Other coercing comparisons are rejected — use `===`/`!==`.
- Loose `==`/`!=` comparisons compile across statically represented primitive and primitive-union operands, including number/string/boolean/BigInt coercions and the `x == null`/`x != null` nullish idiom. Object-to-primitive comparisons remain fenced because custom `valueOf`/`toString` methods can execute arbitrary code; convert the object explicitly first.
- Block- and function-scoped `using` and `await using` compile for statically represented classes with zero-parameter disposal methods and for the supported FileHandle, timer, immediate, child-process, and readline handles. Array `for (using ... of ...)` loops compile. Top-level and switch-clause declarations, disposal through an engine-held package value, and the `DisposableStack` constructors remain fenced; a caught `SuppressedError` exposes its Node-compatible `name` and `message`, while its `error` and `suppressed` payload properties remain outside the static catch surface.
- Typed async generator functions and methods compile with lazy execution, `await`, direct `yield`, queued `.next()`/`.return()`/`.throw()` requests, and `for await`. `for await` also consumes Node `Readable` streams, Web `ReadableStream` values, and statically represented class iterators whose zero-parameter `next()` returns a promise of a `{ value, done? }` record; abrupt completion runs the iterator's cleanup, including literal `destroyOnReturn` and `preventCancel` options. Async `yield*` delegation, stored Web/Node iterator handles, and other structural async-iterator objects remain fenced. The `node:timers/promises` `setInterval(delay, value)` async iterator compiles, while its AbortSignal options remain fenced.
- Generics monomorphize when the target resolves statically. The remaining edges include generic functions declared inside another function, generic class expressions, generic classes whose base depends on their own type parameters, and generic methods that would require dynamic dispatch.
@@ -2,6 +2,6 @@
"schemaVersion": 3,
"nodeVersion": "24.15.0",
"nodeCommit": "848430679556aed0bd073f2bc263331ad84fa119",
"artifactVersion": "743056178b8be4cb1d5c",
"artifactVersion": "700e6429460b8481a546",
"rowCount": 3662
}
@@ -74,8 +74,8 @@
"apiEntries": 29,
"static": {
"supported": 0,
"partial": 15,
"refused": 6,
"partial": 17,
"refused": 4,
"not-implemented": 8,
"by-design": 0,
"unreviewed": 0,
@@ -1827,9 +1827,9 @@
"inherited": true
},
"static": {
"status": "refused",
"detail": "Explicitly rejected by a named compiler diagnostic.",
"verification": "explicit-refusal"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
},
"dynamic": {
"status": "partial",
@@ -2023,9 +2023,9 @@
"inherited": true
},
"static": {
"status": "refused",
"detail": "Explicitly rejected by a named compiler diagnostic.",
"verification": "explicit-refusal"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
},
"dynamic": {
"status": "partial",
@@ -7,9 +7,9 @@
"tierItemCount": 6663,
"tiers": {
"static": {
"replace-refusal": 48,
"replace-refusal": 46,
"verify-gap": 2714,
"audit-partial": 450,
"audit-partial": 452,
"classify": 2,
"implement": 81
},
@@ -318,7 +318,8 @@
"tests/corpus/1600-assert-passing.ts",
"tests/corpus/1604-assert-deep-structures.ts",
"tests/corpus/1609-assert-async.ts",
"tests/corpus/1721-assert-throws-regex-class.ts"
"tests/corpus/1721-assert-throws-regex-class.ts",
"tests/corpus/2936-static-loose-equality.ts"
]
},
"dynamic": {
@@ -391,7 +392,8 @@
"tests/corpus/1600-assert-passing.ts",
"tests/corpus/1604-assert-deep-structures.ts",
"tests/corpus/1609-assert-async.ts",
"tests/corpus/1721-assert-throws-regex-class.ts"
"tests/corpus/1721-assert-throws-regex-class.ts",
"tests/corpus/2936-static-loose-equality.ts"
]
},
"dynamic": {
@@ -431,7 +433,8 @@
"tests/corpus/1600-assert-passing.ts",
"tests/corpus/1604-assert-deep-structures.ts",
"tests/corpus/1609-assert-async.ts",
"tests/corpus/1721-assert-throws-regex-class.ts"
"tests/corpus/1721-assert-throws-regex-class.ts",
"tests/corpus/2936-static-loose-equality.ts"
]
},
"dynamic": {
@@ -471,7 +474,8 @@
"tests/corpus/1600-assert-passing.ts",
"tests/corpus/1604-assert-deep-structures.ts",
"tests/corpus/1609-assert-async.ts",
"tests/corpus/1721-assert-throws-regex-class.ts"
"tests/corpus/1721-assert-throws-regex-class.ts",
"tests/corpus/2936-static-loose-equality.ts"
]
},
"dynamic": {
@@ -537,12 +541,18 @@
"priority": "high",
"tiers": {
"static": {
"status": "refused",
"action": "replace-refusal",
"verification": "explicit-refusal",
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "surface-manifest:unsupported:node-builtin.assert.equal",
"tests": []
"source": "compiler-dedicated:assert.equal",
"tests": [
"tests/corpus/1600-assert-passing.ts",
"tests/corpus/1604-assert-deep-structures.ts",
"tests/corpus/1609-assert-async.ts",
"tests/corpus/1721-assert-throws-regex-class.ts",
"tests/corpus/2936-static-loose-equality.ts"
]
},
"dynamic": {
"status": "partial",
@@ -581,7 +591,8 @@
"tests/corpus/1600-assert-passing.ts",
"tests/corpus/1604-assert-deep-structures.ts",
"tests/corpus/1609-assert-async.ts",
"tests/corpus/1721-assert-throws-regex-class.ts"
"tests/corpus/1721-assert-throws-regex-class.ts",
"tests/corpus/2936-static-loose-equality.ts"
]
},
"dynamic": {
@@ -621,7 +632,8 @@
"tests/corpus/1600-assert-passing.ts",
"tests/corpus/1604-assert-deep-structures.ts",
"tests/corpus/1609-assert-async.ts",
"tests/corpus/1721-assert-throws-regex-class.ts"
"tests/corpus/1721-assert-throws-regex-class.ts",
"tests/corpus/2936-static-loose-equality.ts"
]
},
"dynamic": {
@@ -661,7 +673,8 @@
"tests/corpus/1600-assert-passing.ts",
"tests/corpus/1604-assert-deep-structures.ts",
"tests/corpus/1609-assert-async.ts",
"tests/corpus/1721-assert-throws-regex-class.ts"
"tests/corpus/1721-assert-throws-regex-class.ts",
"tests/corpus/2936-static-loose-equality.ts"
]
},
"dynamic": {
@@ -701,7 +714,8 @@
"tests/corpus/1600-assert-passing.ts",
"tests/corpus/1604-assert-deep-structures.ts",
"tests/corpus/1609-assert-async.ts",
"tests/corpus/1721-assert-throws-regex-class.ts"
"tests/corpus/1721-assert-throws-regex-class.ts",
"tests/corpus/2936-static-loose-equality.ts"
]
},
"dynamic": {
@@ -776,7 +790,8 @@
"tests/corpus/1600-assert-passing.ts",
"tests/corpus/1604-assert-deep-structures.ts",
"tests/corpus/1609-assert-async.ts",
"tests/corpus/1721-assert-throws-regex-class.ts"
"tests/corpus/1721-assert-throws-regex-class.ts",
"tests/corpus/2936-static-loose-equality.ts"
]
},
"dynamic": {
@@ -807,12 +822,18 @@
"priority": "high",
"tiers": {
"static": {
"status": "refused",
"action": "replace-refusal",
"verification": "explicit-refusal",
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "surface-manifest:unsupported:node-builtin.assert.notEqual",
"tests": []
"source": "compiler-dedicated:assert.notEqual",
"tests": [
"tests/corpus/1600-assert-passing.ts",
"tests/corpus/1604-assert-deep-structures.ts",
"tests/corpus/1609-assert-async.ts",
"tests/corpus/1721-assert-throws-regex-class.ts",
"tests/corpus/2936-static-loose-equality.ts"
]
},
"dynamic": {
"status": "partial",
@@ -851,7 +872,8 @@
"tests/corpus/1600-assert-passing.ts",
"tests/corpus/1604-assert-deep-structures.ts",
"tests/corpus/1609-assert-async.ts",
"tests/corpus/1721-assert-throws-regex-class.ts"
"tests/corpus/1721-assert-throws-regex-class.ts",
"tests/corpus/2936-static-loose-equality.ts"
]
},
"dynamic": {
@@ -891,7 +913,8 @@
"tests/corpus/1600-assert-passing.ts",
"tests/corpus/1604-assert-deep-structures.ts",
"tests/corpus/1609-assert-async.ts",
"tests/corpus/1721-assert-throws-regex-class.ts"
"tests/corpus/1721-assert-throws-regex-class.ts",
"tests/corpus/2936-static-loose-equality.ts"
]
},
"dynamic": {
@@ -931,7 +954,8 @@
"tests/corpus/1600-assert-passing.ts",
"tests/corpus/1604-assert-deep-structures.ts",
"tests/corpus/1609-assert-async.ts",
"tests/corpus/1721-assert-throws-regex-class.ts"
"tests/corpus/1721-assert-throws-regex-class.ts",
"tests/corpus/2936-static-loose-equality.ts"
]
},
"dynamic": {
@@ -971,7 +995,8 @@
"tests/corpus/1600-assert-passing.ts",
"tests/corpus/1604-assert-deep-structures.ts",
"tests/corpus/1609-assert-async.ts",
"tests/corpus/1721-assert-throws-regex-class.ts"
"tests/corpus/1721-assert-throws-regex-class.ts",
"tests/corpus/2936-static-loose-equality.ts"
]
},
"dynamic": {
@@ -1011,7 +1036,8 @@
"tests/corpus/1600-assert-passing.ts",
"tests/corpus/1604-assert-deep-structures.ts",
"tests/corpus/1609-assert-async.ts",
"tests/corpus/1721-assert-throws-regex-class.ts"
"tests/corpus/1721-assert-throws-regex-class.ts",
"tests/corpus/2936-static-loose-equality.ts"
]
},
"dynamic": {
@@ -82287,7 +82313,8 @@
"tests/corpus/1600-assert-passing.ts",
"tests/corpus/1604-assert-deep-structures.ts",
"tests/corpus/1609-assert-async.ts",
"tests/corpus/1721-assert-throws-regex-class.ts"
"tests/corpus/1721-assert-throws-regex-class.ts",
"tests/corpus/2936-static-loose-equality.ts"
]
},
"dynamic": {
@@ -27,8 +27,8 @@
"apiEntries": 29,
"static": {
"supported": 0,
"partial": 15,
"refused": 6,
"partial": 17,
"refused": 4,
"not-implemented": 8,
"by-design": 0,
"unreviewed": 0,
@@ -1534,7 +1534,8 @@
"tests/corpus/1600-assert-passing.ts",
"tests/corpus/1604-assert-deep-structures.ts",
"tests/corpus/1609-assert-async.ts",
"tests/corpus/1721-assert-throws-regex-class.ts"
"tests/corpus/1721-assert-throws-regex-class.ts",
"tests/corpus/2936-static-loose-equality.ts"
]
},
"dynamic": {
@@ -1843,7 +1844,8 @@
"tests/corpus/1600-assert-passing.ts",
"tests/corpus/1604-assert-deep-structures.ts",
"tests/corpus/1609-assert-async.ts",
"tests/corpus/1721-assert-throws-regex-class.ts"
"tests/corpus/1721-assert-throws-regex-class.ts",
"tests/corpus/2936-static-loose-equality.ts"
]
},
"dynamic": {
@@ -1933,7 +1935,8 @@
"tests/corpus/1600-assert-passing.ts",
"tests/corpus/1604-assert-deep-structures.ts",
"tests/corpus/1609-assert-async.ts",
"tests/corpus/1721-assert-throws-regex-class.ts"
"tests/corpus/1721-assert-throws-regex-class.ts",
"tests/corpus/2936-static-loose-equality.ts"
]
},
"dynamic": {
@@ -1996,7 +1999,8 @@
"tests/corpus/1600-assert-passing.ts",
"tests/corpus/1604-assert-deep-structures.ts",
"tests/corpus/1609-assert-async.ts",
"tests/corpus/1721-assert-throws-regex-class.ts"
"tests/corpus/1721-assert-throws-regex-class.ts",
"tests/corpus/2936-static-loose-equality.ts"
]
},
"dynamic": {
@@ -2032,7 +2036,8 @@
"tests/corpus/1600-assert-passing.ts",
"tests/corpus/1604-assert-deep-structures.ts",
"tests/corpus/1609-assert-async.ts",
"tests/corpus/1721-assert-throws-regex-class.ts"
"tests/corpus/1721-assert-throws-regex-class.ts",
"tests/corpus/2936-static-loose-equality.ts"
]
},
"dynamic": {
@@ -2092,8 +2097,15 @@
"inherited": true
},
"static": {
"status": "refused",
"evidence": "surface-manifest:unsupported:node-builtin.assert.equal"
"status": "partial",
"evidence": "compiler-dedicated:assert.equal",
"tests": [
"tests/corpus/1600-assert-passing.ts",
"tests/corpus/1604-assert-deep-structures.ts",
"tests/corpus/1609-assert-async.ts",
"tests/corpus/1721-assert-throws-regex-class.ts",
"tests/corpus/2936-static-loose-equality.ts"
]
},
"dynamic": {
"status": "partial",
@@ -2128,7 +2140,8 @@
"tests/corpus/1600-assert-passing.ts",
"tests/corpus/1604-assert-deep-structures.ts",
"tests/corpus/1609-assert-async.ts",
"tests/corpus/1721-assert-throws-regex-class.ts"
"tests/corpus/1721-assert-throws-regex-class.ts",
"tests/corpus/2936-static-loose-equality.ts"
]
},
"dynamic": {
@@ -2164,7 +2177,8 @@
"tests/corpus/1600-assert-passing.ts",
"tests/corpus/1604-assert-deep-structures.ts",
"tests/corpus/1609-assert-async.ts",
"tests/corpus/1721-assert-throws-regex-class.ts"
"tests/corpus/1721-assert-throws-regex-class.ts",
"tests/corpus/2936-static-loose-equality.ts"
]
},
"dynamic": {
@@ -2200,7 +2214,8 @@
"tests/corpus/1600-assert-passing.ts",
"tests/corpus/1604-assert-deep-structures.ts",
"tests/corpus/1609-assert-async.ts",
"tests/corpus/1721-assert-throws-regex-class.ts"
"tests/corpus/1721-assert-throws-regex-class.ts",
"tests/corpus/2936-static-loose-equality.ts"
]
},
"dynamic": {
@@ -2236,7 +2251,8 @@
"tests/corpus/1600-assert-passing.ts",
"tests/corpus/1604-assert-deep-structures.ts",
"tests/corpus/1609-assert-async.ts",
"tests/corpus/1721-assert-throws-regex-class.ts"
"tests/corpus/1721-assert-throws-regex-class.ts",
"tests/corpus/2936-static-loose-equality.ts"
]
},
"dynamic": {
@@ -2302,7 +2318,8 @@
"tests/corpus/1600-assert-passing.ts",
"tests/corpus/1604-assert-deep-structures.ts",
"tests/corpus/1609-assert-async.ts",
"tests/corpus/1721-assert-throws-regex-class.ts"
"tests/corpus/1721-assert-throws-regex-class.ts",
"tests/corpus/2936-static-loose-equality.ts"
]
},
"dynamic": {
@@ -2332,8 +2349,15 @@
"inherited": true
},
"static": {
"status": "refused",
"evidence": "surface-manifest:unsupported:node-builtin.assert.notEqual"
"status": "partial",
"evidence": "compiler-dedicated:assert.notEqual",
"tests": [
"tests/corpus/1600-assert-passing.ts",
"tests/corpus/1604-assert-deep-structures.ts",
"tests/corpus/1609-assert-async.ts",
"tests/corpus/1721-assert-throws-regex-class.ts",
"tests/corpus/2936-static-loose-equality.ts"
]
},
"dynamic": {
"status": "partial",
@@ -2368,7 +2392,8 @@
"tests/corpus/1600-assert-passing.ts",
"tests/corpus/1604-assert-deep-structures.ts",
"tests/corpus/1609-assert-async.ts",
"tests/corpus/1721-assert-throws-regex-class.ts"
"tests/corpus/1721-assert-throws-regex-class.ts",
"tests/corpus/2936-static-loose-equality.ts"
]
},
"dynamic": {
@@ -2404,7 +2429,8 @@
"tests/corpus/1600-assert-passing.ts",
"tests/corpus/1604-assert-deep-structures.ts",
"tests/corpus/1609-assert-async.ts",
"tests/corpus/1721-assert-throws-regex-class.ts"
"tests/corpus/1721-assert-throws-regex-class.ts",
"tests/corpus/2936-static-loose-equality.ts"
]
},
"dynamic": {
@@ -2440,7 +2466,8 @@
"tests/corpus/1600-assert-passing.ts",
"tests/corpus/1604-assert-deep-structures.ts",
"tests/corpus/1609-assert-async.ts",
"tests/corpus/1721-assert-throws-regex-class.ts"
"tests/corpus/1721-assert-throws-regex-class.ts",
"tests/corpus/2936-static-loose-equality.ts"
]
},
"dynamic": {
@@ -2476,7 +2503,8 @@
"tests/corpus/1600-assert-passing.ts",
"tests/corpus/1604-assert-deep-structures.ts",
"tests/corpus/1609-assert-async.ts",
"tests/corpus/1721-assert-throws-regex-class.ts"
"tests/corpus/1721-assert-throws-regex-class.ts",
"tests/corpus/2936-static-loose-equality.ts"
]
},
"dynamic": {
@@ -2512,7 +2540,8 @@
"tests/corpus/1600-assert-passing.ts",
"tests/corpus/1604-assert-deep-structures.ts",
"tests/corpus/1609-assert-async.ts",
"tests/corpus/1721-assert-throws-regex-class.ts"
"tests/corpus/1721-assert-throws-regex-class.ts",
"tests/corpus/2936-static-loose-equality.ts"
]
},
"dynamic": {
@@ -30275,7 +30304,8 @@
"tests/corpus/1600-assert-passing.ts",
"tests/corpus/1604-assert-deep-structures.ts",
"tests/corpus/1609-assert-async.ts",
"tests/corpus/1721-assert-throws-regex-class.ts"
"tests/corpus/1721-assert-throws-regex-class.ts",
"tests/corpus/2936-static-loose-equality.ts"
]
},
"dynamic": {
@@ -99219,6 +99249,7 @@
"tests/corpus/1604-assert-deep-structures.ts",
"tests/corpus/1609-assert-async.ts",
"tests/corpus/1721-assert-throws-regex-class.ts",
"tests/corpus/2936-static-loose-equality.ts",
"tests/harness/node-test.test.ts"
]
},
@@ -99876,7 +99907,8 @@
"tests/corpus/1600-assert-passing.ts",
"tests/corpus/1604-assert-deep-structures.ts",
"tests/corpus/1609-assert-async.ts",
"tests/corpus/1721-assert-throws-regex-class.ts"
"tests/corpus/1721-assert-throws-regex-class.ts",
"tests/corpus/2936-static-loose-equality.ts"
]
},
"dynamic": {
+1 -1
View File
@@ -107,7 +107,7 @@
],
"dedicated": [
{ "symbols": ["console.log", "console.info", "console.debug", "console.error", "console.warn"], "status": "partial", "evidence": ["tests/corpus/1460-console-error-warn.ts", "tests/corpus/2440-console-inspect-args.ts"] },
{ "symbols": ["assert", "assert.ok", "assert.strictEqual", "assert.notStrictEqual", "assert.deepStrictEqual", "assert.notDeepStrictEqual", "assert.fail", "assert.match", "assert.doesNotMatch", "assert.throws", "assert.rejects", "assert.doesNotReject", "assert.ifError"], "status": "partial", "evidence": ["tests/corpus/1600-assert-passing.ts", "tests/corpus/1604-assert-deep-structures.ts", "tests/corpus/1609-assert-async.ts", "tests/corpus/1721-assert-throws-regex-class.ts"] },
{ "symbols": ["assert", "assert.ok", "assert.equal", "assert.notEqual", "assert.strictEqual", "assert.notStrictEqual", "assert.deepStrictEqual", "assert.notDeepStrictEqual", "assert.fail", "assert.match", "assert.doesNotMatch", "assert.throws", "assert.rejects", "assert.doesNotReject", "assert.ifError"], "status": "partial", "evidence": ["tests/corpus/1600-assert-passing.ts", "tests/corpus/1604-assert-deep-structures.ts", "tests/corpus/1609-assert-async.ts", "tests/corpus/1721-assert-throws-regex-class.ts", "tests/corpus/2936-static-loose-equality.ts"] },
{ "symbols": ["crypto.createHash", "Hash", "hash.update", "hash.digest", "hash.copy", "crypto.createHmac", "Hmac", "hmac.update", "hmac.digest", "crypto.hash", "crypto.timingSafeEqual", "crypto.randomFillSync", "crypto.randomInt", "crypto.pbkdf2", "crypto.pbkdf2Sync"], "status": "partial", "evidence": ["tests/corpus/2890-crypto-static-utility.ts", "tests/harness/npm-static.test.ts"] },
{ "symbols": ["EventEmitter", "emitter.on", "emitter.addListener", "emitter.once", "emitter.prependListener", "emitter.prependOnceListener", "emitter.off", "emitter.removeListener", "emitter.removeAllListeners", "emitter.emit", "emitter.listenerCount", "emitter.listeners", "emitter.rawListeners", "emitter.eventNames", "emitter.setMaxListeners", "emitter.getMaxListeners", "events.getEventListeners", "events.listenerCount", "events.once", "events.on", "events.setMaxListeners"], "status": "partial", "evidence": ["tests/corpus/1677-emitter-listeners.ts", "tests/corpus/2620-ee-override-once-order.ts"] },
{ "symbols": ["AsyncLocalStorage", "asyncLocalStorage.run", "asyncLocalStorage.exit", "asyncLocalStorage.getStore", "asyncLocalStorage.enterWith", "asyncLocalStorage.disable"], "status": "partial", "evidence": ["tests/corpus/2213-async-local-storage.cjs"] },
+4 -3
View File
@@ -1799,9 +1799,10 @@ declare module "node:util/types" {
* Object.is over scalars; deepStrictEqual compares composites
* structurally per their static types. The messages here are plain
* strings (Node accepts Errors — that form fences per site), and the
* loose-equality quartet (equal/notEqual/deepEqual/notDeepEqual) is
* declared so real code typechecks but fences at its use sites (== has
* no lowering; assert/strict's equal IS strictEqual and lowers). */
* shallow legacy pair (equal/notEqual) follows Node's == coercions for
* primitive and primitive-union operands, including Node v24's NaN
* exception. deepEqual/notDeepEqual remain declared-but-fenced;
* assert/strict's loose names alias the strict forms and lower. */
/* node:test — the in-process test runner (prefix-only in Node too:
* require("test") is MODULE_NOT_FOUND). test/it register tests (sync and
* async bodies, skip/todo/only options and method twins), describe/suite
+4
View File
@@ -5188,6 +5188,8 @@ function emitPrimitiveLibCall(state: LibCallState): Temp {
return finish(`scr_bigint_shr(${arg(0)}, ${arg(1)})`);
case "bigint.eq":
return finish(`scr_bigint_eq(${arg(0)}, ${arg(1)})`);
case "bigint.eqString":
return finish(`scr_bigint_eq_string(${arg(0)}, ${arg(1)})`);
case "bigint.cmp":
return finish(`scr_bigint_cmp_f64(${arg(0)}, ${arg(1)})`);
case "bigint.cmpNumber":
@@ -8361,6 +8363,8 @@ function emitAssertInspectLibCall(state: LibCallState): Temp {
// The quartet over checked-dynamic operands: SameValue /
// dyn-walk deep equality, assertion_error.js messages.
return finish(`scr_assert_eq_dyn(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)})`);
case "assert.looseResult":
return finish(`scr_assert_loose_result(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)})`);
case "assert.deepResult":
return finish(`scr_assert_deep_result(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)})`);
case "assert.deqEnter":
@@ -188,6 +188,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"assert.eqBool": "scr_assert_eq_bool",
"assert.eqSym": "scr_assert_eq_sym",
"assert.eqDyn": "scr_assert_eq_dyn",
"assert.looseResult": "scr_assert_loose_result",
"assert.deepResult": "scr_assert_deep_result",
"assert.sameValue": "scr_assert_same_value_f64",
"assert.deqEnter": "scr_assert_deq_enter",
@@ -423,6 +424,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"bigint.shl": "scr_bigint_shl",
"bigint.shr": "scr_bigint_shr",
"bigint.eq": "scr_bigint_eq",
"bigint.eqString": "scr_bigint_eq_string",
"bigint.cmp": "scr_bigint_cmp_f64",
"bigint.cmpNumber": "scr_bigint_cmp_number",
"bigint.truthy": "scr_bigint_truthy",
@@ -236,7 +236,7 @@ export const UNSUPPORTED: Record<string, UnsupportedEntry> = {
SC1040: {
feature: "loose equality (== and !=)",
milestone: "M4",
hint: "use === / !== ('x == null' / 'x != null' — the null-or-undefined test — is supported; other loose comparisons need dynamic coercion semantics)",
hint: "primitive and primitive-union coercions plus the x == null / x != null idiom compile statically; object-to-primitive comparisons can execute valueOf/toString — convert the object explicitly first",
},
// SC1041 (bitwise operators, ToInt32 semantics) shipped — code retired,
// do not reuse.
@@ -0,0 +1,281 @@
import { InternalCompilerError } from "../../errors.js";
import { BOOL, DYN_HANDLE_KINDS, F64, IrExpr, IrStmt, IrType, SrcLoc, isUnitType, typeEquals } from "../../ir/ir.js";
import { boolLit, numLit, varRef } from "../../ir/build.js";
import type { Lowerer } from "./lowerer.js";
/**
* The engine-free part of ECMAScript Abstract Equality Comparison.
*
* Every primitive pair is exact, including StringToNumber,
* StringToBigInt, Boolean-to-number recursion, and BigInt/Number's
* mathematical comparison. Object-to-primitive coercion stays outside
* this helper: user-defined valueOf/toString methods can execute arbitrary
* JavaScript, so a static build must keep that form fenced. Object pairs
* with one exact representation compare by identity, as == and === agree
* there.
*/
const IDENTITY_KINDS: ReadonlySet<IrType["kind"]> = new Set([
"array",
"bytes",
"map",
"set",
"object",
"record",
"promise",
"symbol",
"classval",
"moduleNs",
]);
const PRIMITIVE_KINDS: ReadonlySet<IrType["kind"]> = new Set([
"f64",
"string",
"bool",
"bigint",
"symbol",
]);
function supportsPair(lowerer: Lowerer, left: IrType, right: IrType): boolean {
if (left.kind === "union") {
const def = lowerer.unions.get(left.unionId);
return def !== undefined && def.arms.every((arm) => supportsPair(lowerer, arm, right));
}
if (right.kind === "union") {
const def = lowerer.unions.get(right.unionId);
return def !== undefined && def.arms.every((arm) => supportsPair(lowerer, left, arm));
}
if (isUnitType(left) || isUnitType(right)) {
const other = isUnitType(left) ? right : left;
return other.kind !== "dyn" && other.kind !== "jsval" && other.kind !== "caught" && other.kind !== "void";
}
if (PRIMITIVE_KINDS.has(left.kind) && PRIMITIVE_KINDS.has(right.kind)) return true;
if (left.kind === "func" && right.kind === "func") return true;
if (IDENTITY_KINDS.has(left.kind) && typeEquals(left, right)) return true;
if (DYN_HANDLE_KINDS.has(left.kind) && typeEquals(left, right)) return true;
if (left.kind === "object" && right.kind === "object") {
return lowerer.isSubclassOf(left.className, right.className) || lowerer.isSubclassOf(right.className, left.className);
}
return false;
}
function boolAsNumber(value: IrExpr, loc: SrcLoc): IrExpr {
return {
kind: "ternary",
cond: value,
then: numLit(1, loc),
else_: numLit(0, loc),
type: F64,
loc,
};
}
function numericEq(left: IrExpr, right: IrExpr, nanEqualsNan: boolean, loc: SrcLoc): IrExpr {
const ordinary: IrExpr = { kind: "bin", op: "===", left, right, type: BOOL, loc };
if (!nanEqualsNan) return ordinary;
return {
kind: "logical",
op: "||",
left: ordinary,
right: {
kind: "logical",
op: "&&",
left: { kind: "libCall", fn: "num.isNaN", args: [left], type: BOOL, loc },
right: { kind: "libCall", fn: "num.isNaN", args: [right], type: BOOL, loc },
type: BOOL,
loc,
},
type: BOOL,
loc,
};
}
function primitivePair(
lowerer: Lowerer,
left: IrExpr,
right: IrExpr,
nanEqualsNan: boolean,
loc: SrcLoc,
): IrExpr {
if (isUnitType(left.type) || isUnitType(right.type)) {
return boolLit(isUnitType(left.type) && isUnitType(right.type), loc);
}
if (left.type.kind === "object" && right.type.kind === "object") {
if (typeEquals(left.type, right.type)) {
return { kind: "bin", op: "===", left, right, type: BOOL, loc };
}
if (lowerer.isSubclassOf(left.type.className, right.type.className)) {
return { kind: "bin", op: "===", left: lowerer.upcastTo(left, right.type.className), right, type: BOOL, loc };
}
if (lowerer.isSubclassOf(right.type.className, left.type.className)) {
return { kind: "bin", op: "===", left, right: lowerer.upcastTo(right, left.type.className), type: BOOL, loc };
}
}
if (left.type.kind === right.type.kind) {
switch (left.type.kind) {
case "f64":
return numericEq(left, right, nanEqualsNan, loc);
case "string":
return { kind: "strEq", negated: false, left, right, type: BOOL, loc };
case "bool":
return { kind: "bin", op: "===", left, right, type: BOOL, loc };
case "bigint":
return { kind: "libCall", fn: "bigint.eq", args: [left, right], type: BOOL, loc };
case "symbol":
case "array":
case "bytes":
case "map":
case "set":
case "record":
case "promise":
case "classval":
case "moduleNs":
return { kind: "bin", op: "===", left, right, type: BOOL, loc };
default:
break;
}
}
if (left.type.kind === "func" && right.type.kind === "func") {
return { kind: "bin", op: "===", left, right, type: BOOL, loc };
}
if (DYN_HANDLE_KINDS.has(left.type.kind) && typeEquals(left.type, right.type)) {
return { kind: "bin", op: "===", left, right, type: BOOL, loc };
}
// Boolean first recurses as ToNumber(boolean), per ECMA-262 7.2.13.
if (left.type.kind === "bool") return primitivePair(lowerer, boolAsNumber(left, loc), right, nanEqualsNan, loc);
if (right.type.kind === "bool") return primitivePair(lowerer, left, boolAsNumber(right, loc), nanEqualsNan, loc);
// Symbols never equal a different primitive type. Object/symbol would
// require ToPrimitive and was rejected by supportsPair.
if (left.type.kind === "symbol" || right.type.kind === "symbol") return boolLit(false, loc);
if (left.type.kind === "f64" && right.type.kind === "string") {
const parsed: IrExpr = { kind: "libCall", fn: "num.fromString", args: [right], type: F64, loc };
return numericEq(left, parsed, false, loc);
}
if (left.type.kind === "string" && right.type.kind === "f64") {
const parsed: IrExpr = { kind: "libCall", fn: "num.fromString", args: [left], type: F64, loc };
return numericEq(parsed, right, false, loc);
}
if (left.type.kind === "bigint" && right.type.kind === "string") {
return { kind: "libCall", fn: "bigint.eqString", args: [left, right], type: BOOL, loc };
}
if (left.type.kind === "string" && right.type.kind === "bigint") {
return { kind: "libCall", fn: "bigint.eqString", args: [right, left], type: BOOL, loc };
}
if (left.type.kind === "bigint" && right.type.kind === "f64") {
const cmp: IrExpr = { kind: "libCall", fn: "bigint.cmpNumber", args: [left, right], type: F64, loc };
return { kind: "bin", op: "===", left: cmp, right: numLit(0, loc), type: BOOL, loc };
}
if (left.type.kind === "f64" && right.type.kind === "bigint") {
const cmp: IrExpr = { kind: "libCall", fn: "bigint.cmpNumber", args: [right, left], type: F64, loc };
return { kind: "bin", op: "===", left: cmp, right: numLit(0, loc), type: BOOL, loc };
}
throw new InternalCompilerError(
`abstract equality reached an unplanned pair ${left.type.kind}/${right.type.kind}`,
);
}
function pairExpr(
lowerer: Lowerer,
left: IrExpr,
right: IrExpr,
nanEqualsNan: boolean,
loc: SrcLoc,
): IrExpr {
if (left.type.kind === "union") {
const unionId = left.type.unionId;
const def = lowerer.unions.get(unionId);
if (!def || def.arms.length === 0) throw new InternalCompilerError(`abstract equality over unknown union ${unionId}`);
const branch = (tag: number): IrExpr => {
const arm = def.arms[tag];
if (!arm) throw new InternalCompilerError(`abstract equality union ${unionId} lacks tag ${tag}`);
return pairExpr(lowerer, { kind: "unionNarrow", unionId, tag, value: left, type: arm, loc }, right, nanEqualsNan, loc);
};
let out = branch(def.arms.length - 1);
for (let tag = def.arms.length - 2; tag >= 0; tag--) {
out = {
kind: "ternary",
cond: { kind: "unionIsTag", unionId, tag, negated: false, value: left, type: BOOL, loc },
then: branch(tag),
else_: out,
type: BOOL,
loc,
};
}
return out;
}
if (right.type.kind === "union") {
const unionId = right.type.unionId;
const def = lowerer.unions.get(unionId);
if (!def || def.arms.length === 0) throw new InternalCompilerError(`abstract equality over unknown union ${unionId}`);
const branch = (tag: number): IrExpr => {
const arm = def.arms[tag];
if (!arm) throw new InternalCompilerError(`abstract equality union ${unionId} lacks tag ${tag}`);
return pairExpr(lowerer, left, { kind: "unionNarrow", unionId, tag, value: right, type: arm, loc }, nanEqualsNan, loc);
};
let out = branch(def.arms.length - 1);
for (let tag = def.arms.length - 2; tag >= 0; tag--) {
out = {
kind: "ternary",
cond: { kind: "unionIsTag", unionId, tag, negated: false, value: right, type: BOOL, loc },
then: branch(tag),
else_: out,
type: BOOL,
loc,
};
}
return out;
}
return primitivePair(lowerer, left, right, nanEqualsNan, loc);
}
export function abstractEqualitySupported(lowerer: Lowerer, left: IrType, right: IrType): boolean {
return supportsPair(lowerer, left, right);
}
export function abstractEqualityExpr(
lowerer: Lowerer,
left: IrExpr,
right: IrExpr,
loc: SrcLoc,
nanEqualsNan = false,
): IrExpr {
if (!supportsPair(lowerer, left.type, right.type)) {
throw new InternalCompilerError(`abstract equality expression built for unsupported ${left.type.kind}/${right.type.kind} pair`);
}
return pairExpr(lowerer, left, right, nanEqualsNan, loc);
}
/** Evaluate both operands once, left-to-right, then compare their stable values. */
export function lowerAbstractEquality(
lowerer: Lowerer,
left: IrExpr,
right: IrExpr,
negated: boolean,
loc: SrcLoc,
): IrExpr | null {
if (!supportsPair(lowerer, left.type, right.type)) return null;
const stmts: IrStmt[] = [];
const stable = (value: IrExpr, name: string): IrExpr => {
if (isUnitType(value.type)) {
if (value.kind !== "unitLit") stmts.push({ kind: "exprStmt", expr: value, loc });
return { kind: "unitLit", unit: value.type.kind === "nullT" ? "null" : "undefined", type: value.type, loc };
}
const local = lowerer.declareHiddenLocal(name, value.type);
stmts.push({ kind: "varDecl", localId: local.id, init: value, loc });
return varRef(local.id, value.type, loc);
};
const a = stable(left, "%looseA");
const b = stable(right, "%looseB");
let result = pairExpr(lowerer, a, b, false, loc);
if (negated) result = { kind: "unary", op: "!", operand: result, type: BOOL, loc };
return stmts.length === 0 ? result : { kind: "seqExpr", stmts, result, type: BOOL, loc };
}
@@ -1,7 +1,8 @@
import { InternalCompilerError } from "../../errors.js";
/* The node:assert lowering (a spoke module like lower-dgram.ts): the
* callable module form (`assert(x)` through a default import or a CJS
* require binding), ok, the strict/deep equality quartet, fail, throws,
* require binding), ok, legacy equal/notEqual, the strict/deep equality
* quartet, fail, throws,
* and match/doesNotMatch — for BOTH the "assert" and "assert/strict"
* modules ("assert/strict" additionally binds the loose NAMES to the
* strict comparisons: equal IS strictEqual, exactly Node's aliasing).
@@ -27,7 +28,8 @@ import { resultIsDiscarded } from "./call-position.js";
import type { Lowerer } from "./lowerer.js";
import { jsFuncNameOf, own } from "./lowerer.js";
import { NARROW_FIRST } from "./surfaces.js";
import { typeReachesItself } from "./lower-inspect.js";
import { inspectExpr, typeReachesItself } from "./lower-inspect.js";
import { abstractEqualityExpr, abstractEqualitySupported } from "./abstract-equality.js";
import { BOOL, CAUGHT, DYN, DYN_HANDLE_KINDS, F64, IrExpr, IrLibFn, IrStmt, IrType, REGEX, RUNTIME_ERROR_CLASSES, STRING, SrcLoc, VOID, isUnitType, typeEquals, typeKey } from "../../ir/ir.js";
import { boolLit, countedFor, numLit, strLit, varRef } from "../../ir/build.js";
@@ -96,6 +98,10 @@ export function lowerAssertModuleCall(
switch (canonicalAssertMember(bi.module, bi.member)) {
case "ok":
return lowerAssertOk(lowerer, expr, loc);
case "equal":
return lowerAssertLooseEqual(lowerer, expr, loc, false);
case "notEqual":
return lowerAssertLooseEqual(lowerer, expr, loc, true);
case "strictEqual":
return lowerAssertEqual(lowerer, expr, loc, false, false);
case "notStrictEqual":
@@ -123,6 +129,82 @@ export function lowerAssertModuleCall(
}
}
function looseAssertTypeSupported(lowerer: Lowerer, type: IrType): boolean {
if (
type.kind === "f64" || type.kind === "bigint" || type.kind === "string" ||
type.kind === "bool" || type.kind === "symbol" || isUnitType(type)
) {
return true;
}
if (type.kind !== "union") return false;
const def = lowerer.unions.get(type.unionId);
return def !== undefined && def.arms.every((arm) => looseAssertTypeSupported(lowerer, arm));
}
/** assert.equal / assert.notEqual over statically represented primitives.
* The shared Abstract Equality kernel owns coercion; this wrapper adds
* Node v24's legacy NaN-equals-NaN exception and the exact one-line
* generated message. Both values stabilize first so comparison and
* inspection never re-evaluate user expressions. */
function lowerAssertLooseEqual(
lowerer: Lowerer,
expr: ts.CallExpression,
loc: SrcLoc,
negated: boolean,
): IrExpr {
const surface = negated ? "assert.notEqual" : "assert.equal";
requireStatementPosition(lowerer, expr, surface);
if (expr.arguments.length < 2 || expr.arguments.length > 3) {
lowerer.noLowering(`${surface} with ${expr.arguments.length} arguments`, expr);
}
const aNode = expr.arguments[0];
const bNode = expr.arguments[1];
if (!aNode || !bNode) throw new InternalCompilerError(`${surface} arity fence returned unexpectedly`);
const left = lowerer.lowerExpr(aNode);
const right = lowerer.lowerExpr(bNode);
if (
!looseAssertTypeSupported(lowerer, left.type) ||
!looseAssertTypeSupported(lowerer, right.type) ||
!abstractEqualitySupported(lowerer, left.type, right.type)
) {
lowerer.noLowering(
`${surface} of '${lowerer.fmt(left.type)}' and '${lowerer.fmt(right.type)}' values`,
expr,
"legacy equality lowers for primitive and primitive-union operands; object-to-primitive coercion can execute user-defined valueOf/toString methods",
);
}
const stmts: IrStmt[] = [];
const stable = (value: IrExpr, name: string): IrExpr => {
if (isUnitType(value.type)) {
if (value.kind !== "unitLit") stmts.push({ kind: "exprStmt", expr: value, loc });
return {
kind: "unitLit",
unit: value.type.kind === "nullT" ? "null" : "undefined",
type: value.type,
loc,
};
}
const local = lowerer.declareHiddenLocal(name, value.type);
stmts.push({ kind: "varDecl", localId: local.id, init: value, loc });
return varRef(local.id, value.type, loc);
};
const a = stable(left, "%assertLooseA");
const b = stable(right, "%assertLooseB");
const equal = abstractEqualityExpr(lowerer, a, b, loc, true);
const actual = inspectExpr(lowerer, a.type, a, numLit(0, loc), numLit(2, loc), loc);
const expected = inspectExpr(lowerer, b.type, b, numLit(0, loc), numLit(2, loc), loc);
const { msg, hasMsg } = lowerMessageArg(lowerer, expr.arguments[2], loc);
const result: IrExpr = {
kind: "libCall",
fn: "assert.looseResult",
args: [equal, boolLit(negated, loc), actual, expected, msg, hasMsg],
type: VOID,
loc,
};
return { kind: "seqExpr", stmts, result, type: VOID, loc };
}
/** The callable-module form: `assert(x)` where the callee identifier IS
* the assert module binding — a default import (`import assert from
* "node:assert"`) or the CJS `const assert = require("assert")`. Node's
@@ -35,6 +35,7 @@ import { hasOptionalChainGuard, isOptionalChainTail, isRequireMainFilename } fro
import { conditionalSpreadOf, foldedStringKeyOf } from "./expressions/object-literals.js";
import { tryLowerExpression } from "./expressions/try-lower-expression.js";
import { fenceNodeModuleMutation, isNodeModuleValue, lowerNodeModuleIdentifier, lowerNodeModuleProperty, lowerRequireCacheElement, lowerRequireCacheHas, lowerRequireMainProperty } from "./lower-node-module.js";
import { lowerAbstractEquality } from "./abstract-equality.js";
/** An assignable `obj.field` target — a class field, a record field, or a
* class ACCESSOR property (reads become getter calls, writes setter calls;
@@ -5966,25 +5967,20 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
if (op === ts.SyntaxKind.EqualsEqualsToken || op === ts.SyntaxKind.ExclamationEqualsToken) {
const nullTest = lowerLooseNullCompare(lowerer, expr, loc);
if (nullTest) return nullTest;
// SAME-KIND loose equality IS strict equality (the spec's ==
// dispatches to === when both operands share a type): `typeof v ==
// 'object'`, `n != 0`, `flag == true` all lower exactly. Mixed
// kinds (where == coerces) keep the fence.
{
const negated = op === ts.SyntaxKind.ExclamationEqualsToken;
const left = lowerer.lowerExpr(expr.left);
const right = lowerer.lowerExpr(expr.right);
if (left.type.kind === "string" && right.type.kind === "string") {
return { kind: "strEq", negated, left, right, type: BOOL, loc };
}
if (
(left.type.kind === "f64" && right.type.kind === "f64") ||
(left.type.kind === "bool" && right.type.kind === "bool")
) {
return { kind: "bin", op: negated ? "!==" : "===", left, right, type: BOOL, loc };
}
}
lowerer.unsupported("SC1040", expr);
const loose = lowerAbstractEquality(
lowerer,
lowerer.lowerExpr(expr.left),
lowerer.lowerExpr(expr.right),
op === ts.SyntaxKind.ExclamationEqualsToken,
loc,
);
if (loose) return loose;
lowerer.unsupported(
"SC1040",
expr,
undefined,
"object-to-primitive loose equality can call user-defined valueOf/toString methods — compare explicit primitive conversions instead",
);
}
if (op === ts.SyntaxKind.AmpersandAmpersandToken || op === ts.SyntaxKind.BarBarToken) {
if (op === ts.SyntaxKind.BarBarToken) {
@@ -246,7 +246,7 @@ function isNumberFlag(lowerer: Lowerer, t: IrType, v: () => IrExpr, loc: SrcLoc)
/** The rendering of one value of type `t` at runtime depth `recurse`
* with the depth budget `depth` — a direct scalar libCall or a call of
* the interned per-type helper. */
function inspectExpr(
export function inspectExpr(
lowerer: Lowerer,
t: IrType,
value: IrExpr,
@@ -1301,18 +1301,15 @@ export function builtinModulesArrayLit(loc: { file: string; start: number; end:
const ZLIB_HINT =
"the default-options deflate/inflate, raw, gzip/gunzip, and unzip sync/callback forms plus crc32 are the lowered zlib surface";
/** The loose-equality quartet's shared hint: == coercion has no lowering
* anywhere in this compiler, and Node itself points at the strict forms. */
const ASSERT_LOOSE_HINT =
"loose == equality has no lowering — the strict forms compare with " +
"Object.is/structural equality like Node's assert/strict module, where " +
"equal IS strictEqual";
/** The legacy deep pair remains fenced. The shallow equal/notEqual pair
* lowers through the static Abstract Equality kernel in lower-assert. */
const ASSERT_LOOSE_DEEP_HINT =
"legacy deep equality has no lowering yet — deepStrictEqual and " +
"notDeepStrictEqual compare structures with Node's modern semantics";
const ASSERT_MODULE_HINTS: Record<string, string | undefined> = {
equal: ASSERT_LOOSE_HINT,
notEqual: ASSERT_LOOSE_HINT,
deepEqual: ASSERT_LOOSE_HINT,
notDeepEqual: ASSERT_LOOSE_HINT,
deepEqual: ASSERT_LOOSE_DEEP_HINT,
notDeepEqual: ASSERT_LOOSE_DEEP_HINT,
ifError:
"test explicitly instead: assert.strictEqual(err, null) / " +
"assert.strictEqual(err, undefined)",
+3
View File
@@ -2369,6 +2369,7 @@ export type IrLibFn =
| "bigint.shl"
| "bigint.shr"
| "bigint.eq"
| "bigint.eqString"
| "bigint.cmp"
| "bigint.cmpNumber"
| "bigint.truthy"
@@ -3957,6 +3958,7 @@ export type IrLibFn =
| "assert.eqBool"
| "assert.eqSym"
| "assert.eqDyn"
| "assert.looseResult"
| "assert.deepResult"
| "assert.sameValue"
/* deepStrictEqual's pair memo over cycle-capable types: enter answers
@@ -7601,6 +7603,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"assert.eqBool",
"assert.eqSym",
"assert.eqDyn",
"assert.looseResult",
"assert.deepResult",
"assert.match",
"assert.refEqBytes",
+2
View File
@@ -275,6 +275,7 @@ export const LIB_FN_SIGS: Record<IrLibFn, { argTypes: (IrType | null)[]; result:
"bigint.shl": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T },
"bigint.shr": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T },
"bigint.eq": { argTypes: [BIGINT_T, BIGINT_T], result: BOOL },
"bigint.eqString": { argTypes: [BIGINT_T, STRING], result: BOOL },
"bigint.cmp": { argTypes: [BIGINT_T, BIGINT_T], result: F64 },
"bigint.cmpNumber": { argTypes: [BIGINT_T, F64], result: F64 },
"bigint.truthy": { argTypes: [BIGINT_T], result: BOOL },
@@ -1079,6 +1080,7 @@ export const LIB_FN_SIGS: Record<IrLibFn, { argTypes: (IrType | null)[]; result:
// The equality quartet over checked-dynamic operands (the frontend
// boxes a static side into the checked-dynamic tree first).
"assert.eqDyn": { argTypes: [DYN, DYN, BOOL, BOOL, STRING, BOOL], result: VOID },
"assert.looseResult": { argTypes: [BOOL, BOOL, STRING, STRING, STRING, BOOL], result: VOID },
// The throws(fn, {shape}) accumulator: begin/slot calls never throw;
// shapeEnd throws the Comparison diff. The error slot is the
// %Error-narrowed caught value.
+3 -19
View File
@@ -88,7 +88,7 @@
"name": "loose equality (== and !=)",
"status": "unsupported",
"code": "SC1040",
"note": "use === / !== ('x == null' / 'x != null' — the null-or-undefined test — is supported; other loose comparisons need dynamic coercion semantics)"
"note": "primitive and primitive-union coercions plus the x == null / x != null idiom compile statically; object-to-primitive comparisons can execute valueOf/toString — convert the object explicitly first"
},
{
"id": "diagnostic.sc1042",
@@ -329,7 +329,7 @@
"name": "assert.deepEqual",
"status": "unsupported",
"code": "SC2020",
"note": "loose == equality has no lowering — the strict forms compare with Object.is/structural equality like Node's assert/strict module, where equal IS strictEqual"
"note": "legacy deep equality has no lowering yet — deepStrictEqual and notDeepStrictEqual compare structures with Node's modern semantics"
},
{
"id": "node-builtin.assert.doesNotReject",
@@ -347,14 +347,6 @@
"code": "SC2020",
"note": "call the function directly — an unexpected throw already fails the test"
},
{
"id": "node-builtin.assert.equal",
"kind": "node-builtin",
"name": "assert.equal",
"status": "unsupported",
"code": "SC2020",
"note": "loose == equality has no lowering — the strict forms compare with Object.is/structural equality like Node's assert/strict module, where equal IS strictEqual"
},
{
"id": "node-builtin.assert.ifError",
"kind": "node-builtin",
@@ -369,15 +361,7 @@
"name": "assert.notDeepEqual",
"status": "unsupported",
"code": "SC2020",
"note": "loose == equality has no lowering — the strict forms compare with Object.is/structural equality like Node's assert/strict module, where equal IS strictEqual"
},
{
"id": "node-builtin.assert.notEqual",
"kind": "node-builtin",
"name": "assert.notEqual",
"status": "unsupported",
"code": "SC2020",
"note": "loose == equality has no lowering — the strict forms compare with Object.is/structural equality like Node's assert/strict module, where equal IS strictEqual"
"note": "legacy deep equality has no lowering yet — deepStrictEqual and notDeepStrictEqual compare structures with Node's modern semantics"
},
{
"id": "node-builtin.assert.rejects",
@@ -6426,6 +6426,12 @@
],
"diags": []
},
"<repo>/tests/corpus/2936-static-loose-equality.ts": {
"order": [
"<repo>/tests/corpus/2936-static-loose-equality.ts"
],
"diags": []
},
"<repo>/tests/corpus/300-if-else.ts": {
"order": [
"<repo>/tests/corpus/300-if-else.ts"
+19
View File
@@ -317,6 +317,25 @@ void scr_assert_eq_bool(bool a, bool b, bool negated, bool deep,
has_msg);
}
/* assert.equal / assert.notEqual: the frontend supplies the completed
* Abstract Equality verdict (including Node v24's NaN-equals-NaN legacy
* exception) and util.inspect renderings of the original operands. Node's
* generated legacy message is one line: "actual == expected" / "actual !=
* expected". A supplied message, including the empty string, replaces it. */
void scr_assert_loose_result(bool equal, bool negated, ScrStr *actual,
ScrStr *expected, ScrStr *msg, bool has_msg) {
if ((negated && !equal) || (!negated && equal)) return;
if (has_msg) {
scr_assert_fail_msg(scr_str_retain(msg));
return;
}
ScrAssertBuf b = {0};
ab_str(&b, actual);
ab_cstr(&b, negated ? " != " : " == ");
ab_str(&b, expected);
scr_assert_fail_msg(ab_take(&b));
}
/* deepStrictEqual / notDeepStrictEqual over COMPOSITE values: the frontend
* synthesized the honest structural comparison; this only turns its verdict
* into Node's throw. The generated message is the header line alone —
+23 -6
View File
@@ -510,7 +510,11 @@ static ScrBigInt *bi_parse_fail(const ScrStr *s) {
return NULL;
}
ScrBigInt *scr_bigint_parse(ScrStr *s) {
static ScrBigInt *bi_parse_invalid(const ScrStr *s, bool report_error) {
return report_error ? bi_parse_fail(s) : NULL;
}
static ScrBigInt *bi_parse_string(ScrStr *s, bool report_error) {
size_t lo = 0, hi = s->len;
size_t width;
while (lo < hi && (width = bi_space_prefix(s->data + lo, hi - lo)) != 0) lo += width;
@@ -521,7 +525,7 @@ ScrBigInt *scr_bigint_parse(ScrStr *s) {
if (s->data[lo] == '+' || s->data[lo] == '-') {
signed_input = true;
if (s->data[lo++] == '-') sign = -1;
if (lo == hi) return bi_parse_fail(s);
if (lo == hi) return bi_parse_invalid(s, report_error);
}
unsigned base = 10;
if (hi - lo >= 2 && s->data[lo] == '0') {
@@ -530,9 +534,9 @@ ScrBigInt *scr_bigint_parse(ScrStr *s) {
else if (p == 'o' || p == 'O') base = 8;
else if (p == 'b' || p == 'B') base = 2;
if (base != 10) {
if (signed_input) return bi_parse_fail(s);
if (signed_input) return bi_parse_invalid(s, report_error);
lo += 2;
if (lo == hi) return bi_parse_fail(s);
if (lo == hi) return bi_parse_invalid(s, report_error);
}
}
ScrBigInt *v = bi_zero();
@@ -541,16 +545,29 @@ ScrBigInt *scr_bigint_parse(ScrStr *s) {
int d = bi_digit((unsigned char)s->data[lo]);
if (d < 0 || (unsigned)d >= base) {
scr_bigint_release(v);
return bi_parse_fail(s);
return bi_parse_invalid(s, report_error);
}
bi_mul_small(&v, base, (uint32_t)d);
any = true;
}
if (!any) { scr_bigint_release(v); return bi_parse_fail(s); }
if (!any) { scr_bigint_release(v); return bi_parse_invalid(s, report_error); }
if (v->sign) v->sign = sign;
return v;
}
ScrBigInt *scr_bigint_parse(ScrStr *s) { return bi_parse_string(s, true); }
/* Abstract Equality's String/BigInt arm uses StringToBigInt without
* surfacing its parse failure: an invalid string compares false instead
* of throwing. BigInt(string) keeps the public throwing parser above. */
bool scr_bigint_eq_string(ScrBigInt *a, ScrStr *b) {
ScrBigInt *parsed = bi_parse_string(b, false);
if (!parsed) return false;
bool equal = scr_bigint_eq(a, parsed);
scr_bigint_release(parsed);
return equal;
}
ScrBigInt *scr_bigint_from_f64(double value) {
if (!isfinite(value) || trunc(value) != value) {
char num[64], msg[192];
+5
View File
@@ -3018,6 +3018,7 @@ ScrBigInt *scr_bigint_xor(ScrBigInt *a, ScrBigInt *b);
ScrBigInt *scr_bigint_shl(ScrBigInt *a, ScrBigInt *count);
ScrBigInt *scr_bigint_shr(ScrBigInt *a, ScrBigInt *count);
bool scr_bigint_eq(ScrBigInt *a, ScrBigInt *b);
bool scr_bigint_eq_string(ScrBigInt *a, ScrStr *b); /* StringToBigInt failure is false */
double scr_bigint_cmp_f64(ScrBigInt *a, ScrBigInt *b); /* -1, 0, 1 */
double scr_bigint_cmp_number(ScrBigInt *a, double b); /* -1, 0, 1; 2 = NaN */
bool scr_bigint_truthy(ScrBigInt *value);
@@ -6594,6 +6595,10 @@ void scr_assert_neq_fail(const char *insp, size_t ilen, bool deep,
* the divergences). Borrows everything; throws on the failing verdict. */
void scr_assert_eq_dyn(ScrDyn *a, ScrDyn *b, bool negated, bool deep,
ScrStr *msg, bool has_msg);
/* assert.equal/notEqual over the frontend's Abstract Equality verdict;
* actual/expected are borrowed util.inspect renderings. */
void scr_assert_loose_result(bool equal, bool negated, ScrStr *actual,
ScrStr *expected, ScrStr *msg, bool has_msg);
/* assert.throws / assert.rejects whose callback returned (or whose
* promise fulfilled): "Missing expected exception|rejection" with Node's
* details — ` (${expected.name})` when the expected class/shape carries a
@@ -0,0 +1,85 @@
// Static Abstract Equality Comparison: primitive coercions, union-arm
// dispatch, evaluate-once ordering, and node:assert's legacy shallow pair.
import assert, { equal, notEqual } from "node:assert";
type Primitive = string | number | boolean | bigint | null | undefined;
function eq(a: Primitive, b: Primitive): boolean {
return a == b;
}
function neq(a: Primitive, b: Primitive): boolean {
return a != b;
}
console.log(eq("1", 1), eq("", 0), eq(" \t", 0), eq("0x10", 16), eq("no", 0));
console.log(eq(false, 0), eq(true, 1), eq(true, 2), eq(false, ""), eq(true, "1"));
console.log(eq(1n, 1), eq(1n, "01"), eq(1n, "1.0"), eq(0n, ""), eq(1n, true));
console.log(eq(9007199254740992n, 9007199254740992), eq(9007199254740993n, 9007199254740992));
console.log(eq(null, undefined), eq(null, 0), eq(undefined, false));
console.log(eq(0 / 0, 0 / 0), eq(0, -0), neq("2", 2), neq("2", 3));
type SymbolOrString = symbol | string;
function symbolEq(a: SymbolOrString, b: SymbolOrString): boolean {
return a == b;
}
const shared = Symbol.for("shared");
console.log(symbolEq(shared, shared), symbolEq(Symbol("x"), Symbol("x")), symbolEq(shared, "shared"));
const sameArray = [1, 2];
function arrayEq(a: number[], b: number[]): boolean {
return a == b;
}
console.log(arrayEq(sameArray, sameArray), arrayEq(sameArray, [1, 2]));
let order = "";
let leftCalls = 0;
let rightCalls = 0;
function left(): Primitive {
order += "L";
leftCalls++;
return "1";
}
function right(): Primitive {
order += "R";
rightCalls++;
return 1;
}
console.log(left() == right(), order, leftCalls, rightCalls);
assert.equal("1", 1);
assert.equal("", 0);
assert.equal(false, 0);
assert.equal(1n, "01");
assert.equal(0n, "");
assert.equal(null, undefined);
assert.equal(0 / 0, 0 / 0); // Node's legacy NaN exception.
assert.equal(0, -0);
assert.notEqual("2", 3);
equal(true, 1);
notEqual(2n, "3");
function unionAssert(a: Primitive, b: Primitive): void {
assert.equal(a, b);
}
unionAssert(1n, "01");
unionAssert(false, "");
function messageOf(fn: () => void): string {
try {
fn();
return "DID NOT THROW";
} catch (error) {
return error instanceof Error ? `${error.name}:${error.message}` : "not an Error";
}
}
console.log(JSON.stringify(messageOf(() => assert.equal("1", 2))));
console.log(JSON.stringify(messageOf(() => assert.notEqual("1", 1))));
console.log(JSON.stringify(messageOf(() => assert.notEqual(0 / 0, 0 / 0))));
console.log(JSON.stringify(messageOf(() => assert.equal(1n, "2"))));
console.log(JSON.stringify(messageOf(() => assert.equal(null, 0))));
console.log(JSON.stringify(messageOf(() => assert.equal(Symbol("x"), Symbol("y")))));
console.log(JSON.stringify(messageOf(() => assert.equal(1, 2, "custom"))));
console.log(JSON.stringify(messageOf(() => assert.equal(1, 2, ""))));
console.log("done");
+1 -1
View File
@@ -10,5 +10,5 @@ const up = (19.99).toPrecision(3);
const parsed = Number.parseFloat("1.5"); // the global's string form is static now; the Number static keeps the island
const raw = __island_eval("6 * 7");
const unknownScore: unknown = 81;
const flags = unknownScore == 81; // mixed-kind loose equality keeps the fence (same-kind == lowers)
const flags = unknownScore == 81; // unknown/object coercion keeps the fence; static primitive pairs lower
console.log("done");
+1 -1
View File
@@ -9,7 +9,7 @@ const items = [1, 2, 3];
const handler = (x: number) => x * 2;
const anyone: unknown = score(1);
const flags = anyone == score(2); // mixed-kind loose equality keeps the fence (same-kind == lowers)
const flags = anyone == score(2); // unknown/object coercion keeps the fence; static primitive pairs lower
const { length: firstItem } = items; // object patterns over arrays keep the fence (patterns and defaults compile)
let label = "score: ";
label += score(10);
+4 -4
View File
@@ -1,6 +1,6 @@
// The assert surface's honest fences: the loose == quartet (assert's own
// equal/deepEqual — assert/strict's same-named members ARE the strict
// comparisons and lower), reference-equality strictEqual on objects,
// The assert surface's honest fences: legacy deepEqual/notDeepEqual
// (shallow equal/notEqual and assert/strict's same-named aliases lower),
// reference-equality strictEqual on objects,
// union operands (narrow first), mismatched or uncomparable deep types
// (typed arrays, class instances), namespace-object calls, unsupported
// throws/rejects expectations (properties outside the static error
@@ -11,7 +11,7 @@
import assert from "node:assert";
import * as assertNs from "node:assert";
assert.equal(1, 1); // loose == — points at the strict forms
assert.equal(1, 1); // shallow legacy equality lowers for primitives
assert.notEqual(1, 2);
assert.deepEqual([1], [1]);
assert.notDeepEqual([1], [2]);
@@ -1,21 +1,3 @@
assert-fences.ts:14:1 - error SC2020: 'assert.equal' is part of the standard library types but has no scriptc lowering yet
13 |
14 | assert.equal(1, 1); // loose == — points at the strict forms
| ^~~~~~~~~~~~~~~~~~
15 | assert.notEqual(1, 2);
hint: loose == equality has no lowering — the strict forms compare with Object.is/structural equality like Node's assert/strict module, where equal IS strictEqual
assert-fences.ts:15:1 - error SC2020: 'assert.notEqual' is part of the standard library types but has no scriptc lowering yet
14 | assert.equal(1, 1); // loose == — points at the strict forms
15 | assert.notEqual(1, 2);
| ^~~~~~~~~~~~~~~~~~~~~
16 | assert.deepEqual([1], [1]);
hint: loose == equality has no lowering — the strict forms compare with Object.is/structural equality like Node's assert/strict module, where equal IS strictEqual
assert-fences.ts:16:1 - error SC2020: 'assert.deepEqual' is part of the standard library types but has no scriptc lowering yet
15 | assert.notEqual(1, 2);
@@ -23,7 +5,7 @@ assert-fences.ts:16:1 - error SC2020: 'assert.deepEqual' is part of the standard
| ^~~~~~~~~~~~~~~~~~~~~~~~~~
17 | assert.notDeepEqual([1], [2]);
hint: loose == equality has no lowering — the strict forms compare with Object.is/structural equality like Node's assert/strict module, where equal IS strictEqual
hint: legacy deep equality has no lowering yet — deepStrictEqual and notDeepStrictEqual compare structures with Node's modern semantics
assert-fences.ts:17:1 - error SC2020: 'assert.notDeepEqual' is part of the standard library types but has no scriptc lowering yet
@@ -32,7 +14,7 @@ assert-fences.ts:17:1 - error SC2020: 'assert.notDeepEqual' is part of the stand
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~
18 |
hint: loose == equality has no lowering — the strict forms compare with Object.is/structural equality like Node's assert/strict module, where equal IS strictEqual
hint: legacy deep equality has no lowering yet — deepStrictEqual and notDeepStrictEqual compare structures with Node's modern semantics
assert-fences.ts:20:1 - error SC2020: 'assert.strictEqual of '{ n: number }' and '{ n: number }' values' is part of the standard library types but has no scriptc lowering yet
@@ -14,4 +14,4 @@ multiple-errors.ts:13:13 - error SC1040: loose equality (== and !=) is not suppo
| ^~~~~~~~~~~
14 |
hint: use === / !== ('x == null' / 'x != null' — the null-or-undefined test — is supported; other loose comparisons need dynamic coercion semantics)
hint: object-to-primitive loose equality can call user-defined valueOf/toString methods — compare explicit primitive conversions instead