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 628381aaa..a6bca8dcc 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": "821b258757b8e2c146c4",
+ "artifactVersion": "797302915a7200576d4e",
"rowCount": 3880
}
diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json
index 4c2bba0de..aaf46a51d 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": 101,
+ "partial": 131,
"refused": 0,
- "not-implemented": 172,
+ "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"
}
},
{
@@ -33554,9 +33554,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"
}
},
{
@@ -33577,9 +33577,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"
}
},
{
@@ -33600,9 +33600,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"
}
},
{
@@ -33623,9 +33623,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"
}
},
{
@@ -33646,9 +33646,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"
}
},
{
@@ -33669,9 +33669,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"
}
},
{
@@ -33692,9 +33692,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"
}
},
{
@@ -33715,9 +33715,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"
}
},
{
@@ -33784,9 +33784,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"
}
},
{
@@ -33807,9 +33807,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"
}
},
{
@@ -33830,9 +33830,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"
}
},
{
@@ -33853,9 +33853,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"
}
},
{
@@ -33876,9 +33876,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"
}
},
{
@@ -33899,9 +33899,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"
}
},
{
@@ -33922,9 +33922,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"
}
},
{
@@ -33945,9 +33945,9 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "partial",
+ "detail": "Filesystem-capacity fields preserve unsigned platform values as numbers or BigInts.",
+ "verification": "test-backed"
}
},
{
@@ -33968,9 +33968,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"
}
},
{
@@ -33991,9 +33991,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"
}
},
{
@@ -34037,9 +34037,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"
}
},
{
@@ -34060,9 +34060,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"
}
},
{
@@ -34083,9 +34083,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"
}
},
{
@@ -34106,9 +34106,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"
}
},
{
@@ -34129,9 +34129,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"
}
},
{
@@ -34152,9 +34152,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"
}
},
{
@@ -34175,9 +34175,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"
}
},
{
@@ -34198,9 +34198,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"
}
},
{
@@ -34221,9 +34221,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 e1f5c97c4..b76824134 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": 35,
- "verify-gap": 2457,
- "audit-partial": 893,
+ "verify-gap": 2427,
+ "audit-partial": 923,
"classify": 2,
"implement": 85
}
@@ -26182,12 +26182,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"
+ ]
}
}
},
@@ -27362,12 +27369,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"
+ ]
}
}
},
@@ -28783,12 +28797,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"
+ ]
}
}
},
@@ -30683,12 +30704,14 @@
"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.Stats",
+ "tests": [
+ "tests/corpus/5541-fs-stats-fields.cjs"
+ ]
}
}
},
@@ -30708,12 +30731,14 @@
"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.stats.isBlockDevice",
+ "tests": [
+ "tests/corpus/5541-fs-stats-fields.cjs"
+ ]
}
}
},
@@ -30733,12 +30758,14 @@
"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.stats.isCharacterDevice",
+ "tests": [
+ "tests/corpus/5541-fs-stats-fields.cjs"
+ ]
}
}
},
@@ -30758,12 +30785,14 @@
"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.stats.isDirectory",
+ "tests": [
+ "tests/corpus/5541-fs-stats-fields.cjs"
+ ]
}
}
},
@@ -30783,12 +30812,14 @@
"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.stats.isFIFO",
+ "tests": [
+ "tests/corpus/5541-fs-stats-fields.cjs"
+ ]
}
}
},
@@ -30808,12 +30839,14 @@
"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.stats.isFile",
+ "tests": [
+ "tests/corpus/5541-fs-stats-fields.cjs"
+ ]
}
}
},
@@ -30833,12 +30866,14 @@
"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.stats.isSocket",
+ "tests": [
+ "tests/corpus/5541-fs-stats-fields.cjs"
+ ]
}
}
},
@@ -30858,12 +30893,14 @@
"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.stats.isSymbolicLink",
+ "tests": [
+ "tests/corpus/5541-fs-stats-fields.cjs"
+ ]
}
}
},
@@ -30937,12 +30974,14 @@
"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.mode",
+ "tests": [
+ "tests/corpus/5541-fs-stats-fields.cjs"
+ ]
}
}
},
@@ -30962,12 +31001,14 @@
"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.nlink",
+ "tests": [
+ "tests/corpus/5541-fs-stats-fields.cjs"
+ ]
}
}
},
@@ -30987,12 +31028,14 @@
"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.uid",
+ "tests": [
+ "tests/corpus/5541-fs-stats-fields.cjs"
+ ]
}
}
},
@@ -31012,12 +31055,14 @@
"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.gid",
+ "tests": [
+ "tests/corpus/5541-fs-stats-fields.cjs"
+ ]
}
}
},
@@ -31037,12 +31082,14 @@
"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.rdev",
+ "tests": [
+ "tests/corpus/5541-fs-stats-fields.cjs"
+ ]
}
}
},
@@ -31062,12 +31109,14 @@
"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.size",
+ "tests": [
+ "tests/corpus/5541-fs-stats-fields.cjs"
+ ]
}
}
},
@@ -31087,12 +31136,14 @@
"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.blksize",
+ "tests": [
+ "tests/corpus/5541-fs-stats-fields.cjs"
+ ]
}
}
},
@@ -31112,12 +31163,14 @@
"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.blocks",
+ "tests": [
+ "tests/corpus/5541-fs-stats-fields.cjs"
+ ]
}
}
},
@@ -31137,12 +31190,14 @@
"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.atimeMs",
+ "tests": [
+ "tests/corpus/5541-fs-stats-fields.cjs"
+ ]
}
}
},
@@ -31162,12 +31217,14 @@
"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.mtimeMs",
+ "tests": [
+ "tests/corpus/5541-fs-stats-fields.cjs"
+ ]
}
}
},
@@ -31214,12 +31271,14 @@
"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.birthtimeMs",
+ "tests": [
+ "tests/corpus/5541-fs-stats-fields.cjs"
+ ]
}
}
},
@@ -31239,12 +31298,14 @@
"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.atimeNs",
+ "tests": [
+ "tests/corpus/5541-fs-stats-fields.cjs"
+ ]
}
}
},
@@ -31264,12 +31325,14 @@
"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.mtimeNs",
+ "tests": [
+ "tests/corpus/5541-fs-stats-fields.cjs"
+ ]
}
}
},
@@ -31289,12 +31352,14 @@
"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.ctimeNs",
+ "tests": [
+ "tests/corpus/5541-fs-stats-fields.cjs"
+ ]
}
}
},
@@ -31314,12 +31379,14 @@
"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.birthtimeNs",
+ "tests": [
+ "tests/corpus/5541-fs-stats-fields.cjs"
+ ]
}
}
},
@@ -31339,12 +31406,14 @@
"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.atime",
+ "tests": [
+ "tests/corpus/5541-fs-stats-fields.cjs"
+ ]
}
}
},
@@ -31364,12 +31433,14 @@
"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.mtime",
+ "tests": [
+ "tests/corpus/5541-fs-stats-fields.cjs"
+ ]
}
}
},
@@ -31389,12 +31460,14 @@
"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.ctime",
+ "tests": [
+ "tests/corpus/5541-fs-stats-fields.cjs"
+ ]
}
}
},
@@ -31414,12 +31487,14 @@
"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.birthtime",
+ "tests": [
+ "tests/corpus/5541-fs-stats-fields.cjs"
+ ]
}
}
},
diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json
index c6e0d6831..ea2c34fed 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": 101,
+ "partial": 131,
"refused": 0,
- "not-implemented": 172,
+ "not-implemented": 142,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -43939,7 +43939,13 @@
"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",
"tests/corpus/fs-read-encodings.ts",
"tests/corpus/node24-fs-links-callbacks.cjs",
@@ -43957,6 +43963,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"
@@ -44984,8 +44991,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"
},
@@ -46143,8 +46158,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"
},
@@ -47645,8 +47668,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"
},
@@ -49453,8 +49484,11 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:fs"
+ "status": "partial",
+ "evidence": "compiler-feature:fs.fs.Stats",
+ "tests": [
+ "tests/corpus/5541-fs-stats-fields.cjs"
+ ]
},
"anchorSource": "exact"
},
@@ -49499,8 +49533,11 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:fs"
+ "status": "partial",
+ "evidence": "compiler-feature:fs.stats.isBlockDevice",
+ "tests": [
+ "tests/corpus/5541-fs-stats-fields.cjs"
+ ]
},
"anchorSource": "exact"
},
@@ -49522,8 +49559,11 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:fs"
+ "status": "partial",
+ "evidence": "compiler-feature:fs.stats.isCharacterDevice",
+ "tests": [
+ "tests/corpus/5541-fs-stats-fields.cjs"
+ ]
},
"anchorSource": "exact"
},
@@ -49545,8 +49585,11 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:fs"
+ "status": "partial",
+ "evidence": "compiler-feature:fs.stats.isDirectory",
+ "tests": [
+ "tests/corpus/5541-fs-stats-fields.cjs"
+ ]
},
"anchorSource": "exact"
},
@@ -49568,8 +49611,11 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:fs"
+ "status": "partial",
+ "evidence": "compiler-feature:fs.stats.isFIFO",
+ "tests": [
+ "tests/corpus/5541-fs-stats-fields.cjs"
+ ]
},
"anchorSource": "exact"
},
@@ -49591,8 +49637,11 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:fs"
+ "status": "partial",
+ "evidence": "compiler-feature:fs.stats.isFile",
+ "tests": [
+ "tests/corpus/5541-fs-stats-fields.cjs"
+ ]
},
"anchorSource": "exact"
},
@@ -49614,8 +49663,11 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:fs"
+ "status": "partial",
+ "evidence": "compiler-feature:fs.stats.isSocket",
+ "tests": [
+ "tests/corpus/5541-fs-stats-fields.cjs"
+ ]
},
"anchorSource": "exact"
},
@@ -49637,8 +49689,11 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:fs"
+ "status": "partial",
+ "evidence": "compiler-feature:fs.stats.isSymbolicLink",
+ "tests": [
+ "tests/corpus/5541-fs-stats-fields.cjs"
+ ]
},
"anchorSource": "exact"
},
@@ -49712,8 +49767,11 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:fs"
+ "status": "partial",
+ "evidence": "compiler-feature:fs.mode",
+ "tests": [
+ "tests/corpus/5541-fs-stats-fields.cjs"
+ ]
},
"anchorSource": "ancestor"
},
@@ -49735,8 +49793,11 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:fs"
+ "status": "partial",
+ "evidence": "compiler-feature:fs.nlink",
+ "tests": [
+ "tests/corpus/5541-fs-stats-fields.cjs"
+ ]
},
"anchorSource": "ancestor"
},
@@ -49758,8 +49819,11 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:fs"
+ "status": "partial",
+ "evidence": "compiler-feature:fs.uid",
+ "tests": [
+ "tests/corpus/5541-fs-stats-fields.cjs"
+ ]
},
"anchorSource": "ancestor"
},
@@ -49781,8 +49845,11 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:fs"
+ "status": "partial",
+ "evidence": "compiler-feature:fs.gid",
+ "tests": [
+ "tests/corpus/5541-fs-stats-fields.cjs"
+ ]
},
"anchorSource": "ancestor"
},
@@ -49804,8 +49871,11 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:fs"
+ "status": "partial",
+ "evidence": "compiler-feature:fs.rdev",
+ "tests": [
+ "tests/corpus/5541-fs-stats-fields.cjs"
+ ]
},
"anchorSource": "ancestor"
},
@@ -49827,8 +49897,11 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:fs"
+ "status": "partial",
+ "evidence": "compiler-feature:fs.size",
+ "tests": [
+ "tests/corpus/5541-fs-stats-fields.cjs"
+ ]
},
"anchorSource": "ancestor"
},
@@ -49850,8 +49923,11 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:fs"
+ "status": "partial",
+ "evidence": "compiler-feature:fs.blksize",
+ "tests": [
+ "tests/corpus/5541-fs-stats-fields.cjs"
+ ]
},
"anchorSource": "ancestor"
},
@@ -49873,8 +49949,11 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:fs"
+ "status": "partial",
+ "evidence": "compiler-feature:fs.blocks",
+ "tests": [
+ "tests/corpus/5541-fs-stats-fields.cjs"
+ ]
},
"anchorSource": "ancestor"
},
@@ -49896,8 +49975,11 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:fs"
+ "status": "partial",
+ "evidence": "compiler-feature:fs.atimeMs",
+ "tests": [
+ "tests/corpus/5541-fs-stats-fields.cjs"
+ ]
},
"anchorSource": "ancestor"
},
@@ -49919,8 +50001,11 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:fs"
+ "status": "partial",
+ "evidence": "compiler-feature:fs.mtimeMs",
+ "tests": [
+ "tests/corpus/5541-fs-stats-fields.cjs"
+ ]
},
"anchorSource": "ancestor"
},
@@ -49968,8 +50053,11 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:fs"
+ "status": "partial",
+ "evidence": "compiler-feature:fs.birthtimeMs",
+ "tests": [
+ "tests/corpus/5541-fs-stats-fields.cjs"
+ ]
},
"anchorSource": "ancestor"
},
@@ -49991,8 +50079,11 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:fs"
+ "status": "partial",
+ "evidence": "compiler-feature:fs.atimeNs",
+ "tests": [
+ "tests/corpus/5541-fs-stats-fields.cjs"
+ ]
},
"anchorSource": "ancestor"
},
@@ -50014,8 +50105,11 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:fs"
+ "status": "partial",
+ "evidence": "compiler-feature:fs.mtimeNs",
+ "tests": [
+ "tests/corpus/5541-fs-stats-fields.cjs"
+ ]
},
"anchorSource": "ancestor"
},
@@ -50037,8 +50131,11 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:fs"
+ "status": "partial",
+ "evidence": "compiler-feature:fs.ctimeNs",
+ "tests": [
+ "tests/corpus/5541-fs-stats-fields.cjs"
+ ]
},
"anchorSource": "ancestor"
},
@@ -50060,8 +50157,11 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:fs"
+ "status": "partial",
+ "evidence": "compiler-feature:fs.birthtimeNs",
+ "tests": [
+ "tests/corpus/5541-fs-stats-fields.cjs"
+ ]
},
"anchorSource": "ancestor"
},
@@ -50083,8 +50183,11 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:fs"
+ "status": "partial",
+ "evidence": "compiler-feature:fs.atime",
+ "tests": [
+ "tests/corpus/5541-fs-stats-fields.cjs"
+ ]
},
"anchorSource": "ancestor"
},
@@ -50106,8 +50209,11 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:fs"
+ "status": "partial",
+ "evidence": "compiler-feature:fs.mtime",
+ "tests": [
+ "tests/corpus/5541-fs-stats-fields.cjs"
+ ]
},
"anchorSource": "ancestor"
},
@@ -50129,8 +50235,11 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:fs"
+ "status": "partial",
+ "evidence": "compiler-feature:fs.ctime",
+ "tests": [
+ "tests/corpus/5541-fs-stats-fields.cjs"
+ ]
},
"anchorSource": "ancestor"
},
@@ -50152,8 +50261,11 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:fs"
+ "status": "partial",
+ "evidence": "compiler-feature:fs.birthtime",
+ "tests": [
+ "tests/corpus/5541-fs-stats-fields.cjs"
+ ]
},
"anchorSource": "ancestor"
},
diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json
index f8f104566..eb5282e61 100644
--- a/internal/compatibility/static-support.json
+++ b/internal/compatibility/static-support.json
@@ -282,7 +282,10 @@
{ "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"] },
{ "chapter": "fs", "signatures": ["`dev` Type: {number|bigint}", "`ino` Type: {number|bigint}", "`ctimeMs` Type: {number|bigint}"], "status": "partial", "evidence": ["tests/corpus/2941-fsp-lstat-identity.ts"] },
{ "chapter": "fs", "signatures": ["`filehandle[Symbol.asyncDispose]()`"], "status": "partial", "evidence": ["tests/corpus/2844-node-disposable-resources.ts"] },
{ "chapter": "fs", "signatures": ["`fsPromises.realpath(path[, options])`"], "status": "partial", "evidence": ["tests/corpus/2940-fsp-realpath.ts"] },
diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts
index eb8e70601..de98b6939 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: {
@@ -1350,14 +1380,28 @@ declare module "node:fs" {
isFile(): boolean;
isDirectory(): boolean;
isSymbolicLink(): boolean;
+ isBlockDevice(): boolean;
+ isCharacterDevice(): boolean;
+ isFIFO(): boolean;
+ isSocket(): boolean;
readonly dev: number;
+ readonly mode: number;
+ readonly nlink: number;
+ readonly uid: number;
+ readonly gid: number;
+ readonly rdev: number;
+ readonly blksize: number;
readonly ino: number;
readonly size: number;
readonly blocks: number;
- readonly nlink: number;
readonly atimeMs: number;
readonly mtimeMs: number;
readonly ctimeMs: number;
+ readonly birthtimeMs: number;
+ readonly atime: Date;
+ readonly mtime: Date;
+ readonly ctime: Date;
+ readonly birthtime: Date;
}
export function statSync(path: string): Stats;
export function lstatSync(path: string): Stats;
@@ -1672,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(
@@ -3413,6 +3466,9 @@ declare module "node:zlib" {
* 'error'. */
declare module "net" {
export interface Socket {
+ /* Whether the socket keeps the event loop alive (ref'd by default). */
+ ref(): void;
+ unref(): void;
readonly remoteAddress: string | undefined;
/* true once the fd is gone (destroy() or full close) — Node's flag. */
readonly destroyed: boolean;
@@ -3477,6 +3533,9 @@ declare module "net" {
once(event: "session", listener: (session: Buffer) => void): void;
}
export interface Server {
+ /* Whether the server keeps the event loop alive (ref'd by default). */
+ ref(): void;
+ unref(): void;
readonly listening: boolean;
/* Node answers the server itself (`return this` chaining). */
listen(port: number, callback?: () => void): Server;
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 b5b8b5596..aca34657d 100644
--- a/packages/compiler/src/backend/llvm/emitter.ts
+++ b/packages/compiler/src/backend/llvm/emitter.ts
@@ -1,4 +1,5 @@
import { type ArraySearchKind, emitArraySearchHelpers } from "./array-search.js";
+import { emitStringInputs } from "./string-lifetimes.js";
import { emitLlvmLayouts } from "./layouts.js";
import { compileRegexMatcher, type RegexMatcherCode } from "./regex-matchers.js";
import { CYC_BUF, CYC_HDR, FIRST_EMITTED_CYC_KIND, inlineAllocSupported } from "./alloc.js";
@@ -77,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";
@@ -6012,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}`;
@@ -6836,10 +6838,9 @@ export class LlEmitter {
storeLocalUnion(this, localStorage, s.value);
break;
}
- const concat = s.value;
- const suffix = matchStringSelfConcat(s.localId, concat);
- if (suffix && concat.kind === "strConcat") {
- this.emitStringSelfConcatAssign(s.localId, concat.left, suffix, false);
+ const self = matchStringSelfConcat(s.localId, s.value);
+ if (self) {
+ this.emitStringSelfConcatAssign(s.localId, self.left, self.suffix, false);
break;
}
const b = this.binding(s.localId);
@@ -8226,7 +8227,11 @@ export class LlEmitter {
e.kind === "seqExpr" &&
isRefCounted(e.result.type) &&
e.result.kind !== "strLit" &&
- this.discardableRead(e.result)
+ (this.discardableRead(e.result) ||
+ // A function-local binding's read has no effect of its own (a
+ // compound assignment's result, say); a module binding's read may
+ // throw before initialization, so it stays.
+ (e.result.kind === "varRef" && this.binding(e.result.localId).kind === "local"))
) {
this.emitSequence(() => {
for (const s of e.stmts) this.emitStmt(s);
@@ -8287,8 +8292,11 @@ export class LlEmitter {
// of first building the suffix as a separate string.
const parts = stringParts(suffix);
const mixed = parts.length > 1 || numberPart(parts[0]!) !== null;
+ const charCode = mixed ? null : this.emitSingleCharCode(suffix);
const inputs = mixed ? emitConcatInputs(this, parts) : [];
- const right = mixed ? null : this.emitExpr(suffix);
+ // The concat only reads the suffix; relinquishing the binding below
+ // runs no code that could rebind a borrowed local.
+ const right = mixed || charCode !== null ? null : emitStringInputs(this, [suffix])[0]!;
const b = this.binding(localId);
const B = this.B;
if (b.kind === "boxed") {
@@ -8301,7 +8309,15 @@ export class LlEmitter {
this.releaseValue(old, b.type);
}
let raw: string;
- if (right === null) {
+ if (charCode !== null) {
+ // The snapshot moves into the consuming character append.
+ this.moveTemp(snapshot);
+ this.declare(`declare ptr @scr_str_concat_move_char_code(ptr, double)`);
+ raw = B.tmp();
+ B.line(
+ `${raw} = call ptr @scr_str_concat_move_char_code(ptr ${snapshot.name}, double ${charCode.name})`,
+ );
+ } else if (right === null) {
raw = emitMixedConcat(this, snapshot.name, inputs);
} else {
this.declare(`declare ptr @scr_str_concat(ptr, ptr)`);
@@ -8321,6 +8337,17 @@ export class LlEmitter {
return result;
}
+ /** The code of a one-argument `String.fromCharCode(c)` suffix, emitted, so
+ * `s += String.fromCharCode(c)` appends the character without building
+ * the one-character string; null for any other suffix. */
+ private emitSingleCharCode(suffix: IrExpr): LlValue | null {
+ if (suffix.kind !== "libCall" || suffix.fn !== "string.fromCharCode") return null;
+ const packed = suffix.args[0];
+ if (packed?.kind !== "arrayLit" || packed.elems.length !== 1 || packed.spreads?.length)
+ return null;
+ return this.emitExpr(packed.elems[0]!);
+ }
+
/** `local = local + suffix` when the suffix cannot read or write the
* local (an unboxed binding is only reachable through this function's own
* expressions): reading the binding after the suffix gives the value the
@@ -8347,14 +8374,22 @@ export class LlEmitter {
return null;
const parts = stringParts(suffix);
const mixed = parts.length > 1 || numberPart(parts[0]!) !== null;
+ const charCode = mixed ? null : this.emitSingleCharCode(suffix);
const inputs = mixed ? emitConcatInputs(this, parts) : [];
- const right = mixed ? null : this.emitExpr(suffix);
+ // The concat only reads the suffix, which may stay borrowed.
+ const right = mixed || charCode !== null ? null : emitStringInputs(this, [suffix])[0]!;
this.materializeSplitLocal(localId);
const B = this.B;
const head = B.tmp();
B.line(`${head} = load ptr, ptr ${b.slot}`);
let raw: string;
- if (right === null) {
+ if (charCode !== null) {
+ this.declare(`declare ptr @scr_str_concat_move_char_code(ptr, double)`);
+ raw = B.tmp();
+ B.line(
+ `${raw} = call ptr @scr_str_concat_move_char_code(ptr ${head}, double ${charCode.name})`,
+ );
+ } else if (right === null) {
raw = emitMixedConcat(this, head, inputs, true);
} else {
this.declare(`declare ptr @scr_str_concat_move(ptr, ptr)`);
diff --git a/packages/compiler/src/backend/llvm/expr-primitives.ts b/packages/compiler/src/backend/llvm/expr-primitives.ts
index 57a5e1bcf..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) {
@@ -443,11 +455,8 @@ export function emitOperatorExpr(
// `x = e` in expression position: the binding takes its OWN
// reference (retain for ref kinds), the temp stays the yielded
// value — release the old value before storing the retained value.
- const concat = e.value;
- const suffix = matchStringSelfConcat(e.localId, concat);
- if (suffix && concat.kind === "strConcat") {
- return host.emitStringSelfConcatAssign(e.localId, concat.left, suffix, true);
- }
+ const self = matchStringSelfConcat(e.localId, e.value);
+ if (self) return host.emitStringSelfConcatAssign(e.localId, self.left, self.suffix, true);
const b = host.binding(e.localId);
const v = host.emitExpr(e.value);
if (host.storeInt32Local(e.localId, v, e.value)) return v;
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 3683a3124..ce5c0cfc0 100644
--- a/packages/compiler/src/backend/llvm/lib-filesystem.ts
+++ b/packages/compiler/src/backend/llvm/lib-filesystem.ts
@@ -381,6 +381,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 7c8693f8a..1e8cf775f 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",
@@ -451,6 +455,20 @@ export const LIB_FN_SYMS: Record = {
"stats.atimeMs": "scr_stats_atime_ms",
"stats.mtimeMs": "scr_stats_mtime_ms",
"stats.ctimeMs": "scr_stats_ctime_ms",
+ "stats.birthtimeMs": "scr_stats_birthtime_ms",
+ "stats.mode": "scr_stats_mode",
+ "stats.uid": "scr_stats_uid",
+ "stats.gid": "scr_stats_gid",
+ "stats.rdev": "scr_stats_rdev",
+ "stats.blksize": "scr_stats_blksize",
+ "stats.isBlockDevice": "scr_stats_is_block_device",
+ "stats.isCharacterDevice": "scr_stats_is_character_device",
+ "stats.isFIFO": "scr_stats_is_fifo",
+ "stats.isSocket": "scr_stats_is_socket",
+ "stats.atime": "scr_stats_atime",
+ "stats.mtime": "scr_stats_mtime",
+ "stats.ctime": "scr_stats_ctime",
+ "stats.birthtime": "scr_stats_birthtime",
"text.decode": "scr_text_decode",
"bytes.bufferSource": "scr_bytes_buffer_source",
"text.decodeBufferSource": "scr_text_decode_buffer_source",
@@ -924,6 +942,8 @@ export const LIB_FN_SYMS: Record = {
// the client request forms have non-generic shapes — special-cased.
"net.serverPort": "scr_net_server_port",
"net.serverListening": "scr_net_server_listening",
+ "net.sockSetRef": "scr_net_sock_set_ref",
+ "net.serverSetRef": "scr_net_server_set_ref",
"net.sockWrite": "scr_net_sock_write_str",
"net.sockWriteBytes": "scr_net_sock_write_bytes",
"net.sockEnd": "scr_net_sock_end",
@@ -1321,6 +1341,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/filesystem.ts b/packages/compiler/src/frontend/lowering/builtins/filesystem.ts
index 92e46caeb..06848e481 100644
--- a/packages/compiler/src/frontend/lowering/builtins/filesystem.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/filesystem.ts
@@ -904,12 +904,21 @@ export function lowerFileHandleMethodCall(
);
}
-/** Method calls on Stats-typed receivers: isFile()/isDirectory()/
- * isSymbolicLink() are pure reads on the stat snapshot (a followed
- * statSync snapshot never answers true to isSymbolicLink — take
- * lstatSync's, Node's own split). Everything else @types/node declares
- * (mtime, mode, ...) fences member-qualified. Null for non-Stats
- * receivers. */
+/** The Stats type tests, each a read of the snapshot's mode. */
+export const STATS_TYPE_TESTS: Record = {
+ isFile: "stats.isFile",
+ isDirectory: "stats.isDirectory",
+ isSymbolicLink: "stats.isSymbolicLink",
+ isBlockDevice: "stats.isBlockDevice",
+ isCharacterDevice: "stats.isCharacterDevice",
+ isFIFO: "stats.isFIFO",
+ isSocket: "stats.isSocket",
+};
+
+/** Method calls on Stats-typed receivers: the type tests are pure reads on
+ * the stat snapshot (a followed statSync snapshot never answers true to
+ * isSymbolicLink — take lstatSync's, Node's own split). Anything else
+ * fences member-qualified. Null for non-Stats receivers. */
export function lowerStatsMethodCall(
lowerer: Lowerer,
call: ts.CallExpression,
@@ -919,23 +928,15 @@ export function lowerStatsMethodCall(
if (lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind !== "stats") return null;
if (!lowerer.isStdlibMember(access)) return null;
const name = access.name.text;
- if (
- (name === "isFile" || name === "isDirectory" || name === "isSymbolicLink") &&
- call.arguments.length === 0
- ) {
+ const fn = Object.hasOwn(STATS_TYPE_TESTS, name) ? STATS_TYPE_TESTS[name] : undefined;
+ if (fn && call.arguments.length === 0) {
const receiver = lowerer.lowerExpr(access.expression);
- const fn =
- name === "isFile"
- ? "stats.isFile"
- : name === "isDirectory"
- ? "stats.isDirectory"
- : "stats.isSymbolicLink";
return { kind: "libCall", fn, args: [receiver], type: BOOL, loc: locOf(call) };
}
lowerer.noLowering(
`Stats.${name}`,
call,
- "isFile(), isDirectory(), isSymbolicLink(), dev, ino, size, blocks, nlink, atimeMs, mtimeMs, and ctimeMs are the supported Stats members",
+ "isFile(), isDirectory(), isSymbolicLink(), isBlockDevice(), isCharacterDevice(), isFIFO(), isSocket(), dev, mode, nlink, uid, gid, rdev, blksize, ino, size, blocks, atimeMs, mtimeMs, ctimeMs, birthtimeMs, atime, mtime, ctime, and birthtime are the supported Stats members",
lowerer.checker.getSymbolAtLocation(access.name),
);
}
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/builtins/properties.ts b/packages/compiler/src/frontend/lowering/builtins/properties.ts
index b7f6ccf8f..93e7e1e02 100644
--- a/packages/compiler/src/frontend/lowering/builtins/properties.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/properties.ts
@@ -8,9 +8,11 @@ import {
BOOL,
CHILDSTREAM_T,
CHILDWRITER_T,
+ DATE_T,
F64,
FILEHANDLE_T,
type IrExpr,
+ type IrLibFn,
type IrType,
STRING,
} from "../../../ir/ir.js";
@@ -23,6 +25,28 @@ import { storedTextCodecClassOf } from "./text-codecs.js";
* — null for a normal exit or spawn failure; a timeout kill reports its
* killSignal, Node's shape). Null for everything else, so the ordinary
* chain (and its fences) keeps going. */
+
+/** Stats data members: each reads the snapshot (scr_stats_*). */
+const STATS_FIELDS: Record = {
+ dev: "stats.dev",
+ mode: "stats.mode",
+ nlink: "stats.nlink",
+ uid: "stats.uid",
+ gid: "stats.gid",
+ rdev: "stats.rdev",
+ blksize: "stats.blksize",
+ ino: "stats.ino",
+ blocks: "stats.blocks",
+ atimeMs: "stats.atimeMs",
+ mtimeMs: "stats.mtimeMs",
+ ctimeMs: "stats.ctimeMs",
+ birthtimeMs: "stats.birthtimeMs",
+ atime: "stats.atime",
+ mtime: "stats.mtime",
+ ctime: "stats.ctime",
+ birthtime: "stats.birthtime",
+};
+const DATE_FIELDS = new Set(["atime", "mtime", "ctime", "birthtime"]);
export function lowerBuiltinExtraProperty(
lowerer: Lowerer,
expr: ts.PropertyAccessExpression,
@@ -199,26 +223,12 @@ export function lowerBuiltinExtraProperty(
lowerer.checker.getSymbolAtLocation(expr.name),
);
}
- if (
- kind === "stats" &&
- (name === "dev" ||
- name === "ino" ||
- name === "blocks" ||
- name === "nlink" ||
- name === "atimeMs" ||
- name === "mtimeMs" ||
- name === "ctimeMs")
- ) {
+ if (kind === "stats" && Object.hasOwn(STATS_FIELDS, name)) {
const receiver = lowerer.lowerExpr(expr.expression);
- const fn = `stats.${name}` as
- | "stats.dev"
- | "stats.ino"
- | "stats.blocks"
- | "stats.nlink"
- | "stats.atimeMs"
- | "stats.mtimeMs"
- | "stats.ctimeMs";
- return { kind: "libCall", fn, args: [receiver], type: F64, loc };
+ const fn = STATS_FIELDS[name]!;
+ // The Date fields are the *Ms values rounded, as Node's dateFromMs builds them.
+ const type = DATE_FIELDS.has(name) ? DATE_T : F64;
+ return { kind: "libCall", fn, args: [receiver], type, loc };
}
if (kind === "spawnRes" && name === "signal") {
const receiver = lowerer.lowerExpr(expr.expression);
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-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts
index 8864f980d..6e50a4834 100644
--- a/packages/compiler/src/frontend/lowering/lower-exprs.ts
+++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts
@@ -7,6 +7,7 @@ import { isOptionalProcessStreamProperty } from "./builtins/process.js";
import { lowerWorkerMetadata } from "./builtins/workers.js";
import { lowerBlobInstanceof } from "./builtins/blob.js";
import { lowerKeyObjectInstanceof } from "./builtins/key-object.js";
+import { STATS_TYPE_TESTS } from "./builtins/filesystem.js";
import { threadsImportOf } from "../threads-import.js";
import { experimentalImportOf, lifetimeRegionMemberOf } from "../experimental-import.js";
import { dynUndefinedExpr, nodeThrowExpr, numLit, strLit, varRef } from "../../ir/build.js";
@@ -29,7 +30,7 @@ import { checkedClassAssertion, checkedClassUnionAssertion } from "./class-asser
import { checkedArmExtraction } from "./coercions/unions.js";
import { narrowClassUnion, narrowStoredClassValue } from "./class-unions.js";
import { lowerUnionFieldRead, lowerUnionFieldWrite } from "./expressions/union-fields.js";
-import { captureContextArguments } from "./function-context.js";
+import { captureContextArguments, declareContextLocal } from "./function-context.js";
import { OBJECT_CALLABLE_VALUES } from "./surfaces.js";
import { wasiGuestPath } from "../../wasi-paths.js";
import {
@@ -5081,13 +5082,13 @@ export function lowerIntrinsicProperty(
const receiver = lowerer.lowerExpr(expr.expression);
return { kind: "libCall", fn: "stats.size", args: [receiver], type: F64, loc: locOf(expr) };
}
- if (name === "isFile" || name === "isDirectory") {
+ if (Object.hasOwn(STATS_TYPE_TESTS, name)) {
lowerer.unsupported("SC1090", expr, `Stats methods as values (call '${name}' directly)`);
}
lowerer.noLowering(
`Stats.${name}`,
expr,
- "isFile(), isDirectory(), isSymbolicLink(), dev, ino, size, blocks, nlink, atimeMs, mtimeMs, and ctimeMs are the supported Stats members",
+ "isFile(), isDirectory(), isSymbolicLink(), isBlockDevice(), isCharacterDevice(), isFIFO(), isSocket(), dev, mode, nlink, uid, gid, rdev, blksize, ino, size, blocks, atimeMs, mtimeMs, ctimeMs, birthtimeMs, atime, mtime, ctime, and birthtime are the supported Stats members",
lowerer.checker.getSymbolAtLocation(expr.name),
);
}
@@ -15045,7 +15046,29 @@ function lowerFieldCompoundValue(
target.obj = save(target.obj, "%compoundReceiver");
// Accessors observe getter, RHS side effects, then setter, all through
// the saved receiver.
- const read = save(lowerer.fieldGetExpr(target, locOf(access), access), "%compoundOld");
+ const fieldRead = lowerer.fieldGetExpr(target, locOf(access), access);
+ // `+=` on a stored string field reads it into a mutable local, so the
+ // concat below can append to that local in place.
+ const storedString =
+ op === "+" &&
+ fieldRead.type.kind === "string" &&
+ target.fieldType.kind === "string" &&
+ (target.container === "class" || target.container === "record");
+ let read: IrExpr;
+ if (storedString) {
+ const old = declareContextLocal(
+ lowerer.ctx,
+ "%compoundOld",
+ STRING,
+ true,
+ undefined,
+ undefined,
+ );
+ body.push({ kind: "varDecl", localId: old.id, init: fieldRead, loc: fieldRead.loc });
+ read = varRef(old.id, STRING, fieldRead.loc);
+ } else {
+ read = save(fieldRead, "%compoundOld");
+ }
const rhs = save(
rhsNode ? lowerer.lowerExpr(rhsNode) : { kind: "numLit", value: 1, type: F64, loc },
"%compoundRhs",
@@ -15055,16 +15078,12 @@ function lowerFieldCompoundValue(
? { kind: "dynCheck", value: rhs, type: F64, loc: rhs.loc }
: lowerOptionalNumber(lowerer, rhs, loc);
let value: IrExpr;
+ let appended: IrExpr | null = null;
const numericRead = lowerOptionalNumber(lowerer, read, loc, access);
const bigintValue = rhsNode ? bigintCompoundValue(op, read, rhs, loc) : null;
if (op === "+" && read.type.kind === "string") {
- value = {
- kind: "strConcat",
- left: read,
- right: lowerer.ensureString(rhs, rhsNode ?? access),
- type: STRING,
- loc,
- };
+ appended = lowerer.ensureString(rhs, rhsNode ?? access);
+ value = { kind: "strConcat", left: read, right: appended, type: STRING, loc };
} else if (numericRead.type.kind === "f64" && numericRhs.type.kind === "f64") {
value = { kind: "bin", op, left: numericRead, right: numericRhs, type: F64, loc };
} else if (bigintValue) {
@@ -15086,6 +15105,30 @@ function lowerFieldCompoundValue(
} else {
lowerer.unsupported("SC1043", access);
}
+ if (storedString && rhs.type.kind === "string" && read.kind === "varRef" && appended !== null) {
+ // The field gives up its value before the concat (no code runs in
+ // between: both operands are already strings), and the concat is the
+ // self-append `old = old + rhs`, which hands the local's reference to
+ // the runtime: the old string is then uniquely owned unless something
+ // else holds it, and is appended to in place. Repeated `this.text += s`
+ // is linear rather than a copy per append.
+ const oldId = read.localId;
+ const oldLoc = read.loc;
+ const oldRef = (): IrExpr => varRef(oldId, STRING, oldLoc);
+ const concat: IrExpr = {
+ kind: "strConcat",
+ left: oldRef(),
+ right: appended,
+ type: STRING,
+ loc,
+ };
+ body.push(
+ lowerer.fieldSetStmt(target, { kind: "strLit", value: "", type: STRING, loc }, loc, access),
+ { kind: "assign", localId: oldId, value: concat, loc },
+ lowerer.fieldSetStmt(target, oldRef(), loc, access),
+ );
+ return { kind: "seqExpr", stmts: body, result: oldRef(), type: STRING, loc };
+ }
const result = save(value, "%compoundResult");
body.push(
lowerer.fieldSetStmt(target, lowerer.coerceInto(access, result, target.fieldType), loc, access),
diff --git a/packages/compiler/src/frontend/lowering/lower-server.ts b/packages/compiler/src/frontend/lowering/lower-server.ts
index 19918d6e4..050fe9be3 100644
--- a/packages/compiler/src/frontend/lowering/lower-server.ts
+++ b/packages/compiler/src/frontend/lowering/lower-server.ts
@@ -1920,6 +1920,18 @@ function lowerNetServerMethodCall(
loc,
};
}
+ if ((name === "ref" || name === "unref") && args.length === 0) {
+ // Whether the server keeps the event loop alive.
+ requireStatementPosition(lowerer, call, `server.${name}()`);
+ const receiver = coerceToHandle(lowerer, access.expression, NETSERVER_T);
+ return {
+ kind: "libCall",
+ fn: "net.serverSetRef",
+ args: [receiver, boolLit(name === "ref", loc)],
+ type: VOID,
+ loc,
+ };
+ }
if (name === "address") {
// The full AddressInfo record (the dgram.address materialization).
// The composed `server.address().port` read already lowered through
@@ -1964,6 +1976,18 @@ function lowerNetSocketMethodCall(
const name = access.name.text;
const loc = locOf(call);
const args = call.arguments;
+ if ((name === "ref" || name === "unref") && args.length === 0) {
+ // Whether the socket keeps the event loop alive.
+ requireStatementPosition(lowerer, call, `socket.${name}()`);
+ const receiver = lowerer.lowerExpr(access.expression);
+ return {
+ kind: "libCall",
+ fn: "net.sockSetRef",
+ args: [receiver, boolLit(name === "ref", loc)],
+ type: VOID,
+ loc,
+ };
+ }
if (name === "write" || name === "end") {
requireStatementPosition(lowerer, call, `socket.${name}(...)`);
// write(chunk, encoding) — the two-argument encoding form: 'buffer'
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 27726a818..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();
@@ -7906,6 +7909,22 @@ export class Lowerer {
return this.checkedNarrowCall(helper, expr, expected, expr.loc);
}
}
+ // A record arm beside unit arms flowing into another layout of that
+ // record (`R | undefined` into the runtime-optional variant of R): the
+ // same checked extraction of the record arm, then the record's width
+ // lift into the destination layout.
+ if (expr.type.kind === "union" && expected.kind === "record") {
+ const arms = this.unions.get(expr.type.unionId)?.arms ?? [];
+ const records = arms.filter((arm) => !isUnitType(arm));
+ const arm = records.length === 1 && records[0]!.kind === "record" ? records[0]! : null;
+ const lift = arm ? this.widthLiftPlan(arm, expected) : null;
+ const helper =
+ arm && lift ? this.narrowedArmHelper(expr.type.unionId, arm, expr.loc) : null;
+ if (arm && lift && helper) {
+ const narrowed = this.checkedNarrowCall(helper, expr, arm, expr.loc);
+ return this.applyWidthLift(lift, narrowed, expected, expr.loc);
+ }
+ }
if (!typeEquals(expr.type, expected)) {
const w = this.widthCoerce(expr, expected);
if (w) return w;
diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts
index da6dc87c8..9f6c90459 100644
--- a/packages/compiler/src/frontend/lowering/surfaces.ts
+++ b/packages/compiler/src/frontend/lowering/surfaces.ts
@@ -1037,6 +1037,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
@@ -1190,6 +1192,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 } },
@@ -1776,6 +1780,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"],
@@ -1795,6 +1800,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
@@ -1889,7 +1895,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/analysis.test.ts b/packages/compiler/src/ir/analysis.test.ts
index 153f8b5ea..9906bcaee 100644
--- a/packages/compiler/src/ir/analysis.test.ts
+++ b/packages/compiler/src/ir/analysis.test.ts
@@ -42,14 +42,30 @@ test("union widening preserves record identity and recursive element layouts", (
expect(unionWideningTags([node], [other])).toBeNull();
});
-test("matchStringSelfConcat recognizes only the immediate string self-concat", () => {
+test("matchStringSelfConcat recognizes the string self-concat", () => {
const suffix = str("x");
- expect(matchStringSelfConcat("acc", concat(ref("acc"), suffix))).toBe(suffix);
+ const target = ref("acc");
+ expect(matchStringSelfConcat("acc", concat(target, suffix))).toEqual({ left: target, suffix });
+});
+
+test("matchStringSelfConcat takes the operands after a leftmost target as the suffix", () => {
+ const target = ref("acc");
+ const self = matchStringSelfConcat(
+ "acc",
+ concat(concat(concat(target, str("x")), str("y")), str("z")),
+ );
+ expect(self?.left).toBe(target);
+ expect(self?.suffix).toMatchObject({
+ kind: "strConcat",
+ left: { kind: "strConcat", left: { value: "x" }, right: { value: "y" } },
+ right: { value: "z" },
+ });
});
test("matchStringSelfConcat rejects non-canonical and non-string shapes", () => {
expect(matchStringSelfConcat("acc", concat(ref("other"), str("x")))).toBeNull();
- expect(matchStringSelfConcat("acc", concat(concat(ref("acc"), str("x")), str("y")))).toBeNull();
+ expect(matchStringSelfConcat("acc", concat(concat(ref("other"), str("x")), str("y")))).toBeNull();
+ expect(matchStringSelfConcat("acc", concat(str("x"), ref("acc")))).toBeNull();
expect(matchStringSelfConcat("acc", str("x"))).toBeNull();
expect(matchStringSelfConcat("acc", concat(ref("acc", F64), str("x")))).toBeNull();
expect(matchStringSelfConcat("acc", concat(ref("acc"), str("x"), F64))).toBeNull();
diff --git a/packages/compiler/src/ir/analysis.ts b/packages/compiler/src/ir/analysis.ts
index 87a2d280b..cd75ffbe4 100644
--- a/packages/compiler/src/ir/analysis.ts
+++ b/packages/compiler/src/ir/analysis.ts
@@ -27,28 +27,45 @@ export function unionWideningTags(from: readonly IrType[], to: readonly IrType[]
}
/**
- * Recognize the one concat shape whose destination binding can temporarily
+ * Recognize the concat shape whose destination binding can temporarily
* hand off its ownership to the left operand:
*
* target = target + suffix
+ * target = target + a + b (the suffix is a + b)
*
- * Both native emitters use this rather than attempting a wider purity or
- * alias analysis. In particular, fields, nested concat trees, dynamic
- * operations, and a different destination all retain the ordinary borrowed
- * concat lowering.
+ * The target is the leftmost operand, so it is read first either way, and
+ * the remaining operands keep their order in the suffix; every operand is
+ * already a string, so regrouping them does not change the result. Both
+ * native emitters use this rather than attempting a wider purity or alias
+ * analysis. In particular, fields, dynamic operations, and a different
+ * destination all retain the ordinary borrowed concat lowering.
*/
-export function matchStringSelfConcat(targetLocalId: string, value: IrExpr): IrExpr | null {
+export function matchStringSelfConcat(
+ targetLocalId: string,
+ value: IrExpr,
+): { left: IrExpr; suffix: IrExpr } | null {
if (
value.kind !== "strConcat" ||
value.type.kind !== "string" ||
- value.left.kind !== "varRef" ||
- value.left.type.kind !== "string" ||
- value.left.localId !== targetLocalId ||
value.right.type.kind !== "string"
- ) {
+ )
return null;
+ if (value.left.kind === "varRef") {
+ if (value.left.type.kind !== "string" || value.left.localId !== targetLocalId) return null;
+ return { left: value.left, suffix: value.right };
}
- return value.right;
+ const inner = matchStringSelfConcat(targetLocalId, value.left);
+ if (inner === null) return null;
+ return {
+ left: inner.left,
+ suffix: {
+ kind: "strConcat",
+ left: inner.suffix,
+ right: value.right,
+ type: value.type,
+ loc: value.loc,
+ },
+ };
}
/** Descendant tree needed by constructor queries. Parent links deliberately
diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts
index 2e856a571..28380d43e 100644
--- a/packages/compiler/src/ir/builtin-effects.ts
+++ b/packages/compiler/src/ir/builtin-effects.ts
@@ -523,6 +523,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 13a1bf7e9..147948435 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
@@ -1007,6 +1021,21 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
"stats.atimeMs": { argTypes: [STATS_T], result: F64 },
"stats.mtimeMs": { argTypes: [STATS_T], result: F64 },
"stats.ctimeMs": { argTypes: [STATS_T], result: F64 },
+ "stats.birthtimeMs": { argTypes: [STATS_T], result: F64 },
+ "stats.mode": { argTypes: [STATS_T], result: F64 },
+ "stats.uid": { argTypes: [STATS_T], result: F64 },
+ "stats.gid": { argTypes: [STATS_T], result: F64 },
+ "stats.rdev": { argTypes: [STATS_T], result: F64 },
+ "stats.blksize": { argTypes: [STATS_T], result: F64 },
+ "stats.isBlockDevice": { argTypes: [STATS_T], result: BOOL },
+ "stats.isCharacterDevice": { argTypes: [STATS_T], result: BOOL },
+ "stats.isFIFO": { argTypes: [STATS_T], result: BOOL },
+ "stats.isSocket": { argTypes: [STATS_T], result: BOOL },
+ /** The Date fields: the *Ms value rounded (Node's dateFromMs). */
+ "stats.atime": { argTypes: [STATS_T], result: DATE_T },
+ "stats.mtime": { argTypes: [STATS_T], result: DATE_T },
+ "stats.ctime": { argTypes: [STATS_T], result: DATE_T },
+ "stats.birthtime": { argTypes: [STATS_T], result: DATE_T },
// The wider sync fs slice (unlink/chmod/chown/copyfile and the
// mode-carrying write/mkdir forms).
/** The wider sync fs slice (scr_lib.c), all throwing catchably with
@@ -1137,6 +1166,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
},
"net.serverPort": { argTypes: [NETSERVER_T], result: F64 },
"net.serverListening": { argTypes: [NETSERVER_T], result: BOOL },
+ /** socket.ref()/unref() and server.ref()/unref(): whether the handle
+ * keeps the event loop alive. */
+ "net.sockSetRef": { argTypes: [NETSOCKET_T, BOOL], result: VOID },
+ "net.serverSetRef": { argTypes: [NETSERVER_T, BOOL], result: VOID },
// net.serverAddress's record result is shape-checked in the libCall case
// (the dgram.address sentinel pattern).
/** server.address() as the full AddressInfo record (the dgram.address
diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts
index f578e6d8f..bfd2c6a8d 100644
--- a/packages/compiler/src/ir/runtime-features.ts
+++ b/packages/compiler/src/ir/runtime-features.ts
@@ -44,6 +44,7 @@ export interface RuntimeFeatures {
const DYN_ASYNC_LIB_FNS: ReadonlySet = new Set([
"fs.callbackValue",
"fs.callbackCall",
+ "fs.callbackGlob",
"async.awaitDyn",
"timers.immediatePromise",
"crypto.native",
@@ -173,7 +174,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 c639cca05..408ea46cd 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 f7966b311..69b986669 100644
--- a/packages/compiler/surface-manifest.json
+++ b/packages/compiler/surface-manifest.json
@@ -941,6 +941,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",
@@ -990,6 +996,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/5333-record-boundary-nullable-argument.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5333-record-boundary-nullable-argument.ts.json
new file mode 100644
index 000000000..0cf49e98a
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5333-record-boundary-nullable-argument.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/5333-record-boundary-nullable-argument.ts"
+ ],
+ "diags": []
+}
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/5541-fs-stats-fields.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5541-fs-stats-fields.cjs.json
new file mode 100644
index 000000000..b37688cb6
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5541-fs-stats-fields.cjs.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/5541-fs-stats-fields.cjs"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5552-string-appends.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5552-string-appends.ts.json
new file mode 100644
index 000000000..bcdc24046
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5552-string-appends.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/5552-string-appends.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5553-string-append-char-code.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5553-string-append-char-code.ts.json
new file mode 100644
index 000000000..b786ca386
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5553-string-append-char-code.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/5553-string-append-char-code.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/corpus/5595-net-ref-unref.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5595-net-ref-unref.ts.json
new file mode 100644
index 000000000..40fc6e135
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5595-net-ref-unref.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/5595-net-ref-unref.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_async.c b/packages/runtime/src/scr_async.c
index 66ebfc892..54288ac4c 100644
--- a/packages/runtime/src/scr_async.c
+++ b/packages/runtime/src/scr_async.c
@@ -3340,6 +3340,12 @@ static SCR_TL bool (*scr_net_pending_fn)(void) = NULL;
static SCR_TL void (*scr_net_dispatch_fn)(void) = NULL;
static SCR_TL int (*scr_net_pollfd_fn)(void) = NULL;
+/* Net io without liveness: unref'd sockets and servers still deliver
+ * events while the loop runs for other work, but do not keep it alive. */
+static SCR_TL bool (*scr_net_io_fn)(void) = NULL;
+
+void scr_loop_set_net_io(bool (*io)(void)) { scr_net_io_fn = io; }
+
void scr_loop_set_net(bool (*pending)(void), void (*dispatch)(void), int (*pollfd)(void)) {
scr_net_pending_fn = pending;
scr_net_dispatch_fn = dispatch;
@@ -3621,6 +3627,9 @@ static bool scr_loop_run_pass(ScrPromise *top_level, bool first_checkpoint) {
bool kids = scr_children_pending();
bool events = scr_events_pending_fn != NULL && scr_events_pending_fn();
bool net = scr_net_pending_fn != NULL && scr_net_pending_fn();
+ /* `net` keeps the process alive; `net_io` (unref'd handles too) decides
+ * whether the sleep polls the net unit's fd */
+ bool net_io = net || (scr_net_io_fn != NULL && scr_net_io_fn());
bool dgram = scr_dgram_pending_fn != NULL && scr_dgram_pending_fn();
bool watch = scr_watch_pending_fn != NULL && scr_watch_pending_fn();
bool ffi = scr_ffi_pending_fn != NULL && scr_ffi_pending_fn();
@@ -3674,12 +3683,12 @@ static bool scr_loop_run_pass(ScrPromise *top_level, bool first_checkpoint) {
#endif
bool evw = scr_events_watching_fn != NULL && scr_events_watching_fn();
#ifdef SCR_WORKERS
- if (workers && scr_workers_wait_fn && !kids && !evw && !net && !dgram && !watch && !ffi && !renames && !crypto) {
+ if (workers && scr_workers_wait_fn && !kids && !evw && !net_io && !dgram && !watch && !ffi && !renames && !crypto) {
scr_workers_wait_fn(due > now ? due - now : 0);
now = scr_now_ms();
} else
#endif
- if (evw || net || dgram || watch || ffi || workers) {
+ if (evw || net_io || dgram || watch || ffi || workers) {
#if defined(_WIN32) || defined(__wasi__)
/* The win32 arm, and WASI hosts whose poll_oneoff adapters do not
* reliably wake for a closed inherited stdin pipe: the sleep is a capped nanosleep and
@@ -3693,7 +3702,7 @@ static bool scr_loop_run_pass(ScrPromise *top_level, bool first_checkpoint) {
* show up in a profile, the upgrade is a real waitable arm —
* WaitForMultipleObjects over WSAEVENTs, or IOCP. */
if (evw && due > now + SCR_SIGNAL_POLL_MS) due = now + SCR_SIGNAL_POLL_MS;
- if ((net || dgram || watch) && due > now + SCR_CHILD_POLL_MS) due = now + SCR_CHILD_POLL_MS;
+ if ((net_io || dgram || watch) && due > now + SCR_CHILD_POLL_MS) due = now + SCR_CHILD_POLL_MS;
if ((ffi || workers) && due > now + SCR_CHILD_POLL_MS) due = now + SCR_CHILD_POLL_MS;
if (kids && due > now + SCR_CHILD_POLL_MS) due = now + SCR_CHILD_POLL_MS;
if (due > now) {
@@ -3720,7 +3729,7 @@ static bool scr_loop_run_pass(ScrPromise *top_level, bool first_checkpoint) {
fds[nfds].events = POLLIN;
fds[nfds++].revents = 0;
}
- if (net) {
+ if (net_io) {
/* The net unit's poller fd: readable while socket events are
* pending, so arrived data/accepts end the sleep immediately.
* Dispatch (which also DRAINS the poller) happens at the next
diff --git a/packages/runtime/src/scr_bytes_io.c b/packages/runtime/src/scr_bytes_io.c
index 5018f8480..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
@@ -537,91 +530,11 @@ static void scr_fs_cb_copy(const ScrStr *source, const ScrStr *destination, bool
}
}
-static ScrDyn *scr_fs_cb_stat_test(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
- (void)args; (void)argc;
- return scr_dyn_new_bool(scr_box_get_bool(closure->caps[0]));
-}
-
-static void scr_fs_cb_stat_method(ScrDyn *object, const char *name, bool answer) {
- ScrClosure *closure = scr_closure_new(NULL, 1);
- closure->caps[0] = scr_box_new(SCR_BOX_BOOL);
- scr_box_set_bool(closure->caps[0], answer);
- scr_dyn_obj_set(object, name, strlen(name), scr_dyn_new_func(closure, scr_fs_cb_stat_test, 0, "native:fs.Stats", name));
-}
-
-static void scr_fs_cb_stat_number(ScrDyn *object, const char *name, int64_t number, bool bigint) {
- ScrDyn *value;
- if (bigint) {
- ScrBigInt *integer = scr_bigint_from_i64(number);
- value = scr_dyn_new_bigint(integer);
- scr_bigint_release(integer);
- } else value = scr_dyn_new_num((double)number);
- scr_dyn_obj_set(object, name, strlen(name), value);
-}
-
-static void scr_fs_cb_stat_date(ScrDyn *object, const char *name, double milliseconds) {
- ScrDyn *arguments = scr_dyn_new_arr();
- scr_dyn_arr_push(arguments, scr_dyn_new_num(milliseconds));
- ScrDyn *date = scr_dyn_native_date_new(arguments);
- scr_dyn_release(arguments);
- if (date) scr_dyn_obj_set(object, name, strlen(name), date);
-}
-
static ScrDyn *scr_fs_cb_stats(const ScrStr *path, double fd, const ScrDyn *options, bool nofollow) {
- struct stat status;
-#ifdef _WIN32
- (void)nofollow;
- int rc = path ? stat(path->data, &status) : fstat((int)fd, &status);
-#else
- int rc = path ? (nofollow ? lstat(path->data, &status) : stat(path->data, &status)) : fstat((int)fd, &status);
-#endif
- if (rc < 0) { scr_fs_throw(errno, path ? (nofollow ? "lstat" : "stat") : "fstat", path); return NULL; }
- bool bigint = scr_fs_cb_option_bool(options, "bigint");
- ScrDyn *object = scr_dyn_new_obj();
-#define SCR_FS_STAT_FIELD(field) scr_fs_cb_stat_number(object, #field, (int64_t)status.st_##field, bigint)
- SCR_FS_STAT_FIELD(dev); SCR_FS_STAT_FIELD(ino); SCR_FS_STAT_FIELD(mode); SCR_FS_STAT_FIELD(nlink);
- SCR_FS_STAT_FIELD(uid); SCR_FS_STAT_FIELD(gid); SCR_FS_STAT_FIELD(rdev); SCR_FS_STAT_FIELD(size);
-#ifndef _WIN32
- SCR_FS_STAT_FIELD(blksize); SCR_FS_STAT_FIELD(blocks);
-#endif
-#undef SCR_FS_STAT_FIELD
- double atime = (double)status.st_atime * 1000, mtime = (double)status.st_mtime * 1000, ctime = (double)status.st_ctime * 1000;
- double birthtime = 0;
-#ifdef __APPLE__
- atime += status.st_atimespec.tv_nsec / 1000000.0; mtime += status.st_mtimespec.tv_nsec / 1000000.0;
- ctime += status.st_ctimespec.tv_nsec / 1000000.0;
- birthtime = (double)status.st_birthtimespec.tv_sec * 1000 + status.st_birthtimespec.tv_nsec / 1000000.0;
-#elif !defined(_WIN32) && !defined(__wasi__)
- atime += status.st_atim.tv_nsec / 1000000.0; mtime += status.st_mtim.tv_nsec / 1000000.0;
- ctime += status.st_ctim.tv_nsec / 1000000.0;
-#endif
- scr_fs_cb_stat_date(object, "atime", atime); scr_fs_cb_stat_date(object, "mtime", mtime);
- scr_fs_cb_stat_date(object, "ctime", ctime); scr_fs_cb_stat_date(object, "birthtime", birthtime);
- if (bigint) {
- scr_fs_cb_stat_number(object, "atimeMs", (int64_t)atime, true);
- scr_fs_cb_stat_number(object, "mtimeMs", (int64_t)mtime, true);
- scr_fs_cb_stat_number(object, "ctimeMs", (int64_t)ctime, true);
- scr_fs_cb_stat_number(object, "birthtimeMs", (int64_t)birthtime, true);
- } else {
- scr_dyn_obj_set(object, "atimeMs", 7, scr_dyn_new_num(atime));
- scr_dyn_obj_set(object, "mtimeMs", 7, scr_dyn_new_num(mtime));
- scr_dyn_obj_set(object, "ctimeMs", 7, scr_dyn_new_num(ctime));
- scr_dyn_obj_set(object, "birthtimeMs", 11, scr_dyn_new_num(birthtime));
- }
- scr_fs_cb_stat_method(object, "isFile", S_ISREG(status.st_mode));
- scr_fs_cb_stat_method(object, "isDirectory", S_ISDIR(status.st_mode));
-#ifndef _WIN32
- scr_fs_cb_stat_method(object, "isSymbolicLink", S_ISLNK(status.st_mode));
- scr_fs_cb_stat_method(object, "isBlockDevice", S_ISBLK(status.st_mode));
- scr_fs_cb_stat_method(object, "isCharacterDevice", S_ISCHR(status.st_mode));
- scr_fs_cb_stat_method(object, "isFIFO", S_ISFIFO(status.st_mode));
- scr_fs_cb_stat_method(object, "isSocket", S_ISSOCK(status.st_mode));
-#else
- scr_fs_cb_stat_method(object, "isSymbolicLink", false);
- scr_fs_cb_stat_method(object, "isBlockDevice", false);
- scr_fs_cb_stat_method(object, "isCharacterDevice", S_ISCHR(status.st_mode));
- scr_fs_cb_stat_method(object, "isFIFO", false); scr_fs_cb_stat_method(object, "isSocket", false);
-#endif
+ ScrStats *snapshot = !path ? scr_fs_fstat(fd) : nofollow ? scr_fs_lstat((ScrStr *)path) : scr_fs_stat((ScrStr *)path);
+ if (!snapshot) return NULL;
+ ScrDyn *object = scr_stats_dyn(snapshot, scr_fs_cb_option_bool(options, "bigint"));
+ scr_stats_release(snapshot);
return object;
}
@@ -634,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();
@@ -895,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_file_handle.c b/packages/runtime/src/scr_file_handle.c
index 8d0ad072d..32438d524 100644
--- a/packages/runtime/src/scr_file_handle.c
+++ b/packages/runtime/src/scr_file_handle.c
@@ -34,42 +34,6 @@ struct ScrFileHandle {
int fd;
};
-/* ScrStats is opaque at the public runtime boundary. FileHandle's fstat
- * snapshot completes the same layout privately in this translation unit. */
-struct ScrStats {
- size_t rc;
- bool is_file;
- bool is_dir;
- bool is_symlink;
- double dev;
- double ino;
- double size;
- double blocks;
- double nlink;
- double atime_ms;
- double mtime_ms;
- double ctime_ms;
-};
-
-#ifdef _WIN32
-/* Keep the FILETIME conversion identical to scr_lib.c's path-stat arm:
- * libuv splits the 100ns count into Unix seconds and nanoseconds before Node
- * combines it into the public millisecond value. */
-static double scr_file_handle_filetime_ms(FILETIME ft) {
- ULARGE_INTEGER raw;
- raw.LowPart = ft.dwLowDateTime;
- raw.HighPart = ft.dwHighDateTime;
- int64_t ticks = (int64_t)raw.QuadPart - INT64_C(116444736000000000);
- int64_t sec = ticks / INT64_C(10000000);
- int64_t rem = ticks % INT64_C(10000000);
- if (rem < 0) {
- sec--;
- rem += INT64_C(10000000);
- }
- return (double)sec * 1000.0 + (double)rem / 10000.0;
-}
-#endif
-
typedef struct {
char *data;
size_t len;
@@ -395,9 +359,9 @@ void scr_file_handle_write_file_bytes(ScrFileHandle *h, ScrBytes *data) {
ScrStats *scr_file_handle_stat(ScrFileHandle *h) {
if (!scr_file_handle_require_open(h)) return NULL;
- struct stat st;
- if (fstat(h->fd, &st) != 0) {
- int err = errno;
+ int err = 0;
+ ScrStats *out = scr_stats_of_fd(h->fd, &err);
+ if (!out) {
const char *name = err == EBADF ? "EBADF" : err == EIO ? "EIO" : "EUNKNOWN";
const char *text = err == EBADF ? "bad file descriptor" :
err == EIO ? "i/o error" : strerror(err);
@@ -406,66 +370,6 @@ ScrStats *scr_file_handle_stat(ScrFileHandle *h) {
scr_throw_error_msg_code(SCR_ERR_ERROR, msg, (size_t)len, name);
return NULL;
}
- ScrStats *out = malloc(sizeof(ScrStats));
- if (!out) scr_trap("scriptc: out of memory\n");
- out->rc = 1;
- out->is_file = S_ISREG(st.st_mode);
- out->is_dir = S_ISDIR(st.st_mode);
- out->is_symlink = false; /* fstat follows the open descriptor. */
- out->dev = (double)st.st_dev;
- out->ino = (double)st.st_ino;
- out->size = (double)st.st_size;
-#if defined(_WIN32)
- out->blocks = st.st_size <= 0 ? 0.0 : (double)(((uint64_t)st.st_size + 511) >> 9);
- out->nlink = (double)st.st_nlink;
- out->atime_ms = (double)st.st_atime * 1000.0;
- out->mtime_ms = (double)st.st_mtime * 1000.0;
- out->ctime_ms = (double)st.st_ctime * 1000.0;
- HANDLE os_handle = (HANDLE)_get_osfhandle(h->fd);
- if (os_handle != INVALID_HANDLE_VALUE) {
- BY_HANDLE_FILE_INFORMATION basic;
- if (GetFileInformationByHandle(os_handle, &basic)) {
- out->dev = (double)basic.dwVolumeSerialNumber;
- out->ino = (double)basic.nFileIndexHigh * 4294967296.0 +
- (double)basic.nFileIndexLow;
- out->nlink = (double)basic.nNumberOfLinks;
- out->atime_ms = scr_file_handle_filetime_ms(basic.ftLastAccessTime);
- out->mtime_ms = scr_file_handle_filetime_ms(basic.ftLastWriteTime);
- }
- FILE_BASIC_INFO times;
- if (GetFileInformationByHandleEx(
- os_handle, FileBasicInfo, ×, sizeof times)) {
- FILETIME changed;
- changed.dwLowDateTime = times.ChangeTime.LowPart;
- changed.dwHighDateTime = times.ChangeTime.HighPart;
- out->ctime_ms = scr_file_handle_filetime_ms(changed);
- }
- FILE_STANDARD_INFO standard;
- if (GetFileInformationByHandleEx(
- os_handle, FileStandardInfo, &standard, sizeof standard)) {
- out->blocks = (double)((uint64_t)standard.AllocationSize.QuadPart >> 9);
- out->nlink = (double)standard.NumberOfLinks;
- }
- }
-#elif defined(__APPLE__)
- out->blocks = (double)st.st_blocks;
- out->nlink = (double)st.st_nlink;
- out->atime_ms = (double)st.st_atimespec.tv_sec * 1000.0 +
- (double)st.st_atimespec.tv_nsec / 1e6;
- out->mtime_ms = (double)st.st_mtimespec.tv_sec * 1000.0 +
- (double)st.st_mtimespec.tv_nsec / 1e6;
- out->ctime_ms = (double)st.st_ctimespec.tv_sec * 1000.0 +
- (double)st.st_ctimespec.tv_nsec / 1e6;
-#else
- out->blocks = (double)st.st_blocks;
- out->nlink = (double)st.st_nlink;
- out->atime_ms = (double)st.st_atim.tv_sec * 1000.0 +
- (double)st.st_atim.tv_nsec / 1e6;
- out->mtime_ms = (double)st.st_mtim.tv_sec * 1000.0 +
- (double)st.st_mtim.tv_nsec / 1e6;
- out->ctime_ms = (double)st.st_ctim.tv_sec * 1000.0 +
- (double)st.st_ctim.tv_nsec / 1e6;
-#endif
return out;
}
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_http2.h b/packages/runtime/src/scr_http2.h
index 7e8d944de..fdcd741e3 100644
--- a/packages/runtime/src/scr_http2.h
+++ b/packages/runtime/src/scr_http2.h
@@ -199,6 +199,7 @@ void scr_tls_fetch_client_wrap(ScrNetSocket *sock, void *ctx);
* events hook, and `pollfd` exposes the unit's poller fd so the idle
* poll(2) sleep wakes on socket readiness (-1 = no fd yet). */
void scr_loop_set_net(bool (*pending)(void), void (*dispatch)(void), int (*pollfd)(void));
+void scr_loop_set_net_io(bool (*io)(void));
/* True when fibers are queued on the microtask ready queue — dispatch
* hooks use it to yield between event batches so promise jobs interleave
* (net's sweep/drain alternation). */
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_lib.c b/packages/runtime/src/scr_lib.c
index 82ac517d0..b7d2367c6 100644
--- a/packages/runtime/src/scr_lib.c
+++ b/packages/runtime/src/scr_lib.c
@@ -130,6 +130,17 @@ static ScrStr *scr_fs_win_utf8(const WCHAR *text, size_t length) {
#if defined(__linux__)
#include /* sched_getaffinity, CPU_COUNT (os.availableParallelism) */
#include /* PR_SET_NAME (process.title) */
+#include /* SYS_statx (fs stats) */
+#ifndef SYS_statx
+#if defined(__x86_64__)
+#define SYS_statx 332
+#elif defined(__aarch64__)
+#define SYS_statx 291 /* the generic syscall table */
+#endif
+#endif
+#ifndef AT_EMPTY_PATH
+#define AT_EMPTY_PATH 0x1000
+#endif
#elif defined(__APPLE__)
#include /* sysctlbyname (os.availableParallelism) */
#endif
@@ -2140,7 +2151,7 @@ static double scr_filetime_us(FILETIME ft) {
/* A filesystem FILETIME is 100ns ticks since 1601. Match libuv's split into
* Unix seconds + nanoseconds before doing Node's millisecond arithmetic; the
* split matters for the last-bit rounding of dates far from the epoch. */
-static double scr_filetime_unix_ms(FILETIME ft) {
+static void scr_filetime_timespec(FILETIME ft, int64_t *sec_out, int64_t *nsec_out) {
ULARGE_INTEGER raw;
raw.LowPart = ft.dwLowDateTime;
raw.HighPart = ft.dwHighDateTime;
@@ -2151,7 +2162,8 @@ static double scr_filetime_unix_ms(FILETIME ft) {
sec--;
rem += INT64_C(10000000);
}
- return (double)sec * 1000.0 + (double)rem / 10000.0;
+ *sec_out = sec;
+ *nsec_out = rem * 100;
}
double scr_cpu_user(void) {
FILETIME c, e, k, u;
@@ -4815,24 +4827,37 @@ void scr_process_stdin_destroy(void) {
}
/* ── Stats values ────────────────────────────────────────────────────
- * An immutable snapshot of stat(2) results — the lowered type/mode, device,
- * inode, size, allocation/link, and access/write/change-time slice. statSync
- * THROWS like the other sync fs calls; the promise form rejects (see the fsp
- * section). */
+ * An immutable snapshot of one stat(2) result: libuv's integers and its four
+ * timestamps. statSync THROWS like the other sync fs calls; the promise form
+ * rejects (see the fsp section). scr_stats_dyn builds Node's Stats and
+ * BigIntStats objects from the same snapshot. */
+
+/* The integers in the order Node's binding reports them. */
+enum {
+ SCR_STATS_DEV,
+ SCR_STATS_MODE,
+ SCR_STATS_NLINK,
+ SCR_STATS_UID,
+ SCR_STATS_GID,
+ SCR_STATS_RDEV,
+ SCR_STATS_BLKSIZE,
+ SCR_STATS_INO,
+ SCR_STATS_SIZE,
+ SCR_STATS_BLOCKS,
+ SCR_STATS_FIELDS
+};
+
+/* The timestamps, each kept as seconds + nanoseconds. */
+enum { SCR_STATS_ATIME, SCR_STATS_MTIME, SCR_STATS_CTIME, SCR_STATS_BIRTHTIME };
struct ScrStats {
size_t rc;
bool is_file;
bool is_dir;
bool is_symlink; /* lstat only — a followed stat never sees one */
- double dev;
- double ino;
- double size;
- double blocks; /* allocated size in 512-byte units (Node/libuv) */
- double nlink;
- double atime_ms;
- double mtime_ms; /* milliseconds with the nanosecond fraction (Node) */
- double ctime_ms;
+ uint64_t fields[SCR_STATS_FIELDS]; /* blocks counts 512-byte units */
+ int64_t sec[4];
+ int64_t nsec[4];
};
ScrStats *scr_stats_retain(ScrStats *s) {
@@ -4848,20 +4873,66 @@ void scr_stats_release(ScrStats *s) {
void *scr_stats_retain_v(void *p) { return scr_stats_retain(p); }
void scr_stats_release_v(void *p) { scr_stats_release(p); }
+/* The S_IFMT type bits, spelled out because Windows lacks some macros. */
+#define SCR_STATS_IFMT 0xF000u
+#define SCR_STATS_IFIFO 0x1000u
+#define SCR_STATS_IFCHR 0x2000u
+#define SCR_STATS_IFDIR 0x4000u
+#define SCR_STATS_IFBLK 0x6000u
+#define SCR_STATS_IFREG 0x8000u
+#define SCR_STATS_IFLNK 0xA000u
+#define SCR_STATS_IFSOCK 0xC000u
+
+static bool scr_stats_type_is(const ScrStats *s, unsigned type) {
+#ifdef _WIN32
+ /* Node answers false for the types Windows does not have. */
+ if (type == SCR_STATS_IFIFO || type == SCR_STATS_IFBLK || type == SCR_STATS_IFSOCK) return false;
+#endif
+ return (s->fields[SCR_STATS_MODE] & SCR_STATS_IFMT) == type;
+}
+
bool scr_stats_is_file(ScrStats *s) { return s->is_file; }
bool scr_stats_is_dir(ScrStats *s) { return s->is_dir; }
bool scr_stats_is_symlink(ScrStats *s) { return s->is_symlink; }
-double scr_stats_dev(ScrStats *s) { return s->dev; }
-double scr_stats_ino(ScrStats *s) { return s->ino; }
-double scr_stats_size(ScrStats *s) { return s->size; }
-double scr_stats_blocks(ScrStats *s) { return s->blocks; }
-double scr_stats_nlink(ScrStats *s) { return s->nlink; }
-double scr_stats_atime_ms(ScrStats *s) { return s->atime_ms; }
-double scr_stats_mtime_ms(ScrStats *s) { return s->mtime_ms; }
-double scr_stats_ctime_ms(ScrStats *s) { return s->ctime_ms; }
+bool scr_stats_is_block_device(ScrStats *s) { return scr_stats_type_is(s, SCR_STATS_IFBLK); }
+bool scr_stats_is_character_device(ScrStats *s) { return scr_stats_type_is(s, SCR_STATS_IFCHR); }
+bool scr_stats_is_fifo(ScrStats *s) { return scr_stats_type_is(s, SCR_STATS_IFIFO); }
+bool scr_stats_is_socket(ScrStats *s) { return scr_stats_type_is(s, SCR_STATS_IFSOCK); }
+double scr_stats_dev(ScrStats *s) { return (double)s->fields[SCR_STATS_DEV]; }
+double scr_stats_mode(ScrStats *s) { return (double)s->fields[SCR_STATS_MODE]; }
+double scr_stats_nlink(ScrStats *s) { return (double)s->fields[SCR_STATS_NLINK]; }
+double scr_stats_uid(ScrStats *s) { return (double)s->fields[SCR_STATS_UID]; }
+double scr_stats_gid(ScrStats *s) { return (double)s->fields[SCR_STATS_GID]; }
+double scr_stats_rdev(ScrStats *s) { return (double)s->fields[SCR_STATS_RDEV]; }
+double scr_stats_blksize(ScrStats *s) { return (double)s->fields[SCR_STATS_BLKSIZE]; }
+double scr_stats_ino(ScrStats *s) { return (double)s->fields[SCR_STATS_INO]; }
+double scr_stats_size(ScrStats *s) { return (double)s->fields[SCR_STATS_SIZE]; }
+double scr_stats_blocks(ScrStats *s) { return (double)s->fields[SCR_STATS_BLOCKS]; }
+
+/* Node's msFromTimeSpec: seconds * 1000 + nanoseconds / 1e6. */
+static double scr_stats_ms(const ScrStats *s, int which) {
+ return (double)s->sec[which] * 1000.0 + (double)s->nsec[which] / 1e6;
+}
+
+double scr_stats_atime_ms(ScrStats *s) { return scr_stats_ms(s, SCR_STATS_ATIME); }
+double scr_stats_mtime_ms(ScrStats *s) { return scr_stats_ms(s, SCR_STATS_MTIME); }
+double scr_stats_ctime_ms(ScrStats *s) { return scr_stats_ms(s, SCR_STATS_CTIME); }
+double scr_stats_birthtime_ms(ScrStats *s) { return scr_stats_ms(s, SCR_STATS_BIRTHTIME); }
+
+/* Math.round: the nearest integer, halves toward +Infinity. */
+static double scr_stats_round(double ms) {
+ double low = floor(ms);
+ return ms - low >= 0.5 ? low + 1 : low;
+}
+
+/* The Date fields: Node rounds the millisecond value before new Date(). */
+double scr_stats_atime(ScrStats *s) { return scr_date_new_ms(scr_stats_round(scr_stats_ms(s, SCR_STATS_ATIME))); }
+double scr_stats_mtime(ScrStats *s) { return scr_date_new_ms(scr_stats_round(scr_stats_ms(s, SCR_STATS_MTIME))); }
+double scr_stats_ctime(ScrStats *s) { return scr_date_new_ms(scr_stats_round(scr_stats_ms(s, SCR_STATS_CTIME))); }
+double scr_stats_birthtime(ScrStats *s) { return scr_date_new_ms(scr_stats_round(scr_stats_ms(s, SCR_STATS_BIRTHTIME))); }
static ScrStats *scr_stats_new(void) {
- ScrStats *s = malloc(sizeof(ScrStats));
+ ScrStats *s = calloc(1, sizeof(ScrStats));
if (!s) {
scr_trap("scriptc: out of memory\n");
}
@@ -4869,7 +4940,22 @@ static ScrStats *scr_stats_new(void) {
return s;
}
+static void scr_stats_set_time(ScrStats *s, int which, int64_t sec, int64_t nsec) {
+ s->sec[which] = sec;
+ s->nsec[which] = nsec;
+}
+
#ifdef _WIN32
+static void scr_stats_set_filetime(ScrStats *s, int which, FILETIME ft) {
+ scr_filetime_timespec(ft, &s->sec[which], &s->nsec[which]);
+}
+
+/* libuv's Windows mode: the entry type plus read for everyone, and write
+ * unless the entry is read-only. */
+static uint64_t scr_stats_win_mode(unsigned type, bool read_only) {
+ return type | (read_only ? 0444u : 0666u);
+}
+
/* The CRT fallback is deliberately complete: callers either get every field
* from this one stat() result or every field from one Win32 handle below,
* never a snapshot spliced across two path resolutions. */
@@ -4878,14 +4964,22 @@ static ScrStats *scr_stats_of_crt(const struct stat *st) {
s->is_file = S_ISREG(st->st_mode);
s->is_dir = S_ISDIR(st->st_mode);
s->is_symlink = false;
- s->dev = (double)st->st_dev;
- s->ino = (double)st->st_ino;
- s->size = (double)st->st_size;
- s->blocks = st->st_size <= 0 ? 0.0 : (double)(((uint64_t)st->st_size + 511) >> 9);
- s->nlink = (double)st->st_nlink;
- s->atime_ms = (double)st->st_atime * 1000.0;
- s->mtime_ms = (double)st->st_mtime * 1000.0;
- s->ctime_ms = (double)st->st_ctime * 1000.0;
+ s->fields[SCR_STATS_DEV] = (uint64_t)st->st_dev;
+ /* Character devices and pipes carry only their type bits in libuv. */
+ unsigned type = (unsigned)st->st_mode & SCR_STATS_IFMT;
+ s->fields[SCR_STATS_MODE] = type == SCR_STATS_IFREG || type == SCR_STATS_IFDIR
+ ? scr_stats_win_mode(type, !(st->st_mode & 0200))
+ : type;
+ s->fields[SCR_STATS_NLINK] = (uint64_t)st->st_nlink;
+ s->fields[SCR_STATS_BLKSIZE] = 4096;
+ s->fields[SCR_STATS_INO] = (uint64_t)st->st_ino;
+ s->fields[SCR_STATS_SIZE] = st->st_size <= 0 ? 0 : (uint64_t)st->st_size;
+ s->fields[SCR_STATS_BLOCKS] = st->st_size <= 0 ? 0 : ((uint64_t)st->st_size + 511) >> 9;
+ scr_stats_set_time(s, SCR_STATS_ATIME, (int64_t)st->st_atime, 0);
+ scr_stats_set_time(s, SCR_STATS_MTIME, (int64_t)st->st_mtime, 0);
+ /* The CRT's st_ctime is the creation time. */
+ scr_stats_set_time(s, SCR_STATS_CTIME, (int64_t)st->st_ctime, 0);
+ scr_stats_set_time(s, SCR_STATS_BIRTHTIME, (int64_t)st->st_ctime, 0);
return s;
}
@@ -5127,57 +5221,188 @@ static ScrStats *scr_stats_of_path(const ScrStr *path, const char *op,
s->is_file = !is_link && (have_standard ? !standard.Directory : !is_dir);
s->is_dir = !is_link && (have_standard ? standard.Directory : is_dir);
s->is_symlink = is_link;
- s->dev = (double)basic.dwVolumeSerialNumber;
- s->ino = (double)basic.nFileIndexHigh * 4294967296.0 +
- (double)basic.nFileIndexLow;
- s->size = is_link && link_size >= 0 ? link_size : s->is_dir ? 0.0
- : have_standard ? (double)standard.EndOfFile.QuadPart : (double)size;
- s->blocks = have_standard
- ? (double)((uint64_t)standard.AllocationSize.QuadPart >> 9)
- : size == 0 ? 0.0 : (double)((size + 511) >> 9);
- s->nlink = have_standard
- ? (double)standard.NumberOfLinks
- : (double)basic.nNumberOfLinks;
- s->atime_ms = scr_filetime_unix_ms(basic.ftLastAccessTime);
- s->mtime_ms = scr_filetime_unix_ms(basic.ftLastWriteTime);
+ s->fields[SCR_STATS_DEV] = basic.dwVolumeSerialNumber;
+ s->fields[SCR_STATS_MODE] = scr_stats_win_mode(
+ is_link ? SCR_STATS_IFLNK : s->is_dir ? SCR_STATS_IFDIR : SCR_STATS_IFREG,
+ (basic.dwFileAttributes & FILE_ATTRIBUTE_READONLY) != 0);
+ s->fields[SCR_STATS_NLINK] = have_standard ? standard.NumberOfLinks : basic.nNumberOfLinks;
+ s->fields[SCR_STATS_BLKSIZE] = 4096;
+ s->fields[SCR_STATS_INO] = ((uint64_t)basic.nFileIndexHigh << 32) | basic.nFileIndexLow;
+ s->fields[SCR_STATS_SIZE] = is_link && link_size >= 0 ? (uint64_t)link_size : s->is_dir ? 0
+ : have_standard ? (uint64_t)standard.EndOfFile.QuadPart : size;
+ s->fields[SCR_STATS_BLOCKS] = have_standard
+ ? (uint64_t)standard.AllocationSize.QuadPart >> 9
+ : size == 0 ? 0 : (size + 511) >> 9;
+ scr_stats_set_filetime(s, SCR_STATS_ATIME, basic.ftLastAccessTime);
+ scr_stats_set_filetime(s, SCR_STATS_MTIME, basic.ftLastWriteTime);
+ scr_stats_set_filetime(s, SCR_STATS_BIRTHTIME, basic.ftCreationTime);
if (have_times) {
FILETIME changed;
changed.dwLowDateTime = times.ChangeTime.LowPart;
changed.dwHighDateTime = times.ChangeTime.HighPart;
- s->ctime_ms = scr_filetime_unix_ms(changed);
+ scr_stats_set_filetime(s, SCR_STATS_CTIME, changed);
} else {
- s->ctime_ms = s->mtime_ms;
+ scr_stats_set_time(s, SCR_STATS_CTIME, s->sec[SCR_STATS_MTIME], s->nsec[SCR_STATS_MTIME]);
}
return s;
}
+
+/* An open descriptor: the CRT snapshot, completed from the Win32 handle the
+ * way libuv's fstat reads it. */
+static ScrStats *scr_stats_of_fd_win(int fd, const struct stat *st) {
+ ScrStats *out = scr_stats_of_crt(st);
+ HANDLE os_handle = (HANDLE)_get_osfhandle(fd);
+ if (os_handle == INVALID_HANDLE_VALUE) return out;
+ BY_HANDLE_FILE_INFORMATION basic;
+ if (GetFileInformationByHandle(os_handle, &basic)) {
+ out->fields[SCR_STATS_DEV] = basic.dwVolumeSerialNumber;
+ out->fields[SCR_STATS_INO] = ((uint64_t)basic.nFileIndexHigh << 32) | basic.nFileIndexLow;
+ out->fields[SCR_STATS_NLINK] = basic.nNumberOfLinks;
+ out->fields[SCR_STATS_MODE] = scr_stats_win_mode(
+ out->is_dir ? SCR_STATS_IFDIR : SCR_STATS_IFREG,
+ (basic.dwFileAttributes & FILE_ATTRIBUTE_READONLY) != 0);
+ scr_stats_set_filetime(out, SCR_STATS_ATIME, basic.ftLastAccessTime);
+ scr_stats_set_filetime(out, SCR_STATS_MTIME, basic.ftLastWriteTime);
+ scr_stats_set_filetime(out, SCR_STATS_BIRTHTIME, basic.ftCreationTime);
+ }
+ FILE_BASIC_INFO times;
+ if (GetFileInformationByHandleEx(os_handle, FileBasicInfo, ×, sizeof times)) {
+ FILETIME changed;
+ changed.dwLowDateTime = times.ChangeTime.LowPart;
+ changed.dwHighDateTime = times.ChangeTime.HighPart;
+ scr_stats_set_filetime(out, SCR_STATS_CTIME, changed);
+ }
+ FILE_STANDARD_INFO standard;
+ if (GetFileInformationByHandleEx(os_handle, FileStandardInfo, &standard, sizeof standard)) {
+ out->fields[SCR_STATS_BLOCKS] = (uint64_t)standard.AllocationSize.QuadPart >> 9;
+ out->fields[SCR_STATS_NLINK] = standard.NumberOfLinks;
+ }
+ return out;
+}
#else
+/* libuv's uv__to_stat: macOS, FreeBSD and NetBSD report a creation time;
+ * elsewhere birthtime repeats the change time. */
static ScrStats *scr_stats_of(const struct stat *st) {
ScrStats *s = scr_stats_new();
s->is_file = S_ISREG(st->st_mode);
s->is_dir = S_ISDIR(st->st_mode);
s->is_symlink = S_ISLNK(st->st_mode);
- s->dev = (double)st->st_dev;
- s->ino = (double)st->st_ino;
+ s->fields[SCR_STATS_DEV] = (uint64_t)st->st_dev;
+ s->fields[SCR_STATS_MODE] = (uint64_t)st->st_mode;
+ s->fields[SCR_STATS_NLINK] = (uint64_t)st->st_nlink;
+ s->fields[SCR_STATS_UID] = (uint64_t)st->st_uid;
+ s->fields[SCR_STATS_GID] = (uint64_t)st->st_gid;
+ s->fields[SCR_STATS_RDEV] = (uint64_t)st->st_rdev;
+ s->fields[SCR_STATS_BLKSIZE] = (uint64_t)st->st_blksize;
+ s->fields[SCR_STATS_INO] = (uint64_t)st->st_ino;
+ s->fields[SCR_STATS_SIZE] = (uint64_t)st->st_size;
+ s->fields[SCR_STATS_BLOCKS] = (uint64_t)st->st_blocks;
#if defined(__APPLE__)
- s->atime_ms = (double)st->st_atimespec.tv_sec * 1000.0 +
- (double)st->st_atimespec.tv_nsec / 1e6;
- s->mtime_ms = (double)st->st_mtimespec.tv_sec * 1000.0 +
- (double)st->st_mtimespec.tv_nsec / 1e6;
- s->ctime_ms = (double)st->st_ctimespec.tv_sec * 1000.0 +
- (double)st->st_ctimespec.tv_nsec / 1e6;
+ scr_stats_set_time(s, SCR_STATS_ATIME, st->st_atimespec.tv_sec, st->st_atimespec.tv_nsec);
+ scr_stats_set_time(s, SCR_STATS_MTIME, st->st_mtimespec.tv_sec, st->st_mtimespec.tv_nsec);
+ scr_stats_set_time(s, SCR_STATS_CTIME, st->st_ctimespec.tv_sec, st->st_ctimespec.tv_nsec);
+ scr_stats_set_time(s, SCR_STATS_BIRTHTIME, st->st_birthtimespec.tv_sec, st->st_birthtimespec.tv_nsec);
#else
- s->atime_ms = (double)st->st_atim.tv_sec * 1000.0 +
- (double)st->st_atim.tv_nsec / 1e6;
- s->mtime_ms = (double)st->st_mtim.tv_sec * 1000.0 +
- (double)st->st_mtim.tv_nsec / 1e6;
- s->ctime_ms = (double)st->st_ctim.tv_sec * 1000.0 +
- (double)st->st_ctim.tv_nsec / 1e6;
+ scr_stats_set_time(s, SCR_STATS_ATIME, st->st_atim.tv_sec, st->st_atim.tv_nsec);
+ scr_stats_set_time(s, SCR_STATS_MTIME, st->st_mtim.tv_sec, st->st_mtim.tv_nsec);
+ scr_stats_set_time(s, SCR_STATS_CTIME, st->st_ctim.tv_sec, st->st_ctim.tv_nsec);
+#if defined(__FreeBSD__) || defined(__NetBSD__)
+ scr_stats_set_time(s, SCR_STATS_BIRTHTIME, st->st_birthtim.tv_sec, st->st_birthtim.tv_nsec);
+#else
+ scr_stats_set_time(s, SCR_STATS_BIRTHTIME, st->st_ctim.tv_sec, st->st_ctim.tv_nsec);
+#endif
#endif
- s->blocks = (double)st->st_blocks;
- s->nlink = (double)st->st_nlink;
- s->size = (double)st->st_size;
return s;
}
+
+#if defined(__linux__) && defined(SYS_statx)
+/* The kernel's struct statx (include/uapi/linux/stat.h). libuv reads stat
+ * results through statx, which also reports the creation time. */
+typedef struct {
+ int64_t tv_sec;
+ uint32_t tv_nsec;
+ int32_t reserved;
+} ScrStatxTime;
+
+typedef struct {
+ uint32_t mask;
+ uint32_t blksize;
+ uint64_t attributes;
+ uint32_t nlink;
+ uint32_t uid;
+ uint32_t gid;
+ uint16_t mode;
+ uint16_t spare0;
+ uint64_t ino;
+ uint64_t size;
+ uint64_t blocks;
+ uint64_t attributes_mask;
+ ScrStatxTime atime, btime, ctime, mtime;
+ uint32_t rdev_major, rdev_minor;
+ uint32_t dev_major, dev_minor;
+ uint64_t spare[14];
+} ScrStatx;
+
+_Static_assert(sizeof(ScrStatx) == 256, "struct statx is 256 bytes");
+
+/* glibc's and musl's makedev encoding, which libuv applies to statx. */
+static uint64_t scr_stats_makedev(uint64_t major, uint64_t minor) {
+ return ((major & 0xfffff000u) << 32) | ((major & 0xfffu) << 8) |
+ ((minor & 0xffffff00u) << 12) | (minor & 0xffu);
+}
+
+static SCR_TL bool scr_stats_no_statx = false;
+
+static ScrStats *scr_stats_of_statx(const ScrStatx *sx) {
+ ScrStats *s = scr_stats_new();
+ unsigned type = sx->mode & SCR_STATS_IFMT;
+ s->is_file = type == SCR_STATS_IFREG;
+ s->is_dir = type == SCR_STATS_IFDIR;
+ s->is_symlink = type == SCR_STATS_IFLNK;
+ s->fields[SCR_STATS_DEV] = scr_stats_makedev(sx->dev_major, sx->dev_minor);
+ s->fields[SCR_STATS_MODE] = sx->mode;
+ s->fields[SCR_STATS_NLINK] = sx->nlink;
+ s->fields[SCR_STATS_UID] = sx->uid;
+ s->fields[SCR_STATS_GID] = sx->gid;
+ s->fields[SCR_STATS_RDEV] = scr_stats_makedev(sx->rdev_major, sx->rdev_minor);
+ s->fields[SCR_STATS_BLKSIZE] = sx->blksize;
+ s->fields[SCR_STATS_INO] = sx->ino;
+ s->fields[SCR_STATS_SIZE] = sx->size;
+ s->fields[SCR_STATS_BLOCKS] = sx->blocks;
+ scr_stats_set_time(s, SCR_STATS_ATIME, sx->atime.tv_sec, sx->atime.tv_nsec);
+ scr_stats_set_time(s, SCR_STATS_MTIME, sx->mtime.tv_sec, sx->mtime.tv_nsec);
+ scr_stats_set_time(s, SCR_STATS_CTIME, sx->ctime.tv_sec, sx->ctime.tv_nsec);
+ scr_stats_set_time(s, SCR_STATS_BIRTHTIME, sx->btime.tv_sec, sx->btime.tv_nsec);
+ return s;
+}
+#endif
+
+/* stat, lstat, or fstat when path is NULL, as libuv runs them: statx on
+ * Linux, else the classic call. NULL with *error set on failure. */
+static ScrStats *scr_stats_posix(const char *path, int fd, bool no_follow, int *error) {
+#if defined(__linux__) && defined(SYS_statx)
+ if (!scr_stats_no_statx) {
+ ScrStatx sx;
+ /* STATX_BASIC_STATS | STATX_BTIME */
+ long rc = path ? syscall(SYS_statx, AT_FDCWD, path, no_follow ? AT_SYMLINK_NOFOLLOW : 0, 0xfffu, &sx)
+ : syscall(SYS_statx, fd, "", AT_EMPTY_PATH, 0xfffu, &sx);
+ if (rc == 0) return scr_stats_of_statx(&sx);
+ if (rc == -1 && errno != EINVAL && errno != EPERM && errno != ENOSYS && errno != EOPNOTSUPP) {
+ *error = errno;
+ return NULL;
+ }
+ /* A seccomp filter or an old kernel: libuv uses stat from then on. */
+ scr_stats_no_statx = true;
+ }
+#endif
+ struct stat st;
+ int rc = path ? (no_follow ? lstat(path, &st) : stat(path, &st)) : fstat(fd, &st);
+ if (rc != 0) {
+ *error = errno;
+ return NULL;
+ }
+ return scr_stats_of(&st);
+}
#endif
/* Symbolic link targets are stored verbatim on POSIX. Windows junctions
@@ -5418,12 +5643,10 @@ ScrStats *scr_fs_stat(ScrStr *path) {
#ifdef _WIN32
return scr_stats_of_path(path, "stat", false);
#else
- struct stat st;
- if (stat(path->data, &st) != 0) { /* follows symlinks, like Node's statSync */
- scr_fs_throw(errno, "stat", path);
- return NULL;
- }
- return scr_stats_of(&st);
+ int error = 0;
+ ScrStats *s = scr_stats_posix(path->data, -1, false, &error); /* follows symlinks */
+ if (!s) scr_fs_throw(error, "stat", path);
+ return s;
#endif
}
@@ -5431,38 +5654,330 @@ ScrStats *scr_fs_lstat(ScrStr *path) {
#ifdef _WIN32
return scr_stats_of_path(path, "lstat", true);
#else
+ int error = 0;
+ ScrStats *s = scr_stats_posix(path->data, -1, true, &error); /* no follow */
+ if (!s) scr_fs_throw(error, "lstat", path);
+ return s;
+#endif
+}
+
+ScrStats *scr_stats_of_fd(int fd, int *error) {
+#ifdef _WIN32
struct stat st;
- if (lstat(path->data, &st) != 0) { /* no follow; Node reports lstat */
- scr_fs_throw(errno, "lstat", path);
+ if (fstat(fd, &st) != 0) {
+ *error = errno;
return NULL;
}
- return scr_stats_of(&st);
+ return scr_stats_of_fd_win(fd, &st);
+#else
+ return scr_stats_posix(NULL, fd, false, error);
#endif
}
ScrStats *scr_fs_fstat(double fd) {
- struct stat st;
- if (fstat((int)fd, &st) != 0) {
- scr_fs_throw_nopath(errno, "fstat");
- return NULL;
- }
-#ifdef _WIN32
- ScrStats *s = scr_stats_new();
- s->is_file = S_ISREG(st.st_mode);
- s->is_dir = S_ISDIR(st.st_mode);
- s->is_symlink = false;
- s->dev = (double)st.st_dev;
- s->ino = (double)st.st_ino;
- s->size = (double)st.st_size;
- s->blocks = s->size <= 0 ? 0 : ceil(s->size / 512.0);
- s->nlink = (double)st.st_nlink;
- s->atime_ms = (double)st.st_atime * 1000.0;
- s->mtime_ms = (double)st.st_mtime * 1000.0;
- s->ctime_ms = (double)st.st_ctime * 1000.0;
+ int error = 0;
+ ScrStats *s = scr_stats_of_fd((int)fd, &error);
+ if (!s) scr_fs_throw_nopath(error, "fstat");
return s;
-#else
- return scr_stats_of(&st);
+}
+
+/* ── Stats objects ───────────────────────────────────────────────────
+ * Node's Stats and BigIntStats over a snapshot. The integers and the *Ms
+ * fields (and BigIntStats' *Ns fields) are own properties in the binding's
+ * order. Stats.prototype holds _checkModeProperty and lazy atime, mtime,
+ * ctime and birthtime accessors, which define an own Date the first time
+ * they run. The type tests (isFile, ...) sit on StatsBase.prototype and go
+ * through this._checkModeProperty(S_IF...), which reads this.mode. */
+
+static SCR_TL ScrDyn *scr_stats_base_proto = NULL;
+static SCR_TL ScrDyn *scr_stats_protos[2] = { NULL, NULL }; /* Stats, BigIntStats */
+
+static const char *const scr_stats_date_names[4] = { "atime", "mtime", "ctime", "birthtime" };
+
+/* Object.defineProperty(target, name, { value, writable } or { get, set },
+ * enumerable, configurable: true). value, get and set move in. */
+static bool scr_stats_define(ScrDyn *target, const char *name, ScrDyn *value, ScrDyn *get, ScrDyn *set, bool enumerable) {
+ ScrDyn *descriptor = scr_dyn_new_obj();
+ if (value) {
+ scr_dyn_obj_set(descriptor, "value", 5, value);
+ scr_dyn_obj_set(descriptor, "writable", 8, scr_dyn_new_bool(true));
+ }
+ if (get) scr_dyn_obj_set(descriptor, "get", 3, get);
+ if (set) scr_dyn_obj_set(descriptor, "set", 3, set);
+ scr_dyn_obj_set(descriptor, "enumerable", 10, scr_dyn_new_bool(enumerable));
+ scr_dyn_obj_set(descriptor, "configurable", 12, scr_dyn_new_bool(true));
+ ScrStr *text = scr_str_new(name, strlen(name));
+ ScrDyn *key = scr_dyn_new_str(text);
+ scr_str_release(text);
+ ScrDyn *defined = scr_dyn_define_property(target, key, descriptor);
+ scr_dyn_release(key);
+ scr_dyn_release(descriptor);
+ if (!defined) return false;
+ scr_dyn_release(defined);
+ return true;
+}
+
+/* `this` for a prototype member, or NULL with Node's TypeError when the
+ * receiver is undefined or null and a property read would throw. */
+static ScrDyn *scr_stats_receiver(const char *reading) {
+ ScrDyn *self = scr_dyn_this_get();
+ if (self->kind != SCR_DYN_UNDEF && self->kind != SCR_DYN_NULL) return self;
+ char message[96];
+ int n = snprintf(message, sizeof message, "Cannot read properties of %s (reading '%s')",
+ self->kind == SCR_DYN_UNDEF ? "undefined" : "null", reading);
+ scr_dyn_release(self);
+ scr_throw_error_msg(SCR_ERR_TYPE, message, (size_t)n);
+ return NULL;
+}
+
+/* The lazy Date getter: setOwnProperty(this, name, new Date(Math.round(
+ * Number(this.Ms)))). */
+static ScrDyn *scr_stats_date_get(int which) {
+ char ms_name[16];
+ int len = snprintf(ms_name, sizeof ms_name, "%sMs", scr_stats_date_names[which]);
+ ScrDyn *self = scr_stats_receiver(ms_name);
+ if (!self) return NULL;
+ ScrDyn *ms = scr_dyn_obj_read(self, ms_name, (size_t)len);
+ double number = ms ? scr_dyn_number_constructor(ms) : NAN;
+ scr_dyn_release(ms);
+ ScrDyn *date = NULL;
+ if (!scr_exc_pending()) {
+ ScrDyn *arguments = scr_dyn_new_arr();
+ scr_dyn_arr_push(arguments, scr_dyn_new_num(scr_stats_round(number)));
+ date = scr_dyn_native_date_new(arguments);
+ scr_dyn_release(arguments);
+ }
+ if (date && !scr_stats_define(self, scr_stats_date_names[which], scr_dyn_retain(date), NULL, NULL, true)) {
+ scr_dyn_release(date);
+ date = NULL;
+ }
+ scr_dyn_release(self);
+ return date;
+}
+
+/* The setter: setOwnProperty(this, name, value). */
+static ScrDyn *scr_stats_date_set(int which, ScrDyn *const *args, size_t argc) {
+ ScrDyn *self = scr_dyn_this_get();
+ bool ok = scr_stats_define(self, scr_stats_date_names[which],
+ scr_dyn_retain(argc > 0 ? args[0] : scr_dyn_undefined()), NULL, NULL, true);
+ scr_dyn_release(self);
+ return ok ? scr_dyn_retain(scr_dyn_undefined()) : NULL;
+}
+
+#define SCR_STATS_DATE_ACCESSORS(index) \
+ static ScrDyn *scr_stats_date_get_##index(ScrClosure *closure, ScrDyn *const *args, size_t argc) { \
+ (void)closure; \
+ (void)args; \
+ (void)argc; \
+ return scr_stats_date_get(index); \
+ } \
+ static ScrDyn *scr_stats_date_set_##index(ScrClosure *closure, ScrDyn *const *args, size_t argc) { \
+ (void)closure; \
+ return scr_stats_date_set(index, args, argc); \
+ }
+SCR_STATS_DATE_ACCESSORS(0)
+SCR_STATS_DATE_ACCESSORS(1)
+SCR_STATS_DATE_ACCESSORS(2)
+SCR_STATS_DATE_ACCESSORS(3)
+#undef SCR_STATS_DATE_ACCESSORS
+
+static const ScrDynThunk scr_stats_date_getters[4] = {
+ scr_stats_date_get_0, scr_stats_date_get_1, scr_stats_date_get_2, scr_stats_date_get_3,
+};
+static const ScrDynThunk scr_stats_date_setters[4] = {
+ scr_stats_date_set_0, scr_stats_date_set_1, scr_stats_date_set_2, scr_stats_date_set_3,
+};
+
+/* _checkModeProperty(property): (this.mode & S_IFMT) === property, with
+ * BigInt operands on BigIntStats. */
+static ScrDyn *scr_stats_check_mode(ScrDyn *const *args, size_t argc, bool bigint) {
+ const ScrDyn *property = argc > 0 ? args[0] : scr_dyn_undefined();
+#ifdef _WIN32
+ if (property->kind == SCR_DYN_NUM &&
+ (property->v.num == SCR_STATS_IFIFO || property->v.num == SCR_STATS_IFBLK || property->v.num == SCR_STATS_IFSOCK))
+ return scr_dyn_new_bool(false);
#endif
+ ScrDyn *self = scr_stats_receiver("mode");
+ if (!self) return NULL;
+ ScrDyn *mode = scr_dyn_obj_read(self, "mode", 4);
+ scr_dyn_release(self);
+ if (!mode) return NULL;
+ ScrDyn *mask;
+ if (bigint) {
+ ScrBigInt *value = scr_bigint_from_u64(SCR_STATS_IFMT);
+ mask = scr_dyn_new_bigint(value);
+ scr_bigint_release(value);
+ } else mask = scr_dyn_new_num(SCR_STATS_IFMT);
+ ScrStr *operation = scr_str_new("&", 1);
+ ScrDyn *masked = scr_dyn_bitwise(mode, mask, operation);
+ scr_str_release(operation);
+ scr_dyn_release(mask);
+ scr_dyn_release(mode);
+ if (!masked) return NULL;
+ ScrDyn *expected = NULL;
+ if (bigint) {
+ ScrBigInt *value = scr_dyn_bigint_constructor(property);
+ if (value) {
+ expected = scr_dyn_new_bigint(value);
+ scr_bigint_release(value);
+ }
+ } else expected = scr_dyn_retain((ScrDyn *)property);
+ ScrDyn *result = expected ? scr_dyn_new_bool(scr_dyn_strict_eq(masked, expected)) : NULL;
+ scr_dyn_release(expected);
+ scr_dyn_release(masked);
+ return result;
+}
+
+static ScrDyn *scr_stats_check_mode_number(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
+ (void)closure;
+ return scr_stats_check_mode(args, argc, false);
+}
+
+static ScrDyn *scr_stats_check_mode_bigint(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
+ (void)closure;
+ return scr_stats_check_mode(args, argc, true);
+}
+
+/* isFile() and the other type tests: this._checkModeProperty(S_IF...). */
+static ScrDyn *scr_stats_type_test(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
+ (void)args;
+ (void)argc;
+ ScrDyn *self = scr_stats_receiver("_checkModeProperty");
+ if (!self) return NULL;
+ ScrDyn *check = scr_dyn_obj_read(self, "_checkModeProperty", 18);
+ ScrDyn *result = NULL;
+ if (check) {
+ ScrDyn *type = scr_dyn_new_num(scr_box_get_f64(closure->caps[0]));
+ scr_dyn_this_push_dyn(self);
+ result = scr_dyn_call(check, &type, 1, "this._checkModeProperty");
+ scr_dyn_this_pop();
+ scr_dyn_release(type);
+ scr_dyn_release(check);
+ }
+ scr_dyn_release(self);
+ return result;
+}
+
+/* The prototypes' constructor members are only named; calling one has no
+ * lowering. The type tests and _checkModeProperty are anonymous functions in
+ * Node (assigned function expressions), so they carry no name. */
+static ScrDyn *scr_stats_constructor(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
+ (void)closure;
+ (void)args;
+ (void)argc;
+ static const char message[] = "Calling the fs.Stats constructors has no lowering";
+ scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020");
+ return NULL;
+}
+
+static ScrDyn *scr_stats_method(ScrDynThunk thunk, uint32_t arity, const char *name, double type) {
+ ScrClosure *closure = scr_closure_new(NULL, 1);
+ closure->caps[0] = scr_box_new(SCR_BOX_F64);
+ scr_box_set_f64(closure->caps[0], type);
+ return scr_dyn_new_func(closure, thunk, arity, "native:fs.Stats", name);
+}
+
+static void scr_stats_protos_teardown(void) {
+ for (size_t i = 0; i < 2; i++) {
+ if (!scr_stats_protos[i]) continue;
+ scr_stats_protos[i]->v.obj.class_name = NULL;
+ scr_dyn_release(scr_stats_protos[i]);
+ scr_stats_protos[i] = NULL;
+ }
+ if (scr_stats_base_proto) {
+ scr_stats_base_proto->v.obj.class_name = NULL;
+ scr_dyn_release(scr_stats_base_proto);
+ scr_stats_base_proto = NULL;
+ }
+}
+
+static ScrDyn *scr_stats_prototype(bool bigint) {
+ if (scr_stats_protos[bigint]) return scr_stats_protos[bigint];
+ if (!scr_stats_base_proto) {
+ static const struct {
+ const char *name;
+ unsigned type;
+ } tests[] = {
+ { "isDirectory", SCR_STATS_IFDIR },
+ { "isFile", SCR_STATS_IFREG },
+ { "isBlockDevice", SCR_STATS_IFBLK },
+ { "isCharacterDevice", SCR_STATS_IFCHR },
+ { "isSymbolicLink", SCR_STATS_IFLNK },
+ { "isFIFO", SCR_STATS_IFIFO },
+ { "isSocket", SCR_STATS_IFSOCK },
+ };
+ ScrDyn *base = scr_dyn_new_obj();
+ base->v.obj.class_name = "StatsBase";
+ scr_stats_define(base, "constructor", scr_stats_method(scr_stats_constructor, 10, "StatsBase", 0), NULL, NULL, false);
+ for (size_t i = 0; i < sizeof tests / sizeof tests[0]; i++)
+ scr_dyn_obj_set(base, tests[i].name, strlen(tests[i].name),
+ scr_stats_method(scr_stats_type_test, 0, NULL, tests[i].type));
+ scr_stats_base_proto = base;
+ scr_atexit(scr_stats_protos_teardown);
+ }
+ const char *name = bigint ? "BigIntStats" : "Stats";
+ ScrDyn *proto = scr_dyn_new_obj();
+ proto->v.obj.class_name = name;
+ proto->prototype = scr_dyn_retain(scr_stats_base_proto);
+ scr_stats_define(proto, "constructor", scr_stats_method(scr_stats_constructor, 14, name, 0), NULL, NULL, false);
+ for (int i = 0; i < 4; i++)
+ scr_stats_define(proto, scr_stats_date_names[i], NULL,
+ scr_stats_method(scr_stats_date_getters[i], 0, "get", 0),
+ scr_stats_method(scr_stats_date_setters[i], 1, "set", 0), true);
+ scr_dyn_obj_set(proto, "_checkModeProperty", 18,
+ scr_stats_method(bigint ? scr_stats_check_mode_bigint : scr_stats_check_mode_number, 1, NULL, 0));
+ scr_stats_protos[bigint] = proto;
+ return proto;
+}
+
+static ScrDyn *scr_stats_bigint_dyn(ScrBigInt *value /* moves */) {
+ ScrDyn *boxed = scr_dyn_new_bigint(value);
+ scr_bigint_release(value);
+ return boxed;
+}
+
+ScrDyn *scr_stats_dyn(ScrStats *s, bool bigint) {
+ static const char *const names[SCR_STATS_FIELDS] = {
+ "dev", "mode", "nlink", "uid", "gid", "rdev", "blksize", "ino", "size", "blocks",
+ };
+ ScrDyn *object = scr_dyn_new_obj();
+ object->prototype = scr_dyn_retain(scr_stats_prototype(bigint));
+ for (size_t i = 0; i < SCR_STATS_FIELDS; i++) {
+ /* The binding's BigInt64Array holds the integers as signed values. */
+ ScrDyn *value = bigint ? scr_stats_bigint_dyn(scr_bigint_from_i64((int64_t)s->fields[i]))
+ : scr_dyn_new_num((double)s->fields[i]);
+ scr_dyn_obj_set(object, names[i], strlen(names[i]), value);
+ }
+ ScrDyn *ns[4] = { NULL, NULL, NULL, NULL };
+ for (int i = 0; i < 4; i++) {
+ char name[16];
+ int len = snprintf(name, sizeof name, "%sMs", scr_stats_date_names[i]);
+ if (!bigint) {
+ scr_dyn_obj_set(object, name, (size_t)len, scr_dyn_new_num(scr_stats_ms(s, i)));
+ continue;
+ }
+ /* nsFromTimeSpecBigInt, then the millisecond quotient. */
+ ScrBigInt *sec = scr_bigint_from_i64(s->sec[i]);
+ ScrBigInt *billion = scr_bigint_from_i64(1000000000);
+ ScrBigInt *scaled = scr_bigint_mul(sec, billion);
+ ScrBigInt *nsec = scr_bigint_from_i64(s->nsec[i]);
+ ScrBigInt *total = scr_bigint_add(scaled, nsec);
+ ScrBigInt *million = scr_bigint_from_i64(1000000);
+ ScrBigInt *ms = scr_bigint_div(total, million);
+ scr_bigint_release(sec);
+ scr_bigint_release(billion);
+ scr_bigint_release(scaled);
+ scr_bigint_release(nsec);
+ scr_bigint_release(million);
+ scr_dyn_obj_set(object, name, (size_t)len, scr_stats_bigint_dyn(ms));
+ ns[i] = scr_stats_bigint_dyn(total);
+ }
+ for (int i = 0; bigint && i < 4; i++) {
+ char name[16];
+ int len = snprintf(name, sizeof name, "%sNs", scr_stats_date_names[i]);
+ scr_dyn_obj_set(object, name, (size_t)len, ns[i]);
+ }
+ return object;
}
/* The names of one scandir listing: byte order on POSIX (libuv sorts),
diff --git a/packages/runtime/src/scr_lib.h b/packages/runtime/src/scr_lib.h
index 5845a584f..ea9dcadbc 100644
--- a/packages/runtime/src/scr_lib.h
+++ b/packages/runtime/src/scr_lib.h
@@ -365,6 +365,30 @@ double scr_stats_nlink(ScrStats *s);
double scr_stats_atime_ms(ScrStats *s); /* ms with the sub-second fraction */
double scr_stats_mtime_ms(ScrStats *s); /* ms with the ns fraction */
double scr_stats_ctime_ms(ScrStats *s); /* ms with the ns fraction */
+double scr_stats_birthtime_ms(ScrStats *s);
+double scr_stats_mode(ScrStats *s);
+double scr_stats_uid(ScrStats *s);
+double scr_stats_gid(ScrStats *s);
+double scr_stats_rdev(ScrStats *s);
+double scr_stats_blksize(ScrStats *s);
+/* The type tests Node derives from mode (false on Windows for FIFOs, block
+ * devices and sockets). */
+bool scr_stats_is_block_device(ScrStats *s);
+bool scr_stats_is_character_device(ScrStats *s);
+bool scr_stats_is_fifo(ScrStats *s);
+bool scr_stats_is_socket(ScrStats *s);
+/* The Date fields' time values: the *Ms value rounded, as Node's
+ * dateFromMs does. */
+double scr_stats_atime(ScrStats *s);
+double scr_stats_mtime(ScrStats *s);
+double scr_stats_ctime(ScrStats *s);
+double scr_stats_birthtime(ScrStats *s);
+/* fstat without throwing: +1, or NULL with *error set to the errno. */
+ScrStats *scr_stats_of_fd(int fd, int *error);
+/* Node's Stats (or BigIntStats) object for the snapshot: own fields in the
+ * binding's order, lazy Date accessors and the type tests on the
+ * prototypes. Borrows s; +1. */
+ScrDyn *scr_stats_dyn(ScrStats *s, bool bigint);
/* fs/promises: the SAME sync operations, minting an already-settled
* promise — success fulfills, failure REJECTS with the would-be thrown
diff --git a/packages/runtime/src/scr_net.c b/packages/runtime/src/scr_net.c
index 225391c35..461acbedc 100644
--- a/packages/runtime/src/scr_net.c
+++ b/packages/runtime/src/scr_net.c
@@ -365,6 +365,7 @@ struct ScrNetServer {
int fd; /* -1 while not listening */
int port;
bool listening;
+ bool unrefd; /* server.unref(): does not keep the loop alive */
bool closing; /* close() called; 'close' fires when conns drain */
bool close_emitted; /* settled: off the registry, listeners dropped */
bool emit_listening;
@@ -420,6 +421,7 @@ struct ScrNetSocket {
int kind; /* FIRST: poller udata routes on it */
size_t rc;
int fd; /* -1 once closed */
+ bool unrefd; /* socket.unref(): does not keep the loop alive */
bool connecting;
bool rd_eof; /* peer FIN seen (or read half abandoned) */
bool enc_utf8; /* setEncoding('utf8'): 'data' delivers strings */
@@ -3039,11 +3041,24 @@ static void scr_net_sweep(void) {
/* ── the loop hooks (scr_async.c) ────────────────────────────────────── */
+/* Loop liveness: a ref'd server or socket, or pending protocol work. */
static bool scr_net_pending(void) {
- if (scr_net_servers != NULL || scr_net_socks != NULL) return true;
+ for (ScrNetServer *srv = scr_net_servers; srv; srv = srv->next)
+ if (!srv->unrefd) return true;
+ for (ScrNetSocket *sock = scr_net_socks; sock; sock = sock->next)
+ if (!sock->unrefd) return true;
return scr_net_proto_pending();
}
+/* Any handle, ref'd or not: the loop polls the net fd while it runs. */
+static bool scr_net_io(void) { return scr_net_servers != NULL || scr_net_socks != NULL; }
+
+/* socket.ref() / socket.unref() and the server pair: whether the handle
+ * keeps the event loop alive (Node's default is ref'd). */
+void scr_net_sock_set_ref(ScrNetSocket *s, bool ref) { s->unrefd = !ref; }
+void scr_net_server_set_ref(ScrNetServer *s, bool ref) { s->unrefd = !ref; }
+bool scr_net_sock_has_ref(ScrNetSocket *s) { return !s->unrefd; }
+
static int scr_net_pollfd(void) {
return scr_net_poller != NULL ? scrp_poller_fd(scr_net_poller) : -1;
}
@@ -3203,6 +3218,7 @@ void scr_net_install(void) {
installed = true;
scr_atexit(scr_net_cleanup_atexit);
scr_loop_set_net(&scr_net_pending, &scr_net_dispatch, &scr_net_pollfd);
+ scr_loop_set_net_io(&scr_net_io);
}
/* ── checked-dynamic handle dispatch (SCR_DYNH_NET_SOCKET ops) ─────────
@@ -3385,13 +3401,17 @@ static ScrDyn *scr_net_dynh_sock_invoke(void *h, ScrDyn *self, const char *metho
scr_str_release(str);
return d;
}
+ if (strcmp(method, "ref") == 0 || strcmp(method, "unref") == 0) {
+ scr_net_sock_set_ref(s, method[0] == 'r');
+ return scr_dyn_retain(self);
+ }
if (scr_net_dynh_tls != NULL) {
ScrDyn *out = NULL;
if (scr_net_dynh_tls->invoke(s, method, args, argc, &out)) return out;
}
{
static const char *const known[] = { "connect", "setKeepAlive",
- "address", "ref", "unref", "cork", "uncork", "read", "unpipe",
+ "address", "cork", "uncork", "read", "unpipe",
"off", "removeListener", "removeAllListeners", "emit", "prependListener",
"prependOnceListener", "listenerCount", "listeners", "resetAndDestroy", NULL };
for (size_t i = 0; known[i]; i++) {
@@ -3619,6 +3639,10 @@ static ScrDyn *scr_net_dynh_srv_invoke(void *h, ScrDyn *self, const char *method
scr_dyn_arg_type_fail("socket", "an instance of Socket", sockd);
return NULL;
}
+ if (strcmp(method, "ref") == 0 || strcmp(method, "unref") == 0) {
+ scr_net_server_set_ref((ScrNetServer *)h, method[0] == 'r');
+ return scr_dyn_retain(self);
+ }
if (strcmp(method, "toString") == 0) {
ScrStr *str = scr_str_new("[object Object]", 15);
ScrDyn *d = scr_dyn_new_str(str);
@@ -3626,7 +3650,7 @@ static ScrDyn *scr_net_dynh_srv_invoke(void *h, ScrDyn *self, const char *method
return d;
}
{
- static const char *const known[] = { "ref", "unref", "setTimeout", "getConnections",
+ static const char *const known[] = { "setTimeout", "getConnections",
"off", "removeListener", "removeAllListeners", "emit", "prependListener",
"prependOnceListener", "listenerCount", "listeners", "closeAllConnections",
"closeIdleConnections", NULL };
diff --git a/packages/runtime/src/scr_net.h b/packages/runtime/src/scr_net.h
index 668d3014c..117a242c7 100644
--- a/packages/runtime/src/scr_net.h
+++ b/packages/runtime/src/scr_net.h
@@ -137,6 +137,9 @@ void scr_net_listen_opts_reuse_port(ScrNetServer *s, double port, ScrStr *host /
ScrClosure *cb /*moves, nullable*/);
double scr_net_server_port(ScrNetServer *s); /* address().port */
bool scr_net_server_listening(ScrNetServer *s);
+void scr_net_sock_set_ref(ScrNetSocket *s, bool ref);
+void scr_net_server_set_ref(ScrNetServer *s, bool ref);
+bool scr_net_sock_has_ref(ScrNetSocket *s);
/* Writable http.Server timeout property storage. `field` is the compiler
* ABI selector: timeout, keepAliveTimeout, headersTimeout, requestTimeout,
* keepAliveTimeoutBuffer. Typed reads validate that no dynamic write left
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/src/scr_string.c b/packages/runtime/src/scr_string.c
index 88273c6d0..e9e31f7d6 100644
--- a/packages/runtime/src/scr_string.c
+++ b/packages/runtime/src/scr_string.c
@@ -700,6 +700,28 @@ ScrStr *scr_str_concat_move(ScrStr *a, ScrStr *b) {
return r;
}
+ScrStr *scr_str_concat_move_char_code(ScrStr *a, double code) {
+ uint32_t cp = scr_to_uint32(code) & 0xFFFFu;
+ if (cp < 0x80 && a->rc == 1 && a->cap > a->len) {
+ /* scr_str_concat's in-place append of one ASCII byte: the string stays
+ * ASCII if it was, and a non-ASCII string's index facts below the old
+ * end stay valid (scr_sidx_concat_append). */
+ size_t oldlen = a->len;
+ scr_short_forget(a);
+ scr_str_hash_forget(a);
+ a->data[oldlen] = (char)cp;
+ a->len = oldlen + 1;
+ a->data[oldlen + 1] = '\0';
+ if (!a->ascii) scr_sidx_concat_append(a, oldlen);
+ return a;
+ }
+ ScrStr *c = scr_str_from_char_code_one(code);
+ ScrStr *r = scr_str_concat(a, c);
+ scr_str_release(a);
+ scr_str_release(c);
+ return r;
+}
+
ScrStr *scr_str_concat_mixed_move(ScrStr *head, ScrStr *const *parts, const double *nums,
size_t count) {
/* A head selects the general body directly (see scr_str_concat_mixed). */
diff --git a/packages/runtime/src/scr_string.h b/packages/runtime/src/scr_string.h
index 9212effaf..dc942e45d 100644
--- a/packages/runtime/src/scr_string.h
+++ b/packages/runtime/src/scr_string.h
@@ -242,6 +242,10 @@ ScrStr *scr_str_concat_mixed(ScrStr *head, ScrStr *const *parts, const double *n
* else can observe `s`): one call instead of a snapshot retain, the
* binding's release and the snapshot's release around a borrowing concat. */
ScrStr *scr_str_concat_move(ScrStr *a, ScrStr *b);
+/* scr_str_concat_move(a, String.fromCharCode(code)) without building the
+ * one-character string: an ASCII character appends to a uniquely owned a
+ * in place. */
+ScrStr *scr_str_concat_move_char_code(ScrStr *a, double code);
ScrStr *scr_str_concat_mixed_move(ScrStr *head, ScrStr *const *parts, const double *nums,
size_t count);
/* a + String(x): scr_str_concat's contract with a number right operand. */
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/5333-record-boundary-nullable-argument.ts b/tests/corpus/5333-record-boundary-nullable-argument.ts
new file mode 100644
index 000000000..d3d007b75
--- /dev/null
+++ b/tests/corpus/5333-record-boundary-nullable-argument.ts
@@ -0,0 +1,33 @@
+// A record parameter widened from a literal argument also receives a loop
+// element whose storage admits undefined. The element converts to the
+// widened layout after the usual checked extraction of its record arm.
+interface Item {
+ name: string;
+ kind: number;
+}
+class Bag {
+ readonly names = new Map();
+ add(entry: Item): void {
+ const existing = this.names.get(entry.name);
+ if (existing === undefined || existing.kind < entry.kind) this.names.set(entry.name, entry);
+ }
+}
+const seen = new Set();
+seen.add("x");
+const parts: string[] = ["a", "d"];
+const bag = new Bag();
+const at = Number(process.argv.length > 99);
+bag.add({ name: parts[at], kind: 1 });
+function grow(xs: T[], index: number, value: T): T[] {
+ xs[index] = value;
+ return xs;
+}
+const sparse = grow([], 4, 1);
+const groups = [
+ { results: [{ name: "b", kind: 2 }, { name: "c", kind: 3 }] },
+ { results: [{ name: "b", kind: 5 }] },
+];
+for (const g of groups) for (const res of g.results) bag.add(res);
+const order: string[] = [];
+for (const [name, entry] of bag.names) order.push(name + ":" + entry.kind);
+console.log(order.join(","), sparse.length, seen.size);
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/5541-fs-stats-fields.cjs b/tests/corpus/5541-fs-stats-fields.cjs
new file mode 100644
index 000000000..4db83847e
--- /dev/null
+++ b/tests/corpus/5541-fs-stats-fields.cjs
@@ -0,0 +1,89 @@
+// fs.Stats fields: statSync, lstatSync and fstatSync expose mode, uid,
+// gid, rdev, blksize, birthtimeMs, the Date fields (the *Ms values
+// rounded) and every type test. The callback forms build Node's Stats and
+// BigIntStats objects: own fields in the binding's order, lazy Date
+// accessors that become own properties when read, and type tests that go
+// through this._checkModeProperty and read this.mode.
+const fs = require("node:fs");
+const path = require("node:path");
+const os = require("node:os");
+const util = require("node:util");
+
+const dir = fs.mkdtempSync(path.join(os.tmpdir(), "fs-stats-"));
+const file = path.join(dir, "f.txt");
+fs.writeFileSync(file, "stats text");
+const posix = process.platform !== "win32";
+
+// The typed snapshot.
+const s = fs.statSync(file);
+console.log("type bits", (s.mode & 0o170000) === 0o100000, s.mode === fs.lstatSync(file).mode);
+console.log("ids", s.uid >= 0, s.gid >= 0, s.rdev, s.blksize > 0, s.nlink, s.size, s.blocks >= 0);
+console.log("tests", s.isFile(), s.isDirectory(), s.isSymbolicLink(), s.isBlockDevice(), s.isCharacterDevice(), s.isFIFO(), s.isSocket());
+console.log("dates", s.atime.getTime() === Math.round(s.atimeMs), s.mtime.getTime() === Math.round(s.mtimeMs), s.ctime.getTime() === Math.round(s.ctimeMs), s.birthtime.getTime() === Math.round(s.birthtimeMs));
+console.log("iso", s.mtime.toISOString() === new Date(Math.round(s.mtimeMs)).toISOString());
+const d = fs.statSync(dir);
+console.log("directory", d.isDirectory(), d.isFile(), (d.mode & 0o170000) === 0o040000);
+const fd = fs.openSync(file, "r");
+const f = fs.fstatSync(fd);
+console.log("fstat", f.ino === s.ino, f.mode === s.mode, f.birthtimeMs === s.birthtimeMs, f.blksize === s.blksize);
+fs.closeSync(fd);
+if (posix) {
+ const n = fs.statSync("/dev/null");
+ console.log("char device", n.isCharacterDevice(), n.isFile(), n.rdev > 0, (n.mode & 0o170000) === 0o020000);
+ fs.symlinkSync("f.txt", path.join(dir, "link"));
+ const l = fs.lstatSync(path.join(dir, "link"));
+ console.log("link", l.isSymbolicLink(), (l.mode & 0o170000) === 0o120000, l.size);
+} else {
+ console.log("device and link checks need POSIX");
+}
+
+const mask = (text) => text.replace(/\n */g, " ").replace(/\d+(\.\d+)?n?/g, "N");
+
+// The callback objects.
+fs.stat(file, (err, t) => {
+ console.log("keys", err, Object.keys(t).join());
+ const keys = [];
+ for (const key in t) keys.push(key);
+ console.log("for-in", keys.join());
+ console.log("inspect", mask(util.inspect(t)));
+ console.log("same snapshot", t.mode === s.mode, t.ino === s.ino, t.birthtimeMs === s.birthtimeMs, t.blksize === s.blksize);
+ console.log("lazy", Object.keys(t).includes("mtime"), t.mtime === t.mtime, Object.keys(t).includes("mtime"));
+ console.log("rounded", t.mtime.getTime() === Math.round(t.mtimeMs), t.birthtime.getTime() === s.birthtime.getTime());
+ const own = Object.getOwnPropertyDescriptor(t, "mtime");
+ console.log("own date", own.writable, own.enumerable, own.configurable);
+ t.atime = 5;
+ console.log("assigned", t.atime, Object.keys(t).slice(-2).join());
+ const proto = Object.getPrototypeOf(t);
+ console.log("prototypes", Object.getOwnPropertyNames(proto).join(), Object.getOwnPropertyNames(Object.getPrototypeOf(proto)).join());
+ console.log("names", t.constructor.name, t.constructor.length, JSON.stringify(t.isFile.name), t.isFile.length);
+ console.log("json", JSON.stringify(t).startsWith('{"dev":'), JSON.stringify(t).includes('"atime":5'));
+ t.mode = 0o010644;
+ console.log("mode read", t.isFIFO() === posix, t.isFile());
+ t.mode = "x";
+ console.log("odd mode", t.isFile());
+ fs.lstat(file, { bigint: true }, (err2, b) => {
+ console.log("bigint keys", err2, Object.keys(b).join());
+ console.log("bigint inspect", mask(util.inspect(b)), b.constructor.name);
+ console.log("bigint values", typeof b.mode, b.mode === BigInt(s.mode), b.ino === BigInt(s.ino), b.mtimeMs === b.mtimeNs / 1000000n);
+ console.log("bigint dates", b.mtime.getTime() === Number(b.mtimeMs), b.isFile(), b.isDirectory());
+ b.mode = 5;
+ try {
+ b.isFile();
+ } catch (error) {
+ console.log("mixed", error.name, error.message);
+ }
+ const { isFile } = b;
+ try {
+ isFile();
+ } catch (error) {
+ console.log("detached", error.name, error.message);
+ }
+ const handle = fs.openSync(file, "r");
+ fs.fstat(handle, (err3, h) => {
+ console.log("fstat callback", err3, h.isDirectory(), h.isFile(), h.ino === s.ino, h.mode === s.mode, h.birthtimeMs === s.birthtimeMs);
+ fs.closeSync(handle);
+ fs.rmSync(dir, { recursive: true });
+ console.log("removed", fs.existsSync(dir));
+ });
+ });
+});
diff --git a/tests/corpus/5552-string-appends.ts b/tests/corpus/5552-string-appends.ts
new file mode 100644
index 000000000..b4fe12fa4
--- /dev/null
+++ b/tests/corpus/5552-string-appends.ts
@@ -0,0 +1,94 @@
+// String appends to fields, records, captured variables and through
+// concat chains: results, aliases of the old string (which must not
+// change), right sides that reassign the field, accessors, non-string right
+// sides, the value of the compound assignment, and surrogate halves joined
+// by appends, against Node.
+class Writer {
+ text = "";
+ count = 0;
+ write(s: string): void {
+ this.text += s;
+ }
+ reset(): string {
+ this.text = "reset";
+ return "!";
+ }
+}
+class Sub extends Writer {
+ extra = "e";
+}
+const w = new Writer();
+for (let i = 0; i < 300; i++) w.write("ab" + (i % 10));
+console.log(w.text.length, w.text.slice(0, 12), w.text.slice(-6));
+const before = w.text;
+w.text += "tail";
+console.log(before.length, before.endsWith("tail"), w.text.endsWith("tail"), w.text.length);
+w.text += w.reset();
+console.log(w.text.length, w.text.slice(-6));
+const v = (w.text += "X");
+console.log(v.slice(-2), w.text === v);
+w.count += 2;
+const n = new Writer();
+n.text += 5 as unknown as string;
+n.text += String(1.5);
+console.log(n.text, w.count);
+const sub = new Sub();
+sub.text += "s1";
+sub.extra += "x";
+sub.text += sub.extra;
+console.log(sub.text, sub.extra);
+
+class Box {
+ private _v = "a";
+ get v(): string {
+ return this._v;
+ }
+ set v(x: string) {
+ this._v = x + "|";
+ }
+}
+const bx = new Box();
+bx.v += "b";
+bx.v += "c";
+console.log(bx.v);
+
+type Rec = { name: string; size: number };
+const rec: Rec = { name: "r", size: 1 };
+const alias = rec.name;
+for (let i = 0; i < 50; i++) rec.name += String.fromCharCode(97 + (i % 26));
+console.log(rec.name.length, alias, rec.name.slice(0, 5));
+
+// Concat chains whose leftmost operand is the target.
+let s = "x";
+const t = s;
+for (let i = 0; i < 100; i++) s = s + "-" + i;
+console.log(s.length, t, s.slice(-8));
+let u = "u";
+u = u + 1 + 2;
+u = u + "a" + 3.5 + "b";
+console.log(u);
+let pair = "p";
+pair = pair + "\ud83d" + "\ude00";
+console.log(pair, pair.length);
+let joined = "";
+joined += "\ud83d";
+const half = joined;
+joined += "\ude00";
+console.log(joined, joined.length, half.length);
+
+// Captured variables.
+function counter(): [(s: string) => void, () => string] {
+ let out = "";
+ return [
+ (s: string) => {
+ out += s;
+ out = out + "," + s.length;
+ },
+ () => out,
+ ];
+}
+const [add, get] = counter();
+add("aa");
+const snapshot = get();
+add("b");
+console.log(get(), snapshot);
diff --git a/tests/corpus/5553-string-append-char-code.ts b/tests/corpus/5553-string-append-char-code.ts
new file mode 100644
index 000000000..0216830e7
--- /dev/null
+++ b/tests/corpus/5553-string-append-char-code.ts
@@ -0,0 +1,37 @@
+// `s += String.fromCharCode(c)` appends one character: ASCII, two- and
+// three-byte characters, surrogate halves that join into one character,
+// codes that wrap modulo 2^16, aliases of the old string (which must not
+// change), and growth from an empty string, against Node.
+let s = "";
+const seen: string[] = [];
+for (let i = 0; i < 1000; i++) {
+ s += String.fromCharCode(32 + (i % 95));
+ if (i % 250 === 0) seen.push(s);
+}
+console.log(s.length, s.slice(0, 10), s.slice(-10), seen.map((x) => x.length).join(","));
+let t = "abc";
+const alias = t;
+t += String.fromCharCode(100);
+t += String.fromCharCode(0xe9);
+t += String.fromCharCode(0x65e5);
+t += String.fromCharCode(0x10041);
+t += String.fromCharCode(-1);
+console.log(t, t.length, alias, alias.length);
+let pair = "x";
+pair += String.fromCharCode(0xd83d);
+const half = pair;
+pair += String.fromCharCode(0xde00);
+console.log(pair, pair.length, half.length, half.charCodeAt(1).toString(16));
+let lone = "";
+lone += String.fromCharCode(0xde00);
+lone += String.fromCharCode(0xd83d);
+console.log(lone.length, lone.charCodeAt(0).toString(16), lone.charCodeAt(1).toString(16));
+let codes = "";
+for (const c of [72, 101, 108, 108, 111, 1.9, 65.5, 0x7f, 0x80]) codes += String.fromCharCode(c);
+console.log(JSON.stringify(codes), codes.length);
+function build(n: number): string {
+ let out = "";
+ for (let i = 0; i < n; i++) out += String.fromCharCode(97 + (i % 26));
+ return out;
+}
+console.log(build(0), build(1), build(27), build(5000).length);
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/corpus/5595-net-ref-unref.ts b/tests/corpus/5595-net-ref-unref.ts
new file mode 100644
index 000000000..331941314
--- /dev/null
+++ b/tests/corpus/5595-net-ref-unref.ts
@@ -0,0 +1,30 @@
+// socket.unref() and server.unref() let the process exit while the handle
+// is open, and ref() undoes it. An unref'd socket still delivers its events
+// while other work (here a timer) keeps the loop running.
+import * as net from "node:net";
+import * as http from "node:http";
+
+const server = net.createServer((socket) => {
+ socket.unref();
+ socket.on("data", (d: Buffer) => socket.write(`echo ${d.toString()}`));
+});
+server.listen(0, "127.0.0.1", () => {
+ const port = (server.address() as { port: number }).port;
+ server.unref();
+ const client = net.connect({ port, host: "127.0.0.1" }, () => {
+ client.write("hi");
+ });
+ client.on("data", (d: Buffer) => {
+ console.log("client got", d.toString());
+ client.unref();
+ setTimeout(() => console.log("timer still ran"), 50);
+ });
+ const web = http.createServer((req, res) => res.end("ok"));
+ web.listen(0, "127.0.0.1", () => {
+ web.unref();
+ web.ref();
+ web.unref();
+ console.log("web unref'd");
+ });
+});
+process.on("exit", () => console.log("exit"));
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/fixtures/self-hosting/ir-build.ts b/tests/fixtures/self-hosting/ir-build.ts
index 042914571..26b75326d 100644
--- a/tests/fixtures/self-hosting/ir-build.ts
+++ b/tests/fixtures/self-hosting/ir-build.ts
@@ -14,7 +14,7 @@ const concat: IrExpr = { kind: "strConcat", left: text, right: suffix, type: STR
// Execute backend-independent analyses against the same real, recursive
// IR types that the builders consume. Fail before generating any module
// if an analysis loses a variant or reads the wrong payload.
-if (matchStringSelfConcat("text.0", concat) !== suffix) throw new Error("concat recognition");
+if (matchStringSelfConcat("text.0", concat)?.suffix !== suffix) throw new Error("concat recognition");
if (matchStringSelfConcat("other.0", concat) !== null) throw new Error("concat target");
if (matchStringSelfConcat("text.0", numLit(0, loc)) !== null) throw new Error("concat variant");
if (!isStableReceiverOperand(numLit(3, loc), "sum.0")) throw new Error("stable literal");
diff --git a/tests/harness/project-config.test.ts b/tests/harness/project-config.test.ts
index efc2cc5e3..e361b6c4c 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");