Fix eight compiler and type bug reports (#2319)

This commit is contained in:
Chris Tate
2026-10-26 03:10:55 -05:00
committed by GitHub
39 changed files with 653 additions and 7 deletions
+10
View File
@@ -1718,6 +1718,16 @@ declare module "node:fs" {
): void;
export function rmdirSync(path: string | URL): void;
export function readdirSync(path: string | URL): string[];
/* The recursive form: every entry below the path, relative to it. */
export function readdirSync(
path: string | URL,
options: {
recursive?: boolean;
withFileTypes?: false;
encoding?: "utf8" | "utf-8";
[option: string]: unknown;
},
): string[];
/* The withFileTypes form: Dirent rows (name + parentPath + the type
* probes — the honest subset of Node's Dirent surface). The type is
* interned explicitly in type-mapper.ts (the record's hidden %dtype field
@@ -423,6 +423,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"fs.rmRetrySync": "scr_fs_rm_opts_retry",
"fs.rmdirSync": "scr_fs_rmdir",
"fs.readdirSync": "scr_fs_readdir",
"fs.readdirRecursiveSync": "scr_fs_readdir_recursive",
"fs.globSync": "scr_fs_glob_strings",
"fs.callbackGlob": "scr_fs_glob_callback",
"fsp.globStrings": "scr_fsp_glob_strings",
@@ -692,6 +692,9 @@ export function lowerSafeIndexRead(
const elem = arr.type.elem;
if (elem.kind === "void") return null;
const resultT = arrayValueType(lowerer, elem);
// An element union that cannot take an undefined arm has no value for a
// missing element.
if (resultT.kind !== "dyn" && lowerer.wrappedUndefined(resultT, loc) === null) return null;
const key = `idxOr:${typeKey(elem)}`;
let name = lowerer.arrHofHelpers.get(key);
if (!name) {
@@ -1306,6 +1306,7 @@ export function lowerFsReaddirTypesCall(
);
}
let sawWithFileTypes = false;
let recursive: ts.Expression | null = null;
for (const p of optsNode.properties.nodes) {
const m = optionMember(p);
if (!m) {
@@ -1334,9 +1335,11 @@ export function lowerFsReaddirTypesCall(
"names decode as utf8 (the default); encoding: 'buffer' has no lowering",
);
}
} else if (m.name === "recursive" && !promiseForm) {
recursive = m.value;
} else {
// The options-record stance: recursive (a documented knob with
// no lowering) fences by name; undocumented keys drop like Node.
// no lowering here) fences by name; undocumented keys drop like Node.
fenceOrDropOptionKey(
lowerer,
p,
@@ -1347,6 +1350,26 @@ export function lowerFsReaddirTypesCall(
);
}
}
if (recursive !== null) {
// Only the `true`/`false` keywords fold; any other expression, even one
// typed as a literal, evaluates at run time for its effects.
let flag = recursive;
while (ts.isParenthesizedExpression(flag)) flag = flag.expression;
const literal =
flag.kind === ts.SyntaxKind.TrueKeyword
? "true"
: flag.kind === ts.SyntaxKind.FalseKeyword
? "false"
: null;
if (sawWithFileTypes && literal !== "false") {
lowerer.noLowering(
`${operation} with withFileTypes and recursive`,
recursive,
"recursive: true lists relative names; withFileTypes: true lists one directory's Dirents",
);
}
if (!sawWithFileTypes) return lowerFsReaddirRecursive(lowerer, call, recursive, literal, loc);
}
if (!sawWithFileTypes) {
lowerer.noLowering(
`${operation} with 2 arguments`,
@@ -1388,6 +1411,75 @@ export function lowerFsReaddirTypesCall(
};
}
/** `fs.readdirSync(path, { recursive })` without withFileTypes: the
* relative names of every entry below the path when `recursive` is true
* (scr_fs_readdir_recursive, Node's queue order), the directory's own names
* when it is false. A recursive flag that is not a boolean literal selects
* between the two at run time, after the path is evaluated. */
function lowerFsReaddirRecursive(
lowerer: Lowerer,
call: ts.CallExpression,
recursive: ts.Expression,
literal: string | null,
loc: SrcLoc,
): IrExpr {
const names = arrayOf(STRING);
// @types/node types the call `string[] | Buffer[]` when the options name
// no encoding; the names are strings, so the call is the string[] arm.
const callType = lowerer.mapTypeOf(lowerer.typeOf(call));
if (
!callType ||
!(
typeEquals(callType, names) ||
(callType.kind === "union" && lowerer.armTag(callType.unionId, names) >= 0)
)
) {
lowerer.noLowering(
"readdirSync(path, { recursive }) where the result is not a string array",
call,
"names decode as utf8 strings (the default encoding)",
);
}
return lowerFsReaddirNames(lowerer, call, recursive, literal, names, loc);
}
function lowerFsReaddirNames(
lowerer: Lowerer,
call: ts.CallExpression,
recursive: ts.Expression,
literal: string | null,
names: IrType,
loc: SrcLoc,
): IrExpr {
const path = lowerFsPath(lowerer, call.arguments.nodes[0]!);
const listing = (fn: IrLibFn, arg: IrExpr): IrExpr => ({
kind: "libCall",
fn,
args: [arg],
type: names,
loc,
});
if (literal === "true") return listing("fs.readdirRecursiveSync", path);
if (literal === "false") return listing("fs.readdirSync", path);
const pathLocal = lowerer.declareHiddenLocal("%readdirPath", STRING);
const pathRef = varRef(pathLocal.id, STRING, loc);
const flag = lowerer.lowerExprExpecting(recursive, BOOL);
return {
kind: "seqExpr",
stmts: [{ kind: "varDecl", localId: pathLocal.id, init: path, loc }],
result: {
kind: "ternary",
cond: flag,
then: listing("fs.readdirRecursiveSync", pathRef),
else_: listing("fs.readdirSync", pathRef),
type: names,
loc,
},
type: names,
loc,
};
}
/** fs.statSync / fs.lstatSync's documented option keys. */
const FS_STAT_DOCUMENTED_OPTIONS: ReadonlySet<string> = new Set(["bigint", "throwIfNoEntry"]);
@@ -1,5 +1,6 @@
/* Element reads and writes (`a[i]`, `a[i] = v`), compound element updates, and keyed record reads. */
import { OBJECT_PROTOTYPE_MEMBERS } from "../../shape-inference/domain.js";
import { lowerUnionTupleRead } from "./union-fields.js";
import * as ts from "../../ts7/adapter.js";
import {
lowerOptionalIndex,
@@ -466,6 +467,12 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre
"tuple indexing with a non-literal index (tuples are fixed-shape — use t[0], t[1], ...)",
);
}
// A union of tuples asserted to one tuple view (`(r as readonly
// [boolean, T | E])[0]`) still holds the union.
if (obj.type.kind === "union") {
const element = lowerUnionTupleRead(lowerer, obj, idx, locOf(expr));
if (element) return lowerer.maybeNarrow(element, expr);
}
const shapeId = obj.type.kind === "record" ? obj.type.shapeId : receiverIr.shapeId;
const actualShape = lowerer.shapes.get(shapeId) ?? shape;
const fieldType = actualShape.fields.find((f) => f.name === String(idx))?.type;
@@ -598,6 +605,15 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre
}
}
}
if (key.type.kind === "f64" && ts.isNumericLiteral(expr.argumentExpression)) {
const element = lowerUnionTupleRead(
lowerer,
value,
Number(expr.argumentExpression.text),
locOf(expr),
);
if (element) return lowerer.maybeNarrow(element, expr);
}
if (key.type.kind === "f64") {
const optional = optionalArmIndexRead(lowerer, value, key, locOf(expr));
if (optional) return lowerer.maybeNarrow(optional, expr);
@@ -419,7 +419,11 @@ export function lowerDynObjectLiteral(
: !boxValue &&
(ts.isObjectLiteralExpression(valueExpr) ||
(ts.isArrayLiteralExpression(valueExpr) &&
(holdsEvolvingArray(lowerer, valueExpr) ||
// An empty literal takes whatever the object's users
// store into it (`{ [kChildren]: [] }`), so it is a
// checked-dynamic array rather than its `never[]` layout.
(valueExpr.elements.nodes.length === 0 ||
holdsEvolvingArray(lowerer, valueExpr) ||
lowerer.untypedLiteralContext(valueExpr))))
? lowerer.lowerExprExpecting(valueExpr, DYN)
: lowerer.lowerExpr(valueExpr as ts.Expression);
@@ -3376,6 +3376,18 @@ export function lowerUnionProperty(
expr,
);
}
// An array the checker sees through a union of arrays (`readdirSync`'s
// `string[] | Buffer[]`, whose lowering answers the string[] arm).
if (value.type.kind === "array" && expr.name.text === "length") {
return {
kind: "arrIntrinsic",
method: "length",
receiver: value,
args: [],
type: F64,
loc: locOf(expr),
};
}
if (value.type.kind !== "union") {
lowerer.unsupported(
"SC1090",
@@ -281,3 +281,58 @@ export function lowerUnionFieldWrite(
}
return { kind: "seqExpr", stmts: body, result: dispatch, type: result.type, loc };
}
/** Read the element at a literal index from a union of tuples whose element
* types differ (`readonly [true, number] | readonly [false, string]`, index
* 1): each tag reads its own tuple's slot and widens it into the union of
* the arms' element types; the caller narrows the result to the checker's
* flow type. Null when an arm is not a tuple with that position. */
export function lowerUnionTupleRead(
lowerer: Lowerer,
receiver: IrExpr,
index: number,
loc: IrExpr["loc"],
): IrExpr | null {
if (receiver.type.kind !== "union" || !Number.isInteger(index) || index < 0) return null;
const unionId = receiver.type.unionId;
const arms = lowerer.unions.get(unionId)?.arms;
if (!arms || arms.length < 2) return null;
const name = String(index);
const fields: UnionFieldArm[] = [];
const parts: IrType[] = [];
for (const arm of arms) {
if (arm.kind !== "record" || lowerer.shapes.get(arm.shapeId)?.tuple !== true) return null;
const field = unionFieldArm(lowerer, arm, name);
if (!field || field.type.kind === "void") return null;
fields.push(field);
const own =
field.type.kind === "union" ? lowerer.unions.get(field.type.unionId)?.arms : [field.type];
if (!own) return null;
for (const part of own) if (!parts.some((p) => typeEquals(p, part))) parts.push(part);
}
const type: IrType =
parts.length === 1 ? parts[0]! : { kind: "union", unionId: lowerer.unions.intern(parts) };
const object = lowerer.declareHiddenLocal("%unionReadObject", receiver.type);
const objectRef = varRef(object.id, object.type, loc);
const read = (tag: number): IrExpr | null => {
const probe = unionFieldRead(unionId, tag, objectRef, fields[tag]!, name, loc);
const converted = lowerer.coerceToExpected(probe, type);
return typeEquals(converted.type, type) && isWidening(converted, probe) ? converted : null;
};
let dispatch = read(fields.length - 1);
if (!dispatch) return null;
for (let tag = fields.length - 2; tag >= 0; tag--) {
const then = read(tag);
if (!then) return null;
dispatch = {
kind: "ternary",
cond: { kind: "unionIsTag", unionId, tag, value: objectRef, negated: false, type: BOOL, loc },
then,
else_: dispatch,
type,
loc,
};
}
const init: IrStmt = { kind: "varDecl", localId: object.id, init: receiver, loc };
return { kind: "seqExpr", stmts: [init], result: dispatch, type, loc };
}
@@ -1,3 +1,4 @@
import { unionNarrowFailureMessage } from "./union-narrow.js";
import {
compactDeclOrigins,
inPackageFile,
@@ -10,7 +11,7 @@ import { relieveHeapPressure } from "../../heap-pressure.js";
import { moduleNamespaceObject, moduleNamespaceValue } from "./module-namespace-elements.js";
import { keyedHelperName } from "./stable-names.js";
import { positionTag, StructuralPositions } from "./structural-positions.js";
import { onlyForwarded, stripTypeWrappers } from "./typed-arguments.js";
import { bindingSymbolOfRead, onlyForwarded, stripTypeWrappers } from "./typed-arguments.js";
import {
needsGeneralNodeStreams,
noteUntypedStreamFlow,
@@ -6116,6 +6117,22 @@ export class Lowerer {
};
if (!stringRead(e.left) && !stringRead(e.right)) return null;
if (!primitive(e.left) || !primitive(e.right)) return null;
// A missing read is undefined, and `+` yields a number only when
// neither operand is a string: an operand that is always a string
// (`"x-" + PAD[i]`) makes the result a string. Only literals,
// templates and sums with such an operand are always strings; a
// binding or call can hold a missing read.
const definitelyString = (part: ts.Expression): boolean => {
const p = peel(part);
if (ts.isStringLiteral(p) || ts.isNoSubstitutionTemplateLiteral(p)) return true;
if (ts.isTemplateExpression(p)) return true;
return (
ts.isBinaryExpression(p) &&
p.operatorToken.kind === ts.SyntaxKind.PlusToken &&
(definitelyString(p.left) || definitelyString(p.right))
);
};
if (definitelyString(e.left) || definitelyString(e.right)) return null;
return { kind: "union", unionId: this.unions.intern([F64, STRING]) };
};
/** The union of primitive result types, or null when any is missing or
@@ -11617,6 +11634,26 @@ export class Lowerer {
private coerceIntoSlot(node: ts.Node, expr: IrExpr, expected: IrType): IrExpr {
refuseInferredCallbackDrops(this, node, expr.type, expected);
// A union into one of its record arms extracts that arm, trusting the
// checker to have excluded the others. A built-in object arm the checker
// still types the site with (`c ?? new Map()` into an interface the
// untyped Map satisfies only through `any`) holds a value at run time,
// and no conversion makes a record of it, so the flow is refused like
// the arm's value would be.
if (
expr.type.kind === "union" &&
expected.kind === "record" &&
this.armTag(expr.type.unionId, expected) >= 0
) {
const site = this.mapTypeOf(this.typeOf(node));
const siteArms =
site?.kind === "union" ? (this.unions.get(site.unionId)?.arms ?? []) : site ? [site] : [];
for (const arm of this.unions.get(expr.type.unionId)?.arms ?? []) {
if (arm.kind !== "map" && arm.kind !== "set") continue;
if (!siteArms.some((s) => typeEquals(s, arm))) continue;
this.requireExactShape(node, arm, expected);
}
}
// An assertion's scalar check whose value goes straight back into a
// checked-dynamic slot (`process.loadEnvFile({} as unknown as string)`)
// keeps the original value: JavaScript erases assertions, so the check
@@ -11779,6 +11816,25 @@ export class Lowerer {
}
}
// A value the checker types `never` at this site (a read narrowed past
// every member of its union) exists only when an assertion lied about
// its type, so its layout need not fit the slot: it is evaluated, then a
// TypeError is thrown, as checked extraction of an impossible arm does.
if (!typeEquals(e.type, expected) && (this.typeOf(node).flags & ts.TypeFlags.Never) !== 0) {
return {
kind: "seqExpr",
stmts: [{ kind: "exprStmt", expr: e, loc: e.loc }],
result: nodeThrowExpr(
1,
"",
unionNarrowFailureMessage(e.type, (t) => this.fmt(t)),
expected,
e.loc,
),
type: expected,
loc: e.loc,
};
}
this.requireExactShape(node, e.type, expected);
return e;
}
@@ -13443,7 +13499,7 @@ export class Lowerer {
const fn = decl.parent;
const symbol = this.checker.getSymbolAtLocation(nameNode);
const isRead = (id: ts.Identifier): boolean =>
id !== nameNode && this.checker.getSymbolAtLocation(id) === symbol;
id !== nameNode && bindingSymbolOfRead(this, id) === symbol;
const pure = fn !== undefined && symbol !== undefined && onlyForwarded(this, fn, isRead);
// A forwarded-only tuple is never built: a tuple with optional
// positions (`Parameters<(n?: T) => R>`, whose length varies per call)
@@ -2223,8 +2223,8 @@ export const BUILTIN_MODULE_FN_ALIASES: Record<
mkdirSync: ["fs.mkdirModeSync", "fs.mkdirRecursiveSync", "fs.mkdirRecursiveModeSync"],
// The { recursive, force } and maxRetries/retryDelay option lowerings.
rmSync: ["fs.rmOptsSync", "fs.rmRetrySync"],
// The { withFileTypes: true } Dirent form.
readdirSync: ["fs.readdirTypesSync"],
// The { withFileTypes: true } Dirent form and the { recursive: true } walk.
readdirSync: ["fs.readdirTypesSync", "fs.readdirRecursiveSync"],
// The { throwIfNoEntry } probe form.
statSync: ["fs.statProbeSync"],
// realpathSync and realpathSync.native read as function values.
@@ -195,6 +195,15 @@ export function declareTypedArguments(
});
}
/** The binding an identifier reads: its symbol, or for the name of a
* shorthand property (`{ xs }`) the variable whose value the property takes. */
export function bindingSymbolOfRead(lowerer: Lowerer, id: ts.Identifier): ts.Symbol | undefined {
const parent = id.parent;
if (parent && ts.isShorthandPropertyAssignment(parent) && parent.name === id)
return lowerer.checker.getShorthandAssignmentValueSymbol(parent);
return lowerer.checker.getSymbolAtLocation(id);
}
/** Whether every read of a tuple binding in `fn` (identified by `isRoot`)
* only forwards it: `f.apply(t, x)`, `g(...x)` into a program function,
* `x.length`, or a const alias whose own reads only forward. Reads inside
@@ -262,7 +271,7 @@ export function onlyForwarded(
const alias = lowerer.checker.getSymbolAtLocation(p.name);
const aliasName = p.name;
if (alias) {
roots.push((id) => id !== aliasName && lowerer.checker.getSymbolAtLocation(id) === alias);
roots.push((id) => id !== aliasName && bindingSymbolOfRead(lowerer, id) === alias);
return undefined;
}
}
@@ -765,6 +765,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"fs.rmSync",
"fs.rmdirSync",
"fs.readdirSync",
"fs.readdirRecursiveSync",
"fs.readdirTypesSync",
"fs.globSync",
"fs.globTypesSync",
@@ -592,6 +592,9 @@ function buildLibFnSignatures() {
"fs.rmSync": { argTypes: [STRING], result: VOID },
"fs.rmdirSync": { argTypes: [STRING], result: VOID },
"fs.readdirSync": { argTypes: [STRING], result: arrayOf(STRING) },
/** `fs.readdirSync(path, { recursive: true })`: every entry below the
* path, relative to it, in Node's queue order. */
"fs.readdirRecursiveSync": { argTypes: [STRING], result: arrayOf(STRING) },
// Result is the call site's Dirent record array (VOID is the
// structure-checked sentinel, the os.networkInterfaces pattern).
/** `fs.readdirSync(path, { withFileTypes: true })` — Dirent rows over
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/fs-readdir-recursive.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/never-narrowed-literal-argument.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/rest-tuple-shorthand-property.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/string-plus-element-read-return.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/symbol-keyed-recursive-record.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/union-tuple-element-read.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/diagnostics/promise-all-generic-mixed-union.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/diagnostics/untyped-map-into-interface.ts"
],
"diags": []
}
+50
View File
@@ -6344,6 +6344,56 @@ ScrArr *scr_fs_readdir(ScrStr *path) {
return arr;
}
/* fs.readdirSync(path, { recursive: true }): Node's readdirSyncRecursive.
* Directories are listed in queue order: the base, then each directory in
* the order its entry was listed. Each entry's path is path.join of its
* directory and name; the result holds it relative to the base
* (path.relative). An entry stat(2) reports as a directory, a symlink to
* one included, joins the queue. */
ScrArr *scr_fs_readdir_recursive(ScrStr *base) {
if (!scr_fs_path_ok(base, "path")) return NULL;
ScrArr *out = scr_arr_new(SCR_ELEM_STR, 0);
size_t len = 1, cap = 8;
ScrStr **queue = malloc(cap * sizeof *queue);
if (!queue) scr_trap("scriptc: out of memory\n");
queue[0] = scr_str_retain(base);
bool failed = false;
for (size_t at = 0; at < len && !failed; at++) {
ScrScandir *scan = scr_fs_scandir(queue[at]);
if (!scan) {
failed = true;
break;
}
for (size_t i = 0; i < scr_fs_scandir_count(scan); i++) {
ScrArr *parts = scr_arr_new(SCR_ELEM_STR, 2);
scr_arr_push_ref(parts, scr_str_retain(queue[at]));
scr_arr_push_ref(parts, scr_fs_scandir_name(scan, i));
ScrStr *full = scr_path_join(parts);
scr_arr_release(parts);
scr_arr_push_ref(out, scr_path_relative(base, full));
struct stat st;
if (stat(full->data, &st) == 0 && S_ISDIR(st.st_mode)) {
if (len == cap) {
cap *= 2;
queue = realloc(queue, cap * sizeof *queue);
if (!queue) scr_trap("scriptc: out of memory\n");
}
queue[len++] = full;
} else {
scr_str_release(full);
}
}
scr_fs_scandir_free(scan);
}
for (size_t i = 0; i < len; i++) scr_str_release(queue[i]);
free(queue);
if (failed) {
scr_arr_release(out);
return NULL;
}
return out;
}
/* ── the withFileTypes scandir snapshot ──────────────────────────────
* One readdir pass capturing name + entry kind for the emitter's Dirent
* assembly (scr_runtime.h has the contract). Kinds are libuv's UV_DIRENT
+1
View File
@@ -293,6 +293,7 @@ void scr_fs_mkdir(ScrStr *path);
void scr_fs_rm(ScrStr *path);
void scr_fs_rmdir(ScrStr *path);
ScrArr *scr_fs_readdir(ScrStr *path); /* names (byte order on POSIX, as libuv sorts), no "."/".." */
ScrArr *scr_fs_readdir_recursive(ScrStr *path); /* relative paths of every entry below path, Node's queue order */
/* The fs option forms (same throw discipline). mkdir_recursive is Node's
* recursive algorithm (existing-dir target is a no-op; errors report
+52
View File
@@ -0,0 +1,52 @@
// readdirSync(path, { recursive: true }) lists every entry below the path,
// relative to it: the path's own entries first, then each directory's in the
// order its entry was listed. A symlink to a directory is followed.
import { mkdirSync, mkdtempSync, readdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
const root = mkdtempSync(join(tmpdir(), "readdir-recursive-"));
writeFileSync(join(root, "a.txt"), "a");
mkdirSync(join(root, "b", "d"), { recursive: true });
writeFileSync(join(root, "b", "c.txt"), "c");
writeFileSync(join(root, "b", "d", "e.txt"), "e");
mkdirSync(join(root, "empty"));
mkdirSync(join(root, "z"));
writeFileSync(join(root, "z", "y.txt"), "y");
symlinkSync(join(root, "b", "d"), join(root, "link"));
console.log(readdirSync(root, { recursive: true }).join(" "));
console.log(readdirSync(root, { recursive: false }).join(" "));
console.log(readdirSync(`${root}/b/`, { recursive: true }).join(" "));
let deep = false;
for (const flag of [false, true]) {
deep = flag;
console.log(deep, readdirSync(join(root, "z"), { recursive: deep }).length);
}
console.log(readdirSync(join(root, "empty"), { recursive: true }).length);
// A flag expression typed `true` or `false` still evaluates, once.
let probes = 0;
function yes(): true {
probes++;
return true;
}
function no(): false {
probes++;
return false;
}
console.log(
readdirSync(join(root, "b"), { recursive: yes() }).length,
readdirSync(join(root, "b"), { recursive: no() }).length,
probes,
);
try {
readdirSync(join(root, "missing"), { recursive: true });
} catch (e) {
console.log((e as NodeJS.ErrnoException).code);
}
rmSync(root, { recursive: true });
@@ -0,0 +1,46 @@
// A string-literal union narrowed to `never` passes to a `never` parameter:
// the exhaustiveness-check idiom over if-chains and switches compiles, and the
// unreachable call never runs.
function unreachable(_: never): never {
throw new Error("unreachable");
}
type Kind = "a" | "b";
function viaIf(kind: Kind): number {
if (kind === "a") return 1;
if (kind === "b") return 2;
return unreachable(kind);
}
type Shape = "circle" | "square" | "triangle";
function corners(shape: Shape): number {
switch (shape) {
case "circle":
return 0;
case "square":
return 4;
case "triangle":
return 3;
default:
return unreachable(shape);
}
}
function asserted(kind: Kind): string {
if (kind === "a" || kind === "b") return kind.toUpperCase();
return unreachable(kind as never);
}
type Level = 1 | 2;
function viaNumbers(level: Level): string {
if (level === 1) return "one";
if (level === 2) return "two";
return unreachable(level);
}
const kinds: Kind[] = ["a", "b"];
console.log(kinds.map(viaIf).join(","), viaIf(Date.now() > 0 ? "a" : "b"));
const shapes: Shape[] = ["circle", "square", "triangle"];
console.log(shapes.map(corners).join(","));
console.log(kinds.map(asserted).join(""), viaNumbers(1), viaNumbers(2));
@@ -0,0 +1,23 @@
// A rest parameter typed as a fixed tuple, read through a shorthand property
// (`{ xs }`), directly or through a const alias, is the tuple of the call's
// arguments.
function wrap(...xs: [number]) {
return { xs };
}
function pair(...args: [string, number]) {
const kept = args;
return { kept, size: args.length };
}
function add(a: number, b: number): number {
return a + b;
}
function forward(...args: [number, number]): number {
return add(...args);
}
const w = wrap(1);
console.log(w.xs[0], w.xs.length, JSON.stringify(w));
const p = pair("a", 2);
console.log(p.kept[0], p.kept[1], p.size, JSON.stringify(p));
console.log(forward(3, 4));
@@ -0,0 +1,42 @@
// A function declared to return a string concatenates a string with an
// element of a readonly string array. A missing element reads as undefined,
// which `+` turns into "undefined" next to a string, so the result is always
// a string and its call can be used inline.
const PAD: readonly string[] = ["00", "01", "02"];
function label(i: number): string {
return "x-" + PAD[i];
}
function date(y: number, m: number, d: number): string {
return y + "-" + PAD[m] + "-" + PAD[d];
}
function suffix(i: number): string {
return PAD[i] + "!";
}
console.log(label(1).length, label(1), label(7));
console.log(date(2026, 1, 2).length, date(2026, 2, 9));
console.log(suffix(0).toUpperCase(), suffix(5));
// Two possibly missing reads add to NaN when both are missing.
function pair(i: number, j: number): string {
return PAD[i] + PAD[j];
}
const both = pair(8, 9);
console.log(pair(0, 1), typeof both, String(both));
// A missing element held in a binding or returned by a function is undefined,
// so two of them still add to NaN.
function at(i: number): string {
return PAD[i];
}
function viaBinding(i: number, j: number): string {
const head = PAD[i];
return head + PAD[j];
}
function viaCall(i: number, j: number): string {
return at(i) + PAD[j];
}
console.log(viaBinding(0, 1), String(viaBinding(5, 6)), typeof viaBinding(5, 6));
console.log(viaCall(0, 2), String(viaCall(5, 6)), viaBinding(0, 9));
@@ -0,0 +1,35 @@
// A recursive record keyed by unique symbols, built from literals whose
// child arrays start empty, holds the nodes later pushed into those arrays.
const kValue: unique symbol = Symbol("value");
const kChildren: unique symbol = Symbol("children");
interface TreeNode {
[kChildren]: TreeNode[];
[kValue]: number;
}
function node(value: number): TreeNode {
return { [kValue]: value, [kChildren]: [] };
}
const root = node(0);
root[kChildren].push(node(1));
root[kChildren].push(node(2));
root[kChildren][0]![kChildren].push(node(3));
console.log(root[kChildren].length, root[kChildren][0]![kValue], root[kChildren][1]![kValue]);
function sum(n: TreeNode): number {
let total = n[kValue];
for (const child of n[kChildren]) total += sum(child);
return total;
}
function depth(n: TreeNode): number {
return 1 + Math.max(0, ...n[kChildren].map(depth));
}
console.log(sum(root), depth(root));
// A literal that starts with children.
const seeded: TreeNode = { [kValue]: 10, [kChildren]: [node(11)] };
seeded[kChildren].push(node(12));
console.log(seeded[kChildren].map((c) => c[kValue]).join(","), sum(seeded));
+45
View File
@@ -0,0 +1,45 @@
// Literal-index reads from a union of tuples whose element types differ:
// each tuple's own element is read, and the checker's narrowing of the
// tuple (or of the element) applies as usual.
type Ok<T> = readonly [true, T];
type Err<E> = readonly [false, E];
type Result<T, E> = Ok<T> | Err<E>;
function pick(n: number): Result<number, string> {
return n > 0 ? [true, n] : [false, "neg"];
}
for (const n of [1, -1, 5]) {
const r = pick(n);
console.log(r[0], r[1]);
if (r[0]) console.log(r[1] + 1);
else console.log(r[1].length);
}
function describe(r: Result<number, string>): string {
const value = r[1];
return `${r[0] ? "ok" : "err"}:${typeof value}:${value}`;
}
console.log(describe(pick(2)), describe(pick(0)));
function isOk<T, E>(r: Result<T, E>): boolean {
return r[0];
}
function payload<T, E>(r: Result<T, E>): T | E {
return r[1];
}
console.log(isOk(pick(3)), isOk(pick(-3)), payload(pick(4)), payload(pick(-4)));
type Triple = readonly [string, number, boolean] | readonly [number, string, boolean];
const triples: Triple[] = [
["a", 1, true],
[2, "b", false],
];
for (const t of triples) console.log(typeof t[0], typeof t[1], t[2]);
// A read through a single tuple view of the union.
function flag<T, E>(r: Result<T, E>): boolean {
return (r as readonly [boolean, T | E])[0];
}
console.log(flag(pick(1)), flag(pick(-1)));
@@ -0,0 +1,8 @@
// Promise.all over a generic array instantiated with a union of a value and
// a promise, with an element read of its result: the call is refused by name.
async function f<V>(xs: V[]): Promise<void> {
const values = await Promise.all(xs);
values[0];
}
f<string | Promise<number>>(["x"]);
export {};
@@ -0,0 +1,12 @@
// An untyped `new Map()` satisfies a record interface only through `any`;
// its flow into the interface-typed result is refused at build time, like a
// typed Map is.
interface Cache {
get(key: string): number | undefined;
set(key: string, value: number): unknown;
}
function make(c?: Cache): Cache {
return c ?? new Map();
}
console.log(make().get("a"));
export {};
@@ -0,0 +1,8 @@
promise-all-generic-mixed-union.ts:4:36 - error SC2020: 'Promise.all over this argument shape' is part of the standard library types but has no scriptc lowering yet (instantiating 'f' with <Promise<number> | string>)
3 | async function f<V>(xs: V[]): Promise<void> {
4 | const values = await Promise.all(xs);
| ^~
5 | values[0];
hint: an array of promises (Promise<T>[]) is the supported form
@@ -0,0 +1,8 @@
untyped-map-into-interface.ts:9:10 - error SC2002: record shapes must match exactly or width-coerce: expected '{ get: (string) => number | undefined; set: (string, number) => unknown }', got 'Map<unknown, unknown>'
8 | function make(c?: Cache): Cache {
9 | return c ?? new Map();
| ^~~~~~~~~~~~~~
10 | }
hint: width subtyping compiles as a copy (each expected field copied off the source; extra fields drop) — this pair is outside the copy's rules; build a literal with exactly the expected fields instead
@@ -0,0 +1 @@
{ "plain": 3521, "san": 40657 }
@@ -0,0 +1 @@
{ "plain": 2485, "san": 53450 }
@@ -0,0 +1 @@
{ "plain": 2199, "san": 6086 }
@@ -0,0 +1 @@
{ "plain": 2298, "san": 3533 }
@@ -0,0 +1 @@
{ "plain": 2702, "san": 43257 }
@@ -0,0 +1 @@
{ "plain": 2301, "san": 3138 }