mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
Include star re-exports in namespace objects, and read causes and instanceof Error like Node under a runtime Error parent
- A module namespace converted to an untyped object, and a computed namespace read, use the module's resolved exports, which include the members of `export *` declarations. - `super(message, options)` through a runtime Error parent installs the cause as Error does: an inherited `cause` counts, the value is read through its getter, and the getter's exception propagates. - `value instanceof Error` on an instance of a class whose heritage is evaluated at run time tests that class's prototype chain for Error.prototype. A program with such a class checks prototype chains for `instanceof Error` on untyped values. - The cache of names written on the global object is a Lowerer field instead of a WeakMap keyed by the Lowerer. - errors.test.ts: `"prototype" in Map` answers from the builtin constructor's keys now, so it leaves the refused reads and joins a check of `in` probes against Node's answers.
This commit is contained in:
@@ -908,6 +908,11 @@ export class ClassDynamicDispatch {
|
||||
private readonly prototypeClasses = new Set<string>();
|
||||
private prototypeIdentityHelper: IrFunction | null = null;
|
||||
private readonly prototypeIdentityClasses = new Set<string>();
|
||||
/** `value instanceof Error` through Error.prototype: boxed instances of
|
||||
* classes with a heritage evaluated at run time test their prototype
|
||||
* data's chain. */
|
||||
private errorPrototypeHelper: IrFunction | null = null;
|
||||
private readonly errorPrototypeClasses = new Set<string>();
|
||||
private readonly instancePrototypes = new Map<string, { fn: IrFunction; classes: Set<string> }>();
|
||||
private readonly checkedCasts = new Map<string, { fn: IrFunction; classes: Set<string> }>();
|
||||
/** The for-in key helpers, by libCall (dyn and native string[] keys). */
|
||||
@@ -1114,6 +1119,49 @@ export class ClassDynamicDispatch {
|
||||
});
|
||||
changed = true;
|
||||
}
|
||||
if (
|
||||
this.errorPrototypeHelper &&
|
||||
!this.errorPrototypeClasses.has(className) &&
|
||||
hasRuntimeHeritage(info)
|
||||
) {
|
||||
this.errorPrototypeClasses.add(className);
|
||||
const loc = this.errorPrototypeHelper.loc;
|
||||
const type: IrType = { kind: "object", className };
|
||||
const value = varRef("value", DYN, loc);
|
||||
const prototype = classPrototypeData(lowerer, info, loc, {
|
||||
kind: "dynCheck",
|
||||
value,
|
||||
type,
|
||||
loc,
|
||||
});
|
||||
if (prototype)
|
||||
this.errorPrototypeHelper.body.unshift({
|
||||
kind: "if",
|
||||
cond: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.typedRefIs",
|
||||
args: [value, { kind: "strLit", value: typeKey(type), type: STRING, loc }],
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: [
|
||||
{
|
||||
kind: "return",
|
||||
value: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.inheritsErrorPrototype",
|
||||
args: [prototype],
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
],
|
||||
else_: null,
|
||||
loc,
|
||||
});
|
||||
changed = true;
|
||||
}
|
||||
if (
|
||||
this.serializesClasses &&
|
||||
!this.jsonPrototypes.has(className) &&
|
||||
@@ -1516,6 +1564,31 @@ export class ClassDynamicDispatch {
|
||||
}
|
||||
return { kind: "call", callee: cast.fn.name, args: [expr.value], type, loc: expr.loc };
|
||||
}
|
||||
if (expr.kind === "libCall" && expr.fn === "dyn.inheritsErrorPrototype") {
|
||||
if (!this.errorPrototypeHelper) {
|
||||
const loc = expr.loc;
|
||||
this.errorPrototypeHelper = {
|
||||
name: "%dyn.class.inheritsErrorPrototype",
|
||||
params: [{ localId: "value", name: "value", type: DYN }],
|
||||
locals: [{ id: "value", name: "value", type: DYN, mutable: false }],
|
||||
returnType: BOOL,
|
||||
loc,
|
||||
body: [
|
||||
{ kind: "return", value: { ...expr, args: [varRef("value", DYN, loc)] }, loc },
|
||||
],
|
||||
};
|
||||
lowerer.liftedFns.push(this.errorPrototypeHelper);
|
||||
this.generated.add(this.errorPrototypeHelper);
|
||||
changed = true;
|
||||
}
|
||||
return {
|
||||
kind: "call",
|
||||
callee: this.errorPrototypeHelper.name,
|
||||
args: expr.args,
|
||||
type: BOOL,
|
||||
loc: expr.loc,
|
||||
};
|
||||
}
|
||||
if (
|
||||
expr.kind === "libCall" &&
|
||||
expr.fn === "dyn.getPrototype" &&
|
||||
@@ -5942,3 +6015,11 @@ const INHERITED_OBJECT_METHODS: ReadonlySet<string> = new Set([
|
||||
"hasOwnProperty",
|
||||
"propertyIsEnumerable",
|
||||
]);
|
||||
|
||||
/** Whether a class or one of its bases has a heritage evaluated at run
|
||||
* time, whose prototype chain may reach Error.prototype. */
|
||||
function hasRuntimeHeritage(info: ClassInfo): boolean {
|
||||
for (let current: ClassInfo | null = info; current; current = current.base)
|
||||
if (current.callableBase) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -380,6 +380,13 @@ export function collectClassMethodMutations(
|
||||
markTarget(node.initializer, node);
|
||||
if (ts.isPropertyAccessExpression(node) && isErrorPrototypeValue(lowerer, node))
|
||||
lowerer.errorPrototypeValues = true;
|
||||
// A heritage evaluated at run time may be Error, which links the
|
||||
// class's instances to Error.prototype.
|
||||
if (
|
||||
(ts.isClassDeclaration(node) || ts.isClassExpression(node)) &&
|
||||
runtimeHeritage(lowerer, node)
|
||||
)
|
||||
lowerer.errorPrototypeValues = true;
|
||||
if (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) {
|
||||
const base = unwrap(node.expression);
|
||||
const prototype =
|
||||
@@ -621,3 +628,22 @@ export function collectClassMethodMutations(
|
||||
if (!receiverFields.has(field)) lowerer.receiverFreeCallbackFields.add(field);
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether a class's `extends` expression is a value computed at run time:
|
||||
* anything other than a name of a class declaration or of a builtin
|
||||
* constructor (`extends Parent` over `const Parent = params.Parent ?? Object`,
|
||||
* `extends make()`). */
|
||||
function runtimeHeritage(
|
||||
lowerer: Lowerer,
|
||||
node: ts.ClassDeclaration | ts.ClassExpression,
|
||||
): boolean {
|
||||
const heritage = node.heritageClauses?.nodes.find(
|
||||
(clause) => clause.token === ts.SyntaxKind.ExtendsKeyword,
|
||||
)?.types?.nodes[0]?.expression;
|
||||
if (heritage === undefined) return false;
|
||||
if (!ts.isIdentifier(heritage) && !ts.isPropertyAccessExpression(heritage)) return true;
|
||||
let symbol = lowerer.checker.getSymbolAtLocation(heritage);
|
||||
if (symbol === undefined) return true;
|
||||
if (symbol.flags & ts.SymbolFlags.Alias) symbol = lowerer.checker.getAliasedSymbol(symbol);
|
||||
return !(symbol.flags & ts.SymbolFlags.Class) && !lowerer.isStdlibSymbol(symbol);
|
||||
}
|
||||
|
||||
@@ -80,6 +80,7 @@ const DISPATCH_LIB_CALLS = new Set<string>([
|
||||
"dyn.propertyIsEnumerableComputed",
|
||||
"dyn.defineProperty",
|
||||
"dyn.getPrototype",
|
||||
"dyn.inheritsErrorPrototype",
|
||||
"dyn.reflectGet",
|
||||
"dyn.reflectSet",
|
||||
]);
|
||||
|
||||
@@ -4504,15 +4504,13 @@ function contextualArgumentCount(lowerer: Lowerer, fn: ts.Node): number | null {
|
||||
return count;
|
||||
}
|
||||
|
||||
const globalThisWrites = new WeakMap<Lowerer, Set<string>>();
|
||||
|
||||
/** The property names the program assigns on the global object, through
|
||||
* `globalThis` or a binding of its type (`(_a = globalThis).x = v`). */
|
||||
function globalThisWrittenNames(lowerer: Lowerer, globalType: ts.Type): Set<string> {
|
||||
const known = globalThisWrites.get(lowerer);
|
||||
const known = lowerer.globalThisWrittenNames;
|
||||
if (known) return known;
|
||||
const names = new Set<string>();
|
||||
globalThisWrites.set(lowerer, names);
|
||||
lowerer.globalThisWrittenNames = names;
|
||||
for (const sf of lowerer.program.getImplementationSourceFiles()) {
|
||||
if (sf.isDeclarationFile || !sf.text.includes("globalThis")) continue;
|
||||
ts.walkPreorder(sf, (node) => {
|
||||
|
||||
@@ -2969,9 +2969,14 @@ export class Lowerer {
|
||||
* ABI and shared prototype descriptors instead of call-site specialization. */
|
||||
readonly prototypeMethodAccesses = new Map<string, ts.Node>();
|
||||
/** Whether the program reads `Error.prototype`, the checked-dynamic
|
||||
* object an ES5 constructor function can inherit from: only then can a
|
||||
* value that is not an Error instance be `instanceof Error`. */
|
||||
* object an ES5 constructor function can inherit from, or has a class
|
||||
* whose heritage is evaluated at run time (`extends (Base ?? Error)`):
|
||||
* only then can a value that is not an Error instance be
|
||||
* `instanceof Error`. */
|
||||
errorPrototypeValues = false;
|
||||
/** The property names the program assigns on the global object
|
||||
* (property-access.ts), collected on first use. */
|
||||
globalThisWrittenNames: Set<string> | null = null;
|
||||
/** Well-known symbol methods (`sym:iterator`) the program may replace
|
||||
* through a symbol-keyed write: an instance or prototype property, or a
|
||||
* reflection call. */
|
||||
|
||||
@@ -32,12 +32,13 @@ export function lowerModuleNamespaceElement(
|
||||
try {
|
||||
const body: IrStmt[] = [];
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(source);
|
||||
symbol?.getExports().forEach((exported, key) => {
|
||||
for (const exported of symbol ? lowerer.checker.getExportsOfModule(symbol) : []) {
|
||||
const key = ts.symbolName(exported);
|
||||
const target =
|
||||
exported.flags & ts.SymbolFlags.Alias
|
||||
? lowerer.checker.getAliasedSymbol(exported)
|
||||
: exported;
|
||||
if (!(target.flags & ts.SymbolFlags.Value)) return;
|
||||
if (!(target.flags & ts.SymbolFlags.Value)) continue;
|
||||
const declaration = target.valueDeclaration;
|
||||
const identifier = declaration?.name as ts.Node | undefined;
|
||||
if (!identifier || !ts.isIdentifier(identifier)) {
|
||||
@@ -50,7 +51,7 @@ export function lowerModuleNamespaceElement(
|
||||
kind: "strEq",
|
||||
negated: false,
|
||||
left: varRef("key", STRING, loc),
|
||||
right: strLit(String(key), loc),
|
||||
right: strLit(key, loc),
|
||||
type: { kind: "bool" },
|
||||
loc,
|
||||
},
|
||||
@@ -58,7 +59,7 @@ export function lowerModuleNamespaceElement(
|
||||
else_: null,
|
||||
loc,
|
||||
});
|
||||
});
|
||||
}
|
||||
body.push({ kind: "return", value: dynUndefinedExpr(loc), loc });
|
||||
const fn: IrFunction = {
|
||||
name,
|
||||
@@ -111,10 +112,12 @@ export function moduleNamespaceObject(
|
||||
if (!symbol) return null;
|
||||
const exports: { key: string; identifier: ts.Identifier }[] = [];
|
||||
let exact = true;
|
||||
symbol.getExports().forEach((exported, key) => {
|
||||
// The module's resolved exports, which include `export *` members.
|
||||
for (const exported of lowerer.checker.getExportsOfModule(symbol)) {
|
||||
const key = ts.symbolName(exported);
|
||||
const target =
|
||||
exported.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(exported) : exported;
|
||||
if (!(target.flags & ts.SymbolFlags.Value)) return;
|
||||
if (!(target.flags & ts.SymbolFlags.Value)) continue;
|
||||
const declaration = target.valueDeclaration;
|
||||
const identifier = declaration?.name as ts.Node | undefined;
|
||||
const immutable =
|
||||
@@ -125,10 +128,10 @@ export function moduleNamespaceObject(
|
||||
(ts.getCombinedNodeFlags(declaration) & ts.NodeFlags.Const) !== 0));
|
||||
if (!identifier || !ts.isIdentifier(identifier) || !immutable) {
|
||||
exact = false;
|
||||
return;
|
||||
break;
|
||||
}
|
||||
exports.push({ key: String(key), identifier });
|
||||
});
|
||||
exports.push({ key, identifier });
|
||||
}
|
||||
if (!exact) return null;
|
||||
exports.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
|
||||
const name = `%module.object:${ns.type.moduleId}`;
|
||||
|
||||
@@ -631,6 +631,14 @@ export class CheckerFacade {
|
||||
return aliased;
|
||||
}
|
||||
|
||||
/** A module symbol's exports, with the members of its `export *`
|
||||
* declarations resolved (a symbol's own export table lists only the
|
||||
* module's own declarations). */
|
||||
getExportsOfModule(symbol: Ts7Symbol): readonly Ts7Symbol[] {
|
||||
this.ensureActive();
|
||||
return this.raw.getExportsOfModule(symbol);
|
||||
}
|
||||
|
||||
getDeclaredTypeOfSymbol(symbol: Ts7Symbol): Type {
|
||||
this.ensureActive();
|
||||
let type = this.cache.declaredTypeOfSymbol.get(symbol);
|
||||
|
||||
+1
@@ -2,6 +2,7 @@
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7487-namespace-spread-untyped/shapes.js",
|
||||
"<repo>/tests/corpus/7487-namespace-spread-untyped/extra.js",
|
||||
"<repo>/tests/corpus/7487-namespace-spread-untyped/all.js",
|
||||
"<repo>/tests/corpus/7487-namespace-spread-untyped/main.js"
|
||||
],
|
||||
"diags": []
|
||||
|
||||
@@ -5146,10 +5146,17 @@ void scr_dyn_class_super(ScrDyn *constructor, ScrDyn *receiver, ScrDyn *argument
|
||||
scr_dyn_release(value);
|
||||
if (scr_exc_pending()) return;
|
||||
}
|
||||
if (argc > 1 && argv[1]->kind == SCR_DYN_OBJ && scr_dyn_find_entry(argv[1], "cause", 5)) {
|
||||
ScrDyn *cause = scr_dyn_obj_get(argv[1], "cause", 5);
|
||||
/* InstallErrorCause: an options object with a `cause` property, own or
|
||||
* inherited, defines the value its getter or data property reads. */
|
||||
ScrDyn *options = argc > 1 ? argv[1] : NULL;
|
||||
if (options && options->kind != SCR_DYN_NULL &&
|
||||
(options->kind == SCR_DYN_FUNC || scr_dyn_is_object(options))) {
|
||||
ScrStr *key = scr_str_new("cause", 5);
|
||||
scr_dyn_define_hidden(receiver, key, cause ? cause : scr_dyn_undefined());
|
||||
if (scr_dyn_in(options, key) && !scr_exc_pending()) {
|
||||
ScrDyn *cause = scr_dyn_obj_read(options, "cause", 5);
|
||||
if (cause && !scr_exc_pending()) scr_dyn_define_hidden(receiver, key, cause);
|
||||
scr_dyn_release(cause);
|
||||
}
|
||||
scr_str_release(key);
|
||||
if (scr_exc_pending()) return;
|
||||
}
|
||||
|
||||
@@ -22,3 +22,30 @@ try {
|
||||
} catch (err) {
|
||||
console.log(err instanceof E, err.message);
|
||||
}
|
||||
console.log(e instanceof Error, new A() instanceof Error);
|
||||
try {
|
||||
throw new E("caught");
|
||||
} catch (err) {
|
||||
console.log(err instanceof Error, err.message);
|
||||
}
|
||||
// The cause option is read like Error's: inherited and accessor causes
|
||||
// count, and a throwing getter's exception propagates.
|
||||
const inherited = new E("inherited", Object.create({ cause: "from prototype" }));
|
||||
console.log(inherited.cause, Object.prototype.hasOwnProperty.call(inherited, "cause"));
|
||||
let reads = 0;
|
||||
const accessor = new E("accessor", {
|
||||
get cause() {
|
||||
reads++;
|
||||
return "from getter";
|
||||
},
|
||||
});
|
||||
console.log(accessor.cause, reads, "cause" in new E("none", {}), "cause" in new E("undef", { cause: undefined }));
|
||||
try {
|
||||
new E("throws", {
|
||||
get cause() {
|
||||
throw new Error("getter failed");
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err.message);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./shapes.js";
|
||||
export const own = "own";
|
||||
@@ -9,3 +9,8 @@ const z = untyped({ ...shapes, ...extra, extra });
|
||||
console.log(Object.keys(z).join(","));
|
||||
for (const key of ["circle", "square"]) console.log(key, z[key](2).area.toFixed(2));
|
||||
console.log(z.VERSION, typeof z.extra.tag, z.extra.tag("x"), new z.Box(4).size);
|
||||
// A namespace with star re-exports holds the re-exported members too.
|
||||
import * as all from "./all.js";
|
||||
const w = untyped({ ...all });
|
||||
console.log(Object.keys(w).join(","), w.circle(1).area.toFixed(2), w.own, new w.Box(2).size);
|
||||
for (const key of ["circle", "own", "VERSION", "missing"]) console.log(key, typeof all[key]);
|
||||
|
||||
@@ -1711,9 +1711,10 @@ describe("builtin constructor values (JS lane)", () => {
|
||||
test("members without a lowering stay refused instead of reading undefined", async () => {
|
||||
// Builtin constructors read as interned function values that model only
|
||||
// name, length and constructor. Every other static member or prototype
|
||||
// read (direct, bracketed, `in`, through an alias or globalThis), and
|
||||
// read (direct, bracketed, through an alias or globalThis), and
|
||||
// reflection on or redefinition of their members, must stay a located
|
||||
// SC2020/SC1090 refusal, never an undefined or otherwise wrong value.
|
||||
// `key in Ctor` answers from the constructor's fixed keys.
|
||||
// Test262 lost 769 refusals to undefined reads before this was pinned.
|
||||
const reads = [
|
||||
["Map.prototype", "Map.prototype"],
|
||||
@@ -1731,7 +1732,6 @@ describe("builtin constructor values (JS lane)", () => {
|
||||
],
|
||||
["isExtensible(Date.prototype)", "Object.isExtensible(Date.prototype)"],
|
||||
["Map['prototype']", 'Map["prototype"]'],
|
||||
["'prototype' in Map", '"prototype" in Map'],
|
||||
["globalThis.Map.prototype", "globalThis.Map.prototype"],
|
||||
["Map alias prototype", "MapAlias.prototype"],
|
||||
["Date alias prototype", "DateAlias.prototype"],
|
||||
@@ -1754,6 +1754,7 @@ console.log(Date.length, RegExp.length, Promise.length, Object.length, Map.lengt
|
||||
console.log(Date.name, Map.name, Map.constructor === Function, Object.constructor === Function);
|
||||
const promisePrototype = Promise.prototype;
|
||||
console.log(typeof promisePrototype, typeof promisePrototype.then, promisePrototype.constructor === Promise);
|
||||
console.log("prototype" in Map, "groupBy" in Map, "captureStackTrace" in Error, "nope" in Map);
|
||||
`;
|
||||
const r = await compileAndRun("builtin-constructor-members", source, "cjs");
|
||||
expect(r.stderr).toBe("");
|
||||
@@ -1768,11 +1769,13 @@ console.log(typeof promisePrototype, typeof promisePrototype.then, promiseProtot
|
||||
),
|
||||
),
|
||||
);
|
||||
// Promise.prototype reads as an object holding the runtime's methods.
|
||||
// Promise.prototype reads as an object holding the runtime's methods,
|
||||
// and `in` on a builtin constructor answers from its fixed keys.
|
||||
expect(lines.slice(reads.length)).toEqual([
|
||||
"7 2 1 1 0 0",
|
||||
"Date Map true true",
|
||||
"object function true",
|
||||
"true true true false",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user