From 201857eb6c811b4c31b7bd087d17c1e7ff062d10 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 07:54:21 -0500 Subject: [PATCH] Read the class base after the callable-base case in super calls superCallStmt asserted the class base before checking for a callable base. A class that extends an ordinary function constructor has no base class info, so the assertion held null. Node ignores the unused value; the native compiler throws the TypeError of a non-null assertion that held null, and failed to compile programs with such classes (for example the Effect stream fixtures). The base is now read after that case returns. --- .../limitations/runtime-type-checks.mdx | 2 ++ .../node-v24-compatibility-meta.json | 2 +- .../src/generated/node-v24-compatibility.json | 10 +++--- .../generated/node-v24-backlog.json | 19 ++++++----- .../generated/node-v24-internal.json | 12 ++++--- .../src/frontend/lowering/lower-classes.ts | 3 +- ...class-extends-function-constructor.js.json | 6 ++++ ...6020-class-extends-function-constructor.js | 27 ++++++++++++++++ tests/harness/corpus-costs.json | 2 ++ tests/harness/dyncheck.test.ts | 32 +++++++++++++++++++ tests/harness/native-frontend.ts | 1 + 11 files changed, 97 insertions(+), 19 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6020-class-extends-function-constructor.js.json create mode 100644 tests/corpus/6020-class-extends-function-constructor.js diff --git a/docs/content/limitations/runtime-type-checks.mdx b/docs/content/limitations/runtime-type-checks.mdx index 5514fb41ee..ee7b953ced 100644 --- a/docs/content/limitations/runtime-type-checks.mdx +++ b/docs/content/limitations/runtime-type-checks.mdx @@ -3,3 +3,5 @@ Casts from values whose type is only known at runtime validate the value. For ex Casts between program classes are checked too: casting an instance to a class it does not extend throws a `TypeError`, where Node.js reads the members structurally. `SC6004` reports calls that pass such an instance to a function that casts it. A Promise converted from `unknown` to a typed result validates its fulfilled value. A mismatch rejects with a `TypeError`. Rejection reasons and reference equality are preserved across conversions. + +A non-null assertion (`value!`) checks the value when it extracts an object, array or other non-nullable member from a union. If the value holds `null` or `undefined`, the assertion throws a `TypeError`, even when the result is never used; Node.js continues until a property of the value is read. diff --git a/docs/src/generated/node-v24-compatibility-meta.json b/docs/src/generated/node-v24-compatibility-meta.json index 500876593d..db36cdb520 100644 --- a/docs/src/generated/node-v24-compatibility-meta.json +++ b/docs/src/generated/node-v24-compatibility-meta.json @@ -2,6 +2,6 @@ "schemaVersion": 3, "nodeVersion": "24.22.0", "nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93", - "artifactVersion": "56f660f5e2eb7391a048", + "artifactVersion": "cade1b8e6e0c5c42fcc9", "rowCount": 3880 } diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index 568cdb4c9e..dadc9e836d 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -569,9 +569,9 @@ "apiEntries": 120, "static": { "supported": 0, - "partial": 50, + "partial": 51, "refused": 0, - "not-implemented": 70, + "not-implemented": 69, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -57593,9 +57593,9 @@ "anchor": "", "nodeStability": null, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json index 683d477a5f..12b12d63b0 100644 --- a/internal/compatibility/generated/node-v24-backlog.json +++ b/internal/compatibility/generated/node-v24-backlog.json @@ -7,8 +7,8 @@ "tierItemCount": 3456, "tiers": { "static": { - "audit-partial": 1142, - "verify-gap": 2187, + "audit-partial": 1143, + "verify-gap": 2186, "replace-refusal": 40, "classify": 2, "implement": 85 @@ -57507,12 +57507,15 @@ "priority": "normal", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:process", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "surface-manifest:node-builtin.process.execArgv", + "tests": [ + "tests/corpus/990-process-basics.ts", + "tests/corpus/2314-process-introspection.ts" + ] } } }, diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index 393955bc01..2b97f675e0 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -596,9 +596,9 @@ "apiEntries": 120, "static": { "supported": 0, - "partial": 50, + "partial": 51, "refused": 0, - "not-implemented": 70, + "not-implemented": 69, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -80949,8 +80949,12 @@ "anchor": "", "nodeStability": null, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:process" + "status": "partial", + "evidence": "surface-manifest:node-builtin.process.execArgv", + "tests": [ + "tests/corpus/990-process-basics.ts", + "tests/corpus/2314-process-introspection.ts" + ] }, "anchorSource": "chapter" }, diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 8856ae9b5c..06739166c1 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -7861,7 +7861,6 @@ export function superCallStmt( args: IrExpr[], loc: SrcLoc, ): IrStmt { - const base = superBaseOf(info)!; const thisRef: IrExpr = { kind: "varRef", localId: thisLocal.id, @@ -7906,6 +7905,8 @@ export function superCallStmt( }, loc, }; + // A callable base has no class info, so the base is read after that case. + const base = superBaseOf(info)!; if (base.builtinError) { // super(message) into the runtime-provided Error constructor: stamps // name/message on the (already-allocated) object. Receiver + message diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6020-class-extends-function-constructor.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6020-class-extends-function-constructor.js.json new file mode 100644 index 0000000000..b2fa4c986f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6020-class-extends-function-constructor.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6020-class-extends-function-constructor.js" + ], + "diags": [] +} diff --git a/tests/corpus/6020-class-extends-function-constructor.js b/tests/corpus/6020-class-extends-function-constructor.js new file mode 100644 index 0000000000..d98d22a551 --- /dev/null +++ b/tests/corpus/6020-class-extends-function-constructor.js @@ -0,0 +1,27 @@ +// Classes whose base is an ordinary function constructor, with a declared +// constructor calling super and with the implicit derived constructor. +function Shape(name, sides) { + this.name = name; + this.sides = sides; +} +Shape.prototype.describe = function () { + return `${this.name} with ${this.sides} sides`; +}; + +class Polygon extends Shape { + constructor(name, sides, length) { + super(name, sides); + this.length = length; + } + perimeter() { + return this.sides * this.length; + } +} + +class Unnamed extends Shape {} + +const square = new Polygon("square", 4, 3); +console.log(square.describe(), square.perimeter()); +console.log(square instanceof Polygon); +const shape = new Unnamed("triangle", 3); +console.log(shape.describe(), Object.keys(shape).join(",")); diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index 25f00b1f78..a4a12bf4c3 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -1915,6 +1915,7 @@ "600-closures-basic.ts": 2349, "601-closures-loops.ts": 1788, "602-closures-identity-recursion.ts": 2372, + "6020-class-extends-function-constructor.js": 4300, "603-closures-rc-stress.ts": 1832, "604-closures-for-of.ts": 2639, "605-closures-forward-capture-tdz.ts": 3277, @@ -4611,6 +4612,7 @@ "600-closures-basic.ts": 2534, "601-closures-loops.ts": 2297, "602-closures-identity-recursion.ts": 2427, + "6020-class-extends-function-constructor.js": 7500, "603-closures-rc-stress.ts": 2589, "604-closures-for-of.ts": 3514, "605-closures-forward-capture-tdz.ts": 4638, diff --git a/tests/harness/dyncheck.test.ts b/tests/harness/dyncheck.test.ts index ca597c0db6..2ab323f370 100644 --- a/tests/harness/dyncheck.test.ts +++ b/tests/harness/dyncheck.test.ts @@ -764,6 +764,38 @@ console.log("recovered"); expect(r.stdout).toBe("TypeError true\narray: TypeError\nfunc: TypeError\nrecovered\n"); }); + test("a non-null assertion on a null object result throws at the assertion, even when the binding is never read", async () => { + // Node lets the null ride until a member read; the compiled program + // throws the stranded TypeError where `!` extracts the object. + const r = await compileAndRun( + "asserted-null-object-result", + `interface Info { + name: string; + base: Info | null; + callable: boolean; +} +function baseOf(info: Info): Info | null { + return info.base; +} +function describe(info: Info): string { + const base = baseOf(info)!; + if (info.callable) return info.name + ": callable base"; + return info.name + " extends " + base.name; +} +const root: Info = { name: "Root", base: null, callable: false }; +console.log(describe({ name: "Child", base: root, callable: false })); +try { + console.log("unreachable", describe({ name: "Wrapped", base: null, callable: true })); +} catch (e) { + console.log((e as Error).name, (e as Error).message.includes("not representable in the target union")); +} +console.log("recovered"); +`, + ); + expect(r.exitCode).toBe(0); + expect(r.stdout).toBe("Child extends Root\nTypeError true\nrecovered\n"); + }); + test("a checker-approved function mismatch strands per piece: mismatched params compile at the flow, void results trap after the call", async () => { // funcCoerceAdapter's stranded dispositions. Param strand: an array // literal of arrows collapses to one element signature — the diff --git a/tests/harness/native-frontend.ts b/tests/harness/native-frontend.ts index feee2f5911..475db54aa6 100644 --- a/tests/harness/native-frontend.ts +++ b/tests/harness/native-frontend.ts @@ -65,6 +65,7 @@ export const programs = [ "nullish-long-chain.ts", "class-array-optional-return.ts", "contextual-array-union-write.ts", + "6020-class-extends-function-constructor.js", ]; interface LowerRequest {