diff --git a/docs/content/limitations/builtin-apis/assertions.mdx b/docs/content/limitations/builtin-apis/assertions.mdx index 381fc3802..84c830e79 100644 --- a/docs/content/limitations/builtin-apis/assertions.mdx +++ b/docs/content/limitations/builtin-apis/assertions.mdx @@ -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. diff --git a/docs/content/limitations/language-features/commonjs-require.mdx b/docs/content/limitations/language-features/commonjs-require.mdx index f0bf67c4e..f13ca143f 100644 --- a/docs/content/limitations/language-features/commonjs-require.mdx +++ b/docs/content/limitations/language-features/commonjs-require.mdx @@ -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. diff --git a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts index 66cc8c1c7..7beff1c8f 100644 --- a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts +++ b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts @@ -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, + }; +} diff --git a/packages/compiler/src/frontend/lowering/lower-assert.ts b/packages/compiler/src/frontend/lowering/lower-assert.ts index 704314eaf..d167bb036 100644 --- a/packages/compiler/src/frontend/lowering/lower-assert.ts +++ b/packages/compiler/src/frontend/lowering/lower-assert.ts @@ -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" diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index a5fada25e..3afa145b3 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -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; diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 3aeda4db1..9a2e99e9c 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -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 = 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 { + const bindings = new Set(); + 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, + 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( diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 0fb5f9d8d..52d822fcc 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -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(); + /** 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(); /** Import-cycle bindings with an initialization flag, by global id, and * the top-level statements declaring them (cycle-initialization.ts). */ readonly cycleInitFlags = new Map(); @@ -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), diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/assert-strict-equal-nullable.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/assert-strict-equal-nullable.ts.json new file mode 100644 index 000000000..92d5e4440 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/assert-strict-equal-nullable.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/assert-strict-equal-nullable.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/cjs-top-level-return-later-bindings.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/cjs-top-level-return-later-bindings.cjs.json new file mode 100644 index 000000000..938628e19 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/cjs-top-level-return-later-bindings.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/cjs-top-level-return-later-bindings.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/evolving-let-child-process.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/evolving-let-child-process.cjs.json new file mode 100644 index 000000000..9ab5efe31 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/evolving-let-child-process.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/evolving-let-child-process.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/evolving-let-socket-optional-reads.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/evolving-let-socket-optional-reads.cjs.json new file mode 100644 index 000000000..b5701c7db --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/evolving-let-socket-optional-reads.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/evolving-let-socket-optional-reads.cjs" + ], + "diags": [] +} diff --git a/tests/corpus/assert-strict-equal-nullable.ts b/tests/corpus/assert-strict-equal-nullable.ts new file mode 100644 index 000000000..0a9051c56 --- /dev/null +++ b/tests/corpus/assert-strict-equal-nullable.ts @@ -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); +}); diff --git a/tests/corpus/cjs-top-level-return-later-bindings.cjs b/tests/corpus/cjs-top-level-return-later-bindings.cjs new file mode 100644 index 000000000..3cde1f32b --- /dev/null +++ b/tests/corpus/cjs-top-level-return-later-bindings.cjs @@ -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'; +} diff --git a/tests/corpus/evolving-let-child-process.cjs b/tests/corpus/evolving-let-child-process.cjs new file mode 100644 index 000000000..2cdc34700 --- /dev/null +++ b/tests/corpus/evolving-let-child-process.cjs @@ -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); +}); diff --git a/tests/corpus/evolving-let-socket-optional-reads.cjs b/tests/corpus/evolving-let-socket-optional-reads.cjs new file mode 100644 index 000000000..959191850 --- /dev/null +++ b/tests/corpus/evolving-let-socket-optional-reads.cjs @@ -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(); + }); +}); diff --git a/tests/diagnostics/assert-fences.ts b/tests/diagnostics/assert-fences.ts index 7cf96bc5b..0a8cd9c0e 100644 --- a/tests/diagnostics/assert-fences.ts +++ b/tests/diagnostics/assert-fences.ts @@ -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 diff --git a/tests/harness/__snapshots__/assert-fences.ts.txt b/tests/harness/__snapshots__/assert-fences.ts.txt index 041c20e9b..8ee90b796 100644 --- a/tests/harness/__snapshots__/assert-fences.ts.txt +++ b/tests/harness/__snapshots__/assert-fences.ts.txt @@ -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 | diff --git a/tests/harness/corpus-costs/assert-strict-equal-nullable.ts.json b/tests/harness/corpus-costs/assert-strict-equal-nullable.ts.json new file mode 100644 index 000000000..f316dc50b --- /dev/null +++ b/tests/harness/corpus-costs/assert-strict-equal-nullable.ts.json @@ -0,0 +1 @@ +{ "plain": 5208, "san": 7954 } diff --git a/tests/harness/corpus-costs/cjs-top-level-return-later-bindings.cjs.json b/tests/harness/corpus-costs/cjs-top-level-return-later-bindings.cjs.json new file mode 100644 index 000000000..5f011f8c6 --- /dev/null +++ b/tests/harness/corpus-costs/cjs-top-level-return-later-bindings.cjs.json @@ -0,0 +1 @@ +{ "plain": 3714, "san": 4365 } diff --git a/tests/harness/corpus-costs/evolving-let-child-process.cjs.json b/tests/harness/corpus-costs/evolving-let-child-process.cjs.json new file mode 100644 index 000000000..5196b4f3b --- /dev/null +++ b/tests/harness/corpus-costs/evolving-let-child-process.cjs.json @@ -0,0 +1 @@ +{ "plain": 4340, "san": 4815 } diff --git a/tests/harness/corpus-costs/evolving-let-socket-optional-reads.cjs.json b/tests/harness/corpus-costs/evolving-let-socket-optional-reads.cjs.json new file mode 100644 index 000000000..1f406eb04 --- /dev/null +++ b/tests/harness/corpus-costs/evolving-let-socket-optional-reads.cjs.json @@ -0,0 +1 @@ +{ "plain": 5449, "san": 15501 }