From 0b98a8c71340bc7d12914c330359ecdbd429a3a6 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 19 Oct 2026 23:28:36 -0500 Subject: [PATCH] Read an Error subclass's own code through NodeJS.ErrnoException `(err as NodeJS.ErrnoException).code` read the runtime error's errno slot, which only runtime errors set, so an Error subclass with its own `code` field answered undefined where Node reads the own property. When a class in the program under the receiver's class has its own `code` field or a dynamic property bag, the read goes through the error's dynamic view, which answers the own property and the errno code alike. --- .../src/frontend/lowering/builtins/errors.ts | 36 +++++++++++++-- .../corpus/6348-errno-code-own-field.ts.json | 6 +++ tests/corpus/6348-errno-code-own-field.ts | 46 +++++++++++++++++++ .../6348-errno-code-own-field.ts.json | 1 + 4 files changed, 86 insertions(+), 3 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6348-errno-code-own-field.ts.json create mode 100644 tests/corpus/6348-errno-code-own-field.ts create mode 100644 tests/harness/corpus-costs/6348-errno-code-own-field.ts.json diff --git a/packages/compiler/src/frontend/lowering/builtins/errors.ts b/packages/compiler/src/frontend/lowering/builtins/errors.ts index 0313cdd10..a8b4dbcd6 100644 --- a/packages/compiler/src/frontend/lowering/builtins/errors.ts +++ b/packages/compiler/src/frontend/lowering/builtins/errors.ts @@ -1,7 +1,16 @@ import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; import { locOf } from "../../program.js"; -import { BOOL, CAUGHT, DYN, F64, type IrExpr, type IrType, STRING } from "../../../ir/ir.js"; +import { + BOOL, + CAUGHT, + DYN, + DYN_CLASS_PROPERTIES, + F64, + type IrExpr, + type IrType, + STRING, +} from "../../../ir/ir.js"; const ASSERTION_ERROR_PROPERTIES: ReadonlySet = new Set([ "actual", @@ -126,7 +135,13 @@ export function lowerErrorCodeProperty( declared, ); } - const systemField = ["errno", "syscall", "path", "dest"].includes(expr.name.text); + // A program class under the receiver's class can carry its own `code` + // (a declared field or a property added through an untyped reference), + // which Node reads instead of the errno code: read through the error's + // dynamic view, which answers both. + const systemField = + ["errno", "syscall", "path", "dest"].includes(expr.name.text) || + (expr.name.text === "code" && programClassMayOwnCode(lowerer, recvT.className)); if ( !systemField && expr.name.text !== "code" && @@ -162,7 +177,9 @@ export function lowerErrorCodeProperty( type: DYN, loc, }, - lowerer.withUndefinedArm(expr.name.text === "errno" ? F64 : STRING), + expr.name.text === "code" + ? lowerer.envValueType() + : lowerer.withUndefinedArm(expr.name.text === "errno" ? F64 : STRING), ); } return { @@ -184,6 +201,19 @@ export function lowerErrorCodeProperty( }; } +/** Whether a class declared in the program, `className` or one of its + * subclasses, has its own `code` field or a dynamic property bag. The + * runtime error classes keep `code` in the errno slot. */ +function programClassMayOwnCode(lowerer: Lowerer, className: string): boolean { + for (const [name, info] of lowerer.classes) { + if (name.startsWith("%")) continue; + if (name !== className && !lowerer.isSubclassOf(name, className)) continue; + if (info.def.fields.some((f) => f.name === "code" || f.name === DYN_CLASS_PROPERTIES)) + return true; + } + return false; +} + /** Whether values of `type` are errors: class instances whose class is * %Error or extends it. A subclass of a class that does not extend Error * cannot be an error either, so the answer depends on the static class diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6348-errno-code-own-field.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6348-errno-code-own-field.ts.json new file mode 100644 index 000000000..b5c392112 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6348-errno-code-own-field.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6348-errno-code-own-field.ts" + ], + "diags": [] +} diff --git a/tests/corpus/6348-errno-code-own-field.ts b/tests/corpus/6348-errno-code-own-field.ts new file mode 100644 index 000000000..02a3d722a --- /dev/null +++ b/tests/corpus/6348-errno-code-own-field.ts @@ -0,0 +1,46 @@ +// `(err as NodeJS.ErrnoException).code` reads an Error subclass's own +// `code` field, as Node reads the own property, and still reads the errno +// code of runtime errors and undefined for plain errors. +import { readFileSync } from "node:fs"; + +class AppError extends Error { + code: string; + constructor(message: string, code = "E_APP") { + super(message); + this.name = "AppError"; + this.code = code; + } +} +class DbError extends AppError { + constructor() { + super("db", "E_DB"); + } +} +class Later extends Error { + code?: string; +} + +function codeOf(err: unknown): string { + if (err instanceof Error) return (err as NodeJS.ErrnoException).code ?? "(none)"; + return "(not an error)"; +} + +console.log(codeOf(new AppError("direct"))); +console.log(codeOf(new DbError())); +const wrapped = new TypeError("wrapped", { cause: new AppError("cause", "E_CAUSE") }); +console.log(codeOf(wrapped.cause), codeOf(wrapped)); +try { + throw new DbError(); +} catch (err) { + console.log(codeOf(err)); +} +const later = new Later("later"); +console.log(codeOf(later)); +later.code = "E_LATER"; +console.log(codeOf(later)); +try { + readFileSync("/nonexistent/scriptc-corpus-6348"); +} catch (err) { + console.log(codeOf(err)); +} +console.log(codeOf(new Error("plain")), codeOf(new RangeError("range")), codeOf("text")); diff --git a/tests/harness/corpus-costs/6348-errno-code-own-field.ts.json b/tests/harness/corpus-costs/6348-errno-code-own-field.ts.json new file mode 100644 index 000000000..28b033d9b --- /dev/null +++ b/tests/harness/corpus-costs/6348-errno-code-own-field.ts.json @@ -0,0 +1 @@ +{ "plain": 1200, "san": 1500 }