mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Keep frozen checked records frozen, and give sloppy-mode this the global object (#2453)
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
A write to a read-only field throws Node's `TypeError` in strict mode code and leaves the field unchanged in sloppy mode code.
|
||||
`Object.defineProperty` with a string-literal key changes the value, `writable`, `enumerable`, and `configurable` attributes of a declared field; an object-literal accessor descriptor (`get` or `set`) for a declared field is refused at compile time with `SC2020`, and another accessor descriptor for a declared field throws `SC2020` when the call runs.
|
||||
`Object.freeze`, `Object.seal`, and `Object.preventExtensions` of a class instance held in an untyped value change the instance itself, so writes through its typed references are checked the same way.
|
||||
A typed record checked from a frozen untyped object (an object literal frozen in a JavaScript file, or a frozen `JSON.parse` result) is frozen too, and writes through its typed references are checked the same way.
|
||||
The `TypeError` for a write to a read-only property that was added to a class instance through an untyped value names `#<Object>` where Node names the class.
|
||||
Functions support `Object.freeze`, `Object.seal`, `Object.preventExtensions`, `Object.isFrozen`, `Object.isSealed`, and `Object.isExtensible`; the operations apply to the function's own properties (`name`, `length`, `prototype`, and the ones a program assigns).
|
||||
Class constructors are not supported.
|
||||
|
||||
@@ -5,3 +5,5 @@
|
||||
- **JavaScript parameters:** In a JavaScript file TypeScript does not check, a prototype method (`C.prototype.m = function (...) {}`) takes whatever values its callers pass; its JSDoc types document the usual values without constraining them.
|
||||
A sloppy-mode function with named parameters may read `arguments` only where the aliasing of its parameters cannot show: it reads `arguments.length`, or reads elements and never writes a parameter or an element.
|
||||
Other uses of `arguments` in such a function are refused with `SC1090`; ES modules, `'use strict'` code, classes, and functions with default, rest, or destructured parameters have no such restriction.
|
||||
A sloppy-mode function called with `undefined` or `null` as its receiver gets the global object as `this`, as in Node.
|
||||
Called with a primitive receiver, its `this` is the primitive itself, where Node passes the primitive's wrapper object (`typeof this` is `"object"` there).
|
||||
|
||||
@@ -2889,6 +2889,21 @@ export class LlDyn {
|
||||
B.line(`call void @scr_str_release(ptr ${ek})`);
|
||||
});
|
||||
}
|
||||
// A record built from a frozen object is frozen too, as the object it
|
||||
// copies (frozen-records.ts makes such shapes freezable).
|
||||
if (shape.freezable) {
|
||||
host.declare(`declare zeroext i1 @scr_dyn_checked_record_frozen(ptr)`);
|
||||
host.declare(`declare void @scr_record_freeze(ptr)`);
|
||||
const frozen = B.tmp();
|
||||
const lFreeze = B.newLabel("dcr.freeze");
|
||||
const lDone = B.newLabel("dcr.frozen");
|
||||
B.line(`${frozen} = call i1 @scr_dyn_checked_record_frozen(ptr %d)`);
|
||||
B.condBr(frozen, lFreeze, lDone);
|
||||
B.startBlock(lFreeze);
|
||||
B.line(`call void @scr_record_freeze(ptr %r0)`);
|
||||
B.br(lDone);
|
||||
B.startBlock(lDone);
|
||||
}
|
||||
B.terminate(`ret ptr %r0`);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -94,6 +94,7 @@ const INERT_LIB_CALLS = new Set([
|
||||
"dyn.compare",
|
||||
// Reads the ambient receiver (a fresh reference to the bound value).
|
||||
"dyn.this",
|
||||
// A sloppy-mode function's receiver from it (a fresh reference).
|
||||
"error.new",
|
||||
"error.nodeThrow",
|
||||
"dyn.typedRefIs",
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
funcOf,
|
||||
} from "../../ir/ir.js";
|
||||
import { helperLoc, varRef } from "../../ir/build.js";
|
||||
import { unsupportedDiag } from "../../diagnostics/diagnostic.js";
|
||||
import { unsupportedDiag, type ScrDiagnostic } from "../../diagnostics/diagnostic.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { mathMemberValue } from "./lower-stdlib.js";
|
||||
@@ -117,21 +117,46 @@ const WRITABLE = 1;
|
||||
const CONFIGURABLE = 4;
|
||||
|
||||
/** The namespace object the stdlib global `canonical` reads as in
|
||||
* JavaScript, or null when it is not a namespace. */
|
||||
* JavaScript, or null when it is not a namespace. The fences of its members
|
||||
* without a function value are reported unless `reportFences` is false: the
|
||||
* program's builtin lookup (global-values.ts) holds every namespace without
|
||||
* the program naming it, and its members still throw their fences when
|
||||
* called. */
|
||||
export function builtinNamespaceValue(
|
||||
lowerer: Lowerer,
|
||||
canonical: string,
|
||||
loc: SrcLoc,
|
||||
reportFences = true,
|
||||
): IrExpr | null {
|
||||
const members = namespaceMembers(canonical);
|
||||
if (members === null) return null;
|
||||
const name = `%builtin.namespace.${canonical}`;
|
||||
if (!lowerer.liftedFns.some((fn) => fn.name === name))
|
||||
defineNamespaceBuilder(lowerer, canonical, members, name, helperLoc(loc));
|
||||
if (reportFences) reportMemberFences(lowerer, canonical, members, helperLoc(loc));
|
||||
lowerer.noteEdge(name);
|
||||
return { kind: "call", callee: name, args: [], type: DYN, loc };
|
||||
}
|
||||
|
||||
/** Reports the SC1090 fence of each member of `canonical` whose function
|
||||
* throws it (fencedMember), once per program. */
|
||||
function reportMemberFences(
|
||||
lowerer: Lowerer,
|
||||
canonical: string,
|
||||
members: readonly NamespaceMember[],
|
||||
loc: SrcLoc,
|
||||
): void {
|
||||
for (const { key, length } of members) {
|
||||
if (length < 0) continue;
|
||||
const display = `${canonical}.${key}`;
|
||||
if (!lowerer.liftedFns.some((fn) => fn.name === fenceName(display))) continue;
|
||||
const diag = fenceDiag(display, loc);
|
||||
if (lowerer.runtimeFences.some((d) => d.code === diag.code && d.message === diag.message))
|
||||
continue;
|
||||
lowerer.runtimeFences.push(diag);
|
||||
}
|
||||
}
|
||||
|
||||
/** The lifted function that builds the namespace object once and answers
|
||||
* the same object afterwards. */
|
||||
function defineNamespaceBuilder(
|
||||
@@ -253,18 +278,17 @@ function functionValue(lowerer: Lowerer, canonical: string, key: string, loc: Sr
|
||||
return fencedMember(lowerer, `${canonical}.${key}`, key, loc);
|
||||
}
|
||||
|
||||
const fenceName = (display: string): string => `%builtin.fence.${display}`;
|
||||
const fenceDiag = (display: string, loc: SrcLoc): ScrDiagnostic =>
|
||||
unsupportedDiag("SC1090", loc, `'${display}' as a function value (call it directly)`);
|
||||
|
||||
/** One function per member without a function value: calling it throws the
|
||||
* SC1090 fence, while `typeof`, identity and the property's presence stay
|
||||
* as in Node. */
|
||||
* as in Node. builtinNamespaceValue reports the fence. */
|
||||
function fencedMember(lowerer: Lowerer, display: string, key: string, loc: SrcLoc): IrExpr {
|
||||
const name = `%builtin.fence.${display}`;
|
||||
const name = fenceName(display);
|
||||
if (!lowerer.liftedFns.some((fn) => fn.name === name)) {
|
||||
const diag = unsupportedDiag(
|
||||
"SC1090",
|
||||
loc,
|
||||
`'${display}' as a function value (call it directly)`,
|
||||
);
|
||||
lowerer.runtimeFences.push(diag);
|
||||
const diag = fenceDiag(display, loc);
|
||||
lowerer.liftedFns.push({
|
||||
name,
|
||||
params: [],
|
||||
|
||||
@@ -121,6 +121,8 @@ function freezableTypes(
|
||||
const shapes = new Set<string>();
|
||||
const arrays = new Set<string>();
|
||||
const live: IrType[] = [];
|
||||
const checked: IrType[] = [];
|
||||
let freezesUntyped = false;
|
||||
for (const fn of functions)
|
||||
everyStmtList(fn.body, {
|
||||
expr: (e) => {
|
||||
@@ -130,19 +132,28 @@ function freezableTypes(
|
||||
} else if (e.kind === "libCall" && e.fn === "array.freeze") {
|
||||
const target = e.args[0];
|
||||
if (target && target.type.kind === "array") arrays.add(typeKey(target.type));
|
||||
} else if (e.kind === "dynFrom") live.push(e.value.type);
|
||||
} else if (e.kind === "libCall" && e.fn === "dyn.freeze") freezesUntyped = true;
|
||||
else if (e.kind === "dynFrom") live.push(e.value.type);
|
||||
else if (e.kind === "dynCheck") checked.push(e.type);
|
||||
return true;
|
||||
},
|
||||
stmt: () => true,
|
||||
});
|
||||
if (!lowerer.untypedIntegrity) return { shapes, arrays };
|
||||
// An untyped reference to a class instance, or to a record or array
|
||||
// boxed live, reaches the values its fields and elements hold as further
|
||||
// live references. Class instances also cross into untyped code through
|
||||
// compiled callbacks, so every class is a root.
|
||||
const stack = [...live];
|
||||
for (const info of lowerer.classes.values())
|
||||
for (const field of info.def.fields) stack.push(field.type);
|
||||
if (!lowerer.untypedIntegrity && !freezesUntyped) return { shapes, arrays };
|
||||
const stack: IrType[] = [];
|
||||
// A record checked from an untyped value copies it, frozen when the value
|
||||
// is (a fresh literal frozen in a JavaScript file included), so the shapes
|
||||
// a check builds are freezable in a program that freezes untyped values.
|
||||
if (freezesUntyped) stack.push(...checked);
|
||||
if (lowerer.untypedIntegrity) {
|
||||
// An untyped reference to a class instance, or to a record or array
|
||||
// boxed live, reaches the values its fields and elements hold as further
|
||||
// live references. Class instances also cross into untyped code through
|
||||
// compiled callbacks, so every class is a root.
|
||||
stack.push(...live);
|
||||
for (const info of lowerer.classes.values())
|
||||
for (const field of info.def.fields) stack.push(field.type);
|
||||
}
|
||||
const seen = new Set<string>();
|
||||
while (stack.length > 0) {
|
||||
const type = stack.pop()!;
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
type SrcLoc,
|
||||
} from "../../ir/ir.js";
|
||||
import { isJsSourceFile } from "../program.js";
|
||||
import { isStrictCode } from "./strict-mode.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { TEMPORAL_GLOBAL } from "../builtin-modules.js";
|
||||
import { stdlibGlobalNameOf } from "./surfaces.js";
|
||||
@@ -122,11 +123,17 @@ function readsBuiltinByName(lowerer: Lowerer, node: ts.ElementAccessExpression):
|
||||
/** The value a JavaScript source reads for the declared global `name`
|
||||
* taken as a value, whatever its spelling (`name`, `globalThis.name`,
|
||||
* `const { name } = globalThis`, `globalThis[name]`), or null when it
|
||||
* reads the opaque identity token. */
|
||||
export function jsGlobalValueByName(lowerer: Lowerer, name: string, loc: SrcLoc): IrExpr | null {
|
||||
* reads the opaque identity token. `reportFences` (builtinNamespaceValue)
|
||||
* is false for the program's lookup. */
|
||||
export function jsGlobalValueByName(
|
||||
lowerer: Lowerer,
|
||||
name: string,
|
||||
loc: SrcLoc,
|
||||
reportFences = true,
|
||||
): IrExpr | null {
|
||||
return (
|
||||
jsSpecialGlobalValue(lowerer, name, loc) ??
|
||||
builtinNamespaceValue(lowerer, name, loc) ??
|
||||
builtinNamespaceValue(lowerer, name, loc, reportFences) ??
|
||||
builtinGlobalValue(name, loc, true)
|
||||
);
|
||||
}
|
||||
@@ -169,22 +176,150 @@ const URI_FUNCTIONS = new Set([
|
||||
"decodeURIComponent",
|
||||
]);
|
||||
|
||||
/** The builtin globals that sloppy-mode functions of the JavaScript source
|
||||
* `sf` read through their `this` (`this.Array`, `this["Array"]`) or through
|
||||
* a variable it initializes (`var root = this; root.Array`); every declared
|
||||
* global when such a read takes a computed key (`this[name]`). Called with
|
||||
* undefined or null as `this`, such a function reads them on the global
|
||||
* object, which answers them through the program's lookup. */
|
||||
export function sloppyThisGlobalReadsOf(lowerer: Lowerer, sf: ts.SourceFile): ReadonlySet<string> {
|
||||
const cached = lowerer.sloppyThisGlobalReads.get(sf);
|
||||
if (cached !== undefined) return cached;
|
||||
const reads = new Set<string>();
|
||||
if (isJsSourceFile(sf)) {
|
||||
const declared = new Set(declaredGlobalNames(lowerer, sf));
|
||||
// Element reads of `this` or of a variable, and the names of the
|
||||
// variables initialized with `this`: the checker resolves only the
|
||||
// element reads of `this` and of those variables.
|
||||
const elementReads: ts.ElementAccessExpression[] = [];
|
||||
const thisAliases = new Set<string>();
|
||||
ts.walkPreorder(sf, (node) => {
|
||||
if (
|
||||
ts.isPropertyAccessExpression(node) &&
|
||||
ts.isIdentifier(node.name) &&
|
||||
declared.has(node.name.text) &&
|
||||
readsSloppyThis(lowerer, node.expression)
|
||||
)
|
||||
reads.add(node.name.text);
|
||||
else if (ts.isElementAccessExpression(node)) {
|
||||
const target = withoutParentheses(node.expression);
|
||||
if (target.kind === ts.SyntaxKind.ThisKeyword || ts.isIdentifier(target))
|
||||
elementReads.push(node);
|
||||
} else if (
|
||||
ts.isVariableDeclaration(node) &&
|
||||
ts.isIdentifier(node.name) &&
|
||||
node.initializer !== undefined &&
|
||||
withoutParentheses(node.initializer).kind === ts.SyntaxKind.ThisKeyword
|
||||
)
|
||||
thisAliases.add(node.name.text);
|
||||
return undefined;
|
||||
});
|
||||
for (const node of elementReads) {
|
||||
const target = withoutParentheses(node.expression);
|
||||
if (ts.isIdentifier(target) && !thisAliases.has(target.text)) continue;
|
||||
if (!readsSloppyThis(lowerer, node.expression) || !readsBuiltinByName(lowerer, node))
|
||||
continue;
|
||||
const key = withoutParentheses(node.argumentExpression);
|
||||
if (ts.isStringLiteralLike(key)) {
|
||||
if (declared.has(key.text)) reads.add(key.text);
|
||||
} else if (!ts.isNumericLiteral(key)) {
|
||||
for (const name of declared) reads.add(name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
lowerer.sloppyThisGlobalReads.set(sf, reads);
|
||||
return reads;
|
||||
}
|
||||
|
||||
/** Whether `expr` is the `this` of a sloppy-mode function, or a variable
|
||||
* whose only declaration initializes it with that `this`. */
|
||||
function readsSloppyThis(lowerer: Lowerer, expr: ts.Expression): boolean {
|
||||
const inner = withoutParentheses(expr);
|
||||
if (inner.kind === ts.SyntaxKind.ThisKeyword) return sloppyReceiverOf(inner);
|
||||
if (!ts.isIdentifier(inner)) return false;
|
||||
const declarations = lowerer.checker.getSymbolAtLocation(inner)?.declarations ?? [];
|
||||
const declaration = declarations[0];
|
||||
if (
|
||||
declarations.length !== 1 ||
|
||||
declaration === undefined ||
|
||||
!ts.isVariableDeclaration(declaration)
|
||||
)
|
||||
return false;
|
||||
const init =
|
||||
declaration.initializer === undefined ? undefined : withoutParentheses(declaration.initializer);
|
||||
return init !== undefined && init.kind === ts.SyntaxKind.ThisKeyword && sloppyReceiverOf(init);
|
||||
}
|
||||
|
||||
function withoutParentheses(expr: ts.Expression): ts.Expression {
|
||||
let inner = expr;
|
||||
while (ts.isParenthesizedExpression(inner)) inner = inner.expression;
|
||||
return inner;
|
||||
}
|
||||
|
||||
/** Whether the `this` at `node` is a sloppy-mode function's receiver
|
||||
* (lower-functions.ts declareCallReceiver): arrows take the enclosing one. */
|
||||
function sloppyReceiverOf(node: ts.Node): boolean {
|
||||
for (let parent = node.parent; parent !== undefined; parent = parent.parent) {
|
||||
if (ts.isArrowFunction(parent)) continue;
|
||||
if (ts.isFunctionLike(parent)) return parent.body !== undefined && !isStrictCode(parent.body);
|
||||
if (
|
||||
ts.isSourceFile(parent) ||
|
||||
ts.isClassDeclaration(parent) ||
|
||||
ts.isClassExpression(parent) ||
|
||||
ts.isClassStaticBlockDeclaration(parent)
|
||||
)
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** The names of the global object's declared properties that the lookup can
|
||||
* answer. */
|
||||
function declaredGlobalNames(lowerer: Lowerer, at: ts.Node): string[] {
|
||||
const symbol = lowerer.checker.resolveName(
|
||||
"globalThis",
|
||||
ts.SymbolFlags.Value,
|
||||
ts.getSourceFileOfNode(at),
|
||||
false,
|
||||
);
|
||||
if (symbol === undefined) return [];
|
||||
return lowerer.checker
|
||||
.getPropertiesOfType(lowerer.checker.getTypeOfSymbol(symbol))
|
||||
.map((property) => ts.symbolName(property))
|
||||
.filter((name) => name !== TEMPORAL_GLOBAL && !RUNTIME_ANSWERED.has(name));
|
||||
}
|
||||
|
||||
/** The names the program's lookup answers: every declared global when a
|
||||
* module reads the global object by computed names, else the ones its
|
||||
* sloppy-mode functions read through `this`. */
|
||||
function lookupNames(lowerer: Lowerer): readonly string[] {
|
||||
if (lowerer.globalLookupNames !== undefined) return lowerer.globalLookupNames;
|
||||
const modules = lowerer.moduleOrder.length > 0 ? lowerer.moduleOrder : [lowerer.entry];
|
||||
const names = modules.some((sf) => computedGlobalReadOf(lowerer, sf) !== null)
|
||||
? declaredGlobalNames(lowerer, lowerer.entry)
|
||||
: modules.flatMap((sf) => [...sloppyThisGlobalReadsOf(lowerer, sf)]);
|
||||
lowerer.globalLookupNames = [...new Set(names)].sort();
|
||||
return lowerer.globalLookupNames;
|
||||
}
|
||||
|
||||
/** Whether the JavaScript source `sf` installs the program's lookup when its
|
||||
* body starts: it reads builtin globals through the global object by
|
||||
* computed names, or through a sloppy-mode function's `this`. */
|
||||
export function installsGlobalValues(lowerer: Lowerer, sf: ts.SourceFile): boolean {
|
||||
return (
|
||||
computedGlobalReadOf(lowerer, sf) !== null || sloppyThisGlobalReadsOf(lowerer, sf).size > 0
|
||||
);
|
||||
}
|
||||
|
||||
/** The statement that installs the program's lookup, built on first use
|
||||
* from the names the checker declares on `receiver` (a global object). */
|
||||
export function globalValuesInstall(
|
||||
lowerer: Lowerer,
|
||||
receiver: ts.Expression,
|
||||
loc: SrcLoc,
|
||||
): IrStmt {
|
||||
* from the names it answers (lookupNames). */
|
||||
export function globalValuesInstall(lowerer: Lowerer, loc: SrcLoc): IrStmt {
|
||||
const fnType: IrType & { kind: "func" } = { kind: "func", params: [STRING], ret: DYN };
|
||||
if (!lowerer.liftedFnNamed(LOOKUP_FN)) {
|
||||
const names = lowerer.checker
|
||||
.getPropertiesOfType(lowerer.typeOf(receiver))
|
||||
.map((property) => ts.symbolName(property))
|
||||
.filter((name) => name !== TEMPORAL_GLOBAL && !RUNTIME_ANSWERED.has(name));
|
||||
const name: IrExpr = { kind: "varRef", localId: "name.0", type: STRING, loc };
|
||||
const body: IrStmt[] = [];
|
||||
for (const global of [...new Set(names)].sort()) {
|
||||
for (const global of lookupNames(lowerer)) {
|
||||
const webStream = (WEB_STREAM_CLASSES as readonly string[]).indexOf(global);
|
||||
const value: IrExpr | null = REFUSED.has(global)
|
||||
? { kind: "unitLit", unit: "null", type: { kind: "nullT" }, loc }
|
||||
@@ -192,7 +327,7 @@ export function globalValuesInstall(
|
||||
? programNamesWebStreamClass(lowerer)
|
||||
? webStreamClassValue(webStream, loc)
|
||||
: null
|
||||
: jsGlobalValueByName(lowerer, global, loc);
|
||||
: jsGlobalValueByName(lowerer, global, loc, false);
|
||||
if (value === null) continue;
|
||||
body.push({
|
||||
kind: "if",
|
||||
|
||||
@@ -38,6 +38,8 @@ import {
|
||||
blockBodyOf,
|
||||
} from "./function-returns.js";
|
||||
import { generatorMeta, isThisParameter, type ParamShape } from "./call-signatures.js";
|
||||
import { isStrictCode } from "./strict-mode.js";
|
||||
import { lowerSloppyGlobalReceiver } from "./lower-global-value.js";
|
||||
|
||||
/** Save the call-site receiver before defaults or body effects. Escaping
|
||||
* arrows capture this binding instead of reading a later call's receiver. */
|
||||
@@ -48,16 +50,28 @@ function declareCallReceiver(
|
||||
): IrStmt {
|
||||
const receiver = lowerer.declareThis(DYN);
|
||||
const loc = locOf(node);
|
||||
const bound: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: generator ? "dyn.generatorThis" : "dyn.this",
|
||||
args: [],
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
// A sloppy-mode function's `this` is the global object when it is called
|
||||
// with undefined or null. The function's own body decides its mode.
|
||||
const sloppy = !ts.isArrowFunction(node) && node.body !== undefined && !isStrictCode(node.body);
|
||||
return {
|
||||
kind: "varDecl",
|
||||
localId: receiver.id,
|
||||
init: {
|
||||
kind: "libCall",
|
||||
fn: generator ? "dyn.generatorThis" : "dyn.this",
|
||||
args: [],
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
init: sloppy
|
||||
? {
|
||||
kind: "nullish",
|
||||
left: bound,
|
||||
right: lowerSloppyGlobalReceiver(lowerer, node, loc),
|
||||
type: DYN,
|
||||
loc,
|
||||
}
|
||||
: bound,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { DYN, STRING, arrayOf, type IrExpr } from "../../ir/ir.js";
|
||||
import { DYN, STRING, arrayOf, type IrExpr, type SrcLoc } from "../../ir/ir.js";
|
||||
import { locOf } from "../program.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { TEMPORAL_GLOBAL } from "../builtin-modules.js";
|
||||
@@ -8,11 +8,29 @@ import { TEMPORAL_GLOBAL } from "../builtin-modules.js";
|
||||
* Known globals without a value implementation refuse at runtime; absent host
|
||||
* capabilities answer undefined. The checker supplies names, never values. */
|
||||
export function lowerGlobalValue(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
const loc = locOf(expr);
|
||||
return lowerGlobalValueOfType(lowerer, lowerer.typeOf(expr), locOf(expr));
|
||||
}
|
||||
|
||||
/** The global object a sloppy-mode function receives as `this` when it is
|
||||
* called with undefined or null: the value `globalThis` reads. */
|
||||
export function lowerSloppyGlobalReceiver(lowerer: Lowerer, node: ts.Node, loc: SrcLoc): IrExpr {
|
||||
const symbol = lowerer.checker.resolveName(
|
||||
"globalThis",
|
||||
ts.SymbolFlags.Value,
|
||||
ts.getSourceFileOfNode(node),
|
||||
false,
|
||||
);
|
||||
return lowerGlobalValueOfType(
|
||||
lowerer,
|
||||
symbol === undefined ? undefined : lowerer.checker.getTypeOfSymbol(symbol),
|
||||
loc,
|
||||
);
|
||||
}
|
||||
|
||||
function lowerGlobalValueOfType(lowerer: Lowerer, type: ts.Type | undefined, loc: SrcLoc): IrExpr {
|
||||
// The temporal global is typed for the programs that name it (TS 7's
|
||||
// lib slice) but absent in Node 24: reads answer undefined.
|
||||
const names = lowerer.checker
|
||||
.getPropertiesOfType(lowerer.typeOf(expr))
|
||||
const names = (type === undefined ? [] : lowerer.checker.getPropertiesOfType(type))
|
||||
.map((property) => ts.symbolName(property))
|
||||
.filter((name) => name !== TEMPORAL_GLOBAL);
|
||||
return {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { isIslandPromisifyCall, noteIslandSubclass } from "./lower-node-island.js";
|
||||
import { isIslandSubclassDecl } from "../../node-islands/boundary.js";
|
||||
import { computedGlobalReadOf, globalValuesInstall } from "./global-values.js";
|
||||
import { globalValuesInstall, installsGlobalValues } from "./global-values.js";
|
||||
import type { TypeFailureSlot } from "../../diagnostics/diagnostic.js";
|
||||
import { THREADS_MODULE } from "../threads-import.js";
|
||||
import { collectIslandImport, collectIslandRequires } from "./lower-node-island.js";
|
||||
@@ -3172,10 +3172,10 @@ export function lowerFileInit(
|
||||
// (NI-6); the plan also creates their declarations' TDZ flags.
|
||||
if (sharedModulePlans(lowerer).length > 0 && sf === lowerer.entry)
|
||||
header.push(...sharedModuleRegistrations(lowerer));
|
||||
// A module that reads builtin globals through the global object by
|
||||
// computed names installs the program's lookup before its body runs.
|
||||
const computedGlobalRead = computedGlobalReadOf(lowerer, sf);
|
||||
if (computedGlobalRead) header.push(globalValuesInstall(lowerer, computedGlobalRead, loc0));
|
||||
// A module that reads builtin globals through the global object, by
|
||||
// computed names or through a sloppy-mode function's `this`, installs
|
||||
// the program's lookup before its body runs.
|
||||
if (installsGlobalValues(lowerer, sf)) header.push(globalValuesInstall(lowerer, loc0));
|
||||
// Top-level `this` of a CommonJS module: its object exists before the
|
||||
// module's first statement (lowerCjsTopLevelThis).
|
||||
const exportsThis = lowerer.cjsExportsThis.get(sf);
|
||||
|
||||
@@ -3774,6 +3774,12 @@ export class Lowerer {
|
||||
/** global-values.ts's answer per JavaScript source: the receiver of its
|
||||
* first read of the global object by a computed name, or null. */
|
||||
readonly computedGlobalReads = new Map<ts.SourceFile, ts.Expression | null>();
|
||||
/** global-values.ts's answer per JavaScript source: the builtin globals
|
||||
* its sloppy-mode functions read through their `this`. */
|
||||
readonly sloppyThisGlobalReads = new Map<ts.SourceFile, ReadonlySet<string>>();
|
||||
/** global-values.ts's answer: the names the program's global lookup
|
||||
* answers; undefined until asked. */
|
||||
globalLookupNames: readonly string[] | undefined = undefined;
|
||||
/** web-streams-dyn.ts's answer: whether a JavaScript source names a web
|
||||
* stream class; undefined until asked. */
|
||||
namesWebStreamClass: boolean | undefined = undefined;
|
||||
@@ -4404,6 +4410,8 @@ export class Lowerer {
|
||||
"createRequireBindingTargets",
|
||||
"createRequireElementTargets",
|
||||
"computedGlobalReads",
|
||||
"sloppyThisGlobalReads",
|
||||
"globalLookupNames",
|
||||
"namesWebStreamClass",
|
||||
"timeValueDateDecisions",
|
||||
"timeValueDateWrites",
|
||||
|
||||
@@ -3603,6 +3603,8 @@ function buildLibFnSignatures() {
|
||||
"process.pid": { argTypes: [], result: F64 },
|
||||
"dyn.this": { argTypes: [], result: DYN },
|
||||
"dyn.generatorThis": { argTypes: [], result: DYN },
|
||||
/** A sloppy-mode function's `this` from its receiver: the global object
|
||||
* for undefined and null, else the receiver. Never throws. */
|
||||
"process.getuid": { argTypes: [], result: F64 },
|
||||
"process.getgid": { argTypes: [], result: F64 },
|
||||
/** geteuid(2) / getegid(2) / getgroups(2) (the effective gid appended
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/frozen-checked-records-sloppy.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/frozen-checked-records.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/sloppy-this-global-computed.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/sloppy-this-global.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -1095,6 +1095,11 @@ ScrDyn *scr_dyn_freeze(ScrDyn *value);
|
||||
* the programs that link it. */
|
||||
ScrDyn *scr_dyn_freeze_data(ScrDyn *value);
|
||||
bool scr_dyn_is_frozen(const ScrDyn *value);
|
||||
/* Whether a record checked from `value` is frozen: `value` is. It answers
|
||||
* for every value, where scr_dyn_is_frozen refuses values without a modeled
|
||||
* integrity level: those are not frozen, as Object.freeze refuses them. A
|
||||
* proxy is frozen exactly when its target is (the proxy invariants). */
|
||||
bool scr_dyn_checked_record_frozen(const ScrDyn *value);
|
||||
ScrDyn *scr_dyn_new_bigint(ScrBigInt *value); /* borrows and retains value */
|
||||
ScrDyn *scr_dyn_new_symbol(ScrSym *value); /* borrows and retains value */
|
||||
ScrSym *scr_sym_well_known(ScrStr *name);
|
||||
|
||||
@@ -19270,26 +19270,29 @@ ScrDyn *scr_dyn_freeze_data(ScrDyn *value) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bool scr_dyn_is_frozen(const ScrDyn *value) {
|
||||
if (scr_dyn_class_reflection_fence(value)) return false;
|
||||
/* Object.isFrozen of `value` with `self` for the values it contains. A value
|
||||
* without an integrity level the runtime models refuses with SC2020 when
|
||||
* `refuse` is set, and otherwise answers false: Object.freeze refuses such
|
||||
* a value too, so it is never frozen. */
|
||||
static bool scr_dyn_frozen_answer(const ScrDyn *value, bool (*self)(const ScrDyn *), bool refuse) {
|
||||
if (scr_dyn_published_object(value)) return true;
|
||||
bool answer;
|
||||
if (scr_dyn_typed_ref_view_answer((ScrDyn *)value, scr_dyn_is_frozen, &answer)) return answer;
|
||||
if (scr_dyn_typed_ref_view_answer((ScrDyn *)value, self, &answer)) return answer;
|
||||
ScrMap *collection = scr_dyn_native_collection(value);
|
||||
if (collection) return !scr_map_is_extensible(collection);
|
||||
if (value->symbol_properties && !scr_dyn_is_frozen(value->symbol_properties)) return false;
|
||||
if (value->symbol_properties && !self(value->symbol_properties)) return false;
|
||||
if (scr_dyn_plain_fn(value)) {
|
||||
ScrDyn *table = scr_dyn_fn_existing_table(value);
|
||||
answer = table && scr_dyn_is_frozen(table);
|
||||
answer = table && self(table);
|
||||
scr_dyn_release(table);
|
||||
return answer;
|
||||
}
|
||||
if (value->kind == SCR_DYN_ARR)
|
||||
return scr_dyn_is_sealed(value) && (value->v.arr.len == 0 || value->v.arr.frozen) &&
|
||||
(!value->v.arr.properties || scr_dyn_is_frozen(value->v.arr.properties));
|
||||
(!value->v.arr.properties || self(value->v.arr.properties));
|
||||
if (value->kind == SCR_DYN_BYTES)
|
||||
return (value->v.bytes->is_data_view || value->v.bytes->len == 0) && value->v.bytes->properties &&
|
||||
scr_dyn_is_frozen(value->v.bytes->properties);
|
||||
self(value->v.bytes->properties);
|
||||
if (value->kind == SCR_DYN_OBJ && !value->copied_from_native) {
|
||||
if (!value->non_extensible) return false;
|
||||
for (size_t i = 0; i < value->v.obj.len; i++) {
|
||||
@@ -19301,11 +19304,29 @@ bool scr_dyn_is_frozen(const ScrDyn *value) {
|
||||
}
|
||||
if (value->kind == SCR_DYN_NULL || value->kind == SCR_DYN_UNDEF || value->kind == SCR_DYN_NUM ||
|
||||
value->kind == SCR_DYN_BOOL || value->kind == SCR_DYN_STR || value->kind == SCR_DYN_BIGINT || value->kind == SCR_DYN_SYMBOL) return true;
|
||||
if (!refuse) return false;
|
||||
static const char message[] = "Object.isFrozen of this native value has no lowering";
|
||||
scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool scr_dyn_is_frozen(const ScrDyn *value) {
|
||||
if (scr_dyn_class_reflection_fence(value)) return false;
|
||||
return scr_dyn_frozen_answer(value, scr_dyn_is_frozen, true);
|
||||
}
|
||||
|
||||
/* A record checked from `value` copies its integrity: whether `value` is
|
||||
* frozen, answered for every value the check can meet. A proxy is frozen
|
||||
* exactly when its target is (the proxy invariants). Object.prototype and
|
||||
* class constructors, which Object.isFrozen fences, and native values
|
||||
* without a modeled integrity level are not frozen. */
|
||||
bool scr_dyn_checked_record_frozen(const ScrDyn *value) {
|
||||
while (value && value->kind == SCR_DYN_PROXY) value = value->v.proxy.target;
|
||||
if (!value || value == scr_builtin_object_prototype) return false;
|
||||
if (value->kind == SCR_DYN_FUNC && value->v.fn.class_obj && !value->v.fn.class_obj->static_data) return false;
|
||||
return scr_dyn_frozen_answer(value, scr_dyn_checked_record_frozen, false);
|
||||
}
|
||||
|
||||
double scr_buffer_byte_length_dyn(ScrDyn *value, ScrStr *encoding) {
|
||||
if (value->kind == SCR_DYN_STR) return scr_bytes_byte_length_str(value->v.str, encoding);
|
||||
if (value->kind == SCR_DYN_BYTES) return scr_bytes_byte_len(value->v.bytes);
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// Untyped values frozen in sloppy-mode code and then held as typed records
|
||||
// stay frozen: writes through typed parameters and a typed `this` leave them
|
||||
// unchanged, as Node does, and Object.isFrozen answers true. An unfrozen value
|
||||
// checked into the same record type stays writable.
|
||||
|
||||
/** @typedef {{ count: number, label: string }} Counter */
|
||||
|
||||
/** @param {Counter} c @param {number} by */
|
||||
function bump(c, by) {
|
||||
c.count += by;
|
||||
c.label = c.label + "!";
|
||||
return c.count;
|
||||
}
|
||||
|
||||
/** @this {Counter} @param {number} by */
|
||||
function bumpThis(by) {
|
||||
this.count += by;
|
||||
this.label = "this";
|
||||
return this.count;
|
||||
}
|
||||
|
||||
/** @type {Counter} */
|
||||
const literal = Object.freeze({ count: 1, label: "a" });
|
||||
console.log(bump(literal, 2), bumpThis.call(literal, 3), JSON.stringify(literal), Object.isFrozen(literal));
|
||||
|
||||
/** @type {Counter} */
|
||||
const parsed = Object.freeze(JSON.parse('{"count":10,"label":"b"}'));
|
||||
console.log(bump(parsed, 2), bumpThis.call(parsed, 3), JSON.stringify(parsed), Object.isFrozen(parsed));
|
||||
|
||||
/** @type {Counter} */
|
||||
const open = JSON.parse('{"count":100,"label":"c"}');
|
||||
console.log(bump(open, 2), bumpThis.call(open, 3), JSON.stringify(open), Object.isFrozen(open));
|
||||
|
||||
/** @type {Counter[]} */
|
||||
const list = [Object.freeze({ count: 5, label: "l" }), { count: 6, label: "m" }];
|
||||
for (const item of list) bump(item, 1);
|
||||
console.log(JSON.stringify(list), Object.isFrozen(list[0]), Object.isFrozen(list[1]));
|
||||
@@ -0,0 +1,29 @@
|
||||
// An untyped value frozen and then held as a typed record stays frozen:
|
||||
// writes to it in strict mode code throw Node's TypeError and leave it
|
||||
// unchanged. An unfrozen value checked into the same record type stays
|
||||
// writable.
|
||||
|
||||
type Counter = { count: number; label: string };
|
||||
|
||||
function bump(c: Counter, by: number): string {
|
||||
try {
|
||||
c.count += by;
|
||||
return "wrote " + c.count;
|
||||
} catch (e) {
|
||||
return e instanceof TypeError ? "TypeError" : "other";
|
||||
}
|
||||
}
|
||||
|
||||
const frozen = Object.freeze(JSON.parse('{"count":1,"label":"a"}')) as Counter;
|
||||
console.log(bump(frozen, 2), frozen.count, Object.isFrozen(frozen));
|
||||
|
||||
const open = JSON.parse('{"count":10,"label":"b"}') as Counter;
|
||||
console.log(bump(open, 2), open.count, Object.isFrozen(open));
|
||||
|
||||
const raw: unknown = Object.freeze(JSON.parse('{"count":20,"label":"c"}'));
|
||||
const viaUnknown = raw as Counter;
|
||||
console.log(bump(viaUnknown, 2), viaUnknown.count, Object.isFrozen(viaUnknown));
|
||||
|
||||
// A proxy checked into the record type stays writable.
|
||||
const proxied = new Proxy({ count: 40, label: "q" }, {}) as Counter;
|
||||
console.log(bump(proxied, 2), proxied.count);
|
||||
@@ -0,0 +1,26 @@
|
||||
// A sloppy-mode function called with undefined or null as its receiver reads
|
||||
// builtin globals on the global object by computed keys, directly and through
|
||||
// a variable its `this` initializes.
|
||||
|
||||
function readByKey(key) {
|
||||
return this[key];
|
||||
}
|
||||
function aliasedRead(key) {
|
||||
var root = this;
|
||||
return root[key];
|
||||
}
|
||||
|
||||
console.log(readByKey("Array") === Array, readByKey("JSON") === JSON);
|
||||
console.log(aliasedRead("Map") === Map, typeof aliasedRead("setTimeout"));
|
||||
console.log(readByKey.call(null, "Promise") === Promise, readByKey.call({ Promise: 1 }, "Promise"));
|
||||
console.log(readByKey("globalThis") === globalThis, readByKey("missingGlobal"));
|
||||
|
||||
const names = ["Object", "Symbol", "Uint8Array"];
|
||||
const values = [Object, Symbol, Uint8Array];
|
||||
console.log(names.map((name, i) => readByKey(name) === values[i]));
|
||||
|
||||
function strictRead(key) {
|
||||
"use strict";
|
||||
return this === undefined ? "no receiver" : this[key];
|
||||
}
|
||||
console.log(strictRead("Array"), strictRead.call({ Array: 2 }, "Array"));
|
||||
@@ -0,0 +1,69 @@
|
||||
// In sloppy-mode functions, `this` is the global object when the function is
|
||||
// called with undefined or null as its receiver: by a plain call, by a
|
||||
// detached method, and through call and apply. Strict functions and arrows
|
||||
// keep the receiver they get.
|
||||
|
||||
// Before anything else reads the global object.
|
||||
function readConsole() {
|
||||
return typeof this.console.log === "function" && this.console === console;
|
||||
}
|
||||
console.log(readConsole(), readConsole.call(null), readConsole.call({ console: 1 }));
|
||||
|
||||
// Builtins read through the global `this`, directly and through a variable.
|
||||
function readBuiltins() {
|
||||
return [this.Array === Array, this.JSON === JSON, typeof this.setTimeout];
|
||||
}
|
||||
function aliasedRoot() {
|
||||
var root = this;
|
||||
return root.Object === Object && typeof root.Promise;
|
||||
}
|
||||
console.log(readBuiltins(), aliasedRoot());
|
||||
|
||||
// Builtins read through the global `this` by literal keys.
|
||||
function readByLiteral() {
|
||||
var root = this;
|
||||
return [this["Map"] === Map, root["Math"] === Math, typeof this[`Set`], this[0]];
|
||||
}
|
||||
console.log(readByLiteral(), readByLiteral.call({ Map: 1 })[0]);
|
||||
|
||||
function receiver() {
|
||||
return this;
|
||||
}
|
||||
function kind() {
|
||||
return typeof this;
|
||||
}
|
||||
function strictReceiver() {
|
||||
"use strict";
|
||||
return this;
|
||||
}
|
||||
|
||||
console.log(receiver() === globalThis, kind());
|
||||
console.log(receiver.call(undefined) === globalThis, receiver.call(null) === globalThis);
|
||||
console.log(receiver.apply(undefined, []) === globalThis, receiver.apply(null) === globalThis);
|
||||
console.log(strictReceiver() === undefined, strictReceiver.call(null) === null);
|
||||
|
||||
const holder = {
|
||||
name: "holder",
|
||||
self() {
|
||||
return this;
|
||||
},
|
||||
};
|
||||
const detached = holder.self;
|
||||
console.log(holder.self() === holder, detached() === globalThis);
|
||||
|
||||
function outer() {
|
||||
function inner() {
|
||||
return this;
|
||||
}
|
||||
return inner() === globalThis;
|
||||
}
|
||||
console.log(outer(), outer.call({ name: "o" }));
|
||||
|
||||
globalThis.sloppyMarker = 42;
|
||||
function readMarker() {
|
||||
return this.sloppyMarker;
|
||||
}
|
||||
console.log(readMarker(), readMarker.call(undefined), readMarker.call({ sloppyMarker: 7 }));
|
||||
|
||||
const arrow = () => this;
|
||||
console.log(arrow() === module.exports, arrow.call(null) === module.exports);
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3590, "san": 12400 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2610, "san": 12260 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3300, "san": 4550 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3030, "san": 3970 }
|
||||
Reference in New Issue
Block a user