mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-26 06:34:20 +08:00
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:
co-authored by
Claude Opus 5.5
parent
ea7bcd9846
commit
11c97074e2
@@ -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>();
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/constructor-returns-instance.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/constructor-returns-instance.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/constructor-returns-instance-super.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -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 | }
|
||||
@@ -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$/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user