Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@
`JSON.stringify` writes Date fields as their ISO text and Map and Set fields as `{}`, as Node.js does, and refuses classes with RegExp or Promise fields with `SC1090`.
`Object.keys` raises an error for classes with opaque fields such as native Maps.
Class fields, inherited accessors, and added properties retain identity through untyped values.
`value.constructor.name` names the class that constructed a class instance or an error, as Node.js does; for an error it is the error's class, not its `name` property.
A read of `value.constructor` alone in TypeScript raises `SC2020`.
`constructor.name` of a plain object, such as an object literal or a `JSON.parse` result, raises `SC2020` in TypeScript and throws a `TypeError` in JavaScript and on untyped values, where Node.js answers `Object`.
1 change: 1 addition & 0 deletions packages/compiler/src/backend/llvm/lib-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"dyn.toStringCoerce": "scr_dyn_string_coerce_js",
"dyn.stringConstructor": "scr_dyn_string_constructor",
"dyn.propertyKey": "scr_dyn_property_key_value",
"dyn.errorConstructorName": "scr_dyn_error_constructor_name",
"dyn.toNumeric": "scr_dyn_to_numeric",
"dyn.increment": "scr_dyn_increment",
"dyn.numberConstructor": "scr_dyn_number_constructor",
Expand Down
227 changes: 227 additions & 0 deletions packages/compiler/src/frontend/lowering/constructor-name.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
import * as ts from "../ts7/adapter.js";
import { BOOL, DYN, STRING, type IrExpr } from "../../ir/ir.js";
import { dynUndefinedExpr, strLit, varRef } from "../../ir/build.js";
import { isJsSourceFile, locOf } from "../program.js";
import type { Lowerer } from "./lowerer.js";
import type { ClassInfo } from "./lower-classes.js";
import { tryLowerExpression } from "./expressions/try-lower-expression.js";

