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");