From da1af7437f6f2c07bb6b65ab6793227cd5975dbb Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 12:20:36 -0500 Subject: [PATCH] Revert "Read fields as undefined before their assignment (#1292)" This reverts commit ff90245b27862eeec38687df9c8f4c078ffc12d0. --- .../frontend/lowering/construction-escapes.ts | 9 - .../src/frontend/lowering/lower-classes.ts | 208 +++++------------- .../src/frontend/lowering/lower-exprs.ts | 94 -------- .../compiler/src/frontend/lowering/lowerer.ts | 12 - .../test/class-early-field-reads.test.ts | 93 -------- ...5943-fields-read-before-assignment.ts.json | 6 - ...4-js-fields-read-before-assignment.js.json | 6 - .../5943-fields-read-before-assignment.ts | 181 --------------- .../5944-js-fields-read-before-assignment.js | 28 --- tests/harness/corpus-costs.json | 4 - 10 files changed, 58 insertions(+), 583 deletions(-) delete mode 100644 packages/compiler/test/class-early-field-reads.test.ts delete mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5943-fields-read-before-assignment.ts.json delete mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5944-js-fields-read-before-assignment.js.json delete mode 100644 tests/corpus/5943-fields-read-before-assignment.ts delete mode 100644 tests/corpus/5944-js-fields-read-before-assignment.js diff --git a/packages/compiler/src/frontend/lowering/construction-escapes.ts b/packages/compiler/src/frontend/lowering/construction-escapes.ts index a1626f12d..484920a1c 100644 --- a/packages/compiler/src/frontend/lowering/construction-escapes.ts +++ b/packages/compiler/src/frontend/lowering/construction-escapes.ts @@ -124,15 +124,6 @@ export function constructionEscape(inputs: ConstructionInputs): ConstructionEsca continue; } if (ts.isExpressionStatement(stmt) && !exposes(stmt.expression, depth)) continue; - // A check that only reads assigned fields and throws or does nothing - // with the receiver (`if (this.size < 0) throw new RangeError()`) - // does not expose the instance; assignments in its branches are not - // definite and still count as exposures. - if ( - (ts.isIfStatement(stmt) || ts.isThrowStatement(stmt) || ts.isBlock(stmt)) && - !exposes(stmt, depth) - ) - continue; return false; } return true; diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 7b2d176c3..f70499e6f 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -143,7 +143,7 @@ import { lowerObjectFactoryNew } from "./object-factory-new.js"; import { tryLowerExpression } from "./expressions/try-lower-expression.js"; import { lowerInstanceConstructorNew } from "./class-instance-constructor.js"; import { classPrototypeData } from "./class-prototypes.js"; -import { constructionEscape, mentionsReceiver } from "./construction-escapes.js"; +import { constructionEscape } from "./construction-escapes.js"; import { checkedClassConstruction, checkedClassConstructionPacked } from "./class-construction.js"; import { initializeRuntimeStatics, hasRuntimeStatics } from "./class-runtime-statics.js"; import { reflectedClassStaticMethodValue } from "./class-method-values.js"; @@ -468,11 +468,6 @@ export interface ClassInfo { * unassigned read throws the catchable TypeError instead of yielding * an undefined the declared type cannot hold (SEMANTICS.md). */ deferredInitFields?: Set; - /** Fields (inherited included) that code construction exposes the - * instance to can read before their assignment: the slot is the - * undefined-armed union, allocation writes undefined, and reads yield the - * union value, so an early read sees undefined as in Node. */ - earlyReadFields?: Set; /** True when construction may hand the instance to other code (a method * call, an argument, a callback, or a base constructor that does) — a * subclass's own fields can then be read before their assignment. */ @@ -3507,54 +3502,55 @@ export function collectClassShapeInner( const derived = decl.heritageClauses?.some((clause) => clause.token === ts.SyntaxKind.ExtendsKeyword) ?? false; - // A generic family is not emitted; its instances and each mixin - // application analyze their own construction with their actual base. - const escape: { escapes: boolean; assigned: Set | null } = familyMode - ? { escapes: derived, assigned: null } - : constructionEscape({ - ctor, - fields: fieldOrder.flatMap((f) => - paramProps.some((pp) => pp.name === f.name) - ? [] - : [{ name: f.name, initializer: f.initializer }], - ), - paramProps: paramProps.map((pp) => pp.name), - derived, - baseEscapes: - base === null || - callableBase !== undefined || - factoryBaseExpression !== undefined || - base.constructionEscapes === true, - isField: (name) => - fields.has(name) && - !methods.has(`set:${name}`) && - !lowerer.findMethodOn(base, `set:${name}`), - privateMethodBody: (call) => { - const callee = call.expression; - if (!ts.isPropertyAccessExpression(callee)) return null; - const name = callee.name.text; - const declaration = decl.members.find( - (member): member is ts.MethodDeclaration => - ts.isMethodDeclaration(member) && - member.body !== undefined && - (ts.isIdentifier(member.name) || ts.isPrivateIdentifier(member.name)) && - member.name.text === name, - ); - if (!declaration?.body || declaration.asteriskToken) return null; - const modifiers = ts.getModifiers(declaration) ?? []; - if ( - modifiers.some( - (m) => - m.kind === ts.SyntaxKind.StaticKeyword || m.kind === ts.SyntaxKind.AsyncKeyword, + // Generic instances and mixins share layouts across declarations, so + // they only report whether construction exposes the instance. + const escape: { escapes: boolean; assigned: Set | null } = + familyMode || mixin + ? { escapes: derived, assigned: null } + : constructionEscape({ + ctor, + fields: fieldOrder.flatMap((f) => + paramProps.some((pp) => pp.name === f.name) + ? [] + : [{ name: f.name, initializer: f.initializer }], + ), + paramProps: paramProps.map((pp) => pp.name), + derived, + baseEscapes: + base === null || + callableBase !== undefined || + factoryBaseExpression !== undefined || + base.constructionEscapes === true, + isField: (name) => + fields.has(name) && + !methods.has(`set:${name}`) && + !lowerer.findMethodOn(base, `set:${name}`), + privateMethodBody: (call) => { + const callee = call.expression; + if (!ts.isPropertyAccessExpression(callee)) return null; + const name = callee.name.text; + const declaration = decl.members.find( + (member): member is ts.MethodDeclaration => + ts.isMethodDeclaration(member) && + member.body !== undefined && + (ts.isIdentifier(member.name) || ts.isPrivateIdentifier(member.name)) && + member.name.text === name, + ); + if (!declaration?.body || declaration.asteriskToken) return null; + const modifiers = ts.getModifiers(declaration) ?? []; + if ( + modifiers.some( + (m) => + m.kind === ts.SyntaxKind.StaticKeyword || m.kind === ts.SyntaxKind.AsyncKeyword, + ) ) - ) - return null; - const isPrivate = - ts.isPrivateIdentifier(declaration.name) || - modifiers.some((m) => m.kind === ts.SyntaxKind.PrivateKeyword); - return isPrivate ? declaration.body : null; - }, - }); + return null; + const isPrivate = + ts.isPrivateIdentifier(declaration.name) || + modifiers.some((m) => m.kind === ts.SyntaxKind.PrivateKeyword); + return isPrivate ? declaration.body : null; + }, + }); // The deferred definite-assignment check: a field on the unguarded // list passes only with an unconditional `this.x = ...` at the // constructor's TOP LEVEL — the same standard the JS-class path @@ -3610,30 +3606,18 @@ export function collectClassShapeInner( } } - // Fields assigned after construction can expose the instance (a method - // call, a base constructor that calls an override) can be read while - // Node still holds undefined. Their slots admit undefined, allocation - // writes it, and reads yield it, as in Node. - const earlyReadFields = new Set(base?.earlyReadFields ?? []); - if (escape.assigned && escape.escapes) { + if (escape.assigned && !inst) { for (const f of fieldOrder) { if (f.redeclared || escape.assigned.has(f.name) || deferredInitFields.has(f.name)) continue; + if (paramProps.some((pp) => pp.name === f.name)) continue; const declared = fields.get(f.name); - if (declared === undefined || !earlyReadable(lowerer, declared)) continue; - // A declared `T | undefined` field already starts undefined. - if (declared.kind === "union" && lowerer.armTag(declared.unionId, UNDEFINED_T) >= 0) + if (declared?.kind !== "object" || lowerer.classes.get(declared.className)?.def.runtime) continue; - const armed = lowerer.runtimeOptionalType(declared); - if (armed.kind !== "union") continue; + const armed = lowerer.withUndefinedArm(declared); + if (armed === null || armed.kind !== "union") continue; fields.set(f.name, armed); f.type = armed; - // Primitive and union slots yield the undefined value itself. - // Reference slots keep the deferred-init reads: consumers that - // observe undefined (printing, equality, typeof, nullish defaults) - // see it, and member access on it throws a TypeError, as in Node. - if (declared.kind === "union" || PRIMITIVE_EARLY_READS.has(declared.kind)) - earlyReadFields.add(f.name); - else deferredInitFields.add(f.name); + deferredInitFields.add(f.name); } } @@ -3738,43 +3722,6 @@ export function collectClassShapeInner( (name) => !hasAccessor(name) && !methods.has(name) && !lowerer.findMethodOn(base, name), ) : null; - // Constructor statements that run after the instance may have been - // exposed (a method call, `this` passed along, a base constructor - // that exposes it): a field they create can be read before its - // assignment, while Node holds undefined. - const exposedBefore = new Set(); - { - let exposed = false; - for (const stmt of ctor?.body?.statements ?? []) { - const expression = ts.isExpressionStatement(stmt) ? stmt.expression : null; - if ( - expression && - ts.isCallExpression(expression) && - expression.expression.kind === ts.SyntaxKind.SuperKeyword - ) { - if (base?.constructionEscapes === true || expression.arguments.some(mentionsReceiver)) - exposed = true; - continue; - } - const assigned = - expression && - ts.isBinaryExpression(expression) && - expression.operatorToken.kind === ts.SyntaxKind.EqualsToken && - ts.isPropertyAccessExpression(expression.left) && - expression.left.expression.kind === ts.SyntaxKind.ThisKeyword - ? expression - : null; - if (exposed || (assigned && mentionsReceiver(assigned.right))) exposedBefore.add(stmt); - const plainAssignment = - assigned !== null && - ts.isPropertyAccessExpression(assigned.left) && - !mentionsReceiver(assigned.right) && - !hasAccessor(assigned.left.name.text) && - !methods.has(assigned.left.name.text) && - !lowerer.findMethodOn(base, assigned.left.name.text); - if (!plainAssignment && mentionsReceiver(stmt)) exposed = true; - } - } if (ctor?.body) { for (const stmt of ctor.body.statements) { const lhs = @@ -3845,15 +3792,8 @@ export function collectClassShapeInner( type = inheritedJsFieldType(lowerer, decl, name, type); // super() can invoke a subclass setter or method before its // constructor assignments run. These slots start undefined. - const sealed = sealedPrefix?.created.has(name) === true; - if (base && lowerer.dynConvertible(type) && !sealed) type = DYN; - if (exposedBefore.has(stmt) && !sealed && earlyReadable(lowerer, type)) { - const armed = lowerer.runtimeOptionalType(type); - if (armed.kind === "union") { - type = armed; - earlyReadFields.add(name); - } - } + if (base && lowerer.dynConvertible(type) && !sealedPrefix?.created.has(name)) + type = DYN; fields.set(name, type); fieldOrder.push({ name, type, initializer: undefined }); continue; @@ -4114,7 +4054,6 @@ export function collectClassShapeInner( ? { classDecorators: { nodes: classDecoratorNodes } } : {}), ...(deferredInitFields.size > 0 ? { deferredInitFields } : {}), - ...(earlyReadFields.size > 0 ? { earlyReadFields } : {}), ...(escape.escapes ? { constructionEscapes: true as const } : {}), }; // GENERIC members get their declaring-class backlink now that the @@ -6289,33 +6228,6 @@ function overrideReturnAbi( return declared; } -/** Field kinds whose early reads yield the undefined value (earlyReadFields). */ -const PRIMITIVE_EARLY_READS: ReadonlySet = new Set([ - "f64", - "bool", - "string", - "bigint", -]); - -/** Field types whose slot can admit undefined for reads before the field's - * assignment. Date values are scalars, so their unassigned slot is memory - * safe; checked-dynamic slots already hold undefined. */ -function earlyReadable(lowerer: Lowerer, type: IrType): boolean { - if (isUnitType(type)) return false; - switch (type.kind) { - case "dyn": - case "void": - case "date": - case "generator": - case "caught": - return false; - case "object": - return lowerer.classes.get(type.className)?.def.runtime !== true; - default: - return true; - } -} - /** The nearest declaration of `name` at or above `info` — the method a * receiver of that static class runs when nothing below overrides it. */ export function findMethodOn( @@ -7753,11 +7665,7 @@ function paramPropInitStmts(lowerer: Lowerer, info: ClassInfo, thisLocal: IrLoca obj: { kind: "varRef", localId: thisLocal.id, type: thisType, loc }, className: info.def.name, field: pp.name, - // An early-readable field's slot admits undefined (earlyReadFields). - value: lowerer.coerceToExpected( - { kind: "varRef", localId: local!.id, type: local!.type, loc }, - info.fields.get(pp.name) ?? local!.type, - ), + value: { kind: "varRef", localId: local!.id, type: local!.type, loc }, loc, }); } diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index 4ef139293..c108ceb71 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -3161,51 +3161,6 @@ export function lowerTernary( * the same union (unnarrowed use), to a SUB-union (partial narrowing — * unrepresentable without a re-tag), or to nothing (`never` in an * exhaustive default) leaves the expression union-typed. */ -/** Uses that read an early-read field's value as Node does when it is - * undefined: printing, templates, operators, tests, member access, calls of - * program functions (whose parameters the optional-value analysis widens), - * and stores into bindings, fields and returns. Other uses, such as library - * arguments, extract the declared type with a check. */ -function observesEarlyRead(node: ts.Node): boolean { - let child = node; - let parent = node.parent; - while (parent && ts.isParenthesizedExpression(parent)) { - child = parent; - parent = parent.parent; - } - if (!parent) return false; - if ( - ts.isTemplateSpan(parent) || - ts.isBinaryExpression(parent) || - ts.isPrefixUnaryExpression(parent) || - ts.isPostfixUnaryExpression(parent) || - ts.isTypeOfExpression(parent) || - ts.isConditionalExpression(parent) || - ts.isIfStatement(parent) || - ts.isWhileStatement(parent) || - ts.isReturnStatement(parent) || - ts.isVariableDeclaration(parent) || - ts.isPropertyAssignment(parent) || - ts.isArrayLiteralExpression(parent) || - (ts.isPropertyAccessExpression(parent) && parent.expression === child) || - (ts.isElementAccessExpression(parent) && parent.expression === child) - ) - return true; - if (ts.isCallExpression(parent) && parent.arguments.includes(child as ts.Expression)) { - const callee = parent.expression; - if ( - ts.isPropertyAccessExpression(callee) && - ts.isIdentifier(callee.expression) && - callee.expression.text === "console" - ) - return true; - return ( - !ts.isPropertyAccessExpression(callee) || callee.expression.kind === ts.SyntaxKind.ThisKeyword - ); - } - return false; -} - export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrExpr { if ( (expr.type.kind === "union" || expr.type.kind === "dyn") && @@ -3416,25 +3371,6 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx if (expr.type.kind !== "union") return expr; if (ts.isPropertyAccessExpression(node) && isOptionalProcessStreamProperty(lowerer, node)) return expr; - // A field read before its assignment holds undefined although its - // declared type omits it; the declared type is no narrowing proof. A flow - // narrowing past the declared type (`if (this.x !== null)`) extracts the - // narrowed arm with a check, since the slot may still hold undefined. - if ( - expr.kind === "fieldGet" && - lowerer.classes.get(expr.className)?.earlyReadFields?.has(expr.field) === true - ) { - const symbol = ts.isPropertyAccessExpression(node) - ? lowerer.checker.getSymbolAtLocation(node.name) - : undefined; - const declared = symbol ? lowerer.mapTypeOf(lowerer.checker.getTypeOfSymbol(symbol)) : null; - const narrowedArm = lowerer.mapTypeOf(lowerer.typeOf(node)); - if (narrowedArm === null || narrowedArm.kind === "union") return expr; - if (declared !== null && typeEquals(declared, narrowedArm) && observesEarlyRead(node)) - return expr; - const helper = lowerer.narrowedArmHelper(expr.type.unionId, narrowedArm, expr.loc); - return helper ? lowerer.checkedNarrowCall(helper, expr, narrowedArm, expr.loc) : expr; - } // Represented fields and index values can include undefined even when // checker inference or lib.d.ts declares strings (RegExp captures). @@ -15465,20 +15401,6 @@ export function fieldGetExpr( const helper = lowerer.deferredReadHelper(target.fieldType.unionId, inner, loc); if (helper) return { kind: "call", callee: helper, args: [read], type: inner, loc }; } - // An early-read field (ClassInfo.earlyReadFields) yields its undefined - // where the use reads it as Node does; elsewhere the declared type is - // extracted with a check. - if ( - target.fieldType.kind === "union" && - lowerer.classes.get(target.className)?.earlyReadFields?.has(target.field) === true && - !observesEarlyRead(blame) - ) { - const declared = lowerer.mapTypeOf(lowerer.typeOf(blame)); - if (declared && declared.kind !== "union") { - const helper = lowerer.narrowedArmHelper(target.fieldType.unionId, declared, loc); - if (helper) return lowerer.checkedNarrowCall(helper, read, declared, loc); - } - } return read; } return { @@ -16019,22 +15941,6 @@ function lowerFieldCompoundValue( if (op === "+" && read.type.kind === "string") { appended = lowerer.ensureString(rhs, rhsNode ?? access); value = { kind: "strConcat", left: read, right: appended, type: STRING, loc }; - } else if ( - op === "+" && - read.type.kind === "union" && - lowerer.stripUndefinedArm(read.type).kind === "string" && - target.container === "class" && - lowerer.classes.get(target.className)?.earlyReadFields?.has(target.field) === true - ) { - // A string field read before its assignment holds undefined, which - // concatenates as "undefined". - value = { - kind: "strConcat", - left: lowerer.ensureString(read, access), - right: lowerer.ensureString(rhs, rhsNode ?? access), - type: STRING, - loc, - }; } else if (numericRead.type.kind === "f64" && numericRhs.type.kind === "f64") { value = { kind: "bin", op, left: numericRead, right: numericRhs, type: F64, loc }; } else if (bigintValue) { diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index b77768c28..33dfcfb19 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -3976,18 +3976,6 @@ export class Lowerer { if (!s) return null; return s.flags & ts.SymbolFlags.Alias ? this.checker.getAliasedSymbol(s) : s; }; - // Fields that can be read before their assignment hold undefined then; - // their reads are runtime-optional values like missing array reads. - for (const info of this.classes.values()) { - if (!info.earlyReadFields || !info.decl?.name) continue; - const classSymbol = symbolOf(info.decl.name); - if (!classSymbol) continue; - const instance = this.checker.getDeclaredTypeOfSymbol(classSymbol); - for (const name of info.earlyReadFields) { - const property = this.checker.getPropertyOfType(instance, name); - if (property) optionalSymbols.add(property); - } - } const signatureOf = (node: ts.Identifier): FnSig | null => { const symbol = symbolOf(node); if (!symbol) return null; diff --git a/packages/compiler/test/class-early-field-reads.test.ts b/packages/compiler/test/class-early-field-reads.test.ts deleted file mode 100644 index c602a2f42..000000000 --- a/packages/compiler/test/class-early-field-reads.test.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { expect, test } from "vitest"; -import { compile, deserializeModule, validateModule } from "../src/index.js"; -import type { IrModule } from "../src/ir/ir.js"; - -async function lower(source: string, file = "main.ts"): Promise { - const dir = await mkdtemp(join(tmpdir(), "scriptc-early-field-reads-")); - try { - const entry = join(dir, file), - output = join(dir, "main.ir.json"); - await writeFile(entry, source); - const result = await compile(entry, { outDir: dir, outPath: output, outputKind: "ir" }); - if (!result.ok) - throw new Error(result.diagnostics.map((item) => `${item.code}: ${item.message}`).join("\n")); - const module = deserializeModule(await readFile(output, "utf8")); - expect(validateModule(module)).toEqual([]); - return module; - } finally { - await rm(dir, { recursive: true, force: true }); - } -} - -/** Field name → IR kind for a class. */ -function fields(module: IrModule, name: string): string { - const cls = module.classes.find((candidate) => candidate.name === name); - expect(cls, name).toBeDefined(); - return cls!.fields - .filter((field) => !field.name.startsWith("%")) - .map((field) => `${field.name}:${field.type.kind}`) - .join(" "); -} - -test("fields assigned after construction can call an override admit undefined", async () => { - const module = await lower(` -abstract class Base { - constructor() { - this.describe(); - } - abstract describe(): void; -} -class Panel extends Base { - label: string; - width: number; - count = 0; - constructor() { - super(); - this.label = "panel"; - this.width = 2; - } - describe(): void { - console.log(this.label, this.width, this.count); - } -} -class Plain { - label: string; - width: number; - constructor() { - this.label = "plain"; - this.width = 1; - this.describe(); - } - describe(): void { - console.log(this.label, this.width); - } -} -new Panel(); -new Plain(); -`); - expect(fields(module, "Panel")).toBe("label:union width:union count:union"); - expect(fields(module, "Plain")).toBe("label:string width:f64"); -}); - -test("JavaScript fields created after the constructor calls a method admit undefined", async () => { - const module = await lower( - ` -class Widget { - constructor() { - this.ready = true; - this.render(); - this.title = "widget"; - } - render() { - console.log(this.ready, this.title); - } -} -new Widget(); -`, - "main.js", - ); - expect(fields(module, "Widget")).toBe("ready:bool title:union"); -}); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5943-fields-read-before-assignment.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5943-fields-read-before-assignment.ts.json deleted file mode 100644 index d2dcdcc24..000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5943-fields-read-before-assignment.ts.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/corpus/5943-fields-read-before-assignment.ts" - ], - "diags": [] -} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5944-js-fields-read-before-assignment.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5944-js-fields-read-before-assignment.js.json deleted file mode 100644 index e9800b71d..000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5944-js-fields-read-before-assignment.js.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/corpus/5944-js-fields-read-before-assignment.js" - ], - "diags": [] -} diff --git a/tests/corpus/5943-fields-read-before-assignment.ts b/tests/corpus/5943-fields-read-before-assignment.ts deleted file mode 100644 index 922b3c34d..000000000 --- a/tests/corpus/5943-fields-read-before-assignment.ts +++ /dev/null @@ -1,181 +0,0 @@ -// @transform-types -// Fields that a base constructor (or the constructor itself) can read -// through an override or a method call before they are assigned hold -// undefined then, as in Node, for every field kind. After construction -// the fields read and update normally. -class Point { - x: number; - constructor(x: number) { - this.x = x; - } -} -function show(label: string, value: number): string { - return `${label}=${value}`; -} -abstract class Widget { - log: string[] = []; - constructor() { - this.describe("base"); - } - abstract describe(when: string): void; -} -class Panel extends Widget { - label: string; - width: number; - visible: boolean; - origin: Point; - items: number[]; - options: { depth: number }; - key: string | number; - index: Map; - tags: Set; - count = 5; - constructor(label: string, readonly id: string) { - super(); - this.label = label; - this.width = 10; - this.visible = true; - this.origin = new Point(1); - this.items = [1, 2]; - this.options = { depth: 2 }; - this.key = 7; - this.index = new Map([["a", 1]]); - this.tags = new Set(["t"]); - this.describe("ctor"); - } - describe(when: string): void { - console.log(when, this.label, this.width, this.visible, this.origin, this.items, this.options, this.key, this.count, this.id); - console.log(when, `${this.label}:${this.width}`, this.width + 1, this.count * 2, this.label === undefined, typeof this.items, this.origin?.x, this.options?.depth ?? -1, this.index?.size, this.tags?.has("t")); - console.log(when, show(this.label, this.width), String(this.key), [this.label].length); - try { - console.log(this.label.length, this.items.length); - } catch (error) { - console.log(when, (error as Error).message); - } - } - grow(): number { - this.count++; - this.width += 5; - this.label += "!"; - this.items.push(this.width); - return this.count + this.width + this.items.length; - } -} -const panel = new Panel("main", "p1"); -console.log(panel.grow(), panel.label, panel.items, panel.count, panel.width); - -class Plain { - total: number; - name: string; - constructor() { - this.total = 1; - this.name = "plain"; - this.bump(); - } - bump(): void { - this.total++; - } -} -const plain = new Plain(); -console.log(plain.total, plain.name); - -class Toucher { - constructor() { - this.touch(); - } - touch(): void {} -} -class Counter extends Toucher { - count = 0; - label: string; - items: string[]; - constructor() { - super(); - this.label = "c"; - this.items = []; - } - override touch(): void { - this.count++; - this.label += "x"; - console.log(this.count, this.label, Number.isNaN(this.count)); - try { - this.items.push("a"); - } catch (error) { - console.log(error instanceof TypeError); - } - } -} -const c = new Counter(); -c.touch(); -console.log(c.count, c.label, c.items); - -class Shower { - constructor() { - this.show(); - } - show(): void {} -} -class Box extends Shower { - label: string; - value: T; - constructor(value: T) { - super(); - this.label = "box"; - this.value = value; - } - override show(): void { - console.log("show", this.label, this.value); - } -} -new Box(3).show(); -new Box("s").show(); - -type Ctor = new (...args: any[]) => T; -class Viewer { - constructor() { - this.show(); - } - show(): void {} -} -function Labeled>(Parent: B) { - return class extends Parent { - label = "mixed"; - override show(): void { - console.log("show", this.label); - } - }; -} -const L = Labeled(Viewer); -new L().show(); - -interface Crash { loc: number; message: string } -class Setupper { - constructor() { - this.setup(); - } - setup(): void {} -} -class Host extends Setupper { - crash: Crash | null; - name: string | null; - constructor(c: Crash | null) { - super(); - this.crash = c; - this.name = "host"; - } - override setup(): void { - console.log("setup", this.crash, this.name); - if (this.crash !== null) { - try { - console.log(this.crash.loc); - } catch (error) { - console.log(error instanceof TypeError); - } - } - } - report(): string { - if (this.crash) return `${this.crash.message} at ${this.crash.loc}`; - return this.name === null ? "none" : this.name.toUpperCase(); - } -} -console.log(new Host({ loc: 3, message: "boom" }).report(), new Host(null).report()); diff --git a/tests/corpus/5944-js-fields-read-before-assignment.js b/tests/corpus/5944-js-fields-read-before-assignment.js deleted file mode 100644 index e313a12b0..000000000 --- a/tests/corpus/5944-js-fields-read-before-assignment.js +++ /dev/null @@ -1,28 +0,0 @@ -// JavaScript class fields that the constructor creates after it calls a -// method, or after a base constructor that does, hold undefined until -// they are assigned, as in Node. -class Widget { - constructor(name) { - this.render(); - this.name = name; - this.size = 3; - this.title = "widget"; - this.parts = ["a"]; - this.meta = { id: 1 }; - } - render() { - console.log("render", this.name, this.title, this.size, this.parts, this.meta, this.size + 1, `${this.name}!`); - } -} -class Big extends Widget { - constructor() { - super("big"); - this.extra = 2; - } - render() { - super.render(); - console.log("big", this.extra); - } -} -new Widget("w").render(); -new Big().render(); diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index e4dd020cd..09e45ea7f 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -2160,8 +2160,6 @@ "5940-callback-helper-specialization.ts": 3109, "5941-callback-specialization-recursion.ts": 3112, "5942-callback-specialization-throws.ts": 3221, - "5943-fields-read-before-assignment.ts": 3902, - "5944-js-fields-read-before-assignment.js": 2868, "5945-local-class-observed-methods.ts": 2284, "5946-nested-record-field-optional-writes.ts": 2987, "5950-tuple-predicate-methods.ts": 4611, @@ -5173,8 +5171,6 @@ "5940-callback-helper-specialization.ts": 4548, "5941-callback-specialization-recursion.ts": 4448, "5942-callback-specialization-throws.ts": 3443, - "5943-fields-read-before-assignment.ts": 4319, - "5944-js-fields-read-before-assignment.js": 3945, "5945-local-class-observed-methods.ts": 4458, "5946-nested-record-field-optional-writes.ts": 3537, "5950-tuple-predicate-methods.ts": 4018,