mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-31 07:23:06 +08:00
Fix eight compiler and type bug reports (#2319)
This commit is contained in:
@@ -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
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/fs-readdir-recursive.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/never-narrowed-literal-argument.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/rest-tuple-shorthand-property.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/string-plus-element-read-return.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/symbol-keyed-recursive-record.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/union-tuple-element-read.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/promise-all-generic-mixed-union.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/untyped-map-into-interface.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
@@ -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 }
|
||||
Reference in New Issue
Block a user