Merge main

This commit is contained in:
Chris Tate
2026-10-15 09:21:42 -05:00
59 changed files with 2627 additions and 134 deletions
+2
View File
@@ -76,6 +76,8 @@ title: "Limitations"
<include>../limitations/builtin-apis/filesystem-flags.mdx</include>
<include>../limitations/builtin-apis/glob.mdx</include>
<include>../limitations/builtin-apis/filehandle-and-descriptor-i-o.mdx</include>
<include>../limitations/builtin-apis/crypto.mdx</include>
@@ -0,0 +1 @@
- **Glob:** `fs.globSync`, `fs.glob` and `fs/promises` `glob` support every option. `withFileTypes` must be a literal `true` or `false`, and `exclude` functions cannot be combined with `withFileTypes`. The `fs/promises` iterator is supported only as the source of a `for await` loop, and its walk completes when the loop starts. `fs.glob` callbacks receive file names, not Dirents.
@@ -15,4 +15,4 @@ Other immutable aliases of table-backed Node builtins support direct calls, but
Synchronous and promise filesystem APIs with trailing arguments that affect behavior remain call-only. This includes writers, open, directory creation/removal, metadata readers, and directory readers. Passing them as values could discard modes, flags, encodings, or options during function-signature adaptation.
Native error-first filesystem callbacks can be stored and passed to platform adapters. Reads support Buffer and UTF-8 results; writes preserve string flags and modes; copy supports regular files and recursive directories. Reads and writes reject already-aborted signals, but native file operations complete synchronously before the deferred callback, so cancellation cannot interrupt I/O in progress. Symbolic-link copy, copy filters and clone modes, recursive glob patterns, glob exclusions, recursive directory reads, and Dirent results report a native limitation.
Native error-first filesystem callbacks can be stored and passed to platform adapters. Reads support Buffer and UTF-8 results; writes preserve string flags and modes; copy supports regular files and recursive directories. Reads and writes reject already-aborted signals, but native file operations complete synchronously before the deferred callback, so cancellation cannot interrupt I/O in progress. Symbolic-link copy, copy filters and clone modes, recursive directory reads, and Dirent results report a native limitation. `fs.glob` compiles only as a direct call; a stored `fs.glob` value throws `SC2020`.
@@ -2,6 +2,6 @@
"schemaVersion": 3,
"nodeVersion": "24.22.0",
"nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93",
"artifactVersion": "bef290787c18ee9ed334",
"artifactVersion": "0ae6cfd9272ef79cd01d",
"rowCount": 3880
}
+11 -11
View File
@@ -314,9 +314,9 @@
"apiEntries": 280,
"static": {
"supported": 7,
"partial": 128,
"partial": 131,
"refused": 0,
"not-implemented": 145,
"not-implemented": 142,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -29690,9 +29690,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -30702,9 +30702,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -31921,9 +31921,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -8,8 +8,8 @@
"tiers": {
"static": {
"replace-refusal": 45,
"verify-gap": 2412,
"audit-partial": 928,
"verify-gap": 2409,
"audit-partial": 931,
"classify": 2,
"implement": 85
}
@@ -26014,12 +26014,19 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:fs",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:fs.fsPromises.glob",
"tests": [
"tests/corpus/5448-fs-glob-patterns.ts",
"tests/corpus/5449-fs-glob-options.ts",
"tests/corpus/5450-fs-glob-validation.js",
"tests/corpus/5451-fs-promises-glob.ts",
"tests/corpus/5452-fs-glob-callback.ts",
"tests/fixtures/node-types/fs-glob.ts"
]
}
}
},
@@ -27194,12 +27201,19 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:fs",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:fs.fs.glob",
"tests": [
"tests/corpus/5448-fs-glob-patterns.ts",
"tests/corpus/5449-fs-glob-options.ts",
"tests/corpus/5450-fs-glob-validation.js",
"tests/corpus/5451-fs-promises-glob.ts",
"tests/corpus/5452-fs-glob-callback.ts",
"tests/fixtures/node-types/fs-glob.ts"
]
}
}
},
@@ -28615,12 +28629,19 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:fs",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:fs.fs.globSync",
"tests": [
"tests/corpus/5448-fs-glob-patterns.ts",
"tests/corpus/5449-fs-glob-options.ts",
"tests/corpus/5450-fs-glob-validation.js",
"tests/corpus/5451-fs-promises-glob.ts",
"tests/corpus/5452-fs-glob-callback.ts",
"tests/fixtures/node-types/fs-glob.ts"
]
}
}
},
@@ -326,9 +326,9 @@
"apiEntries": 280,
"static": {
"supported": 7,
"partial": 128,
"partial": 131,
"refused": 0,
"not-implemented": 145,
"not-implemented": 142,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -43746,6 +43746,11 @@
"tests/corpus/3135-fs-file-handle-controls.ts",
"tests/corpus/3136-fs-file-handle-options.ts",
"tests/corpus/3137-fs-vector-callbacks.cjs",
"tests/corpus/5448-fs-glob-patterns.ts",
"tests/corpus/5449-fs-glob-options.ts",
"tests/corpus/5450-fs-glob-validation.js",
"tests/corpus/5451-fs-promises-glob.ts",
"tests/corpus/5452-fs-glob-callback.ts",
"tests/corpus/5540-fs-callback-forms.cjs",
"tests/corpus/5541-fs-stats-fields.cjs",
"tests/corpus/fs-native-realpath.ts",
@@ -43765,6 +43770,7 @@
"tests/corpus/node24-fs-timestamps-promises.ts",
"tests/corpus/node24-fs-timestamps-symlinks.ts",
"tests/corpus/node24-fs-timestamps.ts",
"tests/fixtures/node-types/fs-glob.ts",
"tests/fixtures/node-types/fs-links.ts",
"tests/fixtures/node-types/fs-statfs.ts",
"tests/fixtures/node-types/fs-timestamps.ts"
@@ -44792,8 +44798,16 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:fs"
"status": "partial",
"evidence": "compiler-feature:fs.fsPromises.glob",
"tests": [
"tests/corpus/5448-fs-glob-patterns.ts",
"tests/corpus/5449-fs-glob-options.ts",
"tests/corpus/5450-fs-glob-validation.js",
"tests/corpus/5451-fs-promises-glob.ts",
"tests/corpus/5452-fs-glob-callback.ts",
"tests/fixtures/node-types/fs-glob.ts"
]
},
"anchorSource": "exact"
},
@@ -45951,8 +45965,16 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:fs"
"status": "partial",
"evidence": "compiler-feature:fs.fs.glob",
"tests": [
"tests/corpus/5448-fs-glob-patterns.ts",
"tests/corpus/5449-fs-glob-options.ts",
"tests/corpus/5450-fs-glob-validation.js",
"tests/corpus/5451-fs-promises-glob.ts",
"tests/corpus/5452-fs-glob-callback.ts",
"tests/fixtures/node-types/fs-glob.ts"
]
},
"anchorSource": "exact"
},
@@ -47453,8 +47475,16 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:fs"
"status": "partial",
"evidence": "compiler-feature:fs.fs.globSync",
"tests": [
"tests/corpus/5448-fs-glob-patterns.ts",
"tests/corpus/5449-fs-glob-options.ts",
"tests/corpus/5450-fs-glob-validation.js",
"tests/corpus/5451-fs-promises-glob.ts",
"tests/corpus/5452-fs-glob-callback.ts",
"tests/fixtures/node-types/fs-glob.ts"
]
},
"anchorSource": "exact"
},
@@ -280,6 +280,7 @@
{ "chapter": "child_process", "signatures": ["`stdout` Type: {stream.Readable|null|undefined}", "`stderr` Type: {stream.Readable|null|undefined}"], "status": "partial", "evidence": ["tests/corpus/1565-spawn-pipe-streams.ts", "tests/corpus/2939-child-shell-text.ts"] },
{ "chapter": "child_process", "signatures": ["`stdin` Type: {stream.Writable|null|undefined}"], "status": "partial", "evidence": ["tests/corpus/2893-child-stdin-duplex.ts", "tests/corpus/2894-child-stdin-backpressure.ts", "tests/corpus/2895-child-stdin-write-after-end.ts", "tests/corpus/2896-child-stdin-write-after-finish.ts", "tests/corpus/2897-child-stdin-destroy.ts", "tests/corpus/2898-child-stdin-spawn-failure.ts"] },
{ "chapter": "fs", "signatures": ["`fsPromises.lstat(path[, options])`"], "status": "partial", "evidence": ["tests/corpus/2941-fsp-lstat-identity.ts"] },
{ "chapter": "fs", "signatures": ["`fs.globSync(pattern[, options])`", "`fs.glob(pattern[, options], callback)`", "`fsPromises.glob(pattern[, options])`"], "status": "partial", "evidence": ["tests/corpus/5448-fs-glob-patterns.ts", "tests/corpus/5449-fs-glob-options.ts", "tests/corpus/5450-fs-glob-validation.js", "tests/corpus/5451-fs-promises-glob.ts", "tests/corpus/5452-fs-glob-callback.ts", "tests/fixtures/node-types/fs-glob.ts"] },
{ "chapter": "process", "signatures": ["`process.send(message[, sendHandle[, options]][, callback])`", "`process.disconnect()`", "`connected` Type: {boolean}", "Event: `'message'`", "Event: `'disconnect'`"], "status": "partial", "evidence": ["tests/corpus/2946-child-fork-ipc/main.ts"] },
{ "chapter": "fs", "symbols": ["fs.Stats", "stats.isBlockDevice", "stats.isCharacterDevice", "stats.isDirectory", "stats.isFIFO", "stats.isFile", "stats.isSocket", "stats.isSymbolicLink"], "status": "partial", "evidence": ["tests/corpus/5541-fs-stats-fields.cjs"] },
{ "chapter": "fs", "parent": "Class: `fs.Stats`", "signatures": ["`mode` Type: {number|bigint}", "`nlink` Type: {number|bigint}", "`uid` Type: {number|bigint}", "`gid` Type: {number|bigint}", "`rdev` Type: {number|bigint}", "`size` Type: {number|bigint}", "`blksize` Type: {number|bigint}", "`blocks` Type: {number|bigint}", "`atimeMs` Type: {number|bigint}", "`mtimeMs` Type: {number|bigint}", "`birthtimeMs` Type: {number|bigint}", "`atimeNs` Type: {bigint}", "`mtimeNs` Type: {bigint}", "`ctimeNs` Type: {bigint}", "`birthtimeNs` Type: {bigint}", "`atime` Type: {Date}", "`mtime` Type: {Date}", "`ctime` Type: {Date}", "`birthtime` Type: {Date}"], "status": "partial", "evidence": ["tests/corpus/5541-fs-stats-fields.cjs"] },
+39
View File
@@ -1324,6 +1324,36 @@ declare module "node:fs" {
* fences by name at the call site; undocumented keys drop like Node. */
options: { withFileTypes: true; recursive?: boolean; [option: string]: unknown },
): Dirent[];
/* fs.globSync / fs.glob: Node's glob walk over the file system. Patterns
* are minimatch globs (case-insensitive on macOS and Windows); results
* are relative to cwd unless the pattern is absolute. withFileTypes must
* be a literal so the result type is known. */
export interface GlobOptions {
cwd?: string | URL;
exclude?: readonly string[] | ((fileName: string) => boolean);
followSymlinks?: boolean;
withFileTypes?: false;
}
export interface GlobOptionsWithFileTypes {
cwd?: string | URL;
exclude?: readonly string[];
followSymlinks?: boolean;
withFileTypes: true;
}
export function globSync(pattern: string | readonly string[], options?: GlobOptions): string[];
export function globSync(
pattern: string | readonly string[],
options: GlobOptionsWithFileTypes,
): Dirent[];
export function glob(
pattern: string | readonly string[],
callback: (err: NodeJS.ErrnoException | null, matches?: string[]) => void,
): void;
export function glob(
pattern: string | readonly string[],
options: GlobOptions,
callback: (err: NodeJS.ErrnoException | null, matches?: string[]) => void,
): void;
export function accessSync(path: string, mode?: number): void;
export function mkdtempSync(prefix: string): string;
export const constants: {
@@ -1686,6 +1716,15 @@ declare module "fs/promises" {
[option: string]: unknown;
},
): Promise<import("node:fs").Dirent[]>;
/* The async iterator form of fs.globSync: iterate it with for await. */
export function glob(
pattern: string | readonly string[],
options?: import("node:fs").GlobOptions,
): AsyncIterableIterator<string>;
export function glob(
pattern: string | readonly string[],
options: import("node:fs").GlobOptionsWithFileTypes,
): AsyncIterableIterator<import("node:fs").Dirent>;
export function rm(path: string): Promise<void>;
export function stat(path: string): Promise<import("node:fs").Stats>;
export function statfs(
@@ -27,7 +27,7 @@ const SCR_ARR_VALUE = 1;
* module-visible memory, or trap without returning to the caller (the trap
* writes runtime-private stderr/TLS state). Declaring this lets LLVM keep
* array header loads out of loops whose only calls are these cold paths. */
const READ_FALLBACK = " cold memory(read, inaccessiblemem: readwrite)";
export const READ_FALLBACK = " cold memory(read, inaccessiblemem: readwrite)";
interface DenseGuard {
/** The checked dense offset, in the target's size type. */
@@ -78,7 +78,7 @@ import {
type StringSplit,
} from "./split-loops.js";
import { scalarizeNumericRecords } from "../../ir/scalar-records.js";
import { addRecordResultEntries } from "./record-results.js";
import { RESULT_ENTRY_PREFIX, addRecordResultEntries } from "./record-results.js";
import { specializeNumericCalls } from "../../ir/numeric-call-specialization.js";
import { everyExpr, everyExprChild, everyStmtChild, everyStmtList } from "../../ir/traverse.js";
import { analyzeIntegerRanges, INT32_RANGE, type IntegerRanges } from "../../ir/integer-ranges.js";
@@ -6013,6 +6013,7 @@ export class LlEmitter {
// tests small enough to inline at each call site.
if (fn.name.startsWith("%dyn.class.protocol.fallback.")) attrs = `noinline ${attrs}`;
else if (fn.name.startsWith("%dyn.class.protocol.")) attrs = `inlinehint ${attrs}`;
if (fn.name.startsWith(RESULT_ENTRY_PREFIX)) attrs = `inlinehint ${attrs}`;
const debug = this.debugScope === null ? "" : ` !dbg ${this.debugScope}`;
const symbol = borrowed ? mangleBorrowedFunction(fn.name) : mangleFunction(fn.name);
let body = `define internal ${ret} @${symbol}${int32Abi ? INT32_ABI_SUFFIX : ""}(${params.join(", ")}) ${attrs}${debug} { ; ${fn.name}\n${B.render()}\n}`;
@@ -26,7 +26,12 @@ import {
withinUint32,
} from "../../ir/integer-ranges.js";
import { emitArrayValues } from "./expr-containers.js";
import { emitCheckedRemainder, emitSignedIntegerRemainder } from "./integer-remainder.js";
import {
constantDivisor,
emitCheckedRemainder,
emitPowerOfTwoRemainder,
emitSignedIntegerRemainder,
} from "./integer-remainder.js";
import { emitDenseArrayGet, emitDenseArrayState } from "./dense-array-access.js";
import { emitWrappingSum, markWrappingOperands } from "./wrapping-sums.js";
@@ -300,7 +305,14 @@ export function emitOperatorExpr(
}
}
if (e.op === "%") {
const checked = emitCheckedRemainder(host, l, r, li, ri);
const constant = constantDivisor(e.right, ri);
const power = constant === null ? null : emitPowerOfTwoRemainder(host, l, constant);
if (power) return power;
// A fractional constant divisor never passes the integer check.
const checked =
constant !== null && !Number.isInteger(constant)
? null
: emitCheckedRemainder(host, l, r, li, ri);
if (checked) return checked;
}
if (arith[e.op] !== undefined) {
@@ -1,7 +1,8 @@
import { F64 } from "../../ir/ir.js";
import { F64, type IrExpr } from "../../ir/ir.js";
import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
import { f64Lit } from "./common.js";
import { integerNumber, widenInteger, type LlInteger } from "./integer-values.js";
import { emitTruncF64 } from "./trunc.js";
/** Integer proofs exclude negative zero and stay inside the exact-number
* range, so signed i64 division cannot overflow. A possibly zero divisor
@@ -43,14 +44,16 @@ export function emitSignedIntegerRemainder(
return { name: result, type: F64 };
}
const EXACT = 2 ** 53;
/** 2^62, the bound on dividends the inline integer path accepts, as an
* i64 literal (`String(2 ** 62)` rounds its digits). */
const DIVIDEND_LIMIT = "4611686018427387904";
/** Convert an unproven double to a candidate i64. `freeze` keeps NaN and
* out-of-range conversions from producing poison; the returned condition
* is the exact round trip, so it rejects every non-integer. Integral
* doubles inside the i64 range convert exactly; a passing value outside
* it (|x| >= 2^63) can only pair with a frozen integer of magnitude above
* 2^53. `-0` converts to 0 and passes. */
* 2^62. `-0` converts to 0 and passes. */
function candidateInteger(host: LlvmEmitterContext, value: string): [string, string] {
const B = host.B;
const raw = B.tmp(),
@@ -66,13 +69,17 @@ function candidateInteger(host: LlvmEmitterContext, value: string): [string, str
/** JS `%` with an inline integer path for operands that are not both
* proven integers with a nonzero divisor. At run time the dividend must be
* an integer of magnitude <= 2^53 and the divisor a nonzero integer; then
* an integer from -2^62 up to but not including 2^62 and the divisor a
* nonzero integer; then
* i64 `srem` is exact (no INT64_MIN / -1 overflow is possible), takes the
* dividend's sign like JS, and an exact zero takes the dividend's sign
* (`-4 % 2` and `-0 % 3` are -0). A divisor that passes the round trip
* with magnitude above 2^53 exceeds the dividend's, so `srem` returns the
* dividend exactly as JS does. Everything else (fractions, NaN, ±Infinity,
* zero divisors, huge dividends) takes the floating remainder, unchanged.
* (`-4 % 2` and `-0 % 3` are -0). The remainder of two doubles is itself a
* double, so its conversion back is exact, including for dividends above
* 2^53 (every double there is an integer). A divisor that passes the round
* trip with magnitude above 2^62 exceeds the dividend's, so `srem` returns
* the dividend exactly as JS does. Everything else (fractions, NaN,
* ±Infinity, zero divisors, huge dividends) takes the floating remainder,
* unchanged.
* Returns null when the divisor is the constant zero (always NaN). */
export function emitCheckedRemainder(
host: LlvmEmitterContext,
@@ -90,8 +97,9 @@ export function emitCheckedRemainder(
const [integer, exact] = candidateInteger(host, left.name);
const biased = B.tmp(),
inRange = B.tmp();
B.line(`${biased} = add i64 ${integer}, ${EXACT}`);
B.line(`${inRange} = icmp ule i64 ${biased}, ${2 * EXACT}`);
// -2^62 <= integer < 2^62: the biased value is nonnegative.
B.line(`${biased} = add i64 ${integer}, ${DIVIDEND_LIMIT}`);
B.line(`${inRange} = icmp sge i64 ${biased}, 0`);
conditions.push(exact, inRange);
x = integer;
}
@@ -147,3 +155,76 @@ export function emitCheckedRemainder(
B.line(`${result} = phi double [ ${fastValue}, %${fast} ], [ ${floating}, %${slow} ]`);
return { name: result, type: F64 };
}
/** The value of a divisor known at compile time: a number literal, its
* negation, `2 ** k` for an integer literal k from 0 to 1023 (exactly 2^k),
* or an integer whose proven range is a single value. */
export function constantDivisor(expr: IrExpr, integer: LlInteger | null): number | null {
if (integer && integer.range.min === integer.range.max) return integer.range.min;
if (expr.kind === "numLit") return expr.value;
if (expr.kind === "unary" && expr.op === "-") {
const value = constantDivisor(expr.operand, null);
return value === null ? null : -value;
}
if (
expr.kind === "bin" &&
expr.op === "**" &&
expr.left.kind === "numLit" &&
expr.left.value === 2 &&
expr.right.kind === "numLit" &&
Number.isInteger(expr.right.value) &&
expr.right.value >= 0 &&
expr.right.value <= 1023
)
return 2 ** expr.right.value;
return null;
}
/** Whether `value` is a power of two of at least 1. */
function isPowerOfTwo(value: number): boolean {
if (!Number.isFinite(value) || value < 1) return false;
let power = 1;
while (power < value) power *= 2;
return power === value;
}
/** JS `%` by a constant divisor whose magnitude c is a power of two of at
* least 1, as `x - trunc(x / c) * c`, which is exact for every double x.
* When |x| < c the quotient's magnitude is below 1 and truncates to zero,
* so the result is x. Otherwise x / c is a normal double and exact, and
* the truncated quotient times c is a multiple of c of magnitude at most
* |x|, also exact. When c is below x's ulp, x is itself a multiple of c and
* the difference is zero; otherwise both terms are multiples of x's ulp and
* the difference, smaller than c, is exact. ±Infinity gives the default
* NaN of `Infinity - Infinity`, the NaN fmod also returns for it, and a NaN
* dividend propagates. A zero difference takes the dividend's sign
* (`-4 % 2` and `-0 % 1` are -0). The divisor's sign does not change the
* result. Returns null for any other divisor; a power of two below 1 keeps
* frem, because x / c can overflow. */
export function emitPowerOfTwoRemainder(
host: LlvmEmitterContext,
left: LlValue,
divisor: number,
): LlValue | null {
const magnitude = Math.abs(divisor);
if (!isPowerOfTwo(magnitude)) return null;
const B = host.B;
const c = f64Lit(magnitude);
const quotient = B.tmp();
B.line(`${quotient} = fdiv double ${left.name}, ${c}`);
const whole = emitTruncF64(host, quotient);
const multiple = B.tmp(),
difference = B.tmp(),
zero = B.tmp(),
signedZero = B.tmp(),
result = B.tmp();
host.declare("declare double @llvm.copysign.f64(double, double)");
B.line(`${multiple} = fmul double ${whole}, ${c}`);
B.line(`${difference} = fsub double ${left.name}, ${multiple}`);
B.line(`${zero} = fcmp oeq double ${difference}, ${f64Lit(0)}`);
B.line(
`${signedZero} = call double @llvm.copysign.f64(double ${f64Lit(0)}, double ${left.name})`,
);
B.line(`${result} = select i1 ${zero}, double ${signedZero}, double ${difference}`);
return { name: result, type: F64 };
}
@@ -434,6 +434,69 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr):
B.line(`call void @scr_watcher_close(ptr ${w.name})`);
return { name: "", type: e.type };
}
if (e.fn === "fs.globTypesSync" || e.fn === "fsp.globTypes") {
// Dirent rows from one glob snapshot (scr_glob.c): mode 1 is the
// globSync form, 3 the fs/promises form with resolved parent paths.
if (e.type.kind !== "array" || e.type.elem.kind !== "record") {
throw new InternalCompilerError(
`llvm emitter bug: ${e.fn} result is not a Dirent record array`,
);
}
const arrayT = e.type;
const recT = e.type.elem;
const pattern = host.emitExpr(e.args[0]!);
const options = host.emitExpr(e.args[1]!);
const mode = e.fn === "fsp.globTypes" ? "3.0" : "1.0";
host.declare(`declare ptr @scr_fs_glob(ptr, ptr, double)`);
host.declare(`declare ${host.sizeType} @scr_fs_glob_count(ptr)`);
host.declare(`declare ptr @scr_fs_glob_name(ptr, ${host.sizeType})`);
host.declare(`declare ptr @scr_fs_glob_parent(ptr, ${host.sizeType})`);
host.declare(`declare double @scr_fs_glob_type(ptr, ${host.sizeType})`);
host.declare(`declare void @scr_fs_glob_free(ptr)`);
const snap = B.tmp();
B.line(
`${snap} = call ptr @scr_fs_glob(ptr ${pattern.name}, ptr ${options.name}, double ${mode})`,
);
host.emitPendingCheck();
const cnt = B.tmp();
B.line(`${cnt} = call ${host.sizeType} @scr_fs_glob_count(ptr ${snap})`);
const arr = B.tmp();
B.line(`${arr} = ${arrNewCall(host.shapeHost, recT, cnt)}`);
const arrayOut = host.own({ name: arr, type: arrayT });
const iSlot = B.slot();
B.entryAllocas.push(`${iSlot} = alloca ${host.sizeType}`);
B.line(`store ${host.sizeType} 0, ptr ${iSlot}`);
const lc = B.newLabel("gl.c");
const lb = B.newLabel("gl.b");
const le = B.newLabel("gl.e");
B.br(lc);
B.startBlock(lc);
const i = B.tmp();
const cont = B.tmp();
B.line(`${i} = load ${host.sizeType}, ptr ${iSlot}`);
B.line(`${cont} = icmp ult ${host.sizeType} ${i}, ${cnt}`);
B.condBr(cont, lb, le);
B.startBlock(lb);
const row = B.tmp();
B.line(`${row} = call ptr @${mangleRecordNew(recT.shapeId)}()`);
const dt = B.tmp();
B.line(`${dt} = call double @scr_fs_glob_type(ptr ${snap}, ${host.sizeType} ${i})`);
host.storeField(host.recordFieldPtr(row, recT.shapeId, "%dtype").ptr, F64, dt, false);
const nm = B.tmp();
B.line(`${nm} = call ptr @scr_fs_glob_name(ptr ${snap}, ${host.sizeType} ${i}) ; +1`);
B.line(`store ptr ${nm}, ptr ${host.recordFieldPtr(row, recT.shapeId, "name").ptr}`);
const parent = B.tmp();
B.line(`${parent} = call ptr @scr_fs_glob_parent(ptr ${snap}, ${host.sizeType} ${i}) ; +1`);
B.line(`store ptr ${parent}, ptr ${host.recordFieldPtr(row, recT.shapeId, "parentPath").ptr}`);
host.arrPush(arr, "ref", row); // push takes ownership of the row
const i2 = B.tmp();
B.line(`${i2} = add ${host.sizeType} ${i}, 1`);
B.line(`store ${host.sizeType} ${i2}, ptr ${iSlot}`);
B.br(lc);
B.startBlock(le);
B.line(`call void @scr_fs_glob_free(ptr ${snap})`);
return arrayOut;
}
if (e.fn === "fs.readdirTypesSync" || e.fn === "fsp.readdirTypes") {
// Dirent rows assembled inline from one scandir snapshot — the C
// emitter's flat loop. The sync form checks a scandir error before
@@ -325,6 +325,10 @@ 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.globSync": "scr_fs_glob_strings",
"fs.callbackGlob": "scr_fs_glob_callback",
"fsp.globStrings": "scr_fsp_glob_strings",
"fsp.globError": "scr_fsp_glob_error",
"fs.realpathSync": "scr_fs_realpath",
"fs.realpathNativeSync": "scr_fs_realpath_promise",
"fs.unlinkSync": "scr_fs_unlink",
@@ -1334,6 +1338,7 @@ export const USES_TIMERS_LIB_FNS = new Set<string>([
"fs.existsChk",
"fs.callbackValue",
"fs.callbackCall",
"fs.callbackGlob",
"http.createServer",
"http.createServerEmpty",
"http.request",
@@ -912,9 +912,16 @@ test.each([
expect(ir).not.toContain("@scr_union_new");
expect(ir).not.toContain("@scr_union_release");
expect(ir).not.toContain("@scr_union_get");
// Holes, sparse and noncanonical indices keep the runtime state check.
expect(ir).toContain("@scr_arr_state");
expect(ir).toContain(elem.kind === "f64" ? "@scr_arr_get_f64" : "@scr_arr_get_bool");
// Holes, sparse and noncanonical indices keep the runtime state check,
// a read-only cold call, so loop-invariant header loads can be hoisted.
expect(ir).toMatch(
/call double @scr_arr_state\(.*\) cold memory\(read, inaccessiblemem: readwrite\)/,
);
expect(ir).toMatch(
elem.kind === "f64"
? /call double @scr_arr_get_f64\(.*\) cold memory\(read, inaccessiblemem: readwrite\)/
: /call zeroext i1 @scr_arr_get_bool\(.*\) cold memory\(read, inaccessiblemem: readwrite\)/,
);
if (consumer !== "local") expect(ir).toContain("local.array.dense");
}
});
@@ -14,6 +14,7 @@ import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
import { ReferenceEffects } from "./reference-effects.js";
import { SCR_ARR_FIELD, exactIndexLines } from "./common.js";
import { exactInteger, widenInteger } from "./integer-values.js";
import { READ_FALLBACK } from "./dense-array-access.js";
export interface LocalArrayRead {
type: IrType;
@@ -696,10 +697,15 @@ export function emitLocalArrayRead(
if (scalar) {
// Holes, present undefined, sparse and noncanonical indices: the
// runtime state decides presence, then the strict getter reads it.
// Both only read (the getter runs for a present element, so it does
// not trap), which lets LLVM keep the array's header loads out of a
// loop around an inlined read.
const state = B.tmp(),
present = B.tmp();
host.declare(`declare double @scr_arr_state(ptr, double)`);
B.line(`${state} = call double @scr_arr_state(ptr ${array.name}, double ${index.name})`);
B.line(
`${state} = call double @scr_arr_state(ptr ${array.name}, double ${index.name})${READ_FALLBACK}`,
);
B.line(`${present} = fcmp oeq double ${state}, 1.0`);
B.condBr(present, slowValue, no);
B.startBlock(slowValue);
@@ -707,12 +713,14 @@ export function emitLocalArrayRead(
bits = B.tmp();
if (read.element.kind === "f64") {
host.declare(`declare double @scr_arr_get_f64(ptr, double)`);
B.line(`${value} = call double @scr_arr_get_f64(ptr ${array.name}, double ${index.name})`);
B.line(
`${value} = call double @scr_arr_get_f64(ptr ${array.name}, double ${index.name})${READ_FALLBACK}`,
);
B.line(`${bits} = bitcast double ${value} to i64`);
} else {
host.declare(`declare zeroext i1 @scr_arr_get_bool(ptr, double)`);
B.line(
`${value} = call zeroext i1 @scr_arr_get_bool(ptr ${array.name}, double ${index.name})`,
`${value} = call zeroext i1 @scr_arr_get_bool(ptr ${array.name}, double ${index.name})${READ_FALLBACK}`,
);
B.line(`${bits} = zext i1 ${value} to i64`);
}
@@ -73,6 +73,9 @@ test("projection-only records live in frame storage and producers fill it", asyn
expect(projected).not.toMatch(/@sc_rnew_/);
expect(projected).toMatch(/call void @sc_\w*into_add\(/);
expect(projected).toMatch(/call void @sc_\w*into_shortest\(/);
// The result-slot entries are small and called per use: hinted inline.
expect(ir).toMatch(/^define internal void @sc_\w*into_add\([^)]*\) inlinehint /m);
expect(ir).toMatch(/^define internal void @sc_\w*into_shortest\([^)]*\) inlinehint /m);
// A literal stores every field: its storage is not zeroed first.
expect(projected).not.toContain("zeroinitializer");
// Stored values keep the heap path and the ordinary producer.
@@ -0,0 +1,268 @@
import * as ts from "../../ts7/adapter.js";
import { type Lowerer } from "../lowerer.js";
import {
BOOL,
DYN,
F64,
STRING,
VOID,
arrayOf,
type IrExpr,
type IrStmt,
type IrType,
type SrcLoc,
} from "../../../ir/ir.js";
import { dynUndefinedExpr, numLit } from "../../../ir/build.js";
import { InternalCompilerError } from "../../../errors.js";
import { locOf } from "../../program.js";
import { checkForAwaitBinding } from "../lower-async-iteration.js";
import { builtinImportOf } from "./module-bindings.js";
/* fs.globSync, fs.glob and fs/promises glob (scr_glob.c, a port of Node's
* lib/internal/fs/glob.js). The pattern and options cross as dyn values so
* the runtime validates them in Node's order. The result form (strings or
* Dirent rows) comes from the call's mapped result type, so withFileTypes
* must be statically known; the runtime checks that the option agrees. */
/** The interned Dirent record array (type-mapper.ts), or null. */
function direntArray(lowerer: Lowerer, type: IrType | null | undefined): boolean {
if (type?.kind !== "array" || type.elem.kind !== "record") return false;
const shape = lowerer.shapes.get(type.elem.shapeId);
return (
!!shape &&
!shape.tuple &&
!shape.indexValue &&
shape.fields.length === 3 &&
shape.fields.some((f) => f.name === "%dtype")
);
}
/** An exclude function receives Node's Dirent objects when withFileTypes is
* set, which have no native representation here. */
function fenceDirentExclude(lowerer: Lowerer, call: ts.CallExpression, what: string): void {
const options = call.arguments[1];
if (!options) return;
const exclude = lowerer.checker.getPropertyOfType(lowerer.typeOf(options), "exclude");
if (!exclude) return;
const type = lowerer.checker.getTypeOfSymbolAtLocation(exclude, options);
if (lowerer.checker.getCallSignatures(type).length === 0) return;
lowerer.noLowering(
`${what} with an exclude function and withFileTypes`,
options,
"pass exclude as a string[] of glob patterns, or match on names without withFileTypes",
);
}
function globArgs(lowerer: Lowerer, call: ts.CallExpression, what: string): IrExpr[] {
if (call.arguments.some(ts.isSpreadElement)) {
lowerer.noLowering(`${what} with spread arguments`, call, "write the arguments positionally");
}
return call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN));
}
/** `globSync(pattern, options?)`: string[] or Dirent[]. */
export function lowerFsGlobSyncCall(
lowerer: Lowerer,
call: ts.CallExpression,
loc: SrcLoc,
): IrExpr {
const args = globArgs(lowerer, call, "fs.globSync");
const callType = lowerer.mapTypeOf(lowerer.typeOf(call));
const types = direntArray(lowerer, callType);
if (types) fenceDirentExclude(lowerer, call, "fs.globSync");
if (!types && !(callType?.kind === "array" && callType.elem.kind === "string")) {
lowerer.noLowering(
"fs.globSync where the result is not string[] or Dirent[]",
call,
"pass withFileTypes as a literal true or false so the result type is known",
);
}
return {
kind: "libCall",
fn: types ? "fs.globTypesSync" : "fs.globSync",
args: [args[0] ?? dynUndefinedExpr(loc), args[1] ?? dynUndefinedExpr(loc)],
type: types ? callType! : arrayOf(STRING),
loc,
};
}
/** `fs.glob(pattern[, options], callback)`: the argument vector goes to the
* runtime, which validates it and calls back with string[]. */
export function lowerFsGlobCallbackCall(
lowerer: Lowerer,
call: ts.CallExpression,
loc: SrcLoc,
): IrExpr {
const args = globArgs(lowerer, call, "fs.glob");
return {
kind: "libCall",
fn: "fs.callbackGlob",
args: [{ kind: "dynArrLit", elems: args, type: DYN, loc }],
type: VOID,
loc,
};
}
/** True when `node` calls fs/promises glob (any import spelling). */
export function isFspGlobCall(lowerer: Lowerer, node: ts.Expression): node is ts.CallExpression {
let expr = node;
while (ts.isParenthesizedExpression(expr)) expr = expr.expression;
if (!ts.isCallExpression(expr) || expr.questionDotToken) return false;
const callee = expr.expression;
const bi = ts.isIdentifier(callee)
? builtinImportOf(lowerer, callee)
: ts.isPropertyAccessExpression(callee) && !callee.questionDotToken
? lowerer.builtinMemberOf(callee)
: null;
return bi?.module === "fs/promises" && bi.member === "glob";
}
/** fs/promises `glob(pattern, options?)` as the head of a `for await` loop
* (lowerForOf marks the call): the matches as an array the loop awaits in
* order. The walk runs when the loop starts, like the other fs/promises
* operations, which complete before their continuations. Other uses of
* the async iterator have no lowering. */
export function lowerFspGlobCall(lowerer: Lowerer, call: ts.CallExpression, loc: SrcLoc): IrExpr {
if (lowerer.fspGlobHead !== call) {
lowerer.noLowering(
"fs/promises glob outside a for await loop",
call,
"iterate the matches directly with for await (const path of glob(pattern)), or use fs.globSync",
);
}
const args = globArgs(lowerer, call, "fs/promises glob");
// The async iterator's yielded type (its first type argument).
const yielded = lowerer.checker.getTypeArguments(lowerer.typeOf(call) as ts.TypeReference)[0];
const element = yielded ? lowerer.mapTypeOf(yielded) : null;
const rows: IrType | null = element ? { kind: "array", elem: element } : null;
const types = direntArray(lowerer, rows);
if (types) fenceDirentExclude(lowerer, call, "fs/promises glob");
if (!types && element?.kind !== "string") {
lowerer.noLowering(
"fs/promises glob where the yielded value is not a string or a Dirent",
call,
"pass withFileTypes as a literal true or false so the yielded type is known",
);
}
return {
kind: "libCall",
fn: types ? "fsp.globTypes" : "fsp.globStrings",
args: [args[0] ?? dynUndefinedExpr(loc), args[1] ?? dynUndefinedExpr(loc)],
type: types ? rows! : arrayOf(STRING),
loc,
};
}
/** `for await (const path of glob(pattern, options))`: the walk runs when
* the loop starts and its matches bind in order, one microtask hop per
* step. When the walk ended with an error (an exclude callback threw), the
* matches found before it bind first and the error is thrown when the loop
* reaches the end, as Node's async iterator yields them before rejecting;
* leaving the loop early with break or return skips it. */
export function lowerFspGlobForAwait(
lowerer: Lowerer,
stmt: ts.ForOfStatement,
call: ts.CallExpression,
): IrStmt {
const binding = checkForAwaitBinding(lowerer, stmt);
const loc = locOf(stmt);
lowerer.fspGlobHead = call;
let rowsExpr: IrExpr;
try {
rowsExpr = lowerer.lowerExpr(call);
} finally {
lowerer.fspGlobHead = null;
}
if (rowsExpr.type.kind !== "array") {
throw new InternalCompilerError("fs/promises glob head did not lower to an array");
}
const rowsT = rowsExpr.type;
const elemT = rowsT.elem;
lowerer.scopes.push(new Map());
try {
const rows = lowerer.declareHiddenLocal("%globRows", rowsT);
const error = lowerer.declareHiddenLocal("%globError", DYN);
const index = lowerer.declareHiddenLocal("%globIndex", F64);
index.mutable = true;
const item = lowerer.declareLocal(
binding.declaration.name,
binding.declaration.name.text,
elemT,
binding.mutable,
);
const rowsRef = (): IrExpr => ({ kind: "varRef", localId: rows.id, type: rowsT, loc });
const indexRef = (): IrExpr => ({ kind: "varRef", localId: index.id, type: F64, loc });
const errorRef = (): IrExpr => ({ kind: "varRef", localId: error.id, type: DYN, loc });
const body = lowerer.inCtl("loop", () => lowerer.lowerScopedBlock(stmt.statement));
const loop: IrStmt = {
kind: "while",
cond: { kind: "boolLit", value: true, type: BOOL, loc },
body: [
{
kind: "exprStmt",
expr: { kind: "libCall", fn: "async.hop", args: [], type: VOID, loc },
loc,
},
{
kind: "if",
cond: {
kind: "unary",
op: "!",
operand: { kind: "arrayHas", arr: rowsRef(), index: indexRef(), type: BOOL, loc },
type: BOOL,
loc,
},
then: [
{
kind: "if",
cond: {
kind: "unary",
op: "!",
operand: { kind: "dynTest", test: "undefined", value: errorRef(), type: BOOL, loc },
type: BOOL,
loc,
},
then: [{ kind: "throw", value: errorRef(), loc }],
else_: null,
loc,
},
{ kind: "break", loc },
],
else_: null,
loc,
},
{
kind: "varDecl",
localId: item.id,
init: { kind: "arrayGet", arr: rowsRef(), index: indexRef(), type: elemT, loc },
loc,
},
{
kind: "assign",
localId: index.id,
value: { kind: "bin", op: "+", left: indexRef(), right: numLit(1, loc), type: F64, loc },
loc,
},
...body,
],
loc,
};
return {
kind: "block",
body: [
{ kind: "varDecl", localId: rows.id, init: rowsExpr, loc },
{
kind: "varDecl",
localId: error.id,
init: { kind: "libCall", fn: "fsp.globError", args: [], type: DYN, loc },
loc,
},
{ kind: "varDecl", localId: index.id, init: numLit(0, loc), loc },
loop,
],
loc,
};
} finally {
lowerer.scopes.pop();
}
}
@@ -42,6 +42,7 @@ import {
lowerPathParseCall,
} from "./operating-system.js";
import { lowerQuerystringParseCall, lowerQuerystringStringifyCall } from "./querystring.js";
import { lowerFsGlobCallbackCall, lowerFsGlobSyncCall, lowerFspGlobCall } from "./fs-glob.js";
export function lowerBuiltinModuleCall(
lowerer: Lowerer,
@@ -59,6 +60,12 @@ export function lowerBuiltinModuleCall(
if (bi.module === "buffer" && ["isAscii", "isUtf8", "transcode"].includes(bi.member)) {
return lowerBufferEncodingCall(lowerer, expr, bi, fn, loc);
}
if (bi.module === "fs" && bi.member === "glob")
return lowerFsGlobCallbackCall(lowerer, expr, loc);
if (bi.module === "fs" && bi.member === "globSync")
return lowerFsGlobSyncCall(lowerer, expr, loc);
if (bi.module === "fs/promises" && bi.member === "glob")
return lowerFspGlobCall(lowerer, expr, loc);
if (fn.fn === "fs.callbackCall" && bi.member !== "rename") {
return lowerFsCallbackCall(lowerer, expr, bi, loc);
}
@@ -279,7 +279,7 @@ function classAsyncIteratorPlan(
};
}
function checkForAwaitBinding(
export function checkForAwaitBinding(
lowerer: Lowerer,
stmt: ts.ForOfStatement,
): { declaration: ts.VariableDeclaration & { name: ts.Identifier }; mutable: boolean } {
@@ -129,6 +129,7 @@ import {
lowerServerCloseOverrideAssignment,
} from "./lower-server.js";
import { lowerUrlAssignment } from "./builtins/url.js";
import { isFspGlobCall, lowerFspGlobForAwait } from "./builtins/fs-glob.js";
import {
isNativeFfiRequire,
builtinMemberRequireDecl,
@@ -7828,6 +7829,11 @@ export function lowerForOf(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt {
// A synchronous array iterator is also valid in `for await`. Its values
// take the async-from-sync path below; other async sources retain their
// dedicated request protocols.
if (stmt.awaitModifier) {
let head: ts.Expression = stmt.expression;
while (ts.isParenthesizedExpression(head)) head = head.expression;
if (isFspGlobCall(lowerer, head)) return lowerFspGlobForAwait(lowerer, stmt, head);
}
const awaitArray =
!!stmt.awaitModifier && lowerer.mapTypeOf(lowerer.typeOf(stmt.expression))?.kind === "array";
if (stmt.awaitModifier && !awaitArray) {
@@ -1819,6 +1819,9 @@ export class Lowerer {
/** for-of loops over arrays that can hold neither holes nor undefined:
* each pass reads the element payload directly. */
readonly presentElementLoops = new Set<ts.ForOfStatement>();
/** The fs/promises glob() call lowerForOf is lowering as a for await
* head; other glob() calls have no lowering. */
fspGlobHead: ts.Node | null = null;
/** Capture entries and their origin share one mutable box. Normalize each
* entry to the origin so writes and flow proofs stay synchronized. */
readonly runtimeOptionalRoots = new Map<IrLocal, IrLocal>();
@@ -1041,6 +1041,8 @@ export const BUILTIN_MODULE_FNS: Record<
rmSync: { fn: "fs.rmSync", params: [STRING], result: VOID },
rmdirSync: { fn: "fs.rmdirSync", params: [STRING], result: VOID },
readdirSync: { fn: "fs.readdirSync", params: [STRING], result: arrayOf(STRING) },
// Pattern and options are checked by the runtime (lowerFsGlobSyncCall).
globSync: { fn: "fs.globSync", params: [DYN, DYN], result: arrayOf(STRING) },
statSync: { fn: "fs.statSync", params: [STRING], result: STATS_T },
mkdtempSync: { fn: "fs.mkdtempSync", params: [STRING], result: STRING },
// accessSync's omitted mode completes to 0 (F_OK) in the special case
@@ -1194,6 +1196,8 @@ export const BUILTIN_MODULE_FNS: Record<
result: { kind: "promise", inner: arrayOf(STRING) },
},
rm: { fn: "fsp.rm", params: [STRING], result: { kind: "promise", inner: VOID } },
// The async iterator lowers only as a for await head (lowerFspGlobCall).
glob: { fn: "fsp.globStrings", params: [DYN, DYN], result: arrayOf(STRING) },
stat: { fn: "fsp.stat", params: [STRING], result: { kind: "promise", inner: STATS_T } },
realpath: { fn: "fsp.realpath", params: [STRING], result: { kind: "promise", inner: STRING } },
lstat: { fn: "fsp.lstat", params: [STRING], result: { kind: "promise", inner: STATS_T } },
@@ -1790,6 +1794,7 @@ export const BUILTIN_MODULE_FN_ALIASES: Record<
rmSync: ["fs.rmOptsSync", "fs.rmRetrySync"],
// The { withFileTypes: true } Dirent form.
readdirSync: ["fs.readdirTypesSync"],
globSync: ["fs.globTypesSync"],
// The JS-source validation-ladder variant (lowerFsLadderCall): the
// real mkdtemp runs when the options leave utf8 semantics.
mkdtempSync: ["fs.mkdtempSyncChk"],
@@ -1809,6 +1814,7 @@ export const BUILTIN_MODULE_FN_ALIASES: Record<
writeFile: ["fsp.writeFileMode"],
// The { withFileTypes: true } Dirent form.
readdir: ["fsp.readdirTypes"],
glob: ["fsp.globTypes"],
},
crypto: {
// The composed randomBytes(n).toString(enc) chain keeps its one-libCall
@@ -1903,7 +1909,7 @@ export const AMBIENT_SURFACE_FNS: readonly AmbientSurfaceRow[] = [
id: "node-builtin.fs.callbacks",
kind: "node-builtin",
name: "fs error-first callbacks",
fns: ["fs.callbackValue", "fs.callbackCall"],
fns: ["fs.callbackValue", "fs.callbackCall", "fs.callbackGlob"],
note: "native filesystem callbacks, including stored platform adapter functions",
},
// ── the Date slice (lowerDateCall/lowerNew): statics, construction,
@@ -521,6 +521,11 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"fs.rmdirSync",
"fs.readdirSync",
"fs.readdirTypesSync",
"fs.globSync",
"fs.globTypesSync",
"fs.callbackGlob",
"fsp.globStrings",
"fsp.globTypes",
"url.new",
"url.newBase",
"url.set",
@@ -444,6 +444,20 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
* sorts), the directory's order on Windows; no "."/"..". Throws
* Node's scandir errno error (may-throw seed set); fresh +1 array. */
"fs.readdirTypesSync": { argTypes: [STRING], result: VOID },
/** fs.globSync / fs.glob / fs/promises glob (scr_glob.c, Node's
* lib/internal/fs/glob.js walk): pattern and options are dyn values the
* runtime validates in Node's order. The string forms answer string[];
* the Dirent forms answer the call site's Dirent record array (validated
* in validate.ts) assembled from the snapshot; fs.callbackGlob takes the
* argument vector and calls back with string[]. */
"fs.globSync": { argTypes: [DYN, DYN], result: arrayOf(STRING) },
"fs.globTypesSync": { argTypes: [DYN, DYN], result: VOID },
"fs.callbackGlob": { argTypes: [DYN], result: VOID },
"fsp.globStrings": { argTypes: [DYN, DYN], result: arrayOf(STRING) },
"fsp.globTypes": { argTypes: [DYN, DYN], result: VOID },
/** The error an fs/promises glob walk ended with (undefined when none),
* read once right after fsp.globStrings / fsp.globTypes. */
"fsp.globError": { argTypes: [], result: DYN },
/** node:path (scr_path.c ports BOTH of Node's implementations
* function-by-function): the `path.*` family is posix, the
* `path.win32*` family is Node v24's path.win32 ported exactly — the
+9 -1
View File
@@ -43,6 +43,7 @@ export interface RuntimeFeatures {
const DYN_ASYNC_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"fs.callbackValue",
"fs.callbackCall",
"fs.callbackGlob",
"async.awaitDyn",
"timers.immediatePromise",
"crypto.native",
@@ -171,7 +172,14 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run
fn === "dyn.nativeRegexIs" ||
fn === "util.stripVTControlCharacters" ||
fn === "path.matchesGlob" ||
fn === "path.win32MatchesGlob"
fn === "path.win32MatchesGlob" ||
// fs.glob's walk compiles its patterns with the glob matcher.
fn === "fs.globSync" ||
fn === "fs.globTypesSync" ||
fn === "fs.callbackGlob" ||
fn === "fsp.globStrings" ||
fn === "fsp.globTypes" ||
fn === "fsp.globError"
)
features.regex = true;
if (
+8 -2
View File
@@ -4682,10 +4682,16 @@ function validateFunction(
}
return true;
}
if (e.fn === "fs.readdirTypesSync" || e.fn === "fsp.readdirTypes") {
if (
e.fn === "fs.readdirTypesSync" ||
e.fn === "fsp.readdirTypes" ||
e.fn === "fs.globTypesSync" ||
e.fn === "fsp.globTypes"
) {
// Result: the interned Dirent record array — {%dtype: f64,
// name: string, parentPath: string} rows (canonical field
// order; the structure lowerFsReaddirTypesCall pinned).
// order; the structure lowerFsReaddirTypesCall pinned). The glob
// forms answer the array itself.
const result =
e.fn === "fsp.readdirTypes"
? e.type.kind === "promise"
+12
View File
@@ -945,6 +945,12 @@
"name": "fs.futimesSync",
"status": "static"
},
{
"id": "node-builtin.fs.globSync",
"kind": "node-builtin",
"name": "fs.globSync",
"status": "static"
},
{
"id": "node-builtin.fs.linkSync",
"kind": "node-builtin",
@@ -994,6 +1000,12 @@
"name": "fs/promises.chmod",
"status": "static"
},
{
"id": "node-builtin.fs.promises.glob",
"kind": "node-builtin",
"name": "fs/promises.glob",
"status": "static"
},
{
"id": "node-builtin.fs.promises.link",
"kind": "node-builtin",
@@ -0,0 +1,53 @@
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { expect, test } from "vitest";
import { compile } from "../src/index.js";
async function compileSource(source: string) {
const directory = mkdtempSync(join(tmpdir(), "scriptc-fs-glob-"));
try {
const entry = join(directory, "main.ts");
writeFileSync(entry, source);
return await compile(entry, {
outDir: directory,
outPath: join(directory, "program.ir"),
outputKind: "ir",
});
} finally {
rmSync(directory, { recursive: true, force: true });
}
}
// Forms whose result type or element values have no native representation
// fail during compilation.
test.each([
[
"fs/promises glob outside a for await loop",
'import { glob } from "node:fs/promises";\nconst matches = glob("*");\nvoid matches;\n',
],
])("%s fails during compilation", async (what, source) => {
const result = await compileSource(source);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("expected an fs.glob diagnostic");
expect(result.diagnostics).toEqual(
expect.arrayContaining([expect.objectContaining({ message: expect.stringContaining(what) })]),
);
expect(result.diagnostics.some((diagnostic) => diagnostic.code === "SC9001")).toBe(false);
});
test("the fs.glob forms compile", async () => {
const result = await compileSource(`import { globSync, glob as globCallback } from "node:fs";
import { glob } from "node:fs/promises";
console.log(globSync("**/*.ts", { cwd: ".", exclude: ["node_modules/**"] }).length);
console.log(globSync(["*.md"], { exclude: (path) => path.startsWith(".") }).length);
console.log(globSync("*", { withFileTypes: true }).map((d) => d.isFile()).length);
globCallback("*", (err, matches) => console.log(err, matches?.length));
async function main(): Promise<void> {
for await (const path of glob("*")) console.log(path);
for await (const entry of glob("*", { withFileTypes: true })) console.log(entry.name, entry.isDirectory());
}
void main();
`);
expect(result.ok, JSON.stringify(result.ok ? [] : result.diagnostics)).toBe(true);
});
+27 -8
View File
@@ -15,14 +15,18 @@ test("remainder uses integer proofs and preserves floating fallback and signed z
`
function remainder(x: number, y: number): number { return x % y; }
function constants(x: number): number {
let value = x % 8;
let value = x % 10;
value %= 3;
return value;
}
function powers(x: number): number {
return (x % 8) + (x % -(2 ** 32)) + (x % 1) + (x % 0.5);
}
function power(x: number, y: number): number { return x ** y; }
function signed(x: number): number { return (x | 0) % -7; }
function maybeZero(x: number, y: number): number { return (x | 0) % (y | 0); }
console.log(remainder(-9.5, 8), constants(17.25), power(2, 3), signed(-7), maybeZero(7, 0));
console.log(powers(-9.75));
// Fractional arguments keep the parameters arbitrary (no call-site proof).
console.log(remainder(5, 2.5), signed(-7.5), maybeZero(7.5, 2.5));
`,
@@ -36,25 +40,40 @@ console.log(remainder(5, 2.5), signed(-7.5), maybeZero(7.5, 2.5));
new RegExp(`^define internal [^\\n]*@sc_[bf]+_${name}(?:\\.i32)?\\([^]*?^}`, "m").exec(
ll,
)![0];
// Four source remainders keep a floating fallback behind a runtime
// integer check; ToUint32 coercion fallbacks use integer arithmetic.
expect(ll.match(/ = frem double /g)).toHaveLength(4);
// Five source remainders keep a floating fallback: four behind a
// runtime integer check and the one by 0.5. ToUint32 coercion
// fallbacks use integer arithmetic.
expect(ll.match(/ = frem double /g)).toHaveLength(5);
expect(ll).toContain("uint32.coerce.slow");
expect(ll.match(/ = srem i64 /g)).toHaveLength(5);
expect(ll).toMatch(/ = select i1 [^\n]+double [^\n]+double /);
// Unproven operands: both round-trip checks, the 2^53 dividend bound,
// Unproven operands: both round-trip checks, the 2^62 dividend bound,
// and a nonzero divisor guard the integer path.
const unproven = fn("remainder");
expect(unproven.match(/ = freeze i64 /g)).toHaveLength(2);
expect(unproven).toMatch(/ = add i64 [^\n]+, 9007199254740992$/m);
expect(unproven).toMatch(/ = icmp ule i64 [^\n]+, 18014398509481984$/m);
expect(unproven).toMatch(/ = add i64 [^\n]+, 4611686018427387904$/m);
expect(unproven).toMatch(/ = icmp sge i64 [^\n]+, 0$/m);
expect(unproven).toMatch(/ = icmp ne i64 [^\n]+, 0$/m);
expect(unproven).toContain("@llvm.expect.i1(");
expect(unproven).toContain("rem.float");
// A constant divisor needs only the dividend check.
const constant = fn("constants");
expect(constant).toMatch(/ = srem i64 [^\n]+, 8$/m);
expect(constant).toMatch(/ = srem i64 [^\n]+, 10$/m);
expect(constant).not.toMatch(/ = icmp ne i64 [^\n]+, 0$/m);
// Power-of-two divisors of at least 1 subtract the truncated multiple,
// with no integer check; the divisor's sign is dropped and 0.5 keeps
// frem.
const powers = fn("powers");
expect(powers).not.toContain("freeze");
expect(powers).not.toContain("srem");
expect(powers.match(/ = fdiv double [^\n]+, 0x/g)).toHaveLength(3);
expect(powers).toMatch(/ = fdiv double [^\n]+, 0x4020000000000000$/m);
expect(powers).toMatch(/ = fdiv double [^\n]+, 0x41F0000000000000$/m);
expect(powers).toMatch(/ = fdiv double [^\n]+, 0x3FF0000000000000$/m);
expect(
powers.match(/ = call double @llvm.copysign.f64\(double 0x0000000000000000, /g),
).toHaveLength(3);
expect(powers).toMatch(/ = frem double [^\n]+, 0x3FE0000000000000$/m);
// Proven integers with a nonzero divisor need no check or fallback.
const signed = fn("signed");
expect(signed).not.toContain("freeze");
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5448-fs-glob-patterns.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5449-fs-glob-options.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5450-fs-glob-validation.js"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5451-fs-promises-glob.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5452-fs-glob-callback.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5455-power-of-two-remainder.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5456-remainder-large-integer-dividends.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5457-lcg-modulus.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5458-fractional-remainder.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5585-record-results-inlined.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/fixtures/node-types/fs-glob.ts"
],
"diags": []
}
+5 -38
View File
@@ -23,13 +23,6 @@ static const ScrArrOps scr_bytes_io_bytes_ops = { scr_bytes_retain_v, scr_bytes_
#ifndef O_SYNC
#define O_SYNC 0 /* Windows CRT opens have no separate synchronous flag. */
#endif
/* Bionic added glob/globfree in API 28; Android packs target API 26. */
#if !defined(_WIN32) && !defined(__wasi__) && (!defined(__ANDROID__) || __ANDROID_API__ >= 28)
#include <glob.h>
#define SCR_FS_HAS_GLOB 1
#else
#define SCR_FS_HAS_GLOB 0
#endif
#ifdef __APPLE__
#include <sys/stat.h> /* lchmod(2) — the fs.lchmodSync ladder's real tail */
#endif
@@ -554,7 +547,7 @@ static void scr_fs_cb_fire(ScrClosure *closure) {
}
#endif
static void scr_fs_cb_schedule(const ScrDyn *callback, ScrDyn *result, const ScrDyn *buffer) {
void scr_fs_cb_schedule(const ScrDyn *callback, ScrDyn *result, const ScrDyn *buffer) {
ScrDyn *arguments = scr_dyn_new_arr();
if (scr_exc_pending()) {
ScrCaught *caught = scr_exc_take();
@@ -815,36 +808,10 @@ static ScrDyn *scr_fs_cb_invoke(ScrStr *member, ScrDyn *const *args, size_t argc
scr_dyn_release(force); scr_str_release(destination);
}
} else if (!strcmp(op, "glob")) {
#if SCR_FS_HAS_GLOB
if (strstr(path->data, "**") || scr_fs_cb_option_bool(ARG(1), "withFileTypes")) {
const char *message = "Recursive glob patterns and Dirent results have no native lowering";
scr_throw_error_msg_code(SCR_ERR_ERROR, message, strlen(message), "SC2020");
goto done;
}
ScrDyn *cwd = scr_fs_cb_option(ARG(1), "cwd");
ScrDyn *exclude = scr_fs_cb_option(ARG(1), "exclude");
if (exclude && exclude->kind != SCR_DYN_UNDEF) {
const char *message = "fs.glob exclude options have no native lowering";
scr_throw_error_msg_code(SCR_ERR_ERROR, message, strlen(message), "SC2020");
} else {
ScrStr *pattern = cwd && cwd->kind == SCR_DYN_STR ? scr_fs_cb_join(cwd->v.str, path->data) : scr_str_retain(path);
glob_t matches = {0};
int rc = glob(pattern->data, 0, NULL, &matches);
if (rc && rc != GLOB_NOMATCH) scr_fs_throw(rc == GLOB_NOSPACE ? ENOMEM : EIO, "glob", path);
else {
result = scr_dyn_new_arr();
size_t prefix = cwd && cwd->kind == SCR_DYN_STR ? cwd->v.str->len + 1 : 0;
for (size_t i = 0; i < matches.gl_pathc; i++) {
const char *name = matches.gl_pathv[i] + prefix;
scr_dyn_arr_push(result, scr_fs_cb_string(scr_str_new(name, strlen(name))));
}
}
globfree(&matches); scr_str_release(pattern);
}
scr_dyn_release(cwd); scr_dyn_release(exclude);
#else
scr_fs_throw(ENOSYS, "glob", path);
#endif
/* Direct fs.glob calls lower to scr_fs_glob_callback (scr_glob.c); a
* stored fs.glob value is not supported. */
const char *message = "fs.glob stored as a value has no native lowering; call fs.glob directly";
scr_throw_error_msg_code(SCR_ERR_ERROR, message, strlen(message), "SC2020");
} else if (!strcmp(op, "write") && ARG(1)->kind == SCR_DYN_STR) {
/* write(fd, string[, position[, encoding]], callback): a non-number
* position writes at the current offset; the encoding defaults to
File diff suppressed because it is too large Load Diff
+4
View File
@@ -101,6 +101,10 @@ ScrStr *scr_fs_read_file_encoded(ScrStr *path, ScrStr *encoding);
ScrStr *scr_fs_read_fd_encoded(double fd, ScrStr *encoding);
ScrDyn *scr_fs_callback_value(ScrStr *member);
ScrDyn *scr_fs_callback_call(ScrStr *member, const ScrDyn *args);
/* The error-first callback queue (scr_bytes_io.c): a pending exception
* becomes [err], otherwise [null, result(, buffer)]; the callback runs on
* a later loop turn. result moves in. */
void scr_fs_cb_schedule(const ScrDyn *callback, ScrDyn *result, const ScrDyn *buffer);
ScrBytes *scr_fs_read_fd_bytes(double fd); /* +1; the fd form (scr_lib.c) */
void scr_fs_write_file_bytes(ScrStr *path, const ScrBytes *data);
void scr_fs_append_file_bytes(ScrStr *path, const ScrBytes *data);
+27
View File
@@ -61,5 +61,32 @@ ScrStr *scr_path_win32_format(ScrStr *root, ScrStr *dir, ScrStr *base, ScrStr *n
bool scr_path_matches_glob(ScrStr *path, ScrStr *pattern);
bool scr_path_win32_matches_glob(ScrStr *path, ScrStr *pattern);
bool scr_glob_match(ScrStr *path, ScrStr *pattern, bool windows, bool nocase);
/* fs.globSync / fs.glob / fs/promises glob (scr_glob.c): Node 24's
* lib/internal/fs/glob.js walk. The pattern (a string or string array) and
* options (exclude, cwd, followSymlinks, withFileTypes) arrive as dyn
* values and are validated in Node's order with Node's errors (borrowed).
* scr_fs_glob answers the result snapshot (+1, or NULL with the exception
* pending); mode bit 0 asks for Dirent rows (name, parentPath, UV_DIRENT
* kind) and must agree with withFileTypes, bit 1 is the fs/promises form,
* whose Dirents carry resolved parent paths. strings is the string[] form.
* callback takes fs.glob's argument vector, throws validation errors
* synchronously and delivers the result or the walk's error through the
* filesystem callback queue. */
typedef struct ScrDyn ScrDyn; /* full definition in scr_dyn.h (C11 repeat) */
typedef struct ScrFsGlob ScrFsGlob;
ScrFsGlob *scr_fs_glob(const ScrDyn *pattern, const ScrDyn *options, double mode);
size_t scr_fs_glob_count(const ScrFsGlob *g); /* NULL -> 0 */
ScrStr *scr_fs_glob_path(const ScrFsGlob *g, size_t i); /* +1 */
ScrStr *scr_fs_glob_name(const ScrFsGlob *g, size_t i); /* +1 */
ScrStr *scr_fs_glob_parent(const ScrFsGlob *g, size_t i); /* +1 */
double scr_fs_glob_type(const ScrFsGlob *g, size_t i);
void scr_fs_glob_free(ScrFsGlob *g); /* NULL-tolerant */
ScrArr *scr_fs_glob_strings(const ScrDyn *pattern, const ScrDyn *options);
void scr_fs_glob_callback(ScrDyn *args);
/* The fs/promises form: a walk error does not throw; the matches found
* before it are returned and scr_fsp_glob_error answers the error (+1 dyn,
* undefined when there is none) once, for the loop to throw after them. */
ScrArr *scr_fsp_glob_strings(const ScrDyn *pattern, const ScrDyn *options);
ScrDyn *scr_fsp_glob_error(void);
#endif /* SCR_PATH_H */
+9
View File
@@ -15,6 +15,15 @@
long scr_str_live_count(void); /* provided by scr_string.c */
/* fs.glob's callback form (scr_glob.c) schedules through the filesystem
* callback queue in scr_bytes_io.c, which this binary does not link; the
* oracle never calls it. */
void scr_fs_cb_schedule(const ScrDyn *callback, ScrDyn *result, const ScrDyn *buffer) {
(void)callback;
(void)buffer;
scr_dyn_release(result);
}
static int hex_val(char c) {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
+82
View File
@@ -0,0 +1,82 @@
// fs.globSync over a fixed tree: globstars, braces, extglobs, classes, dot
// files, "." and ".." segments, trailing slashes, literal paths, missing
// directories, absolute patterns and pattern arrays. Results come out in
// Node's walk order over sorted directory listings.
import { globSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
const root = mkdtempSync(join(tmpdir(), "scriptc-glob-"));
for (const dir of ["a/b/c", "src/lib", ".hidden", "node_modules/x", "docs"]) {
mkdirSync(join(root, dir), { recursive: true });
}
const files = [
"a/one.ts",
"a/b/two.ts",
"a/b/c/three.js",
"src/index.ts",
"src/lib/util.ts",
"src/lib/util.test.ts",
"README.md",
".hidden/secret.ts",
".dot.ts",
"node_modules/x/index.ts",
"Upper.TS",
"docs/guide.md",
"docs/[draft].md",
];
for (const file of files) writeFileSync(join(root, file), "");
const show = (pattern: string | string[]): void => {
console.log(JSON.stringify(pattern), globSync(pattern, { cwd: root }).join(" "));
};
for (const pattern of [
"*.ts",
"*",
"**",
"**/*.ts",
"a/**",
"a/**/*.js",
"**/b",
"**/b/**",
"src/*",
"src/**/*.test.ts",
"{a,src}/*.ts",
"{a,src}/**/*.{ts,js}",
"a/{one,missing}.ts",
"?.ts",
"a/???.ts",
"[ab]/*",
"[!a]*",
"!(src|node_modules)/*.ts",
"@(a|src)/*.ts",
"+(a|b)/**",
"*.md",
"docs/\\[draft\\].md",
".*",
"**/.*",
".hidden/*",
"a/b/../one.ts",
"./a/*.ts",
"a/./b/*.ts",
"a/",
"a/b/",
"**/",
"a/b/c/three.js",
"missing/**",
"missing",
"**/node_modules/**",
"**/*.TS",
"",
]) {
show(pattern);
}
show(["*.md", "a/*.ts", "*.md"]);
show(["src/*", "src/**"]);
show([]);
// Absolute patterns answer absolute paths; print them relative to the root.
const absolute = globSync(join(root, "a", "**", "*.ts")).map((p) => p.slice(root.length));
console.log("absolute", absolute.join(" "));
rmSync(root, { recursive: true, force: true });
+84
View File
@@ -0,0 +1,84 @@
// fs.globSync options: cwd as an absolute path, a relative path and a file:
// URL; exclude as glob patterns (resolved against cwd, matched on whole
// paths and on directories with a trailing slash) or a function of the
// path; withFileTypes Dirents with name, parentPath (cwd-relative when cwd
// is relative) and the type probes; and followSymlinks.
import { type Dirent, globSync, mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, relative } from "node:path";
import { pathToFileURL } from "node:url";
const root = mkdtempSync(join(tmpdir(), "scriptc-glob-options-"));
for (const dir of ["pkg/src/lib", "pkg/test", "pkg/node_modules/dep", "pkg/dist"]) {
mkdirSync(join(root, dir), { recursive: true });
}
for (const file of [
"pkg/package.json",
"pkg/src/index.ts",
"pkg/src/lib/a.ts",
"pkg/src/lib/a.test.ts",
"pkg/test/main.test.ts",
"pkg/node_modules/dep/index.ts",
"pkg/dist/index.js",
]) {
writeFileSync(join(root, file), "{}");
}
let links = true;
try {
symlinkSync("src", join(root, "pkg", "linked"), "dir");
} catch {
links = false;
}
const pkg = join(root, "pkg");
console.log("absolute cwd", globSync("**/*.ts", { cwd: pkg }).join(" "));
process.chdir(root);
console.log("relative cwd", globSync("src/*.ts", { cwd: "pkg" }).join(" "));
console.log("url cwd", globSync("*.json", { cwd: pathToFileURL(pkg) }).join(" "));
console.log("default cwd", globSync("pkg/*.json").join(" "));
console.log(
"exclude patterns",
globSync("**/*.ts", { cwd: pkg, exclude: ["node_modules/**", "**/*.test.ts"] }).join(" "),
);
console.log("exclude directory", globSync("**", { cwd: pkg, exclude: ["dist/"] }).filter((p) => !p.includes("linked")).join(" "));
console.log(
"exclude function",
globSync("**/*.ts", { cwd: pkg, exclude: (path) => path.startsWith("node_modules") || path.endsWith(".test.ts") }).join(
" ",
),
);
const visited: string[] = [];
globSync("src/**", {
cwd: pkg,
exclude: (path) => {
visited.push(path);
return false;
},
});
console.log("exclude sees", visited.join(" "));
const describe = (list: Dirent[]): string =>
list
.map((d) => {
const kind = d.isSymbolicLink() ? "link" : d.isDirectory() ? "dir" : d.isFile() ? "file" : "other";
const parent = d.parentPath.startsWith(root) ? `<root>${d.parentPath.slice(root.length)}` : d.parentPath;
return `${d.name}:${parent}:${kind}`;
})
.join(" ");
console.log("dirents absolute", describe(globSync("src/**", { cwd: pkg, withFileTypes: true })));
console.log("dirents relative", describe(globSync("pkg/src/*", { withFileTypes: true })));
console.log("dirents exclude", describe(globSync("**/*.ts", { cwd: pkg, withFileTypes: true, exclude: ["node_modules/**"] })));
console.log("withFileTypes false", globSync("*", { cwd: pkg, withFileTypes: false }).join(" "));
if (links) {
console.log("links", globSync("**/*.ts", { cwd: pkg, exclude: ["node_modules/**"] }).join(" "));
console.log("links followed", globSync("**/*.ts", { cwd: pkg, followSymlinks: true, exclude: ["node_modules/**"] }).join(" "));
console.log("link entry", describe(globSync("linked", { cwd: pkg, withFileTypes: true })));
console.log("through link", globSync("linked/lib/*", { cwd: pkg }).join(" "));
} else {
console.log("links unavailable");
}
console.log("relative check", relative(root, pkg));
process.chdir(tmpdir());
rmSync(root, { recursive: true, force: true });
+80
View File
@@ -0,0 +1,80 @@
// fs.glob argument handling from untyped code: Node's ERR_INVALID_ARG_TYPE
// errors for patterns, options, exclude, followSymlinks, cwd and the
// callback, the ERR_INTERNAL_ASSERTION a non-string cwd with exclude
// patterns hits in Node, empty patterns, missing directories, exclude
// callbacks that throw or return non-booleans, and the errors fs.glob's
// callback and fs/promises glob deliver.
import { globSync, glob as globCallback, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { glob } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
const any = (value) => /** @type {any} */ (value);
const attempt = (label, run) => {
try {
console.log(label, run());
} catch (error) {
const e = /** @type {any} */ (error);
console.log(label, "threw", e.name, e.code, JSON.stringify(e.message));
}
};
const root = mkdtempSync(join(tmpdir(), "scriptc-glob-validation-"));
mkdirSync(join(root, "a"));
for (const file of ["a/one.ts", "a/two.ts", "top.md"]) writeFileSync(join(root, file), "");
process.chdir(root);
attempt("number pattern", () => globSync(any(5)));
attempt("null pattern", () => globSync(any(null)));
attempt("object pattern", () => globSync(any({})));
attempt("function pattern", () => globSync(any(() => "*")));
attempt("array with a number", () => globSync(any(["*", 5])));
attempt("string options", () => globSync("*", any("x")));
attempt("null options", () => globSync("*", any(null)));
attempt("array options", () => globSync("*", any([])));
attempt("number exclude", () => globSync("*", any({ exclude: 5 })));
attempt("exclude with a number", () => globSync("*", any({ exclude: ["a", 1] })));
attempt("number followSymlinks", () => globSync("*", any({ followSymlinks: 1 })));
attempt("null followSymlinks", () => globSync("*", any({ followSymlinks: null })).join(" "));
attempt("number cwd", () => globSync("*", any({ cwd: 5 })));
attempt("number cwd with exclude", () => globSync("*", any({ cwd: 5, exclude: ["x"] })));
attempt("Buffer cwd", () => globSync("*", any({ cwd: Buffer.from("a") })));
attempt("http URL cwd", () => globSync("*", any({ cwd: new URL("http://example.com/") })));
attempt("missing cwd", () => globSync("*", { cwd: join(root, "missing") }).length);
attempt("empty pattern", () => globSync("").length);
attempt("no patterns", () => globSync([]).length);
attempt("exclude throws", () =>
globSync("*", {
exclude: () => {
throw new RangeError("stop");
},
}),
);
attempt("exclude truthy values", () => globSync("**", { exclude: any((p) => (p.endsWith(".ts") ? 1 : "")) }).join(" "));
attempt("missing callback", () => globCallback("*", any(undefined)));
attempt("bad pattern with callback", () => globCallback(any(5), () => console.log("not reached")));
globCallback("a/*.ts", (err, files) => {
console.log("callback", err, files.join(" "));
globCallback("*", { cwd: "a" }, (err2, files2) => {
console.log("callback with options", err2, files2.join(" "));
globCallback("*", any({ cwd: 5 }), (error) => {
console.log("callback cwd error", error.name, error.code, error.message);
void loop();
});
});
});
async function loop() {
const seen = [];
for await (const path of glob("**/*.ts")) seen.push(path);
console.log("for await", seen.join(" "));
try {
for await (const path of glob(any(5))) console.log("not reached", path);
} catch (error) {
const e = /** @type {any} */ (error);
console.log("for await bad pattern", e.name, e.code);
}
process.chdir(tmpdir());
rmSync(root, { recursive: true, force: true });
}
+74
View File
@@ -0,0 +1,74 @@
// fs/promises glob as a for await source: string matches, Dirent matches
// (fs/promises Dirents carry the resolved parent directory), the named,
// namespace and fs.promises spellings, early exits with break and continue,
// nested loops, and an error from an exclude callback surfacing at the
// loop.
import * as fs from "node:fs";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import * as fsp from "node:fs/promises";
import { glob } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
const root = mkdtempSync(join(tmpdir(), "scriptc-fsp-glob-"));
for (const dir of ["one/deep", "two"]) mkdirSync(join(root, dir), { recursive: true });
for (const file of ["one/a.txt", "one/deep/b.txt", "two/c.txt", "two/d.md", "e.txt"]) {
writeFileSync(join(root, file), "");
}
async function main(): Promise<void> {
const all: string[] = [];
for await (const path of glob("**/*.txt", { cwd: root })) all.push(path);
console.log("named", all.join(" "));
const viaNamespace: string[] = [];
for await (const path of fsp.glob("*/*", { cwd: root, exclude: ["**/*.md"] })) viaNamespace.push(path);
console.log("namespace", viaNamespace.join(" "));
const viaFs: string[] = [];
for await (const path of fs.promises.glob("two/*", { cwd: root })) viaFs.push(path);
console.log("fs.promises", viaFs.join(" "));
const rows: string[] = [];
for await (const entry of glob("one/**", { cwd: root, withFileTypes: true })) {
rows.push(`${entry.name}|${entry.parentPath.slice(root.length)}|${entry.isDirectory()}`);
}
console.log("dirents", rows.join(" "));
let first = "";
for await (const path of glob("**/*.txt", { cwd: root })) {
first = path;
break;
}
console.log("break", first);
const skipped: string[] = [];
for await (const path of glob("**/*.txt", { cwd: root })) {
if (path.startsWith("one")) continue;
skipped.push(path);
}
console.log("continue", skipped.join(" "));
const pairs: string[] = [];
for await (const dir of glob("*/", { cwd: root })) {
for await (const file of glob(`${dir}/*.txt`, { cwd: root })) pairs.push(`${dir}>${file}`);
}
console.log("nested", pairs.join(" "));
try {
for await (const path of glob("**", {
cwd: root,
exclude: (p) => {
if (p === "two") throw new Error(`refused ${p}`);
return false;
},
})) {
console.log("visited", path);
}
} catch (error) {
console.log("caught", (error as Error).message);
}
rmSync(root, { recursive: true, force: true });
}
void main();
+44
View File
@@ -0,0 +1,44 @@
// fs.glob's error-first callback form: with and without options, pattern
// arrays, exclude patterns and functions, the error an exclude callback
// throws arriving at the callback. Each call starts after the previous
// callback ran, so the output order does not depend on I/O timing.
import { glob, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
const root = mkdtempSync(join(tmpdir(), "scriptc-glob-callback-"));
mkdirSync(join(root, "lib/inner"), { recursive: true });
for (const file of ["lib/a.js", "lib/b.ts", "lib/inner/c.ts", "top.json"]) writeFileSync(join(root, file), "");
process.chdir(root);
const steps: (() => void)[] = [
() => glob("lib/**/*.ts", (err, files) => next("no options", err, files)),
() => glob("**/*.ts", { cwd: "lib" }, (err, files) => next("cwd", err, files)),
() => glob(["*.json", "lib/*.js"], {}, (err, files) => next("array", err, files)),
() => glob("**/*", { exclude: ["lib/inner/**"] }, (err, files) => next("exclude patterns", err, files)),
() => glob("**/*", { exclude: (path) => path.endsWith(".js") }, (err, files) => next("exclude function", err, files)),
() =>
glob(
"**",
{
exclude: () => {
throw new Error("exclude failed");
},
},
(err, files) => next("exclude throws", err, files),
),
() => finish(),
];
function next(label: string, err: NodeJS.ErrnoException | null, files: string[] | undefined): void {
console.log(label, err ? `error ${err.message}` : null, files === undefined ? "no files" : files.join(" "));
steps.shift()?.();
}
function finish(): void {
process.chdir(tmpdir());
rmSync(root, { recursive: true, force: true });
console.log("done");
}
steps.shift()?.();
@@ -0,0 +1,66 @@
// `%` by a constant power of two of at least 1 matches Node for every kind
// of dividend: integers, fractions, subnormals, values beyond 2^53, -0,
// NaN and the infinities. The divisor's sign does not change the result,
// and a zero result takes the dividend's sign.
function show(label: string, v: number): void {
console.log(label, Object.is(v, -0) ? "-0" : String(v));
}
const dividends: number[] = [
0, -0, 1, -1, 3, -4, 7.25, -7.25, 0.75, -0.75, 1.5, -1.5, 5e-324, -5e-324,
2.2250738585072014e-308, -2.2250738585072014e-308, 1e-300, 2 ** 31 - 0.5, -(2 ** 31) + 0.5,
2 ** 32 + 0.25, 2 ** 52 + 1, -(2 ** 52 + 3), 2 ** 52 + 0.5, 2 ** 53, -(2 ** 53), 2 ** 53 + 2,
2 ** 60 + 1024, -(2 ** 60), 2 ** 63, 2 ** 64 + 4096, 1e21, -1e21, 1e300, -1e300,
Number.MAX_VALUE, -Number.MAX_VALUE, 123456789.123, -98765.4321, NaN, Infinity, -Infinity,
];
for (const x of dividends) {
show(`${x} % 1`, x % 1);
show(`${x} % -1`, x % -1);
show(`${x} % 2`, x % 2);
show(`${x} % 8`, x % 8);
show(`${x} % -16`, x % -16);
show(`${x} % 1024`, x % 1024);
show(`${x} % 2147483648`, x % 2147483648);
show(`${x} % -2147483648`, x % -2147483648);
show(`${x} % 4294967296`, x % 4294967296);
show(`${x} % 2 ** 32`, x % 2 ** 32);
show(`${x} % -(2 ** 40)`, x % -(2 ** 40));
show(`${x} % 2 ** 52`, x % 2 ** 52);
show(`${x} % 2 ** 53`, x % 2 ** 53);
show(`${x} % 2 ** 64`, x % 2 ** 64);
show(`${x} % 2 ** 1023`, x % 2 ** 1023);
show(`${x} % -(2 ** 1023)`, x % -(2 ** 1023));
}
// Divisors that are not powers of two of at least 1 keep the general path.
for (const x of dividends) {
show(`${x} % 0.5`, x % 0.5);
show(`${x} % 0.125`, x % 0.125);
show(`${x} % 3`, x % 3);
show(`${x} % 6`, x % 6);
show(`${x} % 2 ** 0.5`, x % 2 ** 0.5);
show(`${x} % 2 ** -1`, x % 2 ** -1);
}
// Module constants, compound assignment, record fields and closures.
const WORD = 4294967296;
const HALF_WORD = 65536;
interface Cell {
value: number;
}
const cell: Cell = { value: -12345.678 };
let total = 0;
for (let i = 0; i < 64; i++) {
cell.value = cell.value * -3.7 + i;
let v = cell.value;
v %= HALF_WORD;
total += (v % WORD) + (cell.value % 256);
}
show("total", total);
const wrap = (x: number): number => x % 2 ** 16;
show("wrap", wrap(-1e15 - 0.5) + wrap(2 ** 70) + wrap(65536.5));
let sum = 0;
for (let i = -20; i <= 20; i++) sum += (i * 0.37) % 1;
show("sum", sum);
@@ -0,0 +1,50 @@
// `%` with integer dividends beyond 2^53 (every double there is an integer)
// and integer divisors: constant, int32 and unproven. Results match Node,
// including the sign of zero and the bounds of the inline integer path at
// 2^62 and 2^63.
function show(label: string, v: number): void {
console.log(label, Object.is(v, -0) ? "-0" : String(v));
}
const dividends: number[] = [
2 ** 53, -(2 ** 53), 2 ** 53 + 2, -(2 ** 53 + 2), 2 ** 54 + 4, 2 ** 60 + 1024, -(2 ** 60 + 2048),
2 ** 61 + 3 * 2 ** 9, 2 ** 62 - 1024, -(2 ** 62 - 1024), 2 ** 62, -(2 ** 62), 2 ** 62 + 1024,
-(2 ** 62 + 1024), 2 ** 63 - 1024, -(2 ** 63 - 1024), 2 ** 63, -(2 ** 63), 2 ** 64, -(2 ** 64),
1e19, -1e19, 1e21, 9.999999999999999e22,
];
const divisors: number[] = [
1, -1, 2, 3, -3, 7, 10, 1000, 65535, 1000000007, 2147483647, -2147483647, 2 ** 31, 2 ** 32 - 1,
2 ** 53 - 1, 2 ** 53 + 2, 2 ** 62, -(2 ** 62), 2 ** 62 + 1024, 2 ** 63, -(2 ** 63), 2 ** 64, 0,
-0,
];
// Unproven dividends and divisors read from arrays.
let checksum = 0;
for (let i = 0; i < dividends.length; i++) {
for (let j = 0; j < divisors.length; j++) {
const r = dividends[i]! % divisors[j]!;
show(`${dividends[i]} % ${divisors[j]}`, r);
if (r === r) checksum = (checksum * 31 + (r % 1000003)) % 1000000007;
}
}
show("checksum", checksum);
// Constant divisors.
for (const x of dividends) {
show(`${x} % 3`, x % 3);
show(`${x} % -10`, x % -10);
show(`${x} % 1000000007`, x % 1000000007);
show(`${x} % 2147483647`, x % 2147483647);
show(`${x} % -1`, x % -1);
}
// Int32 divisors: loop counters and `| 0` values.
for (let k = 1; k <= 40; k += 13) {
for (const x of dividends) {
show(`${x} % ${k}`, x % k);
const m = (k * 7919) | 0;
show(`${x} % ${m}`, x % m);
show(`${x} % ${-m}`, x % -m);
}
}
+50
View File
@@ -0,0 +1,50 @@
// Linear congruential generators whose products exceed 2^53, reduced with
// `%` by powers of two and by other constants. The products round, so every
// step must reproduce Node's double arithmetic exactly.
let seed = 42;
function lcg31(): number {
seed = (seed * 1103515245 + 12345) % 2147483648;
return seed;
}
let prime = 42;
function parkMillerWide(): number {
prime = (prime * 1103515245 + 12345) % 2147483647;
return prime;
}
let word = 7;
function lcg32(): number {
word = (word * 6364136223846793 + 1442695040888963) % 2 ** 32;
return word;
}
let negative = -99;
function signedStep(): number {
negative = (negative * -1103515245 - 12345) % 2147483648;
return negative;
}
const seen = new Map<number, number>();
let a = 0;
let b = 0;
let c = 0;
let d = 0;
for (let i = 0; i < 20000; i++) {
const x = lcg31();
a = (a + x) % 1000000007;
b = (b * 31 + parkMillerWide()) % 1000000007;
c = (c + lcg32()) % 2 ** 40;
d = (d + signedStep()) % 1000000007;
if (i < 8) console.log(i, x, prime, word, negative);
seen.set(x, (seen.get(x) ?? 0) + 1);
}
console.log("sums", a, b, c, d, Object.is(d, -0));
console.log("distinct", seen.size);
// Bucket a stream into a small table with the low and high bits.
const buckets = new Int32Array(16);
for (let i = 0; i < 5000; i++) {
const x = lcg31();
buckets[x % 16]++;
buckets[Math.floor(x / 134217728) % 16]++;
}
console.log(Array.from(buckets).join(","));
+34
View File
@@ -0,0 +1,34 @@
// Fractional dividends with `%`: Weyl sequences and hashes reduced `% 1`,
// phases reduced by powers of two and by other divisors, and negative values
// whose remainders keep the dividend's sign.
function show(label: string, v: number): void {
console.log(label, Object.is(v, -0) ? "-0" : String(v));
}
let weyl = 0.5;
let hashSum = 0;
for (let i = 0; i < 2000; i++) {
weyl = (weyl + 0.6180339887498949) % 1;
const h = (weyl * 2654435761.5) % 1;
hashSum += h;
if (i < 6) show(`weyl ${i}`, weyl);
if (i < 6) show(`hash ${i}`, h);
}
show("hashSum", hashSum);
let phase = -3.75;
for (let i = 0; i < 12; i++) {
phase = phase * -2.5 + 0.125;
show(`phase ${i} % 4`, phase % 4);
show(`phase ${i} % 0.25`, phase % 0.25);
show(`phase ${i} % 360`, phase % 360);
show(`phase ${i} % (2 * Math.PI)`, phase % (2 * Math.PI));
}
// Fractional parts of negative values and of values that are integers.
for (const x of [-2.5, -2, -0.5, -0, 0, 0.5, 2, 2.5, -1e-310, 1e-310, 4503599627370495.5]) {
show(`${x} % 1`, x % 1);
show(`${x} % -1`, x % -1);
show(`${x} % 2`, x % 2);
}
@@ -0,0 +1,70 @@
// Records returned from functions that read arrays, called in loops so the
// result-slot entries inline: in-range reads, holes, reads past the end and
// at fractional indices (undefined fields, which make the arithmetic NaN),
// sparse arrays, boolean fields, and a recursive helper, against Node.
interface Point {
x: number;
y: number;
}
function pointAt(xs: number[], ys: number[], i: number): Point {
return { x: xs[i], y: ys[i] };
}
function distance(p: Point, q: Point): number {
const dx = p.x - q.x;
const dy = p.y - q.y;
return Math.sqrt(dx * dx + dy * dy);
}
function pathLength(xs: number[], ys: number[], extra: number): number {
let total = 0;
for (let i = 0; i + 1 + extra < xs.length + extra; i++) total += distance(pointAt(xs, ys, i), pointAt(xs, ys, i + 1));
return total;
}
const xs: number[] = [];
const ys: number[] = [];
for (let i = 0; i < 50; i++) {
xs.push((i * 37) % 11);
ys.push((i * 13) % 7);
}
console.log(pathLength(xs, ys, 0).toFixed(6), pathLength(xs, ys, 1), pathLength(xs, ys, 2));
const p = pointAt(xs, ys, 60);
const q = pointAt(xs, ys, 1.5);
console.log(p.x, p.y, q.x === undefined, distance(p, pointAt(xs, ys, 0)), distance(pointAt(xs, ys, 3), pointAt(xs, ys, 3)));
const holey: number[] = [1, 2, 3];
holey.length = 6;
holey[5] = 9;
let viaHoles = "";
for (let i = 0; i < 7; i++) viaHoles += String(pointAt(holey, holey, i).x) + ",";
console.log(viaHoles);
const sparse: number[] = [4, 5];
sparse[100] = 6;
console.log(pointAt(sparse, sparse, 100).y, pointAt(sparse, sparse, 50).y, distance(pointAt(sparse, xs, 0), pointAt(sparse, xs, 1)));
interface Flags {
on: boolean;
next: boolean;
}
function flagsAt(bits: boolean[], i: number): Flags {
return { on: bits[i], next: bits[i + 1] };
}
const bits = [true, false, true, true];
let flagText = "";
for (let i = 0; i < 5; i++) {
const f = flagsAt(bits, i);
flagText += `${f.on}/${f.next} `;
}
console.log(flagText);
interface Span {
lo: number;
hi: number;
}
function widen(span: Span, values: number[], from: number, to: number): Span {
if (from >= to) return span;
const v = values[from];
return widen({ lo: Math.min(span.lo, v), hi: Math.max(span.hi, v) }, values, from + 1, to);
}
let spans = 0;
for (let i = 0; i + 8 <= xs.length; i += 8) {
const s = widen({ lo: xs[i], hi: xs[i] }, xs, i + 1, i + 8);
spans += s.hi - s.lo;
}
const past = widen({ lo: 0, hi: 0 }, xs, 48, 52);
console.log(spans, past.lo, past.hi);
+26
View File
@@ -0,0 +1,26 @@
/* fs.globSync, fs/promises glob and fs.glob under @types/node: the same
* native walk as under the bundled declarations. */
import { type Dirent, glob as globCallback, globSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { glob } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
const root = mkdtempSync(join(tmpdir(), "scriptc-node-types-glob-"));
mkdirSync(join(root, "src/util"), { recursive: true });
for (const file of ["src/a.ts", "src/util/b.ts", "src/util/b.test.ts", "notes.md"]) writeFileSync(join(root, file), "");
console.log(globSync("**/*.ts", { cwd: root, exclude: ["**/*.test.ts"] }).join(" "));
const entries: Dirent[] = globSync("src/*", { cwd: root, withFileTypes: true });
console.log(entries.map((d) => `${d.name}:${d.isDirectory()}`).join(" "));
async function main(): Promise<void> {
const found: string[] = [];
for await (const path of glob("src/**", { cwd: root })) found.push(path);
console.log(found.join(" "));
globCallback("*.md", { cwd: root }, (err, matches) => {
console.log(err, matches.join(" "));
rmSync(root, { recursive: true, force: true });
});
}
void main();
+14
View File
@@ -548,6 +548,20 @@ test("node-types: Blob and File match Node under @types/node", async () => {
expect(comparableStderr(native.stderr)).toBe(node.stderr);
});
test("node-types: fs.glob forms match Node under @types/node", async () => {
const outDir = outDirFor("node-fs-glob");
const entry = join(nodeTypesDir, "fs-glob.ts");
const result = await compile(entry, { outPath: join(outDir, "fs-glob"), outDir, sanitize });
expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics, null, 2) : "").toBe(true);
if (!result.ok) return;
const [native, node] = await Promise.all([
execFileAsync(result.binaryPath),
execFileAsync(process.execPath, ["--disable-warning=MODULE_TYPELESS_PACKAGE_JSON", entry]),
]);
expect(native.stdout).toBe(node.stdout);
expect(comparableStderr(native.stderr)).toBe(node.stderr);
});
test("node-types: util function helpers match Node under @types/node", async () => {
const outDir = outDirFor("node-util-callables");
const entry = join(nodeTypesDir, "util-callables.ts");