mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Compile three Node test shapes: a top-level return before declarations, nullable strictEqual operands, and a branch-assigned let (#2590)
This commit is contained in:
@@ -5,6 +5,8 @@
|
||||
`assert.AssertionError` used as a value outside `instanceof` throws `SC2020`.
|
||||
The comparison methods, `ok`, `fail` and `ifError` are function values that assert as the methods do; called through a stored value, `ok` without a message reports `value == true`, where Node quotes the call, and `fail` takes at most one argument.
|
||||
`throws`, `rejects`, `doesNotThrow`, `doesNotReject`, `match` and `doesNotMatch` as values throw `SC2020`.
|
||||
`strictEqual` and `notStrictEqual` compare a number, string or boolean that can be `null` or `undefined`, such as `child.signalCode`, with any number, string, boolean, `null` or `undefined`.
|
||||
`notStrictEqual` of another value that can be `null` or `undefined`, such as `child.stdin`, against `null` or `undefined` compiles; `strictEqual` of one throws `SC2020`.
|
||||
`assert.deepEqual` and `assert.notDeepEqual` follow Node's loose comparison, including Maps, Sets, Dates, regular expressions, typed arrays, and errors; operands whose type can be cyclic or holds functions inside a structure are refused.
|
||||
`assert.partialDeepStrictEqual` follows Node's partial comparison over the same values, with the same refusals.
|
||||
Checked-dynamic values compared deeply have no cycle detection.
|
||||
|
||||
@@ -7,3 +7,5 @@
|
||||
Defaults, rest elements, and nested patterns over other modules are unsupported.
|
||||
A require cycle compiles when each of its modules assigns all of its exports before its first `require` and uses the other modules of the cycle only inside functions that its top-level code can't call.
|
||||
Other require cycles are rejected with `SC1016`.
|
||||
A top-level `return` ends the module's evaluation.
|
||||
It is unsupported when the module assigns exports after it, or when a function that exists before it runs reads a binding declared after it, such as a callback created above the `return` or a function declaration.
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
funcOf,
|
||||
isUnitType,
|
||||
recordTextCodecClass,
|
||||
typeEquals,
|
||||
} from "../../../ir/ir.js";
|
||||
import { lowerNodeModuleIdentifier } from "../lower-node-module.js";
|
||||
import { lowerCjsExportsObjectValue } from "../cjs-exports-object.js";
|
||||
@@ -192,6 +193,11 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
|
||||
// when the checker types this USE as a single arm, maybeNarrow
|
||||
// bridges the tagged representation with a unionNarrow.
|
||||
const local = lowerer.resolveLocal(expr);
|
||||
{
|
||||
const slot = local ?? lowerer.globalOf(expr);
|
||||
const closureRead = slot ? closureDynRead(lowerer, expr, slot.id, slot.type, loc) : null;
|
||||
if (closureRead) return closureRead;
|
||||
}
|
||||
// A Date local holding its time value reads as that Date: only uses
|
||||
// that read the time value reach here (date-values.ts).
|
||||
if (local && lowerer.timeValueDateLocals.has(local)) {
|
||||
@@ -1233,3 +1239,60 @@ export function uriFunctionValue(lowerer: Lowerer, which: string, loc: SrcLoc):
|
||||
}
|
||||
return { kind: "closure", fnName: name, captures: [], type, loc };
|
||||
}
|
||||
|
||||
/** A read of a captured handle `let` (Lowerer.closureDynLets) inside a
|
||||
* function nested in the binding's scope. The checker types it as the
|
||||
* handle, but the closure can run before the first write. As a member
|
||||
* receiver (`c.address()`, `child.stdin`) it reads the handle and throws
|
||||
* Node's TypeError when the binding is still undefined; elsewhere it reads
|
||||
* the checked-dynamic value of the handle or undefined. Null for other
|
||||
* reads. */
|
||||
function closureDynRead(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.Identifier,
|
||||
id: string,
|
||||
type: IrType,
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
if (type.kind !== "union" || lowerer.closureDynLets.size === 0) return null;
|
||||
const symbol = lowerer.resolveValueSymbol(expr);
|
||||
if (!symbol || !lowerer.closureDynLets.has(symbol)) return null;
|
||||
const decl = symbol.valueDeclaration;
|
||||
if (!decl) return null;
|
||||
const scopeOf = (node: ts.Node): ts.Node | undefined => {
|
||||
let at = node.parent;
|
||||
while (at !== undefined && !ts.isFunctionLike(at) && !ts.isSourceFile(at)) at = at.parent;
|
||||
return at;
|
||||
};
|
||||
if (scopeOf(expr) === scopeOf(decl)) return null;
|
||||
const parent = expr.parent;
|
||||
const handle = lowerer.unions.get(type.unionId)?.arms.find((arm) => !isUnitType(arm));
|
||||
// Only a use the checker types as the handle narrows: inside a function
|
||||
// declared before the first write the binding reads as `any`.
|
||||
const typedAsHandle =
|
||||
handle !== undefined && typeEquals(lowerer.mapTypeOf(lowerer.typeOf(expr)) ?? DYN, handle);
|
||||
if (typedAsHandle && ts.isPropertyAccessExpression(parent) && parent.expression === expr) {
|
||||
// An optional read (`c?.address`) tests the tagged value: undefined
|
||||
// short-circuits and the handle arm narrows.
|
||||
if (parent.questionDotToken) return { kind: "varRef", localId: id, type, loc };
|
||||
const helper = lowerer.narrowedArmHelper(
|
||||
type.unionId,
|
||||
handle,
|
||||
loc,
|
||||
`Cannot read properties of undefined (reading '${parent.name.text}')`,
|
||||
);
|
||||
if (helper)
|
||||
return lowerer.checkedNarrowCall(
|
||||
helper,
|
||||
{ kind: "varRef", localId: id, type, loc },
|
||||
handle,
|
||||
loc,
|
||||
);
|
||||
}
|
||||
return {
|
||||
kind: "dynFrom",
|
||||
value: { kind: "varRef", localId: id, type, loc },
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1290,12 +1290,28 @@ function lowerAssertEqual(
|
||||
?.arms.every((arm) => instanceType(arm) || isUnitType(arm)) ??
|
||||
false))) &&
|
||||
lowerer.dynConvertible(e.type);
|
||||
// A nullable number, string or boolean (`number | null`) against a
|
||||
// scalar or null/undefined: under the strict pair each arm crosses by
|
||||
// value, so the checked-dynamic SameValue compare answers, and renders
|
||||
// its failure message, as Node does.
|
||||
const strictArm = (t: IrType): boolean =>
|
||||
t.kind === "f64" || t.kind === "string" || t.kind === "bool" || isUnitType(t);
|
||||
const strictOperand = (e: IrExpr): boolean =>
|
||||
!deep &&
|
||||
(e.kind === "unitLit" ||
|
||||
strictArm(e.type) ||
|
||||
(e.type.kind === "union" &&
|
||||
(lowerer.unions.get(e.type.unionId)?.arms.every(strictArm) ?? false))) &&
|
||||
lowerer.dynConvertible(e.type);
|
||||
const nullable = (e: IrExpr): boolean => e.type.kind === "union" || isUnitType(e.type);
|
||||
const strictNullable = (nullable(a) || nullable(b)) && strictOperand(a) && strictOperand(b);
|
||||
if (
|
||||
a.type.kind === "dyn" ||
|
||||
b.type.kind === "dyn" ||
|
||||
a.type.kind === "moduleNs" ||
|
||||
b.type.kind === "moduleNs" ||
|
||||
(instance(a) && instance(b))
|
||||
(instance(a) && instance(b)) ||
|
||||
strictNullable
|
||||
) {
|
||||
const box = (e: IrExpr, node: ts.Expression): IrExpr => {
|
||||
if (e.type.kind === "dyn") return e;
|
||||
@@ -1339,6 +1355,7 @@ function lowerAssertEqual(
|
||||
// re-encoding and losing identity (SEMANTICS.md).
|
||||
(k === "object" && e.type.kind === "object" && e.type.className === "%Error") ||
|
||||
instance(e) ||
|
||||
strictOperand(e) ||
|
||||
// A symbol crosses as itself, so strict compares are identity.
|
||||
(!deep && k === "symbol") ||
|
||||
(deep && k !== "bytes" && k !== "object" && lowerer.dynConvertible(e.type)) ||
|
||||
@@ -1472,6 +1489,66 @@ function lowerAssertEqual(
|
||||
loc,
|
||||
};
|
||||
}
|
||||
// notStrictEqual of a nullable value whose other arms do not convert
|
||||
// (`child.stdin`, a stream or null) against null or undefined fails only
|
||||
// when the value is that unit, and the failure message renders only the
|
||||
// unit. The value's tag test stands in for it: the unit when it matches,
|
||||
// `false` otherwise, which the unit never equals. A union without that
|
||||
// unit arm never equals it.
|
||||
if (!deep && negated) {
|
||||
const unitSide = isUnitType(a.type) ? a : isUnitType(b.type) ? b : null;
|
||||
const other = unitSide === a ? b : a;
|
||||
if (unitSide !== null && other.type.kind === "union") {
|
||||
const tag = lowerer.armTag(other.type.unionId, unitSide.type);
|
||||
const unit = unitSide.type.kind === "nullT" ? "null" : "undefined";
|
||||
const unequal: IrExpr = { kind: "dynFrom", value: boolLit(false, loc), type: DYN, loc };
|
||||
const tested: IrExpr =
|
||||
tag < 0
|
||||
? {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "exprStmt", expr: other, loc }],
|
||||
result: unequal,
|
||||
type: DYN,
|
||||
loc,
|
||||
}
|
||||
: {
|
||||
kind: "ternary",
|
||||
cond: {
|
||||
kind: "unionIsTag",
|
||||
unionId: other.type.unionId,
|
||||
tag,
|
||||
negated: false,
|
||||
value: other,
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: {
|
||||
kind: "dynFrom",
|
||||
value: { kind: "unitLit", unit, type: unitSide.type, loc },
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
else_: unequal,
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
const unitDyn: IrExpr = { kind: "dynFrom", value: unitSide, type: DYN, loc: unitSide.loc };
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "assert.eqDyn",
|
||||
args: [
|
||||
unitSide === a ? unitDyn : tested,
|
||||
unitSide === a ? tested : unitDyn,
|
||||
boolLit(true, loc),
|
||||
boolLit(false, loc),
|
||||
msg,
|
||||
hasMsg,
|
||||
],
|
||||
type: VOID,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
}
|
||||
if (!deep) {
|
||||
const hint =
|
||||
a.type.kind === "union" || b.type.kind === "union"
|
||||
|
||||
@@ -137,6 +137,7 @@ import { funcTypeFromParamShapes, registerOverloadedCallableAlias } from "./call
|
||||
import {
|
||||
jsBindingHasOpenWrites,
|
||||
hasJsTypeAnnotation,
|
||||
inferredEvolvingLetType,
|
||||
isVarDeclared,
|
||||
numericIteratorSourceOf,
|
||||
provenanceElidedConstDecl,
|
||||
@@ -2465,10 +2466,16 @@ export function collectGlobals(
|
||||
) {
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(nameNode);
|
||||
if (symbol && !lowerer.globalsBySymbol.has(symbol)) {
|
||||
// An initializer-less `let` whose writes all agree keeps
|
||||
// their type, as a function's local does (lowerVarDecl).
|
||||
const evolved =
|
||||
isLet && !isVarDeclared(decl) && decl.initializer === undefined
|
||||
? inferredEvolvingLetType(lowerer, decl)
|
||||
: null;
|
||||
const g: IrGlobal = {
|
||||
id: `%g.${tag}${nsPrefix}${nameNode.text}`,
|
||||
name: nameNode.text,
|
||||
type: DYN,
|
||||
type: evolved ?? DYN,
|
||||
mutable: isLet,
|
||||
source: bindingSource(nameNode),
|
||||
};
|
||||
@@ -2477,7 +2484,7 @@ export function collectGlobals(
|
||||
// Mutable dyn globals hold the dyn undefined from module
|
||||
// entry (the main path's rule below): a closure called
|
||||
// above the declaration reads undefined instead of
|
||||
// faulting on NULL.
|
||||
// faulting on NULL. An undefined-armed union does the same.
|
||||
if (g.mutable) noteVarGlobalEntryInit(lowerer, sf, g);
|
||||
}
|
||||
continue;
|
||||
|
||||
@@ -100,6 +100,7 @@ import {
|
||||
BYTES_U8,
|
||||
CAUGHT,
|
||||
DATE_T,
|
||||
DYN_HANDLE_KINDS,
|
||||
DYN,
|
||||
F64,
|
||||
type IrExpr,
|
||||
@@ -1767,10 +1768,12 @@ const THROWN_AS_DYN: ReadonlySet<IrType["kind"]> = new Set([
|
||||
|
||||
/** A `return` at the top level of a CommonJS module (preflight admits it
|
||||
* there only): it ends the module's evaluation, and its value is
|
||||
* evaluated and discarded, as Node's module wrapper does. Top-level
|
||||
* bindings and export assignments after it would be left uninitialized
|
||||
* where Node leaves them in their temporal dead zone or absent, so a
|
||||
* module with any keeps a refusal. Null for a return inside a function. */
|
||||
* evaluated and discarded, as Node's module wrapper does. Bindings
|
||||
* declared after it stay uninitialized when it is taken, which only code
|
||||
* that exists by then can observe: a function created before the return,
|
||||
* or a hoisted function declaration. A module whose such functions read a
|
||||
* later binding, or that assigns exports after the return, keeps a
|
||||
* refusal. Null for a return inside a function. */
|
||||
function lowerModuleLevelReturn(lowerer: Lowerer, stmt: ts.ReturnStatement): IrStmt | null {
|
||||
let top: ts.Node = stmt;
|
||||
while (top.parent !== undefined && !ts.isSourceFile(top.parent)) {
|
||||
@@ -1780,19 +1783,34 @@ function lowerModuleLevelReturn(lowerer: Lowerer, stmt: ts.ReturnStatement): IrS
|
||||
const sf = top.parent;
|
||||
if (sf === undefined || !ts.isSourceFile(sf)) return null;
|
||||
const stmts = sf.statements.nodes;
|
||||
const later = stmts.slice(stmts.indexOf(top as ts.Statement) + 1);
|
||||
if (
|
||||
later.some(
|
||||
(s) =>
|
||||
ts.isVariableStatement(s) || ts.isClassDeclaration(s) || cjsExportAssignmentOf(s) !== null,
|
||||
)
|
||||
) {
|
||||
const index = stmts.indexOf(top as ts.Statement);
|
||||
const later = stmts.slice(index + 1);
|
||||
if (later.some((s) => cjsExportAssignmentOf(s) !== null)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
stmt,
|
||||
"a top-level return in a CommonJS module that declares variables or classes, or assigns exports, after it",
|
||||
"a top-level return in a CommonJS module that assigns exports after it",
|
||||
);
|
||||
}
|
||||
const laterBindings = laterModuleBindings(lowerer, later);
|
||||
if (laterBindings.size > 0) {
|
||||
// Functions that exist when the return is taken: every function in the
|
||||
// statements up to the return, and the hoisted declarations after it.
|
||||
const reachable = [
|
||||
...stmts.slice(0, index + 1),
|
||||
...later.filter((s) => ts.isFunctionDeclaration(s)),
|
||||
];
|
||||
const read = reachable
|
||||
.map((s) => functionReadOf(lowerer, s, laterBindings, ts.isFunctionDeclaration(s)))
|
||||
.find((r) => r !== null);
|
||||
if (read) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
read,
|
||||
`reading '${read.text}', declared after a top-level CommonJS return, from a function that can run once the return is taken`,
|
||||
);
|
||||
}
|
||||
}
|
||||
const loc = locOf(stmt);
|
||||
const ret: IrStmt = { kind: "return", value: null, loc };
|
||||
if (!stmt.expression) return ret;
|
||||
@@ -1803,6 +1821,57 @@ function lowerModuleLevelReturn(lowerer: Lowerer, stmt: ts.ReturnStatement): IrS
|
||||
};
|
||||
}
|
||||
|
||||
/** The module-scope bindings `stmts` declare: variables and classes, with
|
||||
* `var` declarations in nested blocks. Bindings inside functions are not
|
||||
* module-scope and are skipped. */
|
||||
function laterModuleBindings(lowerer: Lowerer, stmts: readonly ts.Statement[]): Set<ts.Symbol> {
|
||||
const bindings = new Set<ts.Symbol>();
|
||||
const addName = (name: ts.BindingName): void => {
|
||||
if (ts.isIdentifier(name)) {
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(name);
|
||||
if (symbol) bindings.add(symbol);
|
||||
return;
|
||||
}
|
||||
for (const element of name.elements.nodes) {
|
||||
if (ts.isBindingElement(element) && element.name) addName(element.name);
|
||||
}
|
||||
};
|
||||
const visit = (node: ts.Node): void => {
|
||||
if (ts.isFunctionLike(node) || ts.isClassStaticBlockDeclaration(node)) return;
|
||||
if (ts.isVariableDeclaration(node)) addName(node.name);
|
||||
if (ts.isClassDeclaration(node) && node.name) addName(node.name);
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
for (const stmt of stmts) visit(stmt);
|
||||
return bindings;
|
||||
}
|
||||
|
||||
/** The first identifier inside a function or class in `node` (anywhere in
|
||||
* `node` when `inFunction`) that reads one of `bindings`, or null. */
|
||||
function functionReadOf(
|
||||
lowerer: Lowerer,
|
||||
node: ts.Node,
|
||||
bindings: ReadonlySet<ts.Symbol>,
|
||||
inFunction: boolean,
|
||||
): ts.Identifier | null {
|
||||
if (inFunction && ts.isIdentifier(node)) {
|
||||
const symbol = lowerer.resolveValueSymbol(node);
|
||||
return symbol && bindings.has(symbol) ? node : null;
|
||||
}
|
||||
// Class bodies count as functions: field initializers run per instance.
|
||||
const nested =
|
||||
inFunction ||
|
||||
ts.isFunctionLike(node) ||
|
||||
ts.isClassDeclaration(node) ||
|
||||
ts.isClassExpression(node);
|
||||
return (
|
||||
ts.forEachChild(
|
||||
node,
|
||||
(child) => functionReadOf(lowerer, child, bindings, nested) ?? undefined,
|
||||
) ?? null
|
||||
);
|
||||
}
|
||||
|
||||
export function lowerStmt(lowerer: Lowerer, stmt: ts.Statement): IrStmt | IrStmt[] | null {
|
||||
if (ownPrototypeResetStatement(lowerer, stmt))
|
||||
return { kind: "block", body: [], loc: locOf(stmt) };
|
||||
@@ -5924,15 +5993,24 @@ function evolvingArrayHolderType(lowerer: Lowerer, decl: ts.VariableDeclaration)
|
||||
/** A single static type for an initializer-less JavaScript `let` whose
|
||||
* later plain writes all agree. An evolving JS local reads as `undefined`
|
||||
* until control flow proves a write, so a scalar slot is sound only when
|
||||
* every ordinary use already has that concrete flow type. Any closure
|
||||
* capture stays checked-dynamic: it can observe the local before an outer
|
||||
* assignment, and the checker deliberately leaves captured evolving lets
|
||||
* broad. */
|
||||
function inferredEvolvingLetType(lowerer: Lowerer, decl: ts.VariableDeclaration): IrType | null {
|
||||
* every ordinary use already has that concrete flow type. A write's call
|
||||
* counts when its signature is declared in the standard library, whose
|
||||
* return types are declared rather than inferred from JavaScript. A
|
||||
* closure capture can observe the local before an outer assignment, and
|
||||
* the checker leaves captured evolving lets broad: a captured runtime
|
||||
* handle (`let child; if (...) child = spawn(...)`) takes the handle or
|
||||
* `undefined`, which nested functions read as a checked-dynamic value
|
||||
* (closureDynLets), and other captured bindings stay checked-dynamic. */
|
||||
export function inferredEvolvingLetType(
|
||||
lowerer: Lowerer,
|
||||
decl: ts.VariableDeclaration,
|
||||
): IrType | null {
|
||||
if (!ts.isIdentifier(decl.name)) return null;
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(decl.name);
|
||||
const scope = decl.parent?.parent?.parent;
|
||||
if (symbol === undefined || (!ts.isBlock(scope) && !ts.isSourceFile(scope))) return null;
|
||||
// Another module reads an exported binding, which this scan does not see.
|
||||
if (ts.getCombinedModifierFlags(decl) & ts.ModifierFlags.Export) return null;
|
||||
|
||||
// A shorthand property's name (`{ width }`) resolves to the property
|
||||
// symbol, but it reads the binding: the checker's shorthand-value symbol.
|
||||
@@ -5967,9 +6045,19 @@ function inferredEvolvingLetType(lowerer: Lowerer, decl: ts.VariableDeclaration)
|
||||
const writes: IrType[] = [];
|
||||
const reads: ts.Identifier[] = [];
|
||||
let invalid = false;
|
||||
ts.walkPreorder(scope, (node) => {
|
||||
if (node !== scope && ts.isFunctionLike(node)) {
|
||||
if (captures(node)) invalid = true;
|
||||
let captured = false;
|
||||
// The capturing closure being scanned: reads inside one carry the
|
||||
// checker's broad type, so only its writes are collected.
|
||||
let closure: ts.Node | null = null;
|
||||
const visit = (node: ts.Node): void | "skip" | "stop" => {
|
||||
if (node !== scope && node !== closure && ts.isFunctionLike(node)) {
|
||||
if (captures(node)) {
|
||||
captured = true;
|
||||
const outer = closure;
|
||||
closure = node;
|
||||
ts.walkPreorder(node, visit);
|
||||
closure = outer;
|
||||
}
|
||||
return "skip";
|
||||
}
|
||||
if (ts.isBinaryExpression(node)) {
|
||||
@@ -5980,11 +6068,7 @@ function inferredEvolvingLetType(lowerer: Lowerer, decl: ts.VariableDeclaration)
|
||||
invalid = true;
|
||||
return "stop";
|
||||
}
|
||||
if (
|
||||
ts.isCallExpression(node.right) &&
|
||||
(implicitMethodCallInfersReturn(lowerer, node.right) ||
|
||||
isJsSourceFile(ts.getSourceFileOfNode(node.right)))
|
||||
) {
|
||||
if (ts.isCallExpression(node.right) && !stdlibDeclaredCall(lowerer, node.right)) {
|
||||
invalid = true;
|
||||
return "stop";
|
||||
}
|
||||
@@ -6013,6 +6097,7 @@ function inferredEvolvingLetType(lowerer: Lowerer, decl: ts.VariableDeclaration)
|
||||
return "stop";
|
||||
}
|
||||
if (
|
||||
closure === null &&
|
||||
ts.isIdentifier(node) &&
|
||||
node !== decl.name &&
|
||||
refersToBinding(node) &&
|
||||
@@ -6021,16 +6106,32 @@ function inferredEvolvingLetType(lowerer: Lowerer, decl: ts.VariableDeclaration)
|
||||
reads.push(node);
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
};
|
||||
ts.walkPreorder(scope, visit);
|
||||
const first = writes[0];
|
||||
if (invalid || first === undefined || !writes.every((write) => typeEquals(write, first)))
|
||||
return null;
|
||||
return reads.every((read) => {
|
||||
const mapped = lowerer.mapTypeOf(lowerer.typeOf(read));
|
||||
return mapped !== null && typeEquals(mapped, first);
|
||||
})
|
||||
? first
|
||||
: null;
|
||||
if (
|
||||
!reads.every((read) => {
|
||||
const mapped = lowerer.mapTypeOf(lowerer.typeOf(read));
|
||||
return mapped !== null && typeEquals(mapped, first);
|
||||
})
|
||||
)
|
||||
return null;
|
||||
if (!captured) return first;
|
||||
if (!DYN_HANDLE_KINDS.has(first.kind)) return null;
|
||||
lowerer.closureDynLets.add(symbol);
|
||||
return { kind: "union", unionId: lowerer.unions.intern([first, UNDEFINED_T]) };
|
||||
}
|
||||
|
||||
/** Whether a call's resolved signature is declared in the standard library
|
||||
* (`spawn(...)`), so its declared return type is the value's type. Calls of
|
||||
* JavaScript functions, and implicit generic methods, infer theirs, and a
|
||||
* `require(...)` result is a module rather than a value of its type. */
|
||||
function stdlibDeclaredCall(lowerer: Lowerer, call: ts.CallExpression): boolean {
|
||||
if (implicitMethodCallInfersReturn(lowerer, call) || requireSpecOf(call) !== null) return false;
|
||||
const declaration = lowerer.checker.getResolvedSignature(call)?.declaration;
|
||||
return declaration !== undefined && lowerer.isStdlibFile(ts.getSourceFileOfNode(declaration));
|
||||
}
|
||||
|
||||
export function lowerVarDecl(
|
||||
|
||||
@@ -4005,6 +4005,10 @@ export class Lowerer {
|
||||
* function called above the declaration statement reads that, never a
|
||||
* NULL slot. Filled by collectGlobals. */
|
||||
readonly varGlobalEntryInits = new Map<ts.SourceFile, IrGlobal[]>();
|
||||
/** Initializer-less JavaScript lets holding a runtime handle or undefined
|
||||
* that a closure captures (inferredEvolvingLetType). Reads inside a nested
|
||||
* function lower as the checked-dynamic value. */
|
||||
readonly closureDynLets = new Set<ts.Symbol>();
|
||||
/** Import-cycle bindings with an initialization flag, by global id, and
|
||||
* the top-level statements declaring them (cycle-initialization.ts). */
|
||||
readonly cycleInitFlags = new Map<string, CycleInitFlag>();
|
||||
@@ -4422,6 +4426,7 @@ export class Lowerer {
|
||||
checkedTable("preparedClassFactories", () => this.preparedClassFactories.size),
|
||||
checkedTable("preparingClassFactories", () => this.preparingClassFactories.size),
|
||||
checkedTable("varGlobalEntryInits", () => this.varGlobalEntryInits.size),
|
||||
checkedTable("closureDynLets", () => this.closureDynLets.size),
|
||||
checkedTable("cycleInitFlags", () => this.cycleInitFlags.size),
|
||||
checkedTable("classInitFlags", () => this.classInitFlags.size),
|
||||
checkedTable("cycleInitDeclarations", () => this.cycleInitDeclarations.size),
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/assert-strict-equal-nullable.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/cjs-top-level-return-later-bindings.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/evolving-let-child-process.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/evolving-let-socket-optional-reads.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// assert.strictEqual and assert.notStrictEqual compare a nullable number,
|
||||
// string or boolean with SameValue against a value of another type or
|
||||
// null/undefined, and notStrictEqual compares a nullable stream against
|
||||
// null or undefined. Failures report Node's messages.
|
||||
import assert from "node:assert";
|
||||
import { spawn, spawnSync } from "node:child_process";
|
||||
|
||||
function find(values: number[], value: number): number | null {
|
||||
const index = values.indexOf(value);
|
||||
return index < 0 ? null : index;
|
||||
}
|
||||
function label(n: number): string | null {
|
||||
return n > 0 ? "positive" : null;
|
||||
}
|
||||
function ratio(n: number): number | undefined {
|
||||
return n === 0 ? undefined : n / n;
|
||||
}
|
||||
function flag(n: number): boolean | null {
|
||||
return n === 0 ? null : n > 0;
|
||||
}
|
||||
|
||||
assert.strictEqual(find([1, 2, 3], 2), 1);
|
||||
assert.strictEqual(find([1, 2, 3], 9), null);
|
||||
assert.notStrictEqual(find([1, 2, 3], 3), null);
|
||||
assert.notStrictEqual(find([1, 2, 3], 3), 1);
|
||||
assert.strictEqual(label(1), "positive");
|
||||
assert.strictEqual(label(-1), null);
|
||||
assert.notStrictEqual(label(-1), undefined);
|
||||
assert.strictEqual(ratio(0), undefined);
|
||||
assert.strictEqual(ratio(-0), undefined);
|
||||
assert.strictEqual(flag(2), true);
|
||||
assert.strictEqual(flag(0), null);
|
||||
assert.strictEqual(null, find([], 1));
|
||||
console.log("passing comparisons ok");
|
||||
|
||||
const nan: number | null = Number("x");
|
||||
assert.strictEqual(nan, NaN);
|
||||
|
||||
function report(run: () => void): void {
|
||||
try {
|
||||
run();
|
||||
console.log("no throw");
|
||||
} catch (e) {
|
||||
const error = e as assert.AssertionError;
|
||||
console.log(error.name, error.code, error.operator, error.generatedMessage);
|
||||
console.log(JSON.stringify(error.message));
|
||||
console.log(String(error.actual), String(error.expected));
|
||||
}
|
||||
}
|
||||
report(() => assert.strictEqual(find([1], 1), null));
|
||||
report(() => assert.strictEqual(find([1], 2), 0));
|
||||
report(() => assert.strictEqual(label(1), null));
|
||||
report(() => assert.strictEqual(label(-1), "positive", "label mismatch"));
|
||||
report(() => assert.notStrictEqual(find([1], 2), null));
|
||||
report(() => assert.notStrictEqual(label(3), "positive"));
|
||||
report(() => assert.strictEqual(flag(0), false));
|
||||
report(() => assert.strictEqual(find([0], 0), -0));
|
||||
|
||||
const result = spawnSync("true");
|
||||
assert.strictEqual(result.status, 0);
|
||||
assert.strictEqual(result.signal, null);
|
||||
const child = spawn("true", [], { stdio: ["ignore", "pipe", "pipe"] });
|
||||
assert.notStrictEqual(child.stdout, null);
|
||||
assert.notStrictEqual(child.stdout, undefined);
|
||||
assert.notStrictEqual(child.stdin, undefined);
|
||||
report(() => assert.notStrictEqual(child.stdin, null));
|
||||
assert.strictEqual(child.signalCode, null);
|
||||
child.on("close", (code) => {
|
||||
assert.strictEqual(child.exitCode, 0);
|
||||
assert.strictEqual(child.signalCode, null);
|
||||
console.log("child closed", code);
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
'use strict';
|
||||
// A top-level return in a CommonJS module can precede declarations that
|
||||
// only the rest of the module reads. The timer and exit handler registered
|
||||
// before the return run, a callback's own binding shadows a later one of
|
||||
// the same name, and a function declared after the return is hoisted.
|
||||
const fs = require('fs');
|
||||
|
||||
console.log('start', describe());
|
||||
setTimeout(() => {
|
||||
const socket = 'inner socket';
|
||||
console.log('timer', socket);
|
||||
}, 1);
|
||||
process.on('exit', (code) => console.log('exit', code));
|
||||
for (const name of ['a', 'b']) {
|
||||
if (name === 'b' && !fs.existsSync('/scriptc-missing-path')) return;
|
||||
console.log('visited', name);
|
||||
}
|
||||
|
||||
const socket = 'outer socket';
|
||||
let count = 2;
|
||||
var total = count + 1;
|
||||
class Server {}
|
||||
const [first, second] = [count, total];
|
||||
for (var i = 0; i < 2; i++) console.log('not reached', i);
|
||||
console.log('not reached', socket, first, second, new Server());
|
||||
function describe() {
|
||||
return 'hoisted';
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// An initializer-less `let` assigned a child process on every branch keeps
|
||||
// the child's type, including when callbacks read it, as Node's
|
||||
// child_process tests do when they choose a command per platform.
|
||||
const { spawn } = require('child_process');
|
||||
|
||||
let child;
|
||||
if (process.platform === 'win32') child = spawn('cmd.exe', ['/c', 'echo', 'hi']);
|
||||
else child = spawn('echo', ['hi']);
|
||||
let text = '';
|
||||
child.stdout.on('data', (chunk) => {
|
||||
text += chunk;
|
||||
});
|
||||
|
||||
let producer, consumer;
|
||||
if (process.platform === 'win32') {
|
||||
producer = spawn('cmd.exe', ['/c', 'echo', 'hello&&', 'echo', 'node']);
|
||||
consumer = spawn('findstr', ['o']);
|
||||
} else {
|
||||
producer = spawn('printf', ['hello\nnode\nand\nworld\n']);
|
||||
consumer = spawn('grep', ['o']);
|
||||
}
|
||||
producer.stdout.on('data', (data) => {
|
||||
if (!consumer.stdin.write(data)) producer.stdout.pause();
|
||||
});
|
||||
consumer.stdin.on('drain', () => producer.stdout.resume());
|
||||
producer.stdout.on('end', () => consumer.stdin.end());
|
||||
let piped = '';
|
||||
consumer.stdout.on('data', (data) => {
|
||||
piped += data.toString('utf8', 0, data.length);
|
||||
});
|
||||
|
||||
let quiet;
|
||||
if (process.platform === 'win32') quiet = spawn('cmd.exe', ['/c', 'echo', 'quiet']);
|
||||
else quiet = spawn('echo', ['quiet']);
|
||||
quiet.stdout.setEncoding('utf8');
|
||||
quiet.stdout.resume();
|
||||
|
||||
let count;
|
||||
if (process.argv.length > 10) count = Math.max(1, 2);
|
||||
else count = Math.min(3, 4);
|
||||
console.log(count.toFixed(1));
|
||||
|
||||
process.on('exit', () => {
|
||||
console.log(JSON.stringify(text), JSON.stringify(piped));
|
||||
console.log(child.exitCode, producer.exitCode, consumer.exitCode, consumer.signalCode, quiet.exitCode);
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
'use strict';
|
||||
// Optional reads and calls on a captured `let` socket, before and after
|
||||
// its first write: undefined short-circuits, the socket answers.
|
||||
const net = require('net');
|
||||
let c;
|
||||
const server = net.createServer((s) => s.end());
|
||||
function report(label) {
|
||||
console.log(label, c?.remoteAddress, c?.address()?.family, c?.remotePort > 0);
|
||||
}
|
||||
report('before');
|
||||
const early = () => console.log('arrow before', c?.remoteAddress ?? 'none');
|
||||
early();
|
||||
server.listen(0, '127.0.0.1', () => {
|
||||
c = net.connect(server.address().port, '127.0.0.1', () => {
|
||||
report('after');
|
||||
console.log('inside', c?.remoteAddress, c?.address()?.family);
|
||||
c.end();
|
||||
server.close();
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
// The assert surface's honest fences: loose deep equality over recursive
|
||||
// types (equal/notEqual, deepEqual/notDeepEqual and assert/strict's aliases lower),
|
||||
// reference-equality strictEqual on objects,
|
||||
// union operands (narrow first), mismatched or uncomparable deep types
|
||||
// reference-equality strictEqual on objects, union operands with object
|
||||
// arms (narrow first), mismatched or uncomparable deep types
|
||||
// (typed arrays, class instances), namespace-object calls, unsupported
|
||||
// throws/rejects expectations (properties outside the static error
|
||||
// surface, empty shapes, non-literal objects, doesNotReject shapes —
|
||||
@@ -19,9 +19,9 @@ assert.notDeepEqual([1], [2]);
|
||||
const box = { n: 1 };
|
||||
assert.strictEqual(box, box); // reference equality — deepStrictEqual hint
|
||||
|
||||
let maybe: string | undefined = "x";
|
||||
let maybe: { n: number } | undefined = box;
|
||||
if ("x".length === 0) maybe = undefined;
|
||||
assert.strictEqual(maybe, "x"); // union operand — narrow first
|
||||
assert.strictEqual(maybe, box); // union operand with an object arm — narrow first
|
||||
|
||||
const unk: unknown = JSON.parse("{}");
|
||||
assert.strictEqual(unk, { n: 1 }); // strict + composite static side: reference identity does not survive the unknown boundary
|
||||
|
||||
@@ -7,10 +7,10 @@ assert-fences.ts:20:1 - error SC2020: 'assert.strictEqual of '{ n: number }' and
|
||||
|
||||
hint: strictEqual on objects is reference equality — deepStrictEqual compares structure
|
||||
|
||||
assert-fences.ts:24:1 - error SC2020: 'assert.strictEqual of 'string | undefined' and 'string' values' is part of the standard library types but has no scriptc lowering yet
|
||||
assert-fences.ts:24:1 - error SC2020: 'assert.strictEqual of '{ n: number } | undefined' and '{ n: number }' values' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
23 | if ("x".length === 0) maybe = undefined;
|
||||
24 | assert.strictEqual(maybe, "x"); // union operand — narrow first
|
||||
24 | assert.strictEqual(maybe, box); // union operand with an object arm — narrow first
|
||||
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
25 |
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 5208, "san": 7954 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3714, "san": 4365 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 4340, "san": 4815 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 5449, "san": 15501 }
|
||||
Reference in New Issue
Block a user