Answer the object a constructor returns from new

A class constructor's `return expr` was evaluated and dropped, so
`new C()` answered the constructed instance even where Node answers the
returned object (`if (C.instance) return C.instance;` built a second
instance).

A return that yields an instance of the class itself stores it in a
per-class result slot, read and cleared by one helper at every
construction site (direct new, class values, the checked dynamic
construction dispatchers, record-to-class coercion). A return of any
other object refuses with SC1090 (compile time in TypeScript, a
deferred fence in JavaScript); a JavaScript value typed only at run time
is tested when it returns. super() into a class that can return an
existing instance refuses, since Node would rebind the subclass's this.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Malte Ubl
2026-10-18 14:42:18 -07:00
co-authored by Claude Opus 5.5
parent ea7bcd9846
commit 11c97074e2
18 changed files with 705 additions and 23 deletions
@@ -1,4 +1,5 @@
import { dynUndefinedExpr, varRef } from "../../ir/build.js";
import { constructedValue } from "./constructor-returns.js";
import {
BOOL,
DYN,
@@ -389,13 +390,13 @@ export class ClassConstructionDispatch {
if (!args) continue;
const type: IrType = { kind: "classval", className: name };
const callee = varRef("p.0", DYN, loc);
const value: IrExpr = {
const value = constructedValue(lowerer, info, {
kind: "newValue",
callee: { kind: "dynCheck", value: callee, type, loc },
args,
type: { kind: "object", className: name },
loc,
};
});
const body: IrStmt[] = [
{ kind: "return", value: lowerer.coerceToExpected(value, DYN), loc },
];
@@ -1,4 +1,5 @@
import { dynUndefinedExpr, helperLoc, nodeThrowExpr, varRef } from "../../ir/build.js";
import { constructedValue } from "./constructor-returns.js";
import { eventTargetApiMembers, eventTargetInheritedMember } from "./lower-event-target.js";
import {
BOOL,
@@ -3158,13 +3159,13 @@ export class ClassDynamicDispatch {
}
if (!supported || !info.decl) continue;
const cls = classValueRef(lowerer, info, info.decl);
const result: IrExpr = {
const result = constructedValue(lowerer, info, {
kind: "new",
className: name,
args,
type: { kind: "object", className: name },
loc,
};
});
this.constructDispatch.body.unshift({
kind: "if",
cond: {
@@ -1,3 +1,4 @@
import { constructedValue } from "../constructor-returns.js";
import { dynUndefinedExpr, varRef } from "../../../ir/build.js";
import type { FieldLift } from "./structural-plans.js";
import { buildArrayConversion } from "./builders.js";
@@ -524,7 +525,13 @@ export function recordClassWidthHelper(
params: [{ localId: "r.0", name: "r", type: fromT }],
returnType: toT,
locals: [{ id: "r.0", name: "r", type: fromT, mutable: true }],
body: [{ kind: "return", value: { kind: "new", className, args, type: toT, loc }, loc }],
body: [
{
kind: "return",
value: constructedValue(lowerer, info, { kind: "new", className, args, type: toT, loc }),
loc,
},
],
loc,
});
return name;
@@ -0,0 +1,377 @@
/** Constructors that `return` a value.
*
* JavaScript's `new C()` answers the constructor's returned value when it
* is an object (functions included) and the constructed `this` otherwise.
* A primitive or bare return therefore keeps the ordinary lowering, which
* evaluates the operand and drops it.
*
* A return whose value is an instance of the constructor's own class keeps
* the class representation (`if (Single.instance) return
* Single.instance;`). The return stores that instance in a per-class
* result slot and every direct `new C()` takes it from there. Any other
* object has no representation as a `C` instance and refuses at the return
* statement, deferred to run time in JavaScript like other statement
* fences. A JavaScript value that is an object only at run time (`any`)
* is tested when it returns: a primitive answers the constructed
* instance, an instance of the class takes the result slot, and any other
* object refuses.
*
* `super()` from a subclass refuses: Node would rebind the subclass's
* `this` to the returned instance. */
import * as ts from "../ts7/adapter.js";
import { varRef } from "../../ir/build.js";
import {
BOOL,
DYN,
type IrExpr,
type IrGlobal,
type IrStmt,
type IrType,
STRING,
typeKey,
} from "../../ir/ir.js";
import { unsupportedDiag } from "../../diagnostics/diagnostic.js";
import { isJsSourceFile, locOf } from "../program.js";
import type { ClassInfo } from "./lower-classes.js";
import type { Lowerer } from "./lowerer.js";
type ReturnKind = "instance" | "object" | "mixed";
export interface ConstructorReturnPlan {
/** Returns whose value may be an object, by kind. Primitive and `this`
* returns are absent: they keep the ordinary lowering. */
returns: Map<ts.ReturnStatement, ReturnKind>;
/** The result slot (`C | undefined`), present when some return yields an
* instance of the class itself. */
slot: IrGlobal | null;
}
const PRIMITIVE_FLAGS =
ts.TypeFlags.Primitive | ts.TypeFlags.EnumLike | ts.TypeFlags.Void | ts.TypeFlags.Never;
/** Types whose values may be primitives or objects: open types, type
* variables and intersections (a branded `string & { tag }` is a string). */
const OPEN_FLAGS =
ts.TypeFlags.AnyOrUnknown | ts.TypeFlags.InstantiableNonPrimitive | ts.TypeFlags.Intersection;
function unparenthesized(node: ts.Expression): ts.Expression {
let e = node;
while (ts.isParenthesizedExpression(e)) e = e.expression;
return e;
}
/** Whether a value of this checker type can be an object: "never",
* "always", or "maybe" (open types and primitive/object unions). */
function objectness(type: ts.Type): "never" | "always" | "maybe" {
const parts = type.isUnionType() ? ts.constituentTypes(type) : [type];
let primitive = false;
let object = false;
for (const part of parts) {
if ((part.flags & OPEN_FLAGS) !== 0) return "maybe";
if ((part.flags & PRIMITIVE_FLAGS) !== 0) primitive = true;
else object = true;
}
if (!object) return "never";
return primitive ? "maybe" : "always";
}
/** The constructor's own `return <expr>` statements: nested functions,
* arrows and classes have their own returns. */
function ownValueReturns(ctor: ts.ConstructorDeclaration): ts.ReturnStatement[] {
const out: ts.ReturnStatement[] = [];
const visit = (node: ts.Node): void => {
if (ts.isFunctionLike(node) || ts.isClassLikeDeclaration(node)) return;
if (ts.isReturnStatement(node) && node.expression !== undefined) out.push(node);
node.forEachChild(visit);
};
ctor.body?.forEachChild(visit);
return out;
}
/** A return inside a `try` with a `finally` block: the finally block runs
* after the result is stored and may throw past it. */
function insideFinallyTry(node: ts.Node, ctor: ts.ConstructorDeclaration): boolean {
for (let p = node.parent; p !== undefined && p !== ctor; p = p.parent) {
if (ts.isTryStatement(p) && p.finallyBlock !== undefined && !nodeWithin(node, p.finallyBlock))
return true;
}
return false;
}
function nodeWithin(node: ts.Node, ancestor: ts.Node): boolean {
return node.pos >= ancestor.pos && node.end <= ancestor.end;
}
function instanceType(info: ClassInfo): IrType {
return { kind: "object", className: info.def.name };
}
function isInstanceOf(lowerer: Lowerer, info: ClassInfo, type: IrType | null): boolean {
if (type === null) return false;
if (type.kind === "object") return type.className === info.def.name;
if (type.kind !== "union") return false;
const arms = lowerer.unions.get(type.unionId)?.arms ?? [];
return (
arms.length === 2 &&
arms.some((arm) => arm.kind === "object" && arm.className === info.def.name) &&
arms.some((arm) => arm.kind === "undefinedT")
);
}
export function constructorReturnPlan(
lowerer: Lowerer,
info: ClassInfo,
): ConstructorReturnPlan | null {
const byClass = lowerer.constructorReturnPlans;
const known = byClass.get(info.def.name);
if (known !== undefined) return known;
const ctor = info.ctor;
let plan: ConstructorReturnPlan | null = null;
if (ctor?.body !== undefined && info.decl !== null) {
const returns = new Map<ts.ReturnStatement, ReturnKind>();
for (const stmt of ownValueReturns(ctor)) {
const value = unparenthesized(stmt.expression!);
if (value.kind === ts.SyntaxKind.ThisKeyword) continue;
const type = lowerer.typeOf(value);
const shape = objectness(type);
if (shape === "never") continue;
// Local classes construct through their own environment, which has
// no result slot.
const instance =
info.localClass === undefined &&
!insideFinallyTry(stmt, ctor) &&
isInstanceOf(lowerer, info, lowerer.mapTypeOf(type));
returns.set(stmt, instance ? "instance" : shape === "always" ? "object" : "mixed");
}
if (returns.size > 0) {
let slot: IrGlobal | null = null;
// A JavaScript value typed only at run time may hold an instance of
// the class too (`if (C.instance) return C.instance;`).
const runtimeInstance =
info.localClass === undefined &&
isJsSourceFile(ctor.getSourceFile()) &&
[...returns].some(([stmt, kind]) => kind === "mixed" && !insideFinallyTry(stmt, ctor));
if ([...returns.values()].includes("instance") || runtimeInstance) {
slot = {
id: `%g.${info.def.name}.%constructorResult`,
name: `${info.def.name}.%constructorResult`,
type: lowerer.withUndefinedArm(instanceType(info)),
mutable: true,
};
lowerer.globalsList.push(slot);
}
plan = { returns, slot };
}
}
byClass.set(info.def.name, plan);
return plan;
}
function foreignObjectFeature(info: ClassInfo): string {
const display = info.def.name.replace(/^%|^%m\d+\./, "");
return `returning an object other than an instance of the class from a constructor (Node's 'new ${display}()' answers the returned object, which has no representation as a '${display}' instance)`;
}
/** `return <expr>` inside a class constructor whose value may be an
* object. Null when the statement keeps the ordinary lowering. */
export function lowerConstructorReturn(lowerer: Lowerer, stmt: ts.ReturnStatement): IrStmt | null {
const info = lowerer.currentClass;
if (info === null || info.ctor === null || stmt.expression === undefined) return null;
let fn: ts.Node | undefined = stmt.parent;
while (fn !== undefined && !ts.isFunctionLike(fn) && !ts.isClassLikeDeclaration(fn))
fn = fn.parent;
if (fn !== info.ctor) return null;
const plan = constructorReturnPlan(lowerer, info);
const kind = plan?.returns.get(stmt);
if (plan === null || kind === undefined) return null;
const loc = locOf(stmt);
if (kind === "instance") {
const value = lowerer.coerceInto(
stmt.expression,
lowerer.lowerExpr(stmt.expression),
plan.slot!.type,
);
return {
kind: "block",
body: [
{ kind: "assign", localId: plan.slot!.id, value, loc },
{ kind: "return", value: null, loc },
],
loc,
};
}
const feature = foreignObjectFeature(info);
if (kind === "object" || !isJsSourceFile(stmt.getSourceFile()))
lowerer.unsupported("SC1090", stmt, feature);
// A JavaScript value that is an object only sometimes: a primitive
// still answers the constructed instance, an object refuses.
const value = lowerer.coerceToExpected(lowerer.lowerExpr(stmt.expression), DYN);
if (value.type.kind !== "dyn") lowerer.unsupported("SC1090", stmt, feature);
const held = lowerer.declareHiddenLocal("%returned", DYN);
const read = varRef(held.id, DYN, loc);
const typeOf: IrExpr = { kind: "libCall", fn: "dyn.typeof", args: [read], type: STRING, loc };
const typeIs = (name: string): IrExpr => ({
kind: "strEq",
negated: false,
left: typeOf,
right: { kind: "strLit", value: name, type: STRING, loc },
type: BOOL,
loc,
});
const diag = unsupportedDiag("SC1090", loc, feature);
lowerer.runtimeFences.push(diag);
const sf = stmt.getSourceFile();
const line = ts.getLineAndCharacterOfPosition(sf, loc.start).line + 1;
return {
kind: "block",
body: [
{ kind: "varDecl", localId: held.id, init: value, loc },
{
kind: "if",
// null answers "object" and is falsy; every object is truthy.
cond: {
kind: "logical",
op: "||",
left: typeIs("function"),
right: {
kind: "logical",
op: "&&",
left: typeIs("object"),
right: lowerer.ensureBool(read, stmt.expression),
type: BOOL,
loc,
},
type: BOOL,
loc,
},
then: [
...(plan.slot !== null && !insideFinallyTry(stmt, info.ctor)
? [
{
kind: "if" as const,
cond: {
kind: "libCall" as const,
fn: "dyn.typedRefIs" as const,
args: [
read,
{
kind: "strLit" as const,
value: typeKey(instanceType(info)),
type: STRING,
loc,
},
],
type: BOOL,
loc,
},
then: [
{
kind: "assign" as const,
localId: plan.slot.id,
value: lowerer.coerceToExpected(
lowerer.coerceToExpected(read, instanceType(info)),
plan.slot.type,
),
loc,
},
{ kind: "return" as const, value: null, loc },
],
else_: null,
loc,
},
]
: []),
{
kind: "runtimeFence",
code: diag.code,
message: `${diag.message} [${diag.code} at ${loc.file}:${line}]`,
loc,
},
],
else_: null,
loc,
},
{ kind: "return", value: null, loc },
],
loc,
};
}
/** The value of a construction of exactly class `C`: the constructed
* instance, or the instance the constructor returned. Unchanged for
* classes without a result slot. The slot is read and cleared by a
* per-class helper, so every construction site (direct `new`, class
* values, the checked dynamic dispatchers) shares one protocol. */
export function constructedValue(lowerer: Lowerer, info: ClassInfo, constructed: IrExpr): IrExpr {
const slot = constructorReturnPlan(lowerer, info)?.slot;
if (slot === null || slot === undefined) return constructed;
const loc = constructed.loc;
const type = instanceType(info);
const name = `%${info.def.name}.%constructed`;
if (!lowerer.liftedFns.some((fn) => fn.name === name)) {
const instance = { id: "instance.0", name: "instance", type, mutable: false };
const returned = { id: "returned.0", name: "returned", type: slot.type, mutable: false };
lowerer.liftedFns.push({
name,
params: [{ localId: instance.id, name: instance.name, type }],
returnType: type,
locals: [instance, returned],
body: [
{ kind: "varDecl", localId: returned.id, init: varRef(slot.id, slot.type, loc), loc },
{ kind: "assign", localId: slot.id, value: lowerer.wrappedUndefined(slot.type, loc)!, loc },
{
kind: "return",
value: {
kind: "orDefault",
left: varRef(returned.id, slot.type, loc),
right: varRef(instance.id, type, loc),
type,
loc,
},
loc,
},
],
loc,
});
}
return { kind: "call", callee: name, args: [constructed], type, loc };
}
/** A construction through a class value whose static class is `info`: the
* value holds `info` itself or a descendant. Descendants of a class with a
* result slot refuse at their `super()`, so only `info`'s own slot can
* apply; a descendant with its own slot has no static home here. */
export function constructedFromValue(
lowerer: Lowerer,
info: ClassInfo,
constructed: IrExpr,
blame: ts.Node,
): IrExpr {
const pending = [...info.subclasses];
while (pending.length > 0) {
const sub = pending.pop()!;
pending.push(...sub.subclasses);
const plan = constructorReturnPlan(lowerer, sub);
if (plan === null || plan.slot === null) continue;
const display = sub.def.name.replace(/^%|^%m\d+\./, "");
lowerer.unsupported(
"SC1090",
blame,
`constructing through a class value that may hold '${display}', whose constructor can return an existing instance`,
);
}
return constructedValue(lowerer, info, constructed);
}
/** `super()` into a class with a result slot: Node rebinds the subclass's
* `this` to the returned instance, which has no representation. */
export function refuseSuperIntoResultSlot(lowerer: Lowerer, base: ClassInfo, blame: ts.Node): void {
const plan = constructorReturnPlan(lowerer, base);
if (plan === null || plan.slot === null) return;
const display = base.def.name.replace(/^%|^%m\d+\./, "");
lowerer.unsupported(
"SC1090",
blame,
`extending a class whose constructor can return an existing instance ('super()' would rebind 'this' to the instance '${display}' returns)`,
);
}
@@ -1,5 +1,10 @@
import { lowerValueReturningConstructorNew } from "./js-dynamic-objects.js";
import { dynUndefinedExpr, nodeThrowExpr, varRef } from "../../ir/build.js";
import {
constructedFromValue,
constructedValue,
refuseSuperIntoResultSlot,
} from "./constructor-returns.js";
import { checkedIterableSpread } from "./checked-iterable-spread.js";
import { lowerReadlineUiNew } from "./builtins/readline.js";
import { collectClassMethodMutations } from "./class-method-mutations.js";
@@ -8020,6 +8025,7 @@ export function superCallStmt(
info.decl ?? info.ctor!,
)[0]!;
}
refuseSuperIntoResultSlot(lowerer, base, info.ctor ?? info.decl!);
lowerer.noteSuperConstructorEdge(`%${base.def.name}.constructor`);
return {
kind: "exprStmt",
@@ -8429,21 +8435,21 @@ function lowerProgramClassNew(
: lowerer.completeArgs(expr.arguments ?? [], info.ctorParams, loc, expr);
for (let current: ClassInfo | null = info; current; current = current.base) {
if (current.def.baseValueGlobal)
return {
return constructedValue(lowerer, info, {
kind: "newValue",
callee: classValueRef(lowerer, info, expr),
args,
type: { kind: "object", className: info.def.name },
loc,
};
});
}
return {
return constructedValue(lowerer, info, {
kind: "new",
className: info.def.name,
args,
type: { kind: "object", className: info.def.name },
loc,
};
});
}
function assignedThisFieldType(lowerer: Lowerer, expr: ts.NewExpression): IrType | null {
@@ -8731,13 +8737,18 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
below(info);
const callee: IrExpr = { kind: "varRef", localId: g.id, type: g.type, loc };
const args = lowerer.completeArgs(expr.arguments ?? [], info.ctorParams, loc, expr);
return {
kind: "newValue",
callee,
args,
type: { kind: "object", className: info.def.name },
loc,
};
return constructedFromValue(
lowerer,
info,
{
kind: "newValue",
callee,
args,
type: { kind: "object", className: info.def.name },
loc,
},
expr,
);
}
}
}
@@ -9638,13 +9649,18 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
};
below(info);
const args = lowerer.completeArgs(expr.arguments ?? [], info.ctorParams, loc, expr);
return {
kind: "newValue",
callee,
args,
type: { kind: "object", className: info.def.name },
loc,
};
return constructedFromValue(
lowerer,
info,
{
kind: "newValue",
callee,
args,
type: { kind: "object", className: info.def.name },
loc,
},
expr,
);
}
if (calleeT?.kind === "union") {
const def = lowerer.unions.get(calleeT.unionId);
@@ -14,6 +14,7 @@ import { processDataMemberValue } from "./builtins/process.js";
import { requireInitFlagStmt } from "./require-initialization.js";
import { isJsonRequireDecl } from "./json-require.js";
import { bindingSource } from "../binding-source.js";
import { lowerConstructorReturn } from "./constructor-returns.js";
import type { Lowerer } from "./lowerer.js";
import {
arrayValueRead,
@@ -1370,6 +1371,11 @@ export function lowerStmt(lowerer: Lowerer, stmt: ts.Statement): IrStmt | IrStmt
// incompatible boundary keeps the ordinary per-statement refusal.
let value: IrExpr | null = null;
if (stmt.expression) {
// A constructor's returned object replaces the constructed `this`.
if (lowerer.currentClass?.ctor) {
const constructorReturn = lowerConstructorReturn(lowerer, stmt);
if (constructorReturn !== null) return constructorReturn;
}
return lowerer.lowerReturnStmt(stmt.expression, locOf(stmt));
} else if (lowerer.ctx.returnType.kind === "union") {
value = lowerer.wrappedUndefined(lowerer.ctx.returnType, locOf(stmt));
@@ -4,6 +4,7 @@ import {
lowerLifetimeRegionCall,
lowerLifetimeRegionNew,
} from "./lifetime-regions.js";
import type { ConstructorReturnPlan } from "./constructor-returns.js";
import { internTableType, lowerInternTableCall, lowerInternTableNew } from "./intern-tables.js";
import { isExperimentalDeclarationFile } from "../experimental-import.js";
import {
@@ -2706,6 +2707,9 @@ export class Lowerer {
* by class name (cycle-initialization.ts). */
readonly classInitFlags = new Map<string, CycleInitFlag>();
readonly cycleInitDeclarations = new Map<ts.Statement, CycleInitFlag[]>();
/** Each class's constructor return plan by class name
* (constructor-returns.ts); null when no return can yield an object. */
readonly constructorReturnPlans = new Map<string, ConstructorReturnPlan | null>();
/** CommonJS require declarations whose bindings code running before the
* require can read, and those references (require-initialization.ts). */
readonly requireInitFlags = new Map<ts.VariableDeclaration, RequireInitFlag>();
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/constructor-returns-instance.js"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/constructor-returns-instance.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/diagnostics/constructor-returns-instance-super.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/diagnostics/constructor-returns-object.ts"
],
"diags": []
}
@@ -0,0 +1,45 @@
// JavaScript constructors whose returned value is typed only at run time:
// an instance of the class replaces `this`, a primitive keeps it.
class Connection {
constructor(url) {
if (Connection.open && Connection.open.url === url) return Connection.open;
this.url = url;
this.uses = 0;
Connection.open = this;
}
use() {
this.uses++;
return this;
}
}
const c1 = new Connection("db://a").use();
const c2 = new Connection("db://a").use();
const c3 = new Connection("db://b").use();
console.log(c1 === c2, c2.uses, c3 === c1, c3.uses, c3.url);
class Counter {
constructor(start, answer) {
this.count = start;
return answer;
}
}
console.log(new Counter(1, 5).count, new Counter(2, "x").count, new Counter(3, null).count);
console.log(new Counter(4, undefined).count, new Counter(5, true).count);
const kept = new Counter(6, 0);
console.log(new Counter(7, kept) === kept, kept.count);
const memoCache = new Map();
let memoCreated = 0;
class Memo {
constructor(key) {
const hit = memoCache.get(key);
if (hit !== undefined) return hit;
this.key = key;
this.created = ++memoCreated;
memoCache.set(key, this);
}
}
const m1 = new Memo("k");
const m2 = new Memo("k");
const m3 = new Memo("j");
console.log(m1 === m2, m3 === m1, m2.created, m3.created, memoCreated);
@@ -0,0 +1,112 @@
// A constructor's returned object replaces the constructed instance when it
// is an object; primitive, bare and `this` returns answer the instance.
class Registry {
static current: Registry | undefined = undefined;
readonly id: number = 0;
entries: string[] = [];
constructor(id: number) {
if (Registry.current) return Registry.current;
this.id = id;
Registry.current = this;
}
add(entry: string): this {
this.entries.push(entry);
return this;
}
}
const first = new Registry(1).add("a");
const second = new Registry(2).add("b");
console.log(first === second, second.id, second.entries.join(","));
// An instance returned only on some paths.
class Slot {
static previous: Slot | undefined = undefined;
label: string;
constructor(label: string, reuse: boolean) {
this.label = label;
const previous = Slot.previous;
Slot.previous = this;
if (reuse && previous) return previous;
}
}
const s1 = new Slot("one", true);
const s2 = new Slot("two", true);
const s3 = new Slot("three", false);
console.log(s1.label, s2.label, s3.label, s2 === s1, s3 === s2);
// Constructing the class again inside its own constructor.
class Node2 {
static made = 0;
value: number;
child: Node2 | null = null;
constructor(depth: number, share: Node2 | null) {
Node2.made++;
this.value = depth;
if (share !== null) return share;
if (depth > 0) this.child = new Node2(depth - 1, null);
}
}
const root = new Node2(2, null);
const alias = new Node2(9, root);
console.log(alias === root, alias.value, root.child?.value, root.child?.child?.value, Node2.made);
// A constructor that throws leaves later constructions unaffected.
class Guarded {
static kept: Guarded | undefined = undefined;
n: number;
constructor(n: number) {
if (n < 0) throw new RangeError(`negative: ${n}`);
this.n = n;
if (n === 0 && Guarded.kept) return Guarded.kept;
Guarded.kept = this;
}
}
try {
new Guarded(-1);
} catch (e) {
console.log((e as Error).message);
}
const g1 = new Guarded(5);
const g2 = new Guarded(0);
const g3 = new Guarded(7);
console.log(g1.n, g2.n, g3.n, g1 === g2, g3 === g1);
// Primitive, bare and `this` returns keep the constructed instance.
class Plain {
v = 3;
constructor(mode: number) {
if (mode === 1) return;
if (mode === 2) return this;
}
}
const plain = new Plain(2);
console.log(new Plain(0).v, new Plain(1).v, plain.v, plain instanceof Plain);
// The derived class's own constructor may return an instance of itself.
class Animal {
sound = "...";
}
class Dog extends Animal {
static pack: Dog[] = [];
name: string;
constructor(name: string) {
super();
this.name = name;
this.sound = "woof";
const known = Dog.pack.find((d) => d.name === name);
if (known) return known;
Dog.pack.push(this);
}
}
const rex = new Dog("rex");
const again = new Dog("rex");
const fido = new Dog("fido");
console.log(rex === again, fido === rex, Dog.pack.length, again.sound, again instanceof Animal);
// Constructions through class values take the returned instance too.
const Ctor: typeof Registry = Registry;
console.log(new Ctor(3) === first);
function build<T>(make: new (id: number) => T, id: number): T {
return new make(id);
}
console.log(build(Registry, 4) === first, build(Registry, 5).entries.length);
@@ -0,0 +1,15 @@
// `super()` into a constructor that can return an existing instance would
// rebind the subclass's `this` to that instance.
class Shared {
static one: Shared | undefined = undefined;
constructor() {
if (Shared.one) return Shared.one;
Shared.one = this;
}
}
class Derived extends Shared {
constructor() {
super();
}
}
console.log(new Derived() instanceof Derived);
@@ -0,0 +1,10 @@
// Node's `new Point()` answers the object a constructor returns. A plain
// object has no representation as a Point instance, so the return refuses
// instead of answering the constructed instance.
class Point {
x = 1;
constructor() {
return { x: 2 };
}
}
console.log(new Point().x);
@@ -0,0 +1,6 @@
constructor-returns-instance-super.ts:11:3 - error SC1090: extending a class whose constructor can return an existing instance ('super()' would rebind 'this' to the instance 'Shared' returns) is not supported yet
10 | class Derived extends Shared {
11 | constructor() {
| ^~~~~~~~~~~~~~~
12 | super();
@@ -0,0 +1,6 @@
constructor-returns-object.ts:7:5 - error SC1090: returning an object other than an instance of the class from a constructor (Node's 'new Point()' answers the returned object, which has no representation as a 'Point' instance) is not supported yet
6 | constructor() {
7 | return { x: 2 };
| ^~~~~~~~~~~~~~~~
8 | }
+52
View File
@@ -1013,3 +1013,55 @@ console.log(typeof Derived);
);
});
});
describe("constructors returning objects (JS lane)", () => {
test("a returned plain object is a named deferred fence", async () => {
// Node's `new Box(2)` answers `{ size: 99 }`; a Box instance cannot
// hold that object, so the return refuses instead of answering `this`.
const r = await compileAndRun(
"constructor-returns-object",
`'use strict';
class Box {
constructor(size) {
this.size = size;
if (size > 1) return { size: 99 };
}
}
console.log(new Box(1).size);
console.log(new Box(2));
`,
"cjs",
);
expect(r.exitCode).toBe(1);
expect(r.stdout).toBe("1\n");
expect(r.stderr).toMatch(
/^Uncaught Error: returning an object other than an instance of the class from a constructor \(Node's 'new Box\(\)' answers the returned object, .*\) is not supported yet \[SC1090 at .*constructor-returns-object\.cjs:5\]\n$/,
);
});
test("an untyped returned value fences only when it holds a foreign object", async () => {
// Primitive results answer the constructed instance, as in Node; an
// object that is not a Wrap instance refuses.
const r = await compileAndRun(
"constructor-returns-untyped",
`'use strict';
class Wrap {
constructor(value) {
this.value = value;
return value;
}
}
console.log(new Wrap(1).value, new Wrap('s').value, new Wrap(null).value);
const w = new Wrap(0);
console.log(new Wrap(w) === w);
console.log(new Wrap({ a: 1 }));
`,
"cjs",
);
expect(r.exitCode).toBe(1);
expect(r.stdout).toBe("1 s null\ntrue\n");
expect(r.stderr).toMatch(
/^Uncaught Error: returning an object other than an instance of the class from a constructor .* is not supported yet \[SC1090 at .*constructor-returns-untyped\.cjs:5\]\n$/,
);
});
});