From 1d1fccccd59249a22cabcb04bae88979879b7a06 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 05:56:14 -0500 Subject: [PATCH] Compare identical references and accept any message in the test262 adapter assert.sameValue, assert.notSameValue and assert.compareArray elements treat identical references as the same value and a reference as different from every primitive. Only two distinct references still refuse, because a conversion at the native boundary can copy a reference but never merge two. Assertion messages may be any value and convert with String, as in the upstream helpers. --- tests/harness/test262.test.ts | 57 +++++++++++++++++++++--- tests/test262/README.md | 2 +- tests/test262/harness.ts | 84 ++++++++++++++++++++--------------- 3 files changed, 101 insertions(+), 42 deletions(-) diff --git a/tests/harness/test262.test.ts b/tests/harness/test262.test.ts index f1b616578..fd35a08fb 100644 --- a/tests/harness/test262.test.ts +++ b/tests/harness/test262.test.ts @@ -237,6 +237,47 @@ assert.notSameValue(1, "1"); status: "fail", source: "assert.throws(TypeError, () => { throw 1; });", }, + { + name: "identical references", + status: "pass", + source: ` +const object = {}; +const list = [1]; +const fn = function () {}; +assert.sameValue(object, object); +assert.sameValue(list, list); +assert.sameValue(fn, fn); +assert.compareArray([object, fn], [object, fn]); +`, + }, + { + name: "a reference is not a primitive", + status: "pass", + source: + "assert.notSameValue({}, 1); assert.notSameValue([], undefined); assert.notSameValue(null, {});", + }, + { name: "reference and primitive mismatch", status: "fail", source: "assert.sameValue([], 0);" }, + { + name: "identical references are not different", + status: "fail", + source: "const object = {}; assert.notSameValue(object, object);", + }, + { + name: "array element reference and primitive mismatch", + status: "fail", + source: "assert.compareArray([{}], [1]);", + }, + { + name: "non-string messages", + status: "pass", + source: + "assert.sameValue(1, 1, false); assert.notSameValue(1, 2, 0); assert(true, null); assert.compareArray([], [], 1);", + }, + { + name: "failure with a non-string message", + status: "fail", + source: "assert.sameValue(1, 2, false);", + }, ]; describe(`Test262 host assertion contract${shardSuffix()}`, () => { @@ -259,8 +300,8 @@ describe(`Test262 host assertion contract${shardSuffix()}`, () => { ["{}", "[1]", "function () {}"], (value) => `reference:${value}`, )) { - test(`reference assertions are explicitly refused: ${value}`, async () => { - const source = `const value = ${value}; try { assert.sameValue(value, value); } catch { }`; + test(`distinct reference assertions are explicitly refused: ${value}`, async () => { + const source = `try { assert.sameValue(${value}, ${value}); } catch { } try { assert.notSameValue(${value}, ${value}); } catch { }`; expect(() => runUpstream(source)).not.toThrow(); const result = await runSource(source, { sanitize }); expect(result, JSON.stringify(result)).toMatchObject({ @@ -270,8 +311,8 @@ describe(`Test262 host assertion contract${shardSuffix()}`, () => { }); } - test("array element identity assertions are refused", async () => { - const source = "const value = {}; try { assert.compareArray([value], [value]); } catch { }"; + test("distinct array element references are refused", async () => { + const source = "try { assert.compareArray([{}], [{}]); } catch { }"; expect(() => runUpstream(source)).not.toThrow(); const result = await runSource(source, { sanitize }); expect(result, JSON.stringify(result)).toMatchObject({ @@ -359,9 +400,15 @@ const propertyControls = shardSelect( }, { name: "reference value", - status: "harness-refusal", + status: "pass", source: "const f = function() {}; const o = { a: f }; verifyProperty(o, 'a', { value: f });", }, + { + name: "distinct reference value", + status: "harness-refusal", + source: + "const o = { a: function() {} }; try { verifyProperty(o, 'a', { value: function() {} }); } catch { }", + }, ], (item) => `property:${item.name}`, ); diff --git a/tests/test262/README.md b/tests/test262/README.md index 2f8a0ed3e..5474fa2c3 100644 --- a/tests/test262/README.md +++ b/tests/test262/README.md @@ -32,7 +32,7 @@ The current profile is `static-adapted-v6`. It adapts strict scripts to standalo The unmodified upstream assertion helper currently reaches scriptc's JavaScript function-expando refusal. Test262 explicitly permits host implementations of harness functions in its [interpretation rules](https://github.com/tc39/test262/blob/7ab7fafa0003f73fc85c1b95d88094d33f7eb8bd/INTERPRETING.md#host-defined-functions). `harness.ts` supplies `assert`, `assert.sameValue`, `assert.notSameValue`, `assert.compareArray`, `assert.throws` for selected Error constructors, `$DONE`, and `Test262Error`. Scalar SameValue and scalar array-element comparisons use the statically compiled `node:assert/strict` implementation. Contract tests compare successful and failing assertions with the original Test262 helpers under Node, including NaN, signed zero, null/undefined, differing primitive types, and premature termination. -Reference-equality assertions terminate with a distinct harness-refusal result: conversion at a native function boundary can copy references, so this adapter cannot faithfully compare every object, array, or function identity. Tests cannot catch this refusal and accidentally pass. Sparse array comparisons are not yet handled faithfully across that boundary. The adapted `propertyHelper.js` admits direct `verifyProperty` calls with an inline descriptor literal or `undefined`; reference-valued comparisons still refuse, while other property helpers and descriptor expressions remain excluded. The adapted `testTypedArray.js` keeps the upstream constructor lists, argument factories and `testWith*` helpers. It omits the `TypedArray` (%TypedArray%) binding, so tests that name it are excluded. Its resizable and immutable buffer factories run only where `ArrayBuffer.prototype` provides `resize` and `transferToImmutable`, as the upstream feature checks intend. Contract tests compare its successful and failing uses with the upstream helper under Node. Assertion aliases, mutation, reflection, additional assertion methods, and other `includes` helpers remain excluded. +Conversion at a native function boundary can copy references, so one object, array, or function may reach the adapter as two distinct references. A copy cannot merge two references into one. `assert.sameValue`, `assert.notSameValue`, and `assert.compareArray` elements therefore treat identical references as the same value and a reference as different from every primitive. Two distinct references terminate with a distinct harness-refusal result, which tests cannot catch and turn into a pass. Assertion messages may be any value; the adapter converts them with `String`, as the upstream helpers do. Sparse array comparisons are not yet handled faithfully across that boundary. The adapted `propertyHelper.js` admits direct `verifyProperty` calls with an inline descriptor literal or `undefined`; reference-valued comparisons follow the same identity rule, while other property helpers and descriptor expressions remain excluded. The adapted `testTypedArray.js` keeps the upstream constructor lists, argument factories and `testWith*` helpers. It omits the `TypedArray` (%TypedArray%) binding, so tests that name it are excluded. Its resizable and immutable buffer factories run only where `ArrayBuffer.prototype` provides `resize` and `transferToImmutable`, as the upstream feature checks intend. Contract tests compare its successful and failing uses with the upstream helper under Node. Assertion aliases, mutation, reflection, additional assertion methods, and other `includes` helpers remain excluded. Raw tests, module variants, agents, resolution negatives, and runtime negatives remain excluded. Parse negatives pass only when a source-matched syntax or vetted JavaScript early-error diagnostic appears; TypeScript type errors and harness-only failures never satisfy a negative expectation. `$262`, dynamic evaluation, observable script-global state, and other unsupported host requirements are excluded as well. The runtime and compiler continue to enforce their existing refusal boundaries for admitted source. diff --git a/tests/test262/harness.ts b/tests/test262/harness.ts index c44d32869..b478b4555 100644 --- a/tests/test262/harness.ts +++ b/tests/test262/harness.ts @@ -17,68 +17,80 @@ export function $DONE(error?: unknown): void { console.log("__scriptc_test262_complete__"); } -function scalar(value: unknown): void { - // Native-to-unknown conversion can copy references (notably arrays). - // Refuse these assertions before comparing altered identities. Terminate +function reference(value: unknown): boolean { + return (typeof value === "object" && value !== null) || typeof value === "function"; +} + +function refuse(): never { + // Native-to-unknown conversion can copy references (notably arrays), so + // two distinct references may be one value in the program. Terminate // instead of throwing so a test's catch block cannot turn this into a pass. - if ((typeof value === "object" && value !== null) || typeof value === "function") { - console.error("SCRIPTC_TEST262_HARNESS: reference equality requires an identity-preserving adapter"); - process.exit(86); + console.error("SCRIPTC_TEST262_HARNESS: reference equality requires an identity-preserving adapter"); + process.exit(86); +} + +function scalar(value: unknown): void { + if (reference(value)) refuse(); +} + +/** The upstream helpers accept any message value and convert it to a string. */ +function text(message: unknown, fallback: string): string { + return message === undefined ? fallback : String(message); +} + +/** SameValue of two operands. A copy made at the conversion boundary can only + * separate one reference into two, so identical references are the same value + * and a reference is never the same value as a primitive. Two distinct + * references refuse. */ +function same(actual: unknown, expected: unknown): boolean { + if (reference(actual) || reference(expected)) { + if (actual === expected) return true; + if (reference(actual) && reference(expected)) refuse(); + return false; + } + try { + nodeAssert.strictEqual(actual, expected); + return true; + } catch { + return false; } } -export function assert(value: unknown, message = "Expected true"): void { - if (value !== true) throw new Test262Error(message); +export function assert(value: unknown, message?: unknown): void { + if (value !== true) throw new Test262Error(text(message, "Expected true")); } export namespace assert { - export function sameValue(actual: unknown, expected: unknown, message = "Expected SameValue"): void { - scalar(actual); - scalar(expected); - try { - nodeAssert.strictEqual(actual, expected); - } catch { - throw new Test262Error(message); - } + export function sameValue(actual: unknown, expected: unknown, message?: unknown): void { + if (!same(actual, expected)) throw new Test262Error(text(message, "Expected SameValue")); } - export function notSameValue(actual: unknown, expected: unknown, message = "Expected different values"): void { - scalar(actual); - scalar(expected); - try { - nodeAssert.notStrictEqual(actual, expected); - } catch { - throw new Test262Error(message); - } + export function notSameValue(actual: unknown, expected: unknown, message?: unknown): void { + if (same(actual, expected)) throw new Test262Error(text(message, "Expected different values")); } - export function throws(expectedErrorConstructor: unknown, func: () => unknown, message = "Expected exception"): void { + export function throws(expectedErrorConstructor: unknown, func: () => unknown, message?: unknown): void { const outcome = throwsOutcome(expectedErrorConstructor, func); if (outcome === "invalid") throw new Test262Error("assert.throws requires a function to run"); if (outcome === "unsupported") { console.error("SCRIPTC_TEST262_HARNESS: assert.throws needs an Error instance"); process.exit(86); } - if (outcome !== "pass") throw new Test262Error(message); + if (outcome !== "pass") throw new Test262Error(text(message, "Expected exception")); } - export function compareArray(actual: unknown, expected: unknown, message = "Expected matching array contents"): void { + export function compareArray(actual: unknown, expected: unknown, message?: unknown): void { + const failure = text(message, "Expected matching array contents"); if (!Array.isArray(actual) || !Array.isArray(expected)) { scalar(actual); scalar(expected); - throw new Test262Error(message); + throw new Test262Error(failure); } - if (actual.length !== expected.length) throw new Test262Error(message); + if (actual.length !== expected.length) throw new Test262Error(failure); for (let i = 0; i < actual.length; i++) { const left: unknown = i in actual ? actual[i] : undefined; const right: unknown = i in expected ? expected[i] : undefined; - scalar(left); - scalar(right); - try { - nodeAssert.strictEqual(left, right); - } catch { - throw new Test262Error(message); - } + if (!same(left, right)) throw new Test262Error(failure); } } }