/** `value.constructor.name`: the name of the class that constructed the
* value.
*
* - Program class instances in TypeScript read `constructor` and then
* `name` through the instance's checked-dynamic view, which JavaScript
* sources already use for `value.constructor`, so a `constructor`
* replaced through an untyped value answers the replacement's name. A
* class without subclasses whose instances have no checked-dynamic view
* answers its name at compile time, as `C.name` does.
* - An error-typed value tests the program's error classes that extend its
* static class, most derived first, and otherwise answers the native
* error's builtin class name.
* - An untyped value reads `constructor` and then `name`. A value that holds
* a native error answers the error's class name, since a native error has
* no constructor value to read.
*
* Answers null for other receivers, which keep their existing lowering. */
export function lowerConstructorName(
lowerer: Lowerer,
expr: ts.PropertyAccessExpression,
): IrExpr | null {
if (expr.name.text !== "name" || expr.questionDotToken) return null;
let access = expr.expression;
while (ts.isParenthesizedExpression(access)) access = access.expression;
if (
!ts.isPropertyAccessExpression(access) ||
access.name.text !== "constructor" ||
access.questionDotToken ||
access.expression.kind === ts.SyntaxKind.SuperKeyword
)
return null;
const target = access.expression;
const js = isJsSourceFile(expr.getSourceFile());
let declared = lowerer.mapTypeOf(lowerer.typeOf(target));
// An unannotated JavaScript parameter has no mapped type; its lowered
// value says whether it is untyped.
let lowered: IrExpr | null = null;
if (declared === null) {
if (!js) return null;
lowered = tryLowerExpression(lowerer, target);
if (lowered?.type.kind !== "dyn") return null;
declared = DYN;
}
const loc = locOf(expr);
const asString = !js && lowerer.mapTypeOf(lowerer.typeOf(expr))?.kind === "string";
if (declared.kind === "dyn") {
const slot = lowerer.declareHiddenLocal("%constructorNameReceiver", DYN);
const constructorSlot = lowerer.declareHiddenLocal("%constructorNameConstructor", DYN);
const value = (): IrExpr => varRef(slot.id, DYN, loc);
const constructor = (): IrExpr => varRef(constructorSlot.id, DYN, loc);
const name: IrExpr = {
kind: "libCall",
fn: "dyn.errorConstructorName",
args: [
value(),
constructor(),
{
kind: "dynKeyGet",
value: constructor(),
key: strLit("name", loc),
optional: true,
type: DYN,
loc,
},
],
type: DYN,
loc,
};
const result = asString ? lowerer.coerceToExpected(name, STRING) : name;
return {
kind: "seqExpr",
stmts: [
{ kind: "varDecl", localId: slot.id, init: lowered ?? lowerer.lowerExpr(target), loc },
{
kind: "varDecl",
localId: constructorSlot.id,
init: {
kind: "dynKeyGet",
value: value(),
key: strLit("constructor", loc),
type: DYN,
loc,
},
loc,
},
],
result,
type: result.type,
loc,
};
}
if (declared.kind !== "object") return null;
const info = lowerer.classes.get(declared.className);
const programClass = info !== undefined && !info.def.runtime ? info : undefined;
if (programClass?.fields.has("constructor")) return null;
if (lowerer.errorHierarchyClassOf(declared.className)) {
if (programClass && js) return null;
return errorConstructorName(lowerer, target, declared.className, programClass, loc);
}
if (!programClass || js || !asString) return null;
// An instance that can become an untyped value can have its `constructor`
// replaced there (`untyped.constructor = Other`), so only a class whose
// instances never convert answers its name at compile time.
if (
programClass.subclasses.length === 0 &&
programClass.def.jsName !== undefined &&
!lowerer.dynConvertible(declared)
) {
return {
kind: "seqExpr",
stmts: [{ kind: "exprStmt", expr: lowerer.lowerExpr(target), loc }],
result: strLit(programClass.def.jsName, loc),
type: STRING,
loc,
};
}
if (!lowerer.dynConvertible(declared)) return null;
const slot = lowerer.declareHiddenLocal("%constructorNameReceiver", DYN);
const value = varRef(slot.id, DYN, loc);
return {
kind: "seqExpr",
stmts: [
{
kind: "varDecl",
localId: slot.id,
init: lowerer.coerceToExpected(lowerer.lowerExpr(target), DYN),
loc,
},
],
result: lowerer.coerceToExpected(constructorNameRead(value, loc), STRING),
type: STRING,
loc,
};
}

/** `value.constructor.name` read through the checked-dynamic tree. */
function constructorNameRead(value: IrExpr, loc: IrExpr["loc"]): IrExpr {
return {
kind: "dynKeyGet",
value: { kind: "dynKeyGet", value, key: strLit("constructor", loc), type: DYN, loc },
key: strLit("name", loc),
type: DYN,
loc,
};
}

/** The constructor name of an error-typed value. */
function errorConstructorName(
lowerer: Lowerer,
target: ts.Expression,
className: string,
programClass: ClassInfo | undefined,
loc: IrExpr["loc"],
): IrExpr | null {
const receiver = lowerer.lowerExpr(target);
if (receiver.type.kind !== "object") return null;
const type = receiver.type;
const extendsStatic = (candidate: ClassInfo): boolean => {
for (let c: ClassInfo | null = candidate; c; c = c.base) {
if (c.def.name === className) return true;
}
return false;
};
const depth = (candidate: ClassInfo): number => {
let n = 0;
for (let c = candidate.base; c; c = c.base) n++;
return n;
};
const candidates = [...lowerer.classes.values()]
.filter(
(candidate) =>
!candidate.def.runtime &&
candidate.def.jsName !== undefined &&
candidate.decl != null &&
extendsStatic(candidate),
)
.sort((a, b) => depth(b) - depth(a));
if (candidates.some((candidate) => candidate.fields.has("constructor"))) return null;
const slot = lowerer.declareHiddenLocal("%constructorNameReceiver", type);
const value = (): IrExpr => varRef(slot.id, type, loc);
// A program class is the most general candidate of its own hierarchy, so
// it answers when no subclass matches. A builtin static class falls back
// to the native error's class.
let result: IrExpr =
programClass?.def.jsName !== undefined && candidates.at(-1) === programClass
? strLit(programClass.def.jsName, loc)
: lowerer.coerceToExpected(
{
kind: "libCall",
fn: "dyn.errorConstructorName",
args: [
lowerer.coerceToExpected(value(), DYN),
dynUndefinedExpr(loc),
dynUndefinedExpr(loc),
],
type: DYN,
loc,
},
STRING,
);
const tested = result.kind === "strLit" ? candidates.slice(0, -1) : candidates;
for (const candidate of tested.toReversed()) {
result = {
kind: "ternary",
cond: { kind: "instanceOf", value: value(), className: candidate.def.name, type: BOOL, loc },
then: strLit(candidate.def.jsName!, loc),
else_: result,
type: STRING,
loc,
};
}
return {
kind: "seqExpr",
stmts: [{ kind: "varDecl", localId: slot.id, init: receiver, loc }],
result,
type: STRING,
loc,
};
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* Property reads (`a.b`): lowerExprInner's property-access branch, intrinsic and union properties, field reads and targets, and match-group reads. */
import * as ts from "../../ts7/adapter.js";
import { lowerConstructorName } from "../constructor-name.js";
import { lowerHistogramProperty } from "../builtins/performance.js";
import { staticFunctionNameRead } from "../static-function-names.js";
import { typedArgumentsArity } from "../typed-arguments.js";
Expand Down Expand Up @@ -313,6 +314,11 @@ export function lowerPropertyAccessExpr(
if (table) return table;
}
lowerThreadSlotsProperty(lowerer, expr);
// `value.constructor.name` (constructor-name.ts).
{
const constructorName = lowerConstructorName(lowerer, expr);
if (constructorName) return constructorName;
}
// `EventEmitter.name`, `zlib.Gzip.name`, `C.name` over a builtin
// class alias.
if (expr.name.text === "name" && !expr.questionDotToken) {
Expand Down
3 changes: 3 additions & 0 deletions packages/compiler/src/ir/builtin-effects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import type { IrLibFn } from "./builtin-signatures.js";
export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
// Node's argument, cipher-name, IV, tag-length and key-length errors.
"crypto.cipherNew",
// value.constructor.name on a value whose constructor is undefined or
// null throws Node's TypeError for the name read.
"dyn.errorConstructorName",
// node:test's { timeout } and { concurrency } range checks.
"test.register",
"test.registerEmpty",
Expand Down
4 changes: 4 additions & 0 deletions packages/compiler/src/ir/builtin-signatures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3311,6 +3311,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
"dyn.toStringCoerce": { argTypes: [DYN], result: STRING },
"dyn.stringConstructor": { argTypes: [DYN], result: STRING },
"dyn.propertyKey": { argTypes: [DYN], result: DYN },
// value.constructor.name from the value, its constructor and the name
// read from it: that name, or the class name of the native error the
// value holds.
"dyn.errorConstructorName": { argTypes: [DYN, DYN, DYN], result: DYN },
"dyn.toNumeric": { argTypes: [DYN], result: DYN },
"dyn.increment": { argTypes: [DYN, BOOL], result: DYN },
/** JS ToNumber over a dyn value WITH the object protocol (number-hint
Expand Down
1 change: 1 addition & 0 deletions packages/compiler/src/ir/int32-slots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,7 @@ const DYN_READ_ONLY = new Set<string>([
"dyn.bagGet",
"dyn.bitwise",
"dyn.compare",
"dyn.errorConstructorName",
"dyn.forInKeyStrings",
"dyn.forInKeys",
"dyn.forInStable",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6006-constructor-name.ts"
],
"diags": []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6007-constructor-name-js.js"
],
"diags": []
}
5 changes: 5 additions & 0 deletions packages/runtime/src/scr_dyn.h
Original file line number Diff line number Diff line change
Expand Up @@ -1112,6 +1112,11 @@ ScrError *scr_error_from_dyn(const ScrDyn *d); /* scr_async_dyn.c (gated) */
/* The cache's runtime-internal access pair (scr_json.c owns the storage;
* the gated extraction reads/writes through these). */
ScrError *scr_errdyn_err_of(const ScrDyn *d); /* +1 or NULL */
/* value.constructor.name, given the value's constructor and the `name`
* read from it (undefined when the constructor is undefined or null).
* Borrows all three; +1, or NULL after throwing Node's TypeError. */
ScrDyn *scr_dyn_error_constructor_name(const ScrDyn *value, const ScrDyn *constructor,
const ScrDyn *name);
void scr_errdyn_put(ScrError *e, ScrDyn *d); /* retains both sides */
/* Sets an own property on an error: its checked-dynamic node, which casts
* and util.inspect read. Moves value. */
Expand Down
9 changes: 9 additions & 0 deletions packages/runtime/src/scr_error.c
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,15 @@ void scr_error_install_name_reader(ScrStr *(*reader)(ScrError *)) {
scr_error_name_reader = reader;
}

/* The builtin class that constructed the error, as `error.constructor.name`
* reads it: the vtable's class, not the error's assignable `name`.
* DOMException is its own class. +1. */
ScrStr *scr_error_class_name(const ScrError *e) {
if (scr_error_vts[SCR_ERR_DOMEX].pre <= e->vt->pre && e->vt->pre <= scr_error_vts[SCR_ERR_DOMEX].post)
return scr_str_new("DOMException", 12);
return scr_error_default_name(e);
}

ScrStr *scr_error_default_name(const ScrError *e) {
for (int i = 1; i < SCR_ERR_COUNT; i++) {
if (i == SCR_ERR_DOMEX) continue;
Expand Down
1 change: 1 addition & 0 deletions packages/runtime/src/scr_error.h
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ ScrStr *scr_error_to_string(ScrError *e);
void scr_error_install_message_reader(ScrStr *(*reader)(ScrError *));
void scr_error_install_name_reader(ScrStr *(*reader)(ScrError *));
ScrStr *scr_error_default_name(const ScrError *e);
ScrStr *scr_error_class_name(const ScrError *e);
ScrStr *scr_error_stack(ScrError *e);
/* Replaces the error's stack string, as an assignment to `stack` does.
* Borrowed operands. */
Expand Down
28 changes: 28 additions & 0 deletions packages/runtime/src/scr_json.c
Original file line number Diff line number Diff line change
Expand Up @@ -7151,6 +7151,34 @@ void scr_error_set_prop(ScrError *e, const char *key, size_t key_len, ScrDyn *va
scr_dyn_release(view);
}

/* `value.constructor.name` after the compiler read the value's
* constructor and its `name` (undefined when the constructor is
* undefined or null). A string name answers itself. A value that holds a
* native error has no constructor value to read, so it answers the error's
* class name, which an assigned `name` property does not change. Any
* other undefined or null constructor throws Node's TypeError for the
* `name` read. */
ScrDyn *scr_dyn_error_constructor_name(const ScrDyn *value, const ScrDyn *constructor,
const ScrDyn *name) {
if (name->kind == SCR_DYN_STR) return scr_dyn_retain((ScrDyn *)name);
ScrError *error = scr_errdyn_err_of(value);
if (error) {
ScrStr *class_name = scr_error_class_name(error);
scr_error_release(error);
ScrDyn *out = scr_dyn_new_str(class_name);
scr_str_release(class_name);
return out;
}
if (constructor->kind == SCR_DYN_UNDEF || constructor->kind == SCR_DYN_NULL) {
Comment on lines +7164 to +7172

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

When an untyped error's constructor is replaced with null or an object whose name is not a string, the fallback returns the error's builtin class name instead of throwing TypeError or returning the actual name value.

Fix on Vercel

char message[64];
int n = snprintf(message, sizeof message, "Cannot read properties of %s (reading 'name')",
constructor->kind == SCR_DYN_NULL ? "null" : "undefined");
scr_throw_error_msg(SCR_ERR_TYPE, message, (size_t)n);
return NULL;
}
return scr_dyn_retain((ScrDyn *)name);
}

/* The error's checked-dynamic node when it has one (borrowed), else NULL:
* util.inspect reads its extra own properties. */
ScrDyn *scr_errdyn_view_of(const ScrError *e) {
Expand Down
Loading
Loading