diff --git a/docs/content/docs/limitations.mdx b/docs/content/docs/limitations.mdx
index a2f05e50d..2d229bd84 100644
--- a/docs/content/docs/limitations.mdx
+++ b/docs/content/docs/limitations.mdx
@@ -76,6 +76,8 @@ title: "Limitations"
../limitations/builtin-apis/filesystem-flags.mdx
+../limitations/builtin-apis/glob.mdx
+
../limitations/builtin-apis/filehandle-and-descriptor-i-o.mdx
../limitations/builtin-apis/crypto.mdx
diff --git a/docs/content/limitations/builtin-apis/glob.mdx b/docs/content/limitations/builtin-apis/glob.mdx
new file mode 100644
index 000000000..5df4d6688
--- /dev/null
+++ b/docs/content/limitations/builtin-apis/glob.mdx
@@ -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.
diff --git a/docs/content/limitations/builtin-function-values.mdx b/docs/content/limitations/builtin-function-values.mdx
index d005a509d..fa61ff1e6 100644
--- a/docs/content/limitations/builtin-function-values.mdx
+++ b/docs/content/limitations/builtin-function-values.mdx
@@ -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`.
diff --git a/docs/src/generated/node-v24-compatibility-meta.json b/docs/src/generated/node-v24-compatibility-meta.json
index da577c2ca..768d0de24 100644
--- a/docs/src/generated/node-v24-compatibility-meta.json
+++ b/docs/src/generated/node-v24-compatibility-meta.json
@@ -2,6 +2,6 @@
"schemaVersion": 3,
"nodeVersion": "24.22.0",
"nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93",
- "artifactVersion": "bef290787c18ee9ed334",
+ "artifactVersion": "0ae6cfd9272ef79cd01d",
"rowCount": 3880
}
diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json
index c5f0cf461..caa0e0d7b 100644
--- a/docs/src/generated/node-v24-compatibility.json
+++ b/docs/src/generated/node-v24-compatibility.json
@@ -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"
}
},
{
diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json
index 732431fd9..85b09ead2 100644
--- a/internal/compatibility/generated/node-v24-backlog.json
+++ b/internal/compatibility/generated/node-v24-backlog.json
@@ -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"
+ ]
}
}
},
diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json
index a8d02f687..853a739cc 100644
--- a/internal/compatibility/generated/node-v24-internal.json
+++ b/internal/compatibility/generated/node-v24-internal.json
@@ -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"
},
diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json
index baa569b78..ed6b485a4 100644
--- a/internal/compatibility/static-support.json
+++ b/internal/compatibility/static-support.json
@@ -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"] },
diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts
index 7de145b7a..2619d65a4 100644
--- a/packages/compiler/ambient/scriptc-node-fallback.d.ts
+++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts
@@ -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;
+ /* 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;
+ export function glob(
+ pattern: string | readonly string[],
+ options: import("node:fs").GlobOptionsWithFileTypes,
+ ): AsyncIterableIterator;
export function rm(path: string): Promise;
export function stat(path: string): Promise;
export function statfs(
diff --git a/packages/compiler/src/backend/llvm/dense-array-access.ts b/packages/compiler/src/backend/llvm/dense-array-access.ts
index 83163d99f..8f2535561 100644
--- a/packages/compiler/src/backend/llvm/dense-array-access.ts
+++ b/packages/compiler/src/backend/llvm/dense-array-access.ts
@@ -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. */
diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts
index 4c8889c9c..aca34657d 100644
--- a/packages/compiler/src/backend/llvm/emitter.ts
+++ b/packages/compiler/src/backend/llvm/emitter.ts
@@ -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}`;
diff --git a/packages/compiler/src/backend/llvm/expr-primitives.ts b/packages/compiler/src/backend/llvm/expr-primitives.ts
index 4b0e35874..07d969f6e 100644
--- a/packages/compiler/src/backend/llvm/expr-primitives.ts
+++ b/packages/compiler/src/backend/llvm/expr-primitives.ts
@@ -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) {
diff --git a/packages/compiler/src/backend/llvm/integer-remainder.ts b/packages/compiler/src/backend/llvm/integer-remainder.ts
index 79939f757..144695a45 100644
--- a/packages/compiler/src/backend/llvm/integer-remainder.ts
+++ b/packages/compiler/src/backend/llvm/integer-remainder.ts
@@ -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 };
+}
diff --git a/packages/compiler/src/backend/llvm/lib-filesystem.ts b/packages/compiler/src/backend/llvm/lib-filesystem.ts
index b8e17fc1c..f60405d7a 100644
--- a/packages/compiler/src/backend/llvm/lib-filesystem.ts
+++ b/packages/compiler/src/backend/llvm/lib-filesystem.ts
@@ -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
diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts
index 33b9fd4ff..0c3a2963c 100644
--- a/packages/compiler/src/backend/llvm/lib-shared.ts
+++ b/packages/compiler/src/backend/llvm/lib-shared.ts
@@ -325,6 +325,10 @@ export const LIB_FN_SYMS: Record = {
"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([
"fs.existsChk",
"fs.callbackValue",
"fs.callbackCall",
+ "fs.callbackGlob",
"http.createServer",
"http.createServerEmpty",
"http.request",
diff --git a/packages/compiler/src/backend/llvm/local-array-reads.test.ts b/packages/compiler/src/backend/llvm/local-array-reads.test.ts
index e177594e6..b16fe2608 100644
--- a/packages/compiler/src/backend/llvm/local-array-reads.test.ts
+++ b/packages/compiler/src/backend/llvm/local-array-reads.test.ts
@@ -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");
}
});
diff --git a/packages/compiler/src/backend/llvm/local-array-reads.ts b/packages/compiler/src/backend/llvm/local-array-reads.ts
index db56b273d..5646de371 100644
--- a/packages/compiler/src/backend/llvm/local-array-reads.ts
+++ b/packages/compiler/src/backend/llvm/local-array-reads.ts
@@ -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`);
}
diff --git a/packages/compiler/src/backend/llvm/stack-objects.test.ts b/packages/compiler/src/backend/llvm/stack-objects.test.ts
index 5869ac3de..845d6ae5e 100644
--- a/packages/compiler/src/backend/llvm/stack-objects.test.ts
+++ b/packages/compiler/src/backend/llvm/stack-objects.test.ts
@@ -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.
diff --git a/packages/compiler/src/frontend/lowering/builtins/fs-glob.ts b/packages/compiler/src/frontend/lowering/builtins/fs-glob.ts
new file mode 100644
index 000000000..389da8dc0
--- /dev/null
+++ b/packages/compiler/src/frontend/lowering/builtins/fs-glob.ts
@@ -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();
+ }
+}
diff --git a/packages/compiler/src/frontend/lowering/builtins/module-calls.ts b/packages/compiler/src/frontend/lowering/builtins/module-calls.ts
index 5c5fbc31f..2512de466 100644
--- a/packages/compiler/src/frontend/lowering/builtins/module-calls.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/module-calls.ts
@@ -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);
}
diff --git a/packages/compiler/src/frontend/lowering/lower-async-iteration.ts b/packages/compiler/src/frontend/lowering/lower-async-iteration.ts
index 62b7a89a5..5957afcaa 100644
--- a/packages/compiler/src/frontend/lowering/lower-async-iteration.ts
+++ b/packages/compiler/src/frontend/lowering/lower-async-iteration.ts
@@ -279,7 +279,7 @@ function classAsyncIteratorPlan(
};
}
-function checkForAwaitBinding(
+export function checkForAwaitBinding(
lowerer: Lowerer,
stmt: ts.ForOfStatement,
): { declaration: ts.VariableDeclaration & { name: ts.Identifier }; mutable: boolean } {
diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts
index 742fdfc3a..5d6305aa8 100644
--- a/packages/compiler/src/frontend/lowering/lower-stmts.ts
+++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts
@@ -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) {
diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts
index b490d42ff..99c6b4d46 100644
--- a/packages/compiler/src/frontend/lowering/lowerer.ts
+++ b/packages/compiler/src/frontend/lowering/lowerer.ts
@@ -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();
+ /** 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();
diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts
index cea9a4086..3373e5316 100644
--- a/packages/compiler/src/frontend/lowering/surfaces.ts
+++ b/packages/compiler/src/frontend/lowering/surfaces.ts
@@ -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,
diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts
index 1e9cf378d..764152126 100644
--- a/packages/compiler/src/ir/builtin-effects.ts
+++ b/packages/compiler/src/ir/builtin-effects.ts
@@ -521,6 +521,11 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([
"fs.rmdirSync",
"fs.readdirSync",
"fs.readdirTypesSync",
+ "fs.globSync",
+ "fs.globTypesSync",
+ "fs.callbackGlob",
+ "fsp.globStrings",
+ "fsp.globTypes",
"url.new",
"url.newBase",
"url.set",
diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts
index 5f09888a2..0b8416fcd 100644
--- a/packages/compiler/src/ir/builtin-signatures.ts
+++ b/packages/compiler/src/ir/builtin-signatures.ts
@@ -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
diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts
index 27326228c..85975adc2 100644
--- a/packages/compiler/src/ir/runtime-features.ts
+++ b/packages/compiler/src/ir/runtime-features.ts
@@ -43,6 +43,7 @@ export interface RuntimeFeatures {
const DYN_ASYNC_LIB_FNS: ReadonlySet = 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 (
diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts
index 55f40c7c4..c9ac64504 100644
--- a/packages/compiler/src/ir/validate.ts
+++ b/packages/compiler/src/ir/validate.ts
@@ -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"
diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json
index 567d0a106..e40d682d2 100644
--- a/packages/compiler/surface-manifest.json
+++ b/packages/compiler/surface-manifest.json
@@ -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",
diff --git a/packages/compiler/test/fs-glob-boundaries.test.ts b/packages/compiler/test/fs-glob-boundaries.test.ts
new file mode 100644
index 000000000..169f40052
--- /dev/null
+++ b/packages/compiler/test/fs-glob-boundaries.test.ts
@@ -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 {
+ 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);
+});
diff --git a/packages/compiler/test/llvm-remainder.test.ts b/packages/compiler/test/llvm-remainder.test.ts
index 7814cfbdb..e86ed4097 100644
--- a/packages/compiler/test/llvm-remainder.test.ts
+++ b/packages/compiler/test/llvm-remainder.test.ts
@@ -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");
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5448-fs-glob-patterns.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5448-fs-glob-patterns.ts.json
new file mode 100644
index 000000000..a373df5a1
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5448-fs-glob-patterns.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/5448-fs-glob-patterns.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5449-fs-glob-options.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5449-fs-glob-options.ts.json
new file mode 100644
index 000000000..802257b14
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5449-fs-glob-options.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/5449-fs-glob-options.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5450-fs-glob-validation.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5450-fs-glob-validation.js.json
new file mode 100644
index 000000000..d88ba07ad
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5450-fs-glob-validation.js.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/5450-fs-glob-validation.js"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5451-fs-promises-glob.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5451-fs-promises-glob.ts.json
new file mode 100644
index 000000000..0594615d1
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5451-fs-promises-glob.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/5451-fs-promises-glob.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5452-fs-glob-callback.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5452-fs-glob-callback.ts.json
new file mode 100644
index 000000000..863acaba2
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5452-fs-glob-callback.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/5452-fs-glob-callback.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5455-power-of-two-remainder.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5455-power-of-two-remainder.ts.json
new file mode 100644
index 000000000..c960ba1cd
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5455-power-of-two-remainder.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/5455-power-of-two-remainder.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5456-remainder-large-integer-dividends.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5456-remainder-large-integer-dividends.ts.json
new file mode 100644
index 000000000..6b48639fb
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5456-remainder-large-integer-dividends.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/5456-remainder-large-integer-dividends.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5457-lcg-modulus.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5457-lcg-modulus.ts.json
new file mode 100644
index 000000000..0e2b42dad
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5457-lcg-modulus.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/5457-lcg-modulus.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5458-fractional-remainder.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5458-fractional-remainder.ts.json
new file mode 100644
index 000000000..edb5a8a41
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5458-fractional-remainder.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/5458-fractional-remainder.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5585-record-results-inlined.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5585-record-results-inlined.ts.json
new file mode 100644
index 000000000..f2956d94b
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5585-record-results-inlined.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/5585-record-results-inlined.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/fs-glob.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/fs-glob.ts.json
new file mode 100644
index 000000000..7a222498b
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/fs-glob.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/fixtures/node-types/fs-glob.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/runtime/src/scr_bytes_io.c b/packages/runtime/src/scr_bytes_io.c
index 547ba1852..152d0b632 100644
--- a/packages/runtime/src/scr_bytes_io.c
+++ b/packages/runtime/src/scr_bytes_io.c
@@ -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
-#define SCR_FS_HAS_GLOB 1
-#else
-#define SCR_FS_HAS_GLOB 0
-#endif
#ifdef __APPLE__
#include /* 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
diff --git a/packages/runtime/src/scr_glob.c b/packages/runtime/src/scr_glob.c
index b2c966fc7..89949c427 100644
--- a/packages/runtime/src/scr_glob.c
+++ b/packages/runtime/src/scr_glob.c
@@ -19,9 +19,11 @@
#include "scr_runtime.h"
#include
+#include
#include
#include
#include
+#include
/* ── arena ───────────────────────────────────────────────────────────── */
@@ -943,6 +945,7 @@ struct Ast {
typedef struct {
Arena *a;
bool nocase;
+ bool magic_only; /* nocaseMagicOnly */
} Ctx;
static void partv_push(Arena *a, PartV *pv, Part p) {
@@ -1601,6 +1604,19 @@ static bool throw_too_long(void) {
return false;
}
+/* glob.toUpperCase() !== glob.toLowerCase(): the segment holds a cased
+ * letter (ASCII, Latin-1, Greek and Cyrillic, the scripts with case that
+ * file names commonly use). */
+static bool s_has_case(S s) {
+ for (size_t i = 0; i < s.n; i++) {
+ uint16_t c = s.p[i];
+ if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')) return true;
+ if (c >= 0xc0 && c <= 0x24f && c != 0xd7 && c != 0xf7) return true;
+ if (c >= 0x370 && c <= 0x52f) return true;
+ }
+ return false;
+}
+
/* Minimatch.parse for one segment. False after arming an exception. */
static bool parse_segment(Ctx *x, S pattern, Pat *out) {
memset(out, 0, sizeof *out);
@@ -1621,7 +1637,10 @@ static bool parse_segment(Ctx *x, S pattern, Pat *out) {
ast_parse(x, pattern, ast, 0, 0);
ast_to_string(x, ast);
Source src = ast_to_regexp_source(x, ast, -1);
- bool any_magic = src.has_magic || ast->has_magic == 1;
+ /* AST#toMMPattern: under nocase without nocaseMagicOnly, a segment whose
+ * upper and lower case differ is matched by a case-insensitive regex. */
+ bool any_magic = src.has_magic || ast->has_magic == 1 ||
+ (x->nocase && !x->magic_only && s_has_case(pattern));
if (!any_magic) {
out->kind = PAT_STRING;
out->str = src.body;
@@ -2022,27 +2041,39 @@ static bool match_pattern(Ctx *x, SV *ff, PatSlice pattern, bool windows) {
return match_one_plain(x, &file, pattern, fi, pi);
}
-/* The whole minimatch(path, pattern, options) call. False with an armed
- * exception on failure (*threw set). */
-static bool glob_match(Arena *a, S path, S pattern, bool windows, bool nocase, bool *threw) {
- *threw = false;
- if (pattern.n > 65536) {
- *threw = true;
- return throw_too_long();
- }
- Ctx x = { a, nocase };
+/* A compiled Minimatch: the pattern set (one segment list per brace
+ * expansion after the optimization phases) and the matching globParts
+ * strings, which fs.glob's walker uses for its cache keys. */
+typedef struct {
+ Ctx x;
+ bool windows;
+ bool empty; /* the empty pattern: matches only the empty path */
+ size_t n;
+ PatV *set;
+ SV *parts;
+} Matcher;
+
+/* new Minimatch(pattern, options): false with an armed exception on
+ * failure. magic_only is nocaseMagicOnly (literal segments stay exact
+ * strings under nocase). Everything lives in the arena. */
+static bool glob_compile(Arena *a, S pattern, bool windows, bool nocase, bool magic_only, Matcher *m) {
+ memset(m, 0, sizeof *m);
+ m->x = (Ctx){ a, nocase, magic_only };
+ m->windows = windows;
+ if (pattern.n > 65536) return throw_too_long();
+ Ctx x = m->x;
/* windowsPathsNoEscape: backslashes are separators, never escapes. */
B nb;
b_init(&nb, a);
for (size_t i = 0; i < pattern.n; i++) b_unit(&nb, pattern.p[i] == '\\' ? '/' : pattern.p[i]);
S pat = b_done(&nb);
- if (!pat.n) return path.n == 0;
+ if (!pat.n) {
+ m->empty = true;
+ return true;
+ }
bool failed = false;
SV expanded = brace_expand(a, pat, &failed);
- if (failed) {
- *threw = true;
- return false;
- }
+ if (failed) return false;
/* [...new Set(...)] */
SV glob_set = { NULL, 0, 0 };
for (size_t i = 0; i < expanded.n; i++) {
@@ -2099,7 +2130,6 @@ static bool glob_match(Arena *a, S path, S pattern, bool windows, bool nocase, b
p.kind = PAT_STRING;
p.str = s->v[k];
} else if (!parse_segment(&x, s->v[k], &p)) {
- *threw = true;
return false;
}
patv_push(a, &pv, p);
@@ -2118,14 +2148,25 @@ static bool glob_match(Arena *a, S path, S pattern, bool windows, bool nocase, b
}
}
}
- /* match(f) */
+ m->n = glob_parts.n;
+ m->set = set;
+ m->parts = glob_parts.v;
+ return true;
+}
+
+/* Minimatch#match(path): false with an armed exception on failure
+ * (*threw set). Scratch allocations go to the matcher's arena. */
+static bool glob_test(Matcher *m, S path, bool *threw) {
+ *threw = false;
+ if (m->empty) return path.n == 0;
+ Arena *a = m->x.a;
B fb;
b_init(&fb, a);
- for (size_t i = 0; i < path.n; i++) b_unit(&fb, windows && path.p[i] == '\\' ? '/' : path.p[i]);
- SV ff = slash_split(a, b_done(&fb), windows);
- for (size_t g = 0; g < glob_parts.n; g++) {
- PatSlice pattern = { set[g].v, set[g].n };
- if (match_pattern(&x, &ff, pattern, windows)) return true;
+ for (size_t i = 0; i < path.n; i++) b_unit(&fb, m->windows && path.p[i] == '\\' ? '/' : path.p[i]);
+ SV ff = slash_split(a, b_done(&fb), m->windows);
+ for (size_t g = 0; g < m->n; g++) {
+ PatSlice pattern = { m->set[g].v, m->set[g].n };
+ if (match_pattern(&m->x, &ff, pattern, m->windows)) return true;
if (scr_exc_pending()) {
*threw = true;
return false;
@@ -2134,6 +2175,17 @@ static bool glob_match(Arena *a, S path, S pattern, bool windows, bool nocase, b
return false;
}
+/* The whole minimatch(path, pattern, options) call. False with an armed
+ * exception on failure (*threw set). */
+static bool glob_match(Arena *a, S path, S pattern, bool windows, bool nocase, bool *threw) {
+ Matcher m;
+ if (!glob_compile(a, pattern, windows, nocase, true, &m)) {
+ *threw = true;
+ return false;
+ }
+ return glob_test(&m, path, threw);
+}
+
/* ── entry points ────────────────────────────────────────────────────── */
static bool scr_glob_host_nocase(void) {
@@ -2161,3 +2213,1016 @@ bool scr_path_matches_glob(ScrStr *path, ScrStr *pattern) {
bool scr_path_win32_matches_glob(ScrStr *path, ScrStr *pattern) {
return scr_glob_match(path, pattern, true, scr_glob_host_nocase());
}
+/* ── fs.glob / fs.globSync / fs/promises glob ────────────────────────────
+ * A port of Node 24.22's lib/internal/fs/glob.js Glob class: the same
+ * walk over a LIFO queue of (path, patterns) items, the same caches
+ * (lstat, stat, readdir, realpath, seen pattern suffixes), the same result
+ * set in insertion order, and the same exclusion checks. readdir listings
+ * come from scr_fs_scandir, which sorts them like libuv, so results come
+ * out in Node's order. All strings the walk creates are kept in one pool
+ * and released together. */
+
+#ifdef _WIN32
+#define GW_WINDOWS true
+#else
+#define GW_WINDOWS false
+#endif
+
+typedef struct {
+ ScrStr **v;
+ size_t n, cap;
+} GwPool;
+
+/* One lstat (or readdir entry) result: Node's Dirent kinds (1 file, 2 dir,
+ * 3 link, 0 other or unknown). */
+typedef struct {
+ ScrStr *name;
+ unsigned char kind;
+} GwStat;
+
+typedef struct {
+ ScrStr *key;
+ void *val;
+} GwSlot;
+
+/* A string-keyed map that keeps insertion order (JS Map semantics for
+ * iteration). */
+typedef struct {
+ GwSlot *slots;
+ size_t cap, n;
+ ScrStr **order;
+ size_t on, ocap;
+} GwMap;
+
+typedef struct {
+ size_t *v;
+ size_t n, cap;
+} GwIdx; /* a Set of indexes in insertion order */
+
+typedef struct {
+ ScrStr **v;
+ size_t n;
+} GwReal; /* realpaths seen on the way down (followSymlinks) */
+
+typedef struct {
+ const PatV *pat;
+ const SV *parts;
+ GwIdx indexes;
+ GwIdx symlinks;
+ const GwReal *realpaths;
+ long last;
+} GwPat;
+
+typedef struct {
+ GwPat **v;
+ size_t n, cap;
+} GwPats;
+
+typedef struct {
+ ScrStr *path;
+ GwPats pats;
+} GwItem;
+
+struct ScrFsGlob {
+ size_t n, cap;
+ ScrStr **paths;
+ ScrStr **names;
+ ScrStr **parents;
+ unsigned char *kinds;
+};
+
+typedef struct {
+ Arena a;
+ GwPool pool;
+ Ctx ctx; /* the include patterns' case mode, for segment tests */
+ ScrStr *root;
+ bool follow;
+ bool with_types;
+ bool async_types; /* fs/promises Dirents carry resolved parent paths */
+ ScrDyn *exclude_fn;
+ Matcher *exclude;
+ size_t nexclude;
+ GwPat **patterns;
+ size_t npatterns;
+ GwMap stats, follow_stats, readdirs, realpaths, seen, subpatterns;
+ GwMap result_set;
+ ScrStr **results;
+ size_t nresults, capresults;
+ GwItem *queue;
+ size_t nqueue, capqueue;
+} Gw;
+
+static void gw_oom(void) { scr_trap("scriptc: out of memory\n"); }
+
+/* The error an fs/promises glob walk ended with, held until the loop has
+ * consumed the matches found before it (scr_fsp_glob_error). */
+static SCR_TL ScrCaught *scr_fs_glob_deferred;
+
+static void *gw_grow(void *v, size_t *cap, size_t need, size_t size) {
+ if (need <= *cap) return v;
+ size_t c = *cap ? *cap * 2 : 8;
+ while (c < need) c *= 2;
+ void *g = realloc(v, c * size);
+ if (!g) gw_oom();
+ *cap = c;
+ return g;
+}
+
+static ScrStr *gw_keep(Gw *w, ScrStr *s) {
+ w->pool.v = gw_grow(w->pool.v, &w->pool.cap, w->pool.n + 1, sizeof *w->pool.v);
+ w->pool.v[w->pool.n++] = s;
+ return s;
+}
+
+static ScrStr *gw_cstr(Gw *w, const char *s) { return gw_keep(w, scr_str_new(s, strlen(s))); }
+
+static bool gw_eq_c(const ScrStr *s, const char *c) {
+ size_t n = strlen(c);
+ return s->len == n && memcmp(s->data, c, n) == 0;
+}
+
+/* ── path.join / resolve / dirname / basename / isAbsolute ────────────── */
+
+static ScrStr *gw_path2(Gw *w, ScrStr *x, ScrStr *y, bool resolve) {
+ ScrArr *parts = scr_arr_new(SCR_ELEM_STR, 2);
+ scr_arr_push_ref(parts, scr_str_retain(x));
+ scr_arr_push_ref(parts, scr_str_retain(y));
+#ifdef _WIN32
+ ScrStr *out = resolve ? scr_path_win32_resolve(parts) : scr_path_win32_join(parts);
+#else
+ ScrStr *out = resolve ? scr_path_resolve(parts) : scr_path_join(parts);
+#endif
+ scr_arr_release(parts);
+ return gw_keep(w, out);
+}
+
+static ScrStr *gw_join(Gw *w, ScrStr *x, ScrStr *y) { return gw_path2(w, x, y, false); }
+
+static ScrStr *gw_resolve(Gw *w, ScrStr *x, ScrStr *y) { return gw_path2(w, x, y, true); }
+
+static ScrStr *gw_dirname(Gw *w, ScrStr *p) {
+#ifdef _WIN32
+ return gw_keep(w, scr_path_win32_dirname(p));
+#else
+ return gw_keep(w, scr_path_dirname(p));
+#endif
+}
+
+static ScrStr *gw_basename(Gw *w, ScrStr *p) {
+ ScrStr *none = scr_str_new("", 0);
+#ifdef _WIN32
+ ScrStr *out = scr_path_win32_basename(p, none);
+#else
+ ScrStr *out = scr_path_basename(p, none);
+#endif
+ scr_str_release(none);
+ return gw_keep(w, out);
+}
+
+static bool gw_is_absolute(ScrStr *p) {
+#ifdef _WIN32
+ return scr_path_win32_is_absolute(p);
+#else
+ return scr_path_is_absolute(p);
+#endif
+}
+
+/* ── maps ─────────────────────────────────────────────────────────────── */
+
+static size_t gw_hash(const ScrStr *k) {
+ size_t h = 1469598103934665603u;
+ for (size_t i = 0; i < k->len; i++) h = (h ^ (unsigned char)k->data[i]) * 1099511628211u;
+ return h;
+}
+
+static GwSlot *gw_slot(GwMap *m, const ScrStr *key) {
+ if (!m->cap) return NULL;
+ size_t i = gw_hash(key) & (m->cap - 1);
+ for (;;) {
+ GwSlot *s = &m->slots[i];
+ if (!s->key) return s;
+ if (s->key->len == key->len && memcmp(s->key->data, key->data, key->len) == 0) return s;
+ i = (i + 1) & (m->cap - 1);
+ }
+}
+
+static void **gw_get(GwMap *m, const ScrStr *key) {
+ GwSlot *s = gw_slot(m, key);
+ return s && s->key ? &s->val : NULL;
+}
+
+/* Map#set: replaces the value of an existing key, keeping its position. */
+static void gw_put(GwMap *m, ScrStr *key, void *val) {
+ if ((m->n + 1) * 2 > m->cap) {
+ size_t cap = m->cap ? m->cap * 2 : 16;
+ GwSlot *old = m->slots;
+ size_t oldcap = m->cap;
+ m->slots = calloc(cap, sizeof *m->slots);
+ if (!m->slots) gw_oom();
+ m->cap = cap;
+ for (size_t i = 0; i < oldcap; i++)
+ if (old[i].key) *gw_slot(m, old[i].key) = old[i];
+ free(old);
+ }
+ GwSlot *s = gw_slot(m, key);
+ if (!s->key) {
+ s->key = key;
+ m->n++;
+ m->order = gw_grow(m->order, &m->ocap, m->on + 1, sizeof *m->order);
+ m->order[m->on++] = key;
+ }
+ s->val = val;
+}
+
+static void gw_map_clear(GwMap *m) {
+ if (m->cap) memset(m->slots, 0, m->cap * sizeof *m->slots);
+ m->n = 0;
+ m->on = 0;
+}
+
+static void gw_map_free(GwMap *m) {
+ free(m->slots);
+ free(m->order);
+}
+
+/* ── index sets and patterns ──────────────────────────────────────────── */
+
+static bool gw_idx_has(const GwIdx *s, size_t i) {
+ for (size_t k = 0; k < s->n; k++)
+ if (s->v[k] == i) return true;
+ return false;
+}
+
+static void gw_idx_add(Gw *w, GwIdx *s, size_t i) {
+ if (gw_idx_has(s, i)) return;
+ if (s->n == s->cap) {
+ size_t cap = s->cap ? s->cap * 2 : 4;
+ size_t *v = ar_alloc(&w->a, cap * sizeof *v);
+ if (s->n) memcpy(v, s->v, s->n * sizeof *v);
+ s->v = v;
+ s->cap = cap;
+ }
+ s->v[s->n++] = i;
+}
+
+static GwIdx gw_idx_one(Gw *w, size_t i) {
+ GwIdx s = { NULL, 0, 0 };
+ gw_idx_add(w, &s, i);
+ return s;
+}
+
+static GwPat *gw_pat_child(Gw *w, const GwPat *p, GwIdx indexes, GwIdx symlinks, const GwReal *realpaths) {
+ GwPat *c = ar_alloc(&w->a, sizeof *c);
+ *c = *p;
+ c->indexes = indexes;
+ c->symlinks = symlinks;
+ c->realpaths = realpaths;
+ return c;
+}
+
+/* pattern.at(i), negative indexes from the end; NULL past either end. */
+static const Pat *gw_at(const GwPat *p, long i) {
+ long n = (long)p->pat->n;
+ if (i < 0) i += n;
+ return i >= 0 && i < n ? &p->pat->v[i] : NULL;
+}
+
+static bool gw_is_globstar(const Pat *p) { return p && p->kind == PAT_GLOBSTAR; }
+static bool gw_is_string(const Pat *p) { return p && p->kind == PAT_STRING; }
+static bool gw_is_str(const Pat *p, const char *c) { return gw_is_string(p) && s_eq_c(p->str, c); }
+
+static bool gw_has_seen_symlinks(const GwPat *p) {
+ for (size_t k = 0; k < p->indexes.n; k++)
+ if (!gw_idx_has(&p->symlinks, p->indexes.v[k])) return true;
+ return false;
+}
+
+static bool gw_is_last(const GwPat *p, bool is_directory) {
+ return gw_idx_has(&p->indexes, (size_t)p->last) ||
+ (gw_is_str(gw_at(p, -1), "") && is_directory && p->last >= 1 &&
+ gw_idx_has(&p->indexes, (size_t)(p->last - 1)) && gw_is_globstar(gw_at(p, -2)));
+}
+
+/* Pattern#test(index, name). */
+static bool gw_test(Gw *w, const GwPat *p, size_t index, ScrStr *name) {
+ const Pat *seg = gw_at(p, (long)index);
+ if (!seg) return false;
+ if (seg->kind == PAT_GLOBSTAR) return true;
+ S f = s_from_str(&w->a, name);
+ if (seg->kind == PAT_STRING) return s_eq(seg->str, f);
+ return pat_test(&w->ctx, seg, f);
+}
+
+/* Pattern#cacheKey(index): the glob strings from index on, '/'-joined. */
+static ScrStr *gw_cache_key(Gw *w, const GwPat *p, size_t index) {
+ B b;
+ b_init(&b, &w->a);
+ for (size_t i = index; i < p->parts->n; i++) {
+ b_s(&b, p->parts->v[i]);
+ if (i != p->parts->n - 1) b_unit(&b, '/');
+ }
+ return gw_keep(w, s_to_str(b_done(&b)));
+}
+
+/* ── the caches ───────────────────────────────────────────────────────── */
+
+static unsigned char gw_kind_of_mode(unsigned mode) {
+ if (S_ISREG(mode)) return 1;
+ if (S_ISDIR(mode)) return 2;
+#ifdef S_ISLNK
+ if (S_ISLNK(mode)) return 3;
+#endif
+ return 0;
+}
+
+/* Cache#statSync: the cached entry, else lstat (NULL when it fails). */
+static GwStat *gw_stat(Gw *w, ScrStr *path) {
+ void **hit = gw_get(&w->stats, path);
+ if (hit && *hit) return *hit;
+ struct stat st;
+#ifdef _WIN32
+ int rc = stat(path->data, &st);
+#else
+ int rc = lstat(path->data, &st);
+#endif
+ GwStat *s = NULL;
+ if (rc == 0) {
+ s = ar_alloc(&w->a, sizeof *s);
+ s->name = gw_basename(w, path);
+ s->kind = gw_kind_of_mode((unsigned)st.st_mode);
+ }
+ gw_put(&w->stats, path, s);
+ return s;
+}
+
+/* Cache#followStatSync: stat(2) through links; NULL when it fails. */
+static bool gw_follow_is_dir(Gw *w, ScrStr *path) {
+ void **hit = gw_get(&w->follow_stats, path);
+ if (hit) return *hit != NULL;
+ struct stat st;
+ bool dir = stat(path->data, &st) == 0 && S_ISDIR(st.st_mode);
+ gw_put(&w->follow_stats, path, dir ? (void *)1 : NULL);
+ return dir;
+}
+
+static ScrStr *gw_realpath(Gw *w, ScrStr *path) {
+ void **hit = gw_get(&w->realpaths, path);
+ if (hit) return *hit;
+ ScrStr *real = scr_fs_realpath(path);
+ if (!real) scr_exc_clear();
+ else gw_keep(w, real);
+ gw_put(&w->realpaths, path, real);
+ return real;
+}
+
+typedef struct {
+ GwStat *v;
+ size_t n;
+} GwDir;
+
+/* Cache#readdirSync: the sorted listing, or [] when it cannot be read. */
+static GwDir *gw_readdir(Gw *w, ScrStr *path) {
+ void **hit = gw_get(&w->readdirs, path);
+ if (hit) return *hit;
+ GwDir *d = ar_alloc(&w->a, sizeof *d);
+ d->v = NULL;
+ d->n = 0;
+ ScrScandir *scan = scr_fs_scandir(path);
+ if (!scan) {
+ scr_exc_clear();
+ } else {
+ d->n = scr_fs_scandir_count(scan);
+ d->v = ar_alloc(&w->a, (d->n ? d->n : 1) * sizeof *d->v);
+ for (size_t i = 0; i < d->n; i++) {
+ d->v[i].name = gw_keep(w, scr_fs_scandir_name(scan, i));
+ d->v[i].kind = (unsigned char)scr_fs_scandir_type(scan, i);
+ }
+ scr_fs_scandir_free(scan);
+ }
+ gw_put(&w->readdirs, path, d);
+ return d;
+}
+
+/* Cache#add: records the pattern's suffixes at path; true when some were
+ * already recorded. */
+static bool gw_seen_add(Gw *w, ScrStr *path, const GwPat *p) {
+ void **hit = gw_get(&w->seen, path);
+ GwMap *set;
+ if (hit) {
+ set = *hit;
+ } else {
+ set = ar_alloc(&w->a, sizeof *set);
+ memset(set, 0, sizeof *set);
+ gw_put(&w->seen, path, set);
+ }
+ size_t before = set->n;
+ for (size_t k = 0; k < p->indexes.n; k++) gw_put(set, gw_cache_key(w, p, p->indexes.v[k]), (void *)1);
+ return set->n != before + p->indexes.n;
+}
+
+static bool gw_seen(Gw *w, ScrStr *path, const GwPat *p, size_t index) {
+ void **hit = gw_get(&w->seen, path);
+ return hit && gw_get(*hit, gw_cache_key(w, p, index)) != NULL;
+}
+
+/* ── exclusion and results ────────────────────────────────────────────── */
+
+static bool gw_is_excluded(Gw *w, ScrStr *value) {
+ if (!w->nexclude) return false;
+ S f = s_from_str(&w->a, value);
+ for (size_t i = 0; i < w->nexclude; i++) {
+ bool threw;
+ if (glob_test(&w->exclude[i], f, &threw)) return true;
+ if (threw) return false;
+ }
+ return false;
+}
+
+/* The exclude callback on a path or entry name; false when it threw (the
+ * exception stays pending and ends the walk). */
+static bool gw_exclude_call(Gw *w, ScrStr *value) {
+ ScrDyn *arg = scr_dyn_new_str(value);
+ ScrDyn *out = scr_dyn_call(w->exclude_fn, &arg, 1, "options.exclude");
+ scr_dyn_release(arg);
+ if (!out) return false;
+ bool yes = scr_dyn_truthy(out);
+ scr_dyn_release(out);
+ return yes;
+}
+
+/* ResultSet#add. */
+static void gw_result(Gw *w, ScrStr *value) {
+ if (gw_is_excluded(w, gw_resolve(w, w->root, value))) return;
+ if (gw_get(&w->result_set, value)) return;
+ gw_put(&w->result_set, value, (void *)1);
+ w->results = gw_grow(w->results, &w->capresults, w->nresults + 1, sizeof *w->results);
+ w->results[w->nresults++] = value;
+}
+
+/* ── the walk ─────────────────────────────────────────────────────────── */
+
+static void gw_pats_push(GwPats *l, GwPat *p) {
+ l->v = gw_grow(l->v, &l->cap, l->n + 1, sizeof *l->v);
+ l->v[l->n++] = p;
+}
+
+/* #subpatterns.set(path, [pattern]) when replace, else push. */
+static void gw_sub(Gw *w, ScrStr *path, GwPat *p, bool replace) {
+ void **hit = gw_get(&w->subpatterns, path);
+ if (hit) {
+ GwPats *l = *hit;
+ if (replace) l->n = 0;
+ gw_pats_push(l, p);
+ return;
+ }
+ GwPats *l = ar_alloc(&w->a, sizeof *l);
+ memset(l, 0, sizeof *l);
+ gw_pats_push(l, p);
+ gw_put(&w->subpatterns, path, l);
+}
+
+/* #addSubpattern. */
+static void gw_add_subpattern(Gw *w, ScrStr *path, GwPat *p) {
+ if (gw_is_excluded(w, path)) return;
+ ScrStr *fullpath = gw_resolve(w, w->root, path);
+ if (w->nexclude) {
+ ScrStr *slash = gw_keep(w, scr_str_concat(fullpath, gw_cstr(w, "/")));
+ GwStat *st = gw_is_excluded(w, slash) ? gw_stat(w, fullpath) : NULL;
+ if (st && st->kind == 2) return;
+ }
+ if (w->exclude_fn) {
+ bool skip = gw_exclude_call(w, path);
+ if (scr_exc_pending() || skip) return;
+ }
+ gw_sub(w, path, p, false);
+}
+
+static bool gw_is_directory(Gw *w, ScrStr *path, const GwStat *st, const GwPat *p) {
+ if (st && st->kind == 2) return true;
+ if (!st || st->kind != 3) return false;
+ if (w->follow) return gw_follow_is_dir(w, path);
+ return gw_has_seen_symlinks(p);
+}
+
+static const GwReal *gw_next_realpaths(Gw *w, ScrStr *path, bool is_directory, const GwPat *p) {
+ if (!w->follow || !is_directory) return p->realpaths;
+ ScrStr *real = gw_realpath(w, path);
+ if (!real) return p->realpaths;
+ GwReal *next = ar_alloc(&w->a, sizeof *next);
+ next->n = p->realpaths->n + 1;
+ next->v = ar_alloc(&w->a, next->n * sizeof *next->v);
+ if (p->realpaths->n) memcpy(next->v, p->realpaths->v, p->realpaths->n * sizeof *next->v);
+ next->v[next->n - 1] = real;
+ return next;
+}
+
+static bool gw_is_cyclic(Gw *w, ScrStr *path, bool is_directory, const GwPat *p) {
+ if (!w->follow || !is_directory) return false;
+ ScrStr *real = gw_realpath(w, path);
+ if (!real) return false;
+ for (size_t i = 0; i < p->realpaths->n; i++)
+ if (scr_str_eq(p->realpaths->v[i], real)) return true;
+ return false;
+}
+
+static ScrStr *gw_str_of(Gw *w, S s) { return gw_keep(w, s_to_str(s)); }
+
+/* #addSubpatterns. */
+static void gw_add_subpatterns(Gw *w, ScrStr *path, GwPat *p) {
+ if (gw_seen_add(w, path, p)) return;
+ ScrStr *fullpath = gw_resolve(w, w->root, path);
+ GwStat *st = gw_stat(w, fullpath);
+ long last = p->last;
+ bool is_directory = gw_is_directory(w, fullpath, st, p);
+ bool is_last = gw_is_last(p, is_directory);
+ bool is_first = gw_idx_has(&p->indexes, 0);
+ if (gw_is_excluded(w, fullpath)) return;
+#ifdef _WIN32
+ if (is_first && gw_is_string(gw_at(p, 0)) && gw_at(p, 0)->str.n &&
+ gw_at(p, 0)->str.p[gw_at(p, 0)->str.n - 1] == ':') {
+ ScrStr *drive = gw_str_of(w, gw_at(p, 0)->str);
+ gw_add_subpattern(w, gw_keep(w, scr_str_concat(drive, gw_cstr(w, "\\"))),
+ gw_pat_child(w, p, gw_idx_one(w, 1), (GwIdx){ 0 }, p->realpaths));
+ return;
+ }
+#endif
+ if (is_first && gw_is_str(gw_at(p, 0), "")) {
+ gw_add_subpattern(w, gw_cstr(w, "/"), gw_pat_child(w, p, gw_idx_one(w, 1), (GwIdx){ 0 }, p->realpaths));
+ return;
+ }
+ if (is_first && gw_is_str(gw_at(p, 0), "..")) {
+ gw_add_subpattern(w, gw_cstr(w, "../"), gw_pat_child(w, p, gw_idx_one(w, 1), (GwIdx){ 0 }, p->realpaths));
+ return;
+ }
+ if (is_first && gw_is_str(gw_at(p, 0), ".")) {
+ gw_add_subpattern(w, gw_cstr(w, "."), gw_pat_child(w, p, gw_idx_one(w, 1), (GwIdx){ 0 }, p->realpaths));
+ return;
+ }
+
+ if (is_last && gw_is_string(gw_at(p, -1))) {
+ ScrStr *tail = gw_str_of(w, gw_at(p, -1)->str);
+ GwStat *found = gw_stat(w, gw_join(w, fullpath, tail));
+ if (found && (tail->len || is_directory)) gw_result(w, gw_join(w, path, tail));
+ if (p->indexes.n == 1 && gw_idx_has(&p->indexes, (size_t)last)) return;
+ } else if (is_last && gw_is_globstar(gw_at(p, -1)) &&
+ (!gw_eq_c(path, ".") || gw_is_str(gw_at(p, 0), ".") || (last == 0 && st))) {
+ gw_result(w, path);
+ }
+ if (scr_exc_pending()) return;
+
+ if (!is_directory || gw_is_cyclic(w, fullpath, is_directory, p)) return;
+ const GwReal *next_realpaths = gw_next_realpaths(w, fullpath, is_directory, p);
+
+ GwStat one;
+ GwDir single = { &one, 1 };
+ GwDir *children;
+ const Pat *first = p->indexes.n == 1 ? gw_at(p, (long)p->indexes.v[0]) : NULL;
+ if (gw_is_string(first)) {
+ ScrStr *name = gw_str_of(w, first->str);
+ GwStat *found = gw_stat(w, gw_join(w, fullpath, name));
+ if (!found) return;
+ one.name = name;
+ one.kind = found->kind;
+ children = &single;
+ } else {
+ children = gw_readdir(w, fullpath);
+ }
+
+ for (size_t i = 0; i < children->n; i++) {
+ GwStat *entry = &children->v[i];
+ ScrStr *entry_path = gw_join(w, path, entry->name);
+ ScrStr *entry_full = gw_join(w, fullpath, entry->name);
+ GwStat *cached = ar_alloc(&w->a, sizeof *cached);
+ *cached = *entry;
+ gw_put(&w->stats, entry_full, cached);
+ bool entry_dir = entry->kind == 2 || (w->follow && entry->kind == 3 && gw_follow_is_dir(w, entry_full));
+
+ GwIdx sub = { NULL, 0, 0 };
+ GwIdx nsym = { NULL, 0, 0 };
+ for (size_t k = 0; k < p->indexes.n; k++) {
+ size_t index = p->indexes.v[k];
+ const Pat *current = gw_at(p, (long)index);
+ size_t next_index = index + 1;
+ const Pat *next = gw_at(p, (long)next_index);
+ bool from_symlink = !w->follow && gw_idx_has(&p->symlinks, index);
+
+ if (gw_is_globstar(current)) {
+ bool is_dot = entry->name->len && entry->name->data[0] == '.';
+ bool next_matches = gw_test(w, p, next_index, entry->name);
+ size_t next_non_glob = next_index;
+ while (gw_is_globstar(gw_at(p, (long)next_non_glob))) next_non_glob++;
+ bool matches_dot = is_dot && gw_test(w, p, next_non_glob, entry->name);
+ if (is_dot && !matches_dot) continue;
+ if (w->exclude_fn) {
+ bool skip = gw_exclude_call(w, entry->name);
+ if (scr_exc_pending()) return;
+ if (skip) continue;
+ }
+ if (!from_symlink && entry_dir) gw_idx_add(w, &sub, index);
+ else if (!from_symlink && (long)index == last) gw_result(w, entry_path);
+ if (next_matches && (long)next_index == last && !is_last) gw_result(w, entry_path);
+ else if (next_matches && entry_dir) gw_idx_add(w, &sub, index + 2);
+ if ((next_matches || gw_is_str(gw_at(p, 0), ".")) && entry_dir && !from_symlink) gw_idx_add(w, &sub, next_index);
+ if (!w->follow && entry->kind == 3) gw_idx_add(w, &nsym, index);
+ if (gw_is_str(next, "..") && entry_dir) {
+ ScrStr *parent = gw_join(w, path, gw_cstr(w, ".."));
+ if ((long)next_index < last) {
+ if (!gw_get(&w->subpatterns, path) && !gw_seen(w, path, p, next_index + 1))
+ gw_sub(w, path, gw_pat_child(w, p, gw_idx_one(w, next_index + 1), (GwIdx){ 0 }, p->realpaths), true);
+ if (!gw_get(&w->subpatterns, parent) && !gw_seen(w, parent, p, next_index + 1))
+ gw_sub(w, parent, gw_pat_child(w, p, gw_idx_one(w, next_index + 1), (GwIdx){ 0 }, p->realpaths), true);
+ } else {
+ if (!gw_seen(w, path, p, next_index)) {
+ gw_seen_add(w, path, gw_pat_child(w, p, gw_idx_one(w, next_index), (GwIdx){ 0 }, p->realpaths));
+ gw_result(w, path);
+ }
+ if (!gw_seen(w, path, p, next_index) || !gw_seen(w, parent, p, next_index)) {
+ gw_seen_add(w, parent, gw_pat_child(w, p, gw_idx_one(w, next_index), (GwIdx){ 0 }, p->realpaths));
+ gw_result(w, parent);
+ }
+ }
+ }
+ }
+ if (gw_is_string(current)) {
+ if (gw_test(w, p, index, entry->name) && (long)index != last) {
+ gw_idx_add(w, &sub, next_index);
+ } else if (gw_is_str(current, ".") && gw_test(w, p, next_index, entry->name)) {
+ if ((long)next_index == last) gw_result(w, entry_path);
+ else gw_idx_add(w, &sub, next_index + 1);
+ }
+ }
+ if (current && current->kind == PAT_REGEX && gw_test(w, p, index, entry->name)) {
+ if ((long)index == last) gw_result(w, entry_path);
+ else if (entry_dir) gw_idx_add(w, &sub, next_index);
+ }
+ if (scr_exc_pending()) return;
+ }
+ if (sub.n > 0) gw_add_subpattern(w, entry_path, gw_pat_child(w, p, sub, nsym, next_realpaths));
+ if (scr_exc_pending()) return;
+ }
+}
+
+static void gw_queue_push(Gw *w, ScrStr *path, GwPats pats) {
+ w->queue = gw_grow(w->queue, &w->capqueue, w->nqueue + 1, sizeof *w->queue);
+ w->queue[w->nqueue++] = (GwItem){ path, pats };
+}
+
+/* Glob#globSync's loop. */
+static void gw_run(Gw *w) {
+ GwPats all = { NULL, 0, 0 };
+ for (size_t i = 0; i < w->npatterns; i++) gw_pats_push(&all, w->patterns[i]);
+ gw_queue_push(w, gw_cstr(w, "."), all);
+ while (w->nqueue && !scr_exc_pending()) {
+ GwItem item = w->queue[--w->nqueue];
+ for (size_t i = 0; i < item.pats.n && !scr_exc_pending(); i++) gw_add_subpatterns(w, item.path, item.pats.v[i]);
+ free(item.pats.v);
+ for (size_t i = 0; i < w->subpatterns.on; i++) {
+ ScrStr *key = w->subpatterns.order[i];
+ GwPats *l = *gw_get(&w->subpatterns, key);
+ gw_queue_push(w, key, *l); /* the queue item owns the list now */
+ }
+ gw_map_clear(&w->subpatterns);
+ }
+ for (size_t i = 0; i < w->nqueue; i++) free(w->queue[i].pats.v);
+ w->nqueue = 0;
+}
+
+/* ── options, patterns and the entry points ───────────────────────────── */
+
+static bool gw_dyn_absent(const ScrDyn *d) { return !d || d->kind == SCR_DYN_UNDEF; }
+static bool gw_dyn_nullish(const ScrDyn *d) { return gw_dyn_absent(d) || d->kind == SCR_DYN_NULL; }
+
+static bool gw_dyn_is_array(const ScrDyn *d) {
+ return d->kind == SCR_DYN_ARR || (d->kind == SCR_DYN_TYPED_REF && scr_dyn_ref_is_array(d));
+}
+
+/* The value as a plain dyn array (+1), materializing typed arrays. */
+static ScrDyn *gw_dyn_array(const ScrDyn *d) {
+ if (d->kind == SCR_DYN_ARR) return scr_dyn_retain((ScrDyn *)d);
+ return scr_dyn_typed_ref_materialize(d);
+}
+
+static ScrDyn *gw_option(const ScrDyn *options, const char *key) {
+ if (gw_dyn_nullish(options)) return scr_dyn_retain(scr_dyn_undefined());
+ if (options->kind == SCR_DYN_TYPED_REF) {
+ ScrDyn *view = scr_dyn_typed_ref_materialize(options);
+ ScrDyn *value = view ? scr_dyn_obj_read(view, key, strlen(key)) : NULL;
+ scr_dyn_release(view);
+ return value;
+ }
+ return scr_dyn_obj_read(options, key, strlen(key));
+}
+
+/* validateObject(options, 'options') with Node's default flags. */
+static bool gw_options_ok(const ScrDyn *options) {
+ if (gw_dyn_absent(options)) return true;
+ bool object = options->kind == SCR_DYN_OBJ || options->kind == SCR_DYN_HANDLE ||
+ options->kind == SCR_DYN_BYTES || options->kind == SCR_DYN_PROXY ||
+ (options->kind == SCR_DYN_TYPED_REF && !scr_dyn_ref_is_array(options));
+ if (object) return true;
+ scr_dyn_arg_type_fail("options", "of type object", options);
+ return false;
+}
+
+/* The maps are keyed by pool strings, so they are freed before the pool. */
+static void gw_free(Gw *w) {
+ GwMap *maps[] = { &w->stats, &w->follow_stats, &w->readdirs, &w->realpaths, &w->seen, &w->subpatterns, &w->result_set };
+ for (size_t i = 0; i < w->seen.on; i++) {
+ GwMap *set = *gw_get(&w->seen, w->seen.order[i]);
+ gw_map_free(set);
+ }
+ for (size_t i = 0; i < sizeof maps / sizeof *maps; i++) gw_map_free(maps[i]);
+ for (size_t i = 0; i < w->pool.n; i++) scr_str_release(w->pool.v[i]);
+ free(w->pool.v);
+ free(w->results);
+ free(w->queue);
+ scr_dyn_release(w->exclude_fn);
+ ar_free(&w->a);
+}
+
+/* new Glob(pattern, options): Node's option reads (exclude, cwd,
+ * followSymlinks, withFileTypes), validations and matcher compilation, in
+ * Node's order. False with the exception pending. */
+static bool gw_begin(Gw *w, const ScrDyn *pattern, const ScrDyn *options, ScrDyn **cwd_out) {
+ if (!gw_options_ok(options)) return false;
+ ScrDyn *exclude = gw_option(options, "exclude");
+ if (!exclude) return false;
+ ScrDyn *cwd = gw_option(options, "cwd");
+ ScrDyn *follow = cwd ? gw_option(options, "followSymlinks") : NULL;
+ ScrDyn *types = follow ? gw_option(options, "withFileTypes") : NULL;
+ bool ok = types != NULL;
+ /* toPathIfFileURL(cwd) ?? '.' */
+ if (ok) {
+ if (gw_dyn_nullish(cwd)) {
+ w->root = gw_cstr(w, ".");
+ } else if (scr_dyn_native_url_is(cwd)) {
+ ScrStr *path = scr_url_to_path(cwd->v.handle.ptr);
+ ok = path != NULL;
+ if (ok) w->root = gw_keep(w, path);
+ } else if (cwd->kind == SCR_DYN_STR) {
+ w->root = gw_keep(w, scr_str_retain(cwd->v.str));
+ } else {
+ *cwd_out = scr_dyn_retain(cwd); /* resolve() rejects it when the walk starts */
+ }
+ }
+ if (ok && !gw_dyn_nullish(follow)) {
+ if (follow->kind != SCR_DYN_BOOL) {
+ scr_dyn_prop_type_fail("options.followSymlinks", "of type boolean", follow);
+ ok = false;
+ } else {
+ w->follow = follow->v.b;
+ }
+ }
+ if (ok) w->with_types = scr_dyn_truthy(types);
+ if (ok && !gw_dyn_nullish(exclude)) {
+ if (gw_dyn_is_array(exclude)) {
+ ScrDyn *list = gw_dyn_array(exclude);
+ ok = list != NULL;
+ for (size_t i = 0; ok && i < list->v.arr.len; i++) {
+ const ScrDyn *item = scr_dyn_arr_has_index(list, i) ? list->v.arr.items[i] : scr_dyn_undefined();
+ if (item->kind != SCR_DYN_STR) {
+ char name[48];
+ snprintf(name, sizeof name, "options.exclude[%zu]", i);
+ scr_dyn_prop_type_fail(name, "of type string", item);
+ ok = false;
+ }
+ }
+ if (ok && !w->root) {
+ /* The constructor's assert(typeof this.#root === 'string'). */
+ static const char message[] =
+ "This is caused by either a bug in Node.js or incorrect usage of Node.js internals.\n"
+ "Please open an issue with this stack trace at https://github.com/nodejs/node/issues\n";
+ scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "ERR_INTERNAL_ASSERTION");
+ ok = false;
+ }
+ if (ok) {
+ w->nexclude = list->v.arr.len;
+ w->exclude = ar_alloc(&w->a, (w->nexclude ? w->nexclude : 1) * sizeof *w->exclude);
+ for (size_t i = 0; ok && i < w->nexclude; i++) {
+ ScrStr *abs = gw_resolve(w, w->root, list->v.arr.items[i]->v.str);
+ ok = glob_compile(&w->a, s_from_str(&w->a, abs), GW_WINDOWS, scr_glob_host_nocase(), false, &w->exclude[i]);
+ }
+ }
+ scr_dyn_release(list);
+ } else if (exclude->kind == SCR_DYN_FUNC) {
+ w->exclude_fn = scr_dyn_retain(exclude);
+ } else {
+ scr_dyn_prop_type_fail("options.exclude", "of type function or string[]", exclude);
+ ok = false;
+ }
+ }
+ /* patterns: a string, or (typeof 'object') a string array. */
+ ScrDyn *list = NULL;
+ if (ok) {
+ if (pattern->kind == SCR_DYN_STR) {
+ list = scr_dyn_new_arr();
+ scr_dyn_arr_push(list, scr_dyn_retain((ScrDyn *)pattern));
+ } else if (pattern->kind == SCR_DYN_NULL || pattern->kind == SCR_DYN_OBJ || pattern->kind == SCR_DYN_HANDLE ||
+ pattern->kind == SCR_DYN_BYTES || pattern->kind == SCR_DYN_TYPED_REF || pattern->kind == SCR_DYN_ARR) {
+ if (!gw_dyn_is_array(pattern)) {
+ scr_dyn_arg_type_fail("patterns", "an instance of Array", pattern);
+ ok = false;
+ } else {
+ list = gw_dyn_array(pattern);
+ ok = list != NULL;
+ for (size_t i = 0; ok && i < list->v.arr.len; i++) {
+ const ScrDyn *item = scr_dyn_arr_has_index(list, i) ? list->v.arr.items[i] : scr_dyn_undefined();
+ if (item->kind != SCR_DYN_STR) {
+ char name[32];
+ snprintf(name, sizeof name, "patterns[%zu]", i);
+ scr_dyn_arg_type_fail(name, "of type string", item);
+ ok = false;
+ }
+ }
+ }
+ } else {
+ scr_dyn_arg_type_fail("patterns", "of type string", pattern);
+ ok = false;
+ }
+ }
+ w->ctx = (Ctx){ &w->a, scr_glob_host_nocase(), true };
+ for (size_t i = 0; ok && i < list->v.arr.len; i++) {
+ Matcher *m = ar_alloc(&w->a, sizeof *m);
+ ok = glob_compile(&w->a, s_from_str(&w->a, list->v.arr.items[i]->v.str), GW_WINDOWS, scr_glob_host_nocase(), true, m);
+ if (!ok || m->empty) continue;
+ GwPat **grown = ar_alloc(&w->a, (w->npatterns + m->n) * sizeof *grown);
+ if (w->npatterns) memcpy(grown, w->patterns, w->npatterns * sizeof *grown);
+ w->patterns = grown;
+ for (size_t g = 0; g < m->n; g++) {
+ GwPat *p = ar_alloc(&w->a, sizeof *p);
+ memset(p, 0, sizeof *p);
+ p->pat = &m->set[g];
+ p->parts = &m->parts[g];
+ p->indexes = gw_idx_one(w, 0);
+ GwReal *none = ar_alloc(&w->a, sizeof *none);
+ none->v = NULL;
+ none->n = 0;
+ p->realpaths = none;
+ p->last = (long)m->set[g].n - 1;
+ w->patterns[w->npatterns++] = p;
+ }
+ }
+ scr_dyn_release(list);
+ scr_dyn_release(exclude);
+ scr_dyn_release(cwd);
+ scr_dyn_release(follow);
+ scr_dyn_release(types);
+ return ok && !scr_exc_pending();
+}
+
+/* The walk and its result snapshot. mode bit 0: Dirent rows; bit 1: the
+ * fs/promises form (Dirents carry resolved parent paths). */
+static ScrFsGlob *gw_snapshot(Gw *w, bool types, bool resolved_parents) {
+ ScrFsGlob *g = calloc(1, sizeof *g);
+ if (!g) gw_oom();
+ g->n = w->nresults;
+ g->paths = malloc((g->n ? g->n : 1) * sizeof *g->paths);
+ if (!g->paths) gw_oom();
+ if (types) {
+ g->names = malloc((g->n ? g->n : 1) * sizeof *g->names);
+ g->parents = malloc((g->n ? g->n : 1) * sizeof *g->parents);
+ g->kinds = malloc(g->n ? g->n : 1);
+ if (!g->names || !g->parents || !g->kinds) gw_oom();
+ }
+ for (size_t i = 0; i < g->n; i++) {
+ ScrStr *path = w->results[i];
+ g->paths[i] = scr_str_retain(path);
+ if (!types) continue;
+ ScrStr *full = resolved_parents ? gw_resolve(w, w->root, path)
+ : gw_is_absolute(path) ? path
+ : gw_join(w, w->root, path);
+ GwStat *st = gw_stat(w, full);
+ g->names[i] = scr_str_retain(gw_basename(w, full));
+ g->parents[i] = scr_str_retain(gw_dirname(w, full));
+ g->kinds[i] = st ? st->kind : 0;
+ }
+ return g;
+}
+
+ScrFsGlob *scr_fs_glob(const ScrDyn *pattern, const ScrDyn *options, double mode) {
+ Gw w;
+ memset(&w, 0, sizeof w);
+ ScrDyn *bad_cwd = NULL;
+ bool types = ((unsigned)mode & 1) != 0;
+ ScrFsGlob *out = NULL;
+ if (gw_begin(&w, pattern, options, &bad_cwd)) {
+ if (w.with_types != types) {
+ static const char message[] = "fs.glob withFileTypes must be a literal true or false that matches the result type";
+ scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020");
+ } else if (!w.root) {
+ scr_dyn_arg_type_fail("paths[0]", "of type string", bad_cwd);
+ } else if (types && w.exclude_fn) {
+ static const char message[] = "fs.glob exclude functions with withFileTypes have no native lowering";
+ scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020");
+ } else {
+ gw_run(&w);
+ bool async_form = ((unsigned)mode & 2) != 0;
+ if (scr_exc_pending() && async_form) {
+ /* The async iterator yields the matches found before the error,
+ * then throws it (scr_fsp_glob_error hands it to the loop). */
+ scr_caught_release(scr_fs_glob_deferred);
+ scr_fs_glob_deferred = scr_exc_take();
+ }
+ if (!scr_exc_pending()) out = gw_snapshot(&w, types, async_form);
+ }
+ }
+ scr_dyn_release(bad_cwd);
+ gw_free(&w);
+ return out;
+}
+
+ScrArr *scr_fsp_glob_strings(const ScrDyn *pattern, const ScrDyn *options) {
+ ScrFsGlob *g = scr_fs_glob(pattern, options, 2);
+ if (!g) return NULL;
+ ScrArr *arr = scr_arr_new(SCR_ELEM_STR, g->n);
+ for (size_t i = 0; i < g->n; i++) scr_arr_push_ref(arr, scr_str_retain(g->paths[i]));
+ scr_fs_glob_free(g);
+ return arr;
+}
+
+ScrDyn *scr_fsp_glob_error(void) {
+ if (!scr_fs_glob_deferred) return scr_dyn_retain(scr_dyn_undefined());
+ ScrDyn *error = scr_caught_to_dyn(scr_fs_glob_deferred);
+ scr_caught_release(scr_fs_glob_deferred);
+ scr_fs_glob_deferred = NULL;
+ return error;
+}
+
+size_t scr_fs_glob_count(const ScrFsGlob *g) { return g ? g->n : 0; }
+ScrStr *scr_fs_glob_path(const ScrFsGlob *g, size_t i) { return scr_str_retain(g->paths[i]); }
+ScrStr *scr_fs_glob_name(const ScrFsGlob *g, size_t i) { return scr_str_retain(g->names[i]); }
+ScrStr *scr_fs_glob_parent(const ScrFsGlob *g, size_t i) { return scr_str_retain(g->parents[i]); }
+double scr_fs_glob_type(const ScrFsGlob *g, size_t i) { return (double)g->kinds[i]; }
+
+void scr_fs_glob_free(ScrFsGlob *g) {
+ if (!g) return;
+ for (size_t i = 0; i < g->n; i++) {
+ scr_str_release(g->paths[i]);
+ if (g->names) {
+ scr_str_release(g->names[i]);
+ scr_str_release(g->parents[i]);
+ }
+ }
+ free(g->paths);
+ free(g->names);
+ free(g->parents);
+ free(g->kinds);
+ free(g);
+}
+
+/* globSync(pattern, options) and the fs/promises string form: string[]. */
+ScrArr *scr_fs_glob_strings(const ScrDyn *pattern, const ScrDyn *options) {
+ ScrFsGlob *g = scr_fs_glob(pattern, options, 0);
+ if (!g) return NULL;
+ ScrArr *arr = scr_arr_new(SCR_ELEM_STR, g->n);
+ for (size_t i = 0; i < g->n; i++) scr_arr_push_ref(arr, scr_str_retain(g->paths[i]));
+ scr_fs_glob_free(g);
+ return arr;
+}
+
+/* fs.glob(pattern[, options], callback): Node checks the callback, then
+ * runs the constructor's validation synchronously; the walk's result or
+ * error reaches the callback later. `args` is the argument vector. */
+void scr_fs_glob_callback(ScrDyn *args) {
+ size_t argc = args->kind == SCR_DYN_ARR ? args->v.arr.len : 0;
+ ScrDyn *const *items = argc ? args->v.arr.items : NULL;
+ const ScrDyn *pattern = argc > 0 ? items[0] : scr_dyn_undefined();
+ const ScrDyn *options = argc > 1 ? items[1] : scr_dyn_undefined();
+ const ScrDyn *callback = argc > 2 ? items[2] : scr_dyn_undefined();
+ if (options->kind == SCR_DYN_FUNC) {
+ callback = options;
+ options = scr_dyn_undefined();
+ }
+ if (callback->kind != SCR_DYN_FUNC) {
+ scr_dyn_arg_type_fail("cb", "of type function", callback);
+ return;
+ }
+ Gw w;
+ memset(&w, 0, sizeof w);
+ ScrDyn *bad_cwd = NULL;
+ if (!gw_begin(&w, pattern, options, &bad_cwd)) {
+ scr_dyn_release(bad_cwd);
+ gw_free(&w);
+ return;
+ }
+ if (w.with_types) {
+ static const char message[] = "fs.glob callback Dirent results have no native lowering; use fs.globSync or fs/promises glob";
+ scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020");
+ scr_dyn_release(bad_cwd);
+ gw_free(&w);
+ return;
+ }
+ ScrDyn *result = NULL;
+ if (!w.root) {
+ scr_dyn_arg_type_fail("paths[0]", "of type string", bad_cwd);
+ } else {
+ gw_run(&w);
+ if (!scr_exc_pending()) {
+ result = scr_dyn_new_arr();
+ for (size_t i = 0; i < w.nresults; i++) scr_dyn_arr_push(result, scr_dyn_new_str(w.results[i]));
+ }
+ }
+ scr_dyn_release(bad_cwd);
+ gw_free(&w);
+ scr_fs_cb_schedule(callback, result, NULL);
+}
diff --git a/packages/runtime/src/scr_inspect.h b/packages/runtime/src/scr_inspect.h
index 16c295b65..e153c18c8 100644
--- a/packages/runtime/src/scr_inspect.h
+++ b/packages/runtime/src/scr_inspect.h
@@ -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);
diff --git a/packages/runtime/src/scr_path.h b/packages/runtime/src/scr_path.h
index 7bf45e55c..9600b5e95 100644
--- a/packages/runtime/src/scr_path.h
+++ b/packages/runtime/src/scr_path.h
@@ -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 */
diff --git a/packages/runtime/test/test_glob.c b/packages/runtime/test/test_glob.c
index 8c9fa62b0..720cb374f 100644
--- a/packages/runtime/test/test_glob.c
+++ b/packages/runtime/test/test_glob.c
@@ -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;
diff --git a/tests/corpus/5448-fs-glob-patterns.ts b/tests/corpus/5448-fs-glob-patterns.ts
new file mode 100644
index 000000000..2a3179341
--- /dev/null
+++ b/tests/corpus/5448-fs-glob-patterns.ts
@@ -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 });
diff --git a/tests/corpus/5449-fs-glob-options.ts b/tests/corpus/5449-fs-glob-options.ts
new file mode 100644
index 000000000..e5e51262c
--- /dev/null
+++ b/tests/corpus/5449-fs-glob-options.ts
@@ -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) ? `${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 });
diff --git a/tests/corpus/5450-fs-glob-validation.js b/tests/corpus/5450-fs-glob-validation.js
new file mode 100644
index 000000000..9914b0c1b
--- /dev/null
+++ b/tests/corpus/5450-fs-glob-validation.js
@@ -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 });
+}
diff --git a/tests/corpus/5451-fs-promises-glob.ts b/tests/corpus/5451-fs-promises-glob.ts
new file mode 100644
index 000000000..5eb2c435e
--- /dev/null
+++ b/tests/corpus/5451-fs-promises-glob.ts
@@ -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 {
+ 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();
diff --git a/tests/corpus/5452-fs-glob-callback.ts b/tests/corpus/5452-fs-glob-callback.ts
new file mode 100644
index 000000000..438505783
--- /dev/null
+++ b/tests/corpus/5452-fs-glob-callback.ts
@@ -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()?.();
diff --git a/tests/corpus/5455-power-of-two-remainder.ts b/tests/corpus/5455-power-of-two-remainder.ts
new file mode 100644
index 000000000..3d8969f37
--- /dev/null
+++ b/tests/corpus/5455-power-of-two-remainder.ts
@@ -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);
diff --git a/tests/corpus/5456-remainder-large-integer-dividends.ts b/tests/corpus/5456-remainder-large-integer-dividends.ts
new file mode 100644
index 000000000..c2588b73c
--- /dev/null
+++ b/tests/corpus/5456-remainder-large-integer-dividends.ts
@@ -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);
+ }
+}
diff --git a/tests/corpus/5457-lcg-modulus.ts b/tests/corpus/5457-lcg-modulus.ts
new file mode 100644
index 000000000..a475642dd
--- /dev/null
+++ b/tests/corpus/5457-lcg-modulus.ts
@@ -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();
+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(","));
diff --git a/tests/corpus/5458-fractional-remainder.ts b/tests/corpus/5458-fractional-remainder.ts
new file mode 100644
index 000000000..f83b2697c
--- /dev/null
+++ b/tests/corpus/5458-fractional-remainder.ts
@@ -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);
+}
diff --git a/tests/corpus/5585-record-results-inlined.ts b/tests/corpus/5585-record-results-inlined.ts
new file mode 100644
index 000000000..47b81ce14
--- /dev/null
+++ b/tests/corpus/5585-record-results-inlined.ts
@@ -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);
diff --git a/tests/fixtures/node-types/fs-glob.ts b/tests/fixtures/node-types/fs-glob.ts
new file mode 100644
index 000000000..2467d73f1
--- /dev/null
+++ b/tests/fixtures/node-types/fs-glob.ts
@@ -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 {
+ 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();
diff --git a/tests/harness/project-config.test.ts b/tests/harness/project-config.test.ts
index 53883e2dc..52b6104a3 100644
--- a/tests/harness/project-config.test.ts
+++ b/tests/harness/project-config.test.ts
@@ -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");