From 7ecef8908a5a36700548183758ecad9e8010ad85 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Tue, 20 Oct 2026 00:10:08 -0500 Subject: [PATCH] Answer value.constructor.name for class instances and errors value.constructor.name names the class that constructed the value, as in Node: - A TypeScript program class without subclasses answers its name at compile time, as C.name does. Instances of classes with subclasses read constructor and name through the instance's checked-dynamic view. - 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 class (RangeError, DOMException, ...). An assigned name property does not change the result. - An untyped value reads constructor and then name. A value that holds a native error answers the error's class, so a caught RangeError answers "RangeError" instead of undefined. A value whose constructor is undefined or null throws Node's TypeError for the name read. Previously the TypeScript form was refused with SC2020. --- .../language-features/class-reflection.mdx | 3 + .../compiler/src/backend/llvm/lib-shared.ts | 1 + .../src/frontend/lowering/constructor-name.ts | 219 ++++++++++++++++++ .../lowering/expressions/property-access.ts | 6 + packages/compiler/src/ir/builtin-effects.ts | 3 + .../compiler/src/ir/builtin-signatures.ts | 4 + packages/compiler/src/ir/int32-slots.ts | 1 + .../corpus/6006-constructor-name.ts.json | 6 + .../corpus/6007-constructor-name-js.js.json | 6 + packages/runtime/src/scr_dyn.h | 5 + packages/runtime/src/scr_error.c | 9 + packages/runtime/src/scr_error.h | 1 + packages/runtime/src/scr_json.c | 28 +++ tests/corpus/6006-constructor-name.ts | 65 ++++++ tests/corpus/6007-constructor-name-js.js | 45 ++++ .../6006-constructor-name.ts.json | 1 + .../6007-constructor-name-js.js.json | 1 + 17 files changed, 404 insertions(+) create mode 100644 packages/compiler/src/frontend/lowering/constructor-name.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6006-constructor-name.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6007-constructor-name-js.js.json create mode 100644 tests/corpus/6006-constructor-name.ts create mode 100644 tests/corpus/6007-constructor-name-js.js create mode 100644 tests/harness/corpus-costs/6006-constructor-name.ts.json create mode 100644 tests/harness/corpus-costs/6007-constructor-name-js.js.json diff --git a/docs/content/limitations/language-features/class-reflection.mdx b/docs/content/limitations/language-features/class-reflection.mdx index f424b72b01..bacce12b44 100644 --- a/docs/content/limitations/language-features/class-reflection.mdx +++ b/docs/content/limitations/language-features/class-reflection.mdx @@ -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`. diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index d013742353..db9a652795 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -437,6 +437,7 @@ export const LIB_FN_SYMS: Record = { "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", diff --git a/packages/compiler/src/frontend/lowering/constructor-name.ts b/packages/compiler/src/frontend/lowering/constructor-name.ts new file mode 100644 index 0000000000..d1d502b37c --- /dev/null +++ b/packages/compiler/src/frontend/lowering/constructor-name.ts @@ -0,0 +1,219 @@ +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. + * + * - A TypeScript program class without subclasses answers its name at + * compile time, as `C.name` does. + * - Other 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`. + * - 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) return null; + if (programClass.subclasses.length === 0 && programClass.def.jsName !== undefined && asString) { + return { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: lowerer.lowerExpr(target), loc }], + result: strLit(programClass.def.jsName, loc), + type: STRING, + loc, + }; + } + if (!asString || !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, + }; +} diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index a3154c45a3..f3e871ad10 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -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 { localPrivateBrandClass } from "../private-brands.js"; import { type Lowerer, PoisonError, jsFuncNameOf, neverTaintedJsType, own } from "../lowerer.js"; import { @@ -290,6 +291,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) { diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 4d4193f730..c3c7c851d8 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -10,6 +10,9 @@ import type { IrLibFn } from "./builtin-signatures.js"; export const MAY_THROW_LIB_FNS: ReadonlySet = 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", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 2a0e5124ad..f933176210 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -3136,6 +3136,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 diff --git a/packages/compiler/src/ir/int32-slots.ts b/packages/compiler/src/ir/int32-slots.ts index fe0467fac4..2f786dde3f 100644 --- a/packages/compiler/src/ir/int32-slots.ts +++ b/packages/compiler/src/ir/int32-slots.ts @@ -357,6 +357,7 @@ const DYN_READ_ONLY = new Set([ "dyn.bagGet", "dyn.bitwise", "dyn.compare", + "dyn.errorConstructorName", "dyn.forInKeyStrings", "dyn.forInKeys", "dyn.forInStable", diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6006-constructor-name.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6006-constructor-name.ts.json new file mode 100644 index 0000000000..20d7abcd59 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6006-constructor-name.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6006-constructor-name.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6007-constructor-name-js.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6007-constructor-name-js.js.json new file mode 100644 index 0000000000..93fb1b7a98 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6007-constructor-name-js.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6007-constructor-name-js.js" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index a3d506a100..73556d93f3 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -1075,6 +1075,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. */ diff --git a/packages/runtime/src/scr_error.c b/packages/runtime/src/scr_error.c index 6f616c35aa..d78d37216b 100644 --- a/packages/runtime/src/scr_error.c +++ b/packages/runtime/src/scr_error.c @@ -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; diff --git a/packages/runtime/src/scr_error.h b/packages/runtime/src/scr_error.h index ce60dac23a..f38d90e327 100644 --- a/packages/runtime/src/scr_error.h +++ b/packages/runtime/src/scr_error.h @@ -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); ScrError *scr_error_retain(ScrError *e); diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 27168dad19..d4bf01233d 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -6908,6 +6908,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) { + 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) { diff --git a/tests/corpus/6006-constructor-name.ts b/tests/corpus/6006-constructor-name.ts new file mode 100644 index 0000000000..1e45f3897e --- /dev/null +++ b/tests/corpus/6006-constructor-name.ts @@ -0,0 +1,65 @@ +// value.constructor.name names the class that constructed the value: +// program classes (with and without subclasses), builtin errors, program +// error classes, DOMException, and errors whose `name` was reassigned. +class Shape { + describe(): string { + return `${this.constructor.name} shape`; + } +} +class Circle extends Shape {} +class Square extends Shape {} +class Lone {} + +const shapes: Shape[] = [new Shape(), new Circle(), new Square()]; +for (const s of shapes) console.log(s.constructor.name, s.describe()); +console.log(new Lone().constructor.name); + +const Anonymous = class {}; +console.log(new Anonymous().constructor.name); + +class AppError extends Error { + constructor(message: string) { + super(message); + this.name = this.constructor.name; + } +} +class NotFound extends AppError {} +class Conflict extends AppError {} + +const failures: AppError[] = [new AppError("a"), new NotFound("b"), new Conflict("c")]; +for (const f of failures) console.log(f.constructor.name, f.name, String(f)); + +const errors: Error[] = [ + new Error("e"), + new TypeError("t"), + new RangeError("r"), + new SyntaxError("s"), + new URIError("u"), + new EvalError("v"), + new ReferenceError("f"), + new AggregateError([], "g"), + new NotFound("n"), +]; +for (const e of errors) console.log(e.constructor.name, e.name); + +const renamed = new RangeError("renamed"); +renamed.name = "Custom"; +console.log(renamed.constructor.name, renamed.name); + +const abort = new DOMException("stopped", "AbortError"); +console.log(abort.constructor.name, abort.name); + +function kind(error: unknown): string { + return error instanceof Error ? error.constructor.name : typeof error; +} +try { + JSON.parse("{"); +} catch (error) { + console.log(kind(error)); +} +try { + throw new Conflict("thrown"); +} catch (error) { + console.log(kind(error), (error as Error).constructor.name); +} +console.log(kind("text")); diff --git a/tests/corpus/6007-constructor-name-js.js b/tests/corpus/6007-constructor-name-js.js new file mode 100644 index 0000000000..8e421344b5 --- /dev/null +++ b/tests/corpus/6007-constructor-name-js.js @@ -0,0 +1,45 @@ +// value.constructor.name in JavaScript: untyped caught errors answer the +// class that constructed them, including builtin errors whose `name` was +// reassigned, DOMException, and program error classes. +class Shape {} +class Circle extends Shape {} +class AppError extends Error { + constructor(message) { + super(message); + this.name = this.constructor.name; + } +} +class NotFound extends AppError {} + +for (const s of [new Shape(), new Circle()]) console.log(s.constructor.name); + +function report(error) { + console.log(error.constructor.name, error.name, error.message); +} +try { + throw new RangeError("range"); +} catch (error) { + report(error); +} +try { + const e = new TypeError("type"); + e.name = "Renamed"; + throw e; +} catch (error) { + report(error); +} +try { + throw new DOMException("stopped", "AbortError"); +} catch (error) { + report(error); +} +try { + throw new NotFound("missing"); +} catch (error) { + report(error); +} +try { + JSON.parse("{"); +} catch (error) { + console.log(error.constructor.name); +} diff --git a/tests/harness/corpus-costs/6006-constructor-name.ts.json b/tests/harness/corpus-costs/6006-constructor-name.ts.json new file mode 100644 index 0000000000..5a4309d85c --- /dev/null +++ b/tests/harness/corpus-costs/6006-constructor-name.ts.json @@ -0,0 +1 @@ +{ "plain": 2094, "san": 2594 } diff --git a/tests/harness/corpus-costs/6007-constructor-name-js.js.json b/tests/harness/corpus-costs/6007-constructor-name-js.js.json new file mode 100644 index 0000000000..61dd852cba --- /dev/null +++ b/tests/harness/corpus-costs/6007-constructor-name-js.js.json @@ -0,0 +1 @@ +{ "plain": 1883, "san": 2430 }