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:
Chris Tate
2026-10-30 01:04:18 -05:00
committed by GitHub
21 changed files with 492 additions and 41 deletions
@@ -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),
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/assert-strict-equal-nullable.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/cjs-top-level-return-later-bindings.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/evolving-let-child-process.cjs"
],
"diags": []
}
@@ -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();
});
});
+4 -4
View File
@@ -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 }