mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-28 06:53:42 +08:00
Hand back untyped wrapper results at the slot's type, and no-op transpiled prototype resets
- A function adapter whose source returns `any`/`unknown` (a generic
wrapper's result handed back `as F`) converts the result through the
checked-dynamic bridge when the slot's result is checkable: a record
checks its shape, and a Promise keeps its identity and checks its
payload when it settles. Before, these adapters threw "the call has
no exact lowering" when called.
- In a class constructor, `Object.setPrototypeOf(this, new.target.prototype)`
(directly, through a const, or the `if (Object.setPrototypeOf) ... else
this.__proto__ = ...` form), and `Object.setPrototypeOf(this,
C.prototype)` in C's own constructor when no class extends C, are
no-ops: a compiled instance already has that prototype.
- Object.assign between records copies a source field into a wider
target field (`{ bail: true }` into `bail?: boolean`).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
7d1d440b7b
commit
0e8b4cf990
@@ -4,6 +4,7 @@ import { InternalCompilerError } from "../../../errors.js";
|
||||
import type { IrExpr, IrParam, IrStmt, IrType, SrcLoc } from "../../../ir/ir.js";
|
||||
import {
|
||||
canAdaptDynFuncTo,
|
||||
canDynCheckTo,
|
||||
DYN,
|
||||
F64,
|
||||
isUnitType,
|
||||
@@ -208,6 +209,19 @@ export function funcCoerceAdapter(
|
||||
voidRet = toT.ret.kind === "dyn" ? "dyn" : "strand";
|
||||
}
|
||||
}
|
||||
// An untyped result (a generic wrapper's `any` handed back `as F`) takes
|
||||
// the checked-dynamic extraction to the slot's result: a record checks
|
||||
// its shape, a Promise keeps its identity and checks its payload when it
|
||||
// settles.
|
||||
const checkedRet =
|
||||
strandRet &&
|
||||
fromT.ret.kind === "dyn" &&
|
||||
canDynCheckTo(
|
||||
toT.ret,
|
||||
(id) => lowerer.shapes.get(id),
|
||||
(id) => lowerer.unions.get(id),
|
||||
);
|
||||
if (checkedRet) strandRet = false;
|
||||
const key = `fnadapt:${typeKey(fromT)}:${typeKey(toT)}`;
|
||||
const existing = lowerer.coercions.retags.get(key);
|
||||
if (existing) return existing;
|
||||
@@ -307,6 +321,8 @@ export function funcCoerceAdapter(
|
||||
`a '${lowerer.fmt(fromT)}' function invoked through a '${lowerer.fmt(toT)}' slot (the result cannot convert to '${lowerer.fmt(toT.ret)}' — the checker's loose function compatibility admitted the assignment, but the call has no exact lowering)`,
|
||||
),
|
||||
];
|
||||
} else if (checkedRet) {
|
||||
body = [{ kind: "return", value: { kind: "dynCheck", value: call, type: toT.ret, loc }, loc }];
|
||||
} else {
|
||||
const result = lowerer.coerceToExpected(call, toT.ret);
|
||||
if (!typeEquals(result.type, toT.ret))
|
||||
|
||||
@@ -6699,6 +6699,7 @@ function recordAssignHelper(
|
||||
const helper = `%obj.assign.${lowerer.arrHofHelpers.size}`;
|
||||
lowerer.arrHofHelpers.set(key, helper);
|
||||
const sShape = lowerer.shapes.get(srcShapeId)!;
|
||||
const tShape = lowerer.shapes.get(targetShapeId)!;
|
||||
const tT: IrType = { kind: "record", shapeId: targetShapeId };
|
||||
const sT: IrType = { kind: "record", shapeId: srcShapeId };
|
||||
const tRef: IrExpr = { kind: "varRef", localId: "t.0", type: tT, loc };
|
||||
@@ -6713,12 +6714,13 @@ function recordAssignHelper(
|
||||
type: f.type,
|
||||
loc,
|
||||
};
|
||||
const targetType = tShape.fields.find((x) => x.name === f.name)!.type;
|
||||
const set: IrStmt = {
|
||||
kind: "recordSet",
|
||||
obj: tRef,
|
||||
shapeId: targetShapeId,
|
||||
field: f.name,
|
||||
value: get,
|
||||
value: typeEquals(targetType, f.type) ? get : lowerer.coerceToExpected(get, targetType),
|
||||
loc,
|
||||
};
|
||||
const present = lowerer.recordFieldPresent(sRef, srcShapeId, f.name, loc);
|
||||
@@ -7338,7 +7340,12 @@ function lowerObjectStaticCall(
|
||||
!shapeHasAccessorSlots(sShape) &&
|
||||
sShape.fields.every((sf) => {
|
||||
const tf = tShape.fields.find((x) => x.name === sf.name);
|
||||
return tf !== undefined && typeEquals(tf.type, sf.type);
|
||||
// A source value converts into a wider target field (`{ bail:
|
||||
// true }` into `bail?: boolean`).
|
||||
return (
|
||||
tf !== undefined &&
|
||||
(typeEquals(tf.type, sf.type) || lowerer.coercibleValue(sf.type, tf.type))
|
||||
);
|
||||
});
|
||||
if (ok) {
|
||||
const loc = locOf(call);
|
||||
|
||||
@@ -4824,6 +4824,15 @@ function nullishBranches(
|
||||
export function lowerCondition(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
let e: ts.Expression = expr;
|
||||
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
// `if (Object.setPrototypeOf)`: the feature test of transpiled Error
|
||||
// subclasses' prototype reset. Node always has it.
|
||||
if (
|
||||
ts.isPropertyAccessExpression(e) &&
|
||||
!e.questionDotToken &&
|
||||
e.name.text === "setPrototypeOf" &&
|
||||
lowerer.isStdlibGlobal(e.expression, "Object")
|
||||
)
|
||||
return { kind: "boolLit", value: true, type: BOOL, loc: locOf(e) };
|
||||
if (ts.isPropertyAccessExpression(e)) {
|
||||
const presence = lowerPromiseThenPresence(lowerer, e, {
|
||||
kind: "boolLit",
|
||||
|
||||
@@ -1350,6 +1350,7 @@ function lowerLabeled(lowerer: Lowerer, stmt: ts.LabeledStatement): IrStmt | IrS
|
||||
}
|
||||
|
||||
export function lowerStmt(lowerer: Lowerer, stmt: ts.Statement): IrStmt | IrStmt[] | null {
|
||||
if (ownPrototypeResetStatement(lowerer, stmt)) return { kind: "block", body: [], loc: locOf(stmt) };
|
||||
if (ts.isClassDeclaration(stmt)) {
|
||||
if (!stmt.name) lowerer.unsupported("SC1090", stmt, "anonymous local class declarations");
|
||||
const info = lowerer.lowerClassExpressionInfo(stmt);
|
||||
@@ -10849,3 +10850,125 @@ function isStringTyped(t: ts.Type): boolean {
|
||||
if (t.isUnionType()) return ts.constituentTypes(t).every(isStringTyped);
|
||||
return (t.flags & (ts.TypeFlags.String | ts.TypeFlags.StringLiteral)) !== 0;
|
||||
}
|
||||
|
||||
/** The Error-subclass prototype reset of transpiled code, inside a class
|
||||
* constructor:
|
||||
*
|
||||
* const proto = new.target.prototype;
|
||||
* if (Object.setPrototypeOf) Object.setPrototypeOf(this, proto);
|
||||
* else this.__proto__ = proto;
|
||||
*
|
||||
* (also `Object.setPrototypeOf(this, new.target.prototype)` and
|
||||
* `Object.setPrototypeOf(this, C.prototype)` in C's own constructor when no
|
||||
* class extends C). A compiled instance already has exactly that prototype,
|
||||
* so each statement is a no-op. True for those statements. */
|
||||
function ownPrototypeResetStatement(lowerer: Lowerer, stmt: ts.Statement): boolean {
|
||||
if (ts.isVariableStatement(stmt)) {
|
||||
const decls = stmt.declarationList.declarations.nodes;
|
||||
if (decls.length !== 1 || (stmt.declarationList.flags & ts.NodeFlags.Const) === 0) return false;
|
||||
const decl = decls[0]!;
|
||||
if (!ts.isIdentifier(decl.name) || !decl.initializer) return false;
|
||||
const ctor = enclosingConstructor(decl);
|
||||
if (!ctor || !ownPrototypeRef(lowerer, decl.initializer, ctor)) return false;
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(decl.name);
|
||||
if (!symbol) return false;
|
||||
// Every other use of the binding must be one of the reset statements.
|
||||
let onlyResets = true;
|
||||
ts.walkPreorder(ctor, (n) => {
|
||||
if (!onlyResets) return "stop";
|
||||
if (!ts.isIdentifier(n) || n === decl.name) return undefined;
|
||||
if (lowerer.checker.getSymbolAtLocation(n) !== symbol) return undefined;
|
||||
const statement = n.parent?.parent;
|
||||
if (
|
||||
!statement ||
|
||||
!ts.isExpressionStatement(statement) ||
|
||||
resetTarget(lowerer, statement.expression) !== n
|
||||
)
|
||||
onlyResets = false;
|
||||
return undefined;
|
||||
});
|
||||
return onlyResets;
|
||||
}
|
||||
if (!ts.isExpressionStatement(stmt)) return false;
|
||||
const ctor = enclosingConstructor(stmt);
|
||||
if (!ctor) return false;
|
||||
const target = resetTarget(lowerer, stmt.expression);
|
||||
return target !== null && ownPrototypeRef(lowerer, target, ctor);
|
||||
}
|
||||
|
||||
/** The prototype operand of `Object.setPrototypeOf(this, P)` or
|
||||
* `this.__proto__ = P`, or null. */
|
||||
function resetTarget(lowerer: Lowerer, expr: ts.Expression): ts.Expression | null {
|
||||
if (
|
||||
ts.isCallExpression(expr) &&
|
||||
expr.arguments.nodes.length === 2 &&
|
||||
expr.arguments.nodes[0]!.kind === ts.SyntaxKind.ThisKeyword &&
|
||||
ts.isPropertyAccessExpression(expr.expression) &&
|
||||
expr.expression.name.text === "setPrototypeOf" &&
|
||||
lowerer.isStdlibGlobal(expr.expression.expression, "Object")
|
||||
)
|
||||
return expr.arguments.nodes[1]!;
|
||||
if (
|
||||
ts.isBinaryExpression(expr) &&
|
||||
expr.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
||||
ts.isPropertyAccessExpression(expr.left) &&
|
||||
expr.left.expression.kind === ts.SyntaxKind.ThisKeyword &&
|
||||
expr.left.name.text === "__proto__"
|
||||
)
|
||||
return expr.right;
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The class constructor whose body holds `node`, crossing only statements
|
||||
* and blocks (any nested function rebinds `this`). */
|
||||
function enclosingConstructor(node: ts.Node): ts.ConstructorDeclaration | null {
|
||||
for (let cur = node.parent; cur !== undefined; cur = cur.parent) {
|
||||
if (ts.isConstructorDeclaration(cur)) return cur;
|
||||
if (ts.isFunctionLike(cur) || (ts.isClassDeclaration(cur) || ts.isClassExpression(cur)) || ts.isSourceFile(cur)) return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Whether `node` denotes the prototype of the object `ctor` constructs:
|
||||
* `new.target.prototype`, a const holding it, or the class's own
|
||||
* `C.prototype` when no class extends C. */
|
||||
function ownPrototypeRef(
|
||||
lowerer: Lowerer,
|
||||
node: ts.Expression,
|
||||
ctor: ts.ConstructorDeclaration,
|
||||
): boolean {
|
||||
while (ts.isParenthesizedExpression(node)) node = node.expression;
|
||||
if (ts.isPropertyAccessExpression(node) && node.name.text === "prototype") {
|
||||
const base = node.expression;
|
||||
if (
|
||||
ts.isMetaProperty(base) &&
|
||||
base.keywordToken === ts.SyntaxKind.NewKeyword &&
|
||||
base.name.text === "target"
|
||||
)
|
||||
return true;
|
||||
const cls = ctor.parent;
|
||||
if (!ts.isIdentifier(base) || !(ts.isClassDeclaration(cls) || ts.isClassExpression(cls))) return false;
|
||||
const symbol = lowerer.resolveValueSymbol(base);
|
||||
const info = symbol ? lowerer.classBySymbol.get(symbol) : undefined;
|
||||
if (!info || lowerer.checker.valueDeclarationOf(symbol!) !== cls) return false;
|
||||
for (const other of lowerer.classes.values()) {
|
||||
if (other !== info && lowerer.isSubclassOf(other.def.name, info.def.name)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (ts.isIdentifier(node)) {
|
||||
const symbol = lowerer.resolveValueSymbol(node);
|
||||
const decl = symbol ? lowerer.checker.valueDeclarationOf(symbol) : undefined;
|
||||
return (
|
||||
decl !== undefined &&
|
||||
ts.isVariableDeclaration(decl) &&
|
||||
decl.initializer !== undefined &&
|
||||
ts.isVariableDeclarationList(decl.parent) &&
|
||||
(decl.parent.flags & ts.NodeFlags.Const) !== 0 &&
|
||||
enclosingConstructor(decl) === ctor &&
|
||||
!ts.isIdentifier(decl.initializer) &&
|
||||
ownPrototypeRef(lowerer, decl.initializer, ctor)
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -103,3 +103,17 @@ function describeApiError(error: Error & { status?: number }): string {
|
||||
return `${error.message}:${error.status ?? "none"}`;
|
||||
}
|
||||
console.log(describeApiError(gateway), describeApiError(new Error("plain")));
|
||||
|
||||
// Object.assign into a record whose fields are wider than the source's.
|
||||
interface BuildFailure {
|
||||
message: string;
|
||||
bail?: boolean;
|
||||
code?: string;
|
||||
}
|
||||
function failure(message: string, bail: boolean): BuildFailure {
|
||||
const err: BuildFailure = { message };
|
||||
if (bail) Object.assign(err, { bail: true });
|
||||
return Object.assign(err, { code: "E_BUILD" });
|
||||
}
|
||||
for (const f of [failure("stop", true), failure("go", false)])
|
||||
console.log(f.message, f.bail, f.code);
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
// Generic wrappers returning their untyped result `as F`: async functions,
|
||||
// records with unknown fields, and rejections passing through.
|
||||
function trace<F extends (...args: any[]) => any>(name: string, fn: F): F {
|
||||
return ((...args: Parameters<F>) => {
|
||||
console.log("span", name);
|
||||
return fn(...args);
|
||||
}) as F;
|
||||
}
|
||||
function traceThis<F extends (...args: any[]) => any>(name: string, fn: F): F {
|
||||
return function (this: unknown, ...args: Parameters<F>) {
|
||||
const result: unknown = fn.apply(this, args);
|
||||
console.log("span", name, typeof result);
|
||||
return result;
|
||||
} as F;
|
||||
}
|
||||
function lookup(key: string): any {
|
||||
return key === "set" ? 7 : undefined;
|
||||
}
|
||||
const fetchCount = trace("fetchCount", async (base: number): Promise<number> => {
|
||||
await null;
|
||||
return base + 1;
|
||||
});
|
||||
const fetchUser = traceThis("fetchUser", async (id: string): Promise<{ id: string; admin: boolean }> => ({
|
||||
id,
|
||||
admin: id === "root",
|
||||
}));
|
||||
const fails = trace("fails", async (why: string): Promise<number> => {
|
||||
throw new Error(why);
|
||||
});
|
||||
const summarize = trace("summarize", (limit = lookup("unset") ?? 2) => ({ kept: limit, dropped: 1 }));
|
||||
const measure = traceThis("measure", (n: number) => ({ size: n, label: lookup("set") }));
|
||||
async function main(): Promise<void> {
|
||||
console.log(await fetchCount(1));
|
||||
const user = await fetchUser("root");
|
||||
console.log(user.id, user.admin);
|
||||
try {
|
||||
await fails("nope");
|
||||
} catch (e) {
|
||||
console.log("caught", e instanceof Error ? e.message : String(e));
|
||||
}
|
||||
const s = summarize();
|
||||
console.log(s.kept, s.dropped, summarize(5).kept);
|
||||
const m = measure(3);
|
||||
console.log(m.size, m.label);
|
||||
const pending = fetchCount(10);
|
||||
console.log("before");
|
||||
console.log(await pending);
|
||||
}
|
||||
main();
|
||||
export {};
|
||||
@@ -0,0 +1,27 @@
|
||||
// An Error subclass in the shape of a validation library's: the constructor
|
||||
// resets the prototype to new.target's, as transpiled code does.
|
||||
export class IssueError extends Error {
|
||||
constructor(issues) {
|
||||
super();
|
||||
this.issues = [];
|
||||
const actualProto = new.target.prototype;
|
||||
if (Object.setPrototypeOf) {
|
||||
Object.setPrototypeOf(this, actualProto);
|
||||
} else {
|
||||
this.__proto__ = actualProto;
|
||||
}
|
||||
this.name = "IssueError";
|
||||
this.issues = issues;
|
||||
}
|
||||
get message() {
|
||||
return JSON.stringify(this.issues);
|
||||
}
|
||||
get isEmpty() {
|
||||
return this.issues.length === 0;
|
||||
}
|
||||
}
|
||||
export class StrictIssueError extends IssueError {
|
||||
get strict() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Constructors that reset the instance prototype, the transpiled idiom for
|
||||
// Error subclasses.
|
||||
import { IssueError, StrictIssueError } from "./issues.js";
|
||||
class AppError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
Object.setPrototypeOf(this, new.target.prototype);
|
||||
this.name = "AppError";
|
||||
}
|
||||
describe(): string {
|
||||
return `${this.name}: ${this.message}`;
|
||||
}
|
||||
}
|
||||
class NotFound extends AppError {
|
||||
constructor(what: string) {
|
||||
super(`${what} not found`);
|
||||
Object.setPrototypeOf(this, NotFound.prototype);
|
||||
this.name = "NotFound";
|
||||
}
|
||||
}
|
||||
const app = new AppError("boom");
|
||||
const missing = new NotFound("user");
|
||||
console.log(app.describe(), app instanceof AppError, app instanceof Error);
|
||||
console.log(missing.describe(), missing instanceof NotFound, missing instanceof AppError);
|
||||
const issues = new IssueError([{ path: "a" }]);
|
||||
console.log(issues.message, issues.isEmpty, issues instanceof IssueError, issues.name);
|
||||
const strict = new StrictIssueError([]);
|
||||
console.log(strict.isEmpty, strict.strict, strict instanceof IssueError, String(strict instanceof StrictIssueError));
|
||||
try {
|
||||
throw new NotFound("page");
|
||||
} catch (e) {
|
||||
console.log(e instanceof NotFound ? e.describe() : "other");
|
||||
}
|
||||
Reference in New Issue
Block a user