Merge main

This commit is contained in:
Chris Tate
2026-10-15 09:20:01 -05:00
85 changed files with 4373 additions and 790 deletions
+2
View File
@@ -76,6 +76,8 @@ title: "Limitations"
<include>../limitations/builtin-apis/filesystem-flags.mdx</include>
<include>../limitations/builtin-apis/glob.mdx</include>
<include>../limitations/builtin-apis/filehandle-and-descriptor-i-o.mdx</include>
<include>../limitations/builtin-apis/crypto.mdx</include>
@@ -0,0 +1 @@
- **Glob:** `fs.globSync`, `fs.glob` and `fs/promises` `glob` support every option. `withFileTypes` must be a literal `true` or `false`, and `exclude` functions cannot be combined with `withFileTypes`. The `fs/promises` iterator is supported only as the source of a `for await` loop, and its walk completes when the loop starts. `fs.glob` callbacks receive file names, not Dirents.
@@ -15,4 +15,4 @@ Other immutable aliases of table-backed Node builtins support direct calls, but
Synchronous and promise filesystem APIs with trailing arguments that affect behavior remain call-only. This includes writers, open, directory creation/removal, metadata readers, and directory readers. Passing them as values could discard modes, flags, encodings, or options during function-signature adaptation.
Native error-first filesystem callbacks can be stored and passed to platform adapters. Reads support Buffer and UTF-8 results; writes preserve string flags and modes; copy supports regular files and recursive directories. Reads and writes reject already-aborted signals, but native file operations complete synchronously before the deferred callback, so cancellation cannot interrupt I/O in progress. Symbolic-link copy, copy filters and clone modes, recursive glob patterns, glob exclusions, recursive directory reads, and Dirent results report a native limitation.
Native error-first filesystem callbacks can be stored and passed to platform adapters. Reads support Buffer and UTF-8 results; writes preserve string flags and modes; copy supports regular files and recursive directories. Reads and writes reject already-aborted signals, but native file operations complete synchronously before the deferred callback, so cancellation cannot interrupt I/O in progress. Symbolic-link copy, copy filters and clone modes, recursive directory reads, and Dirent results report a native limitation. `fs.glob` compiles only as a direct call; a stored `fs.glob` value throws `SC2020`.
@@ -2,6 +2,6 @@
"schemaVersion": 3,
"nodeVersion": "24.22.0",
"nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93",
"artifactVersion": "821b258757b8e2c146c4",
"artifactVersion": "797302915a7200576d4e",
"rowCount": 3880
}
+92 -92
View File
@@ -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"
}
},
{
@@ -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"
]
}
}
},
@@ -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"
},
@@ -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"] },
+60 -1
View File
@@ -1324,6 +1324,36 @@ declare module "node:fs" {
* fences by name at the call site; undocumented keys drop like Node. */
options: { withFileTypes: true; recursive?: boolean; [option: string]: unknown },
): Dirent[];
/* fs.globSync / fs.glob: Node's glob walk over the file system. Patterns
* are minimatch globs (case-insensitive on macOS and Windows); results
* are relative to cwd unless the pattern is absolute. withFileTypes must
* be a literal so the result type is known. */
export interface GlobOptions {
cwd?: string | URL;
exclude?: readonly string[] | ((fileName: string) => boolean);
followSymlinks?: boolean;
withFileTypes?: false;
}
export interface GlobOptionsWithFileTypes {
cwd?: string | URL;
exclude?: readonly string[];
followSymlinks?: boolean;
withFileTypes: true;
}
export function globSync(pattern: string | readonly string[], options?: GlobOptions): string[];
export function globSync(
pattern: string | readonly string[],
options: GlobOptionsWithFileTypes,
): Dirent[];
export function glob(
pattern: string | readonly string[],
callback: (err: NodeJS.ErrnoException | null, matches?: string[]) => void,
): void;
export function glob(
pattern: string | readonly string[],
options: GlobOptions,
callback: (err: NodeJS.ErrnoException | null, matches?: string[]) => void,
): void;
export function accessSync(path: string, mode?: number): void;
export function mkdtempSync(prefix: string): string;
export const constants: {
@@ -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<import("node:fs").Dirent[]>;
/* The async iterator form of fs.globSync: iterate it with for await. */
export function glob(
pattern: string | readonly string[],
options?: import("node:fs").GlobOptions,
): AsyncIterableIterator<string>;
export function glob(
pattern: string | readonly string[],
options: import("node:fs").GlobOptionsWithFileTypes,
): AsyncIterableIterator<import("node:fs").Dirent>;
export function rm(path: string): Promise<void>;
export function stat(path: string): Promise<import("node:fs").Stats>;
export function statfs(
@@ -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;
@@ -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. */
+45 -10
View File
@@ -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)`);
@@ -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;
@@ -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 };
}
@@ -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
@@ -325,6 +325,10 @@ export const LIB_FN_SYMS: Record<string, string> = {
"fs.rmRetrySync": "scr_fs_rm_opts_retry",
"fs.rmdirSync": "scr_fs_rmdir",
"fs.readdirSync": "scr_fs_readdir",
"fs.globSync": "scr_fs_glob_strings",
"fs.callbackGlob": "scr_fs_glob_callback",
"fsp.globStrings": "scr_fsp_glob_strings",
"fsp.globError": "scr_fsp_glob_error",
"fs.realpathSync": "scr_fs_realpath",
"fs.realpathNativeSync": "scr_fs_realpath_promise",
"fs.unlinkSync": "scr_fs_unlink",
@@ -451,6 +455,20 @@ export const LIB_FN_SYMS: Record<string, string> = {
"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<string, string> = {
// 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<string>([
"fs.existsChk",
"fs.callbackValue",
"fs.callbackCall",
"fs.callbackGlob",
"http.createServer",
"http.createServerEmpty",
"http.request",
@@ -912,9 +912,16 @@ test.each([
expect(ir).not.toContain("@scr_union_new");
expect(ir).not.toContain("@scr_union_release");
expect(ir).not.toContain("@scr_union_get");
// Holes, sparse and noncanonical indices keep the runtime state check.
expect(ir).toContain("@scr_arr_state");
expect(ir).toContain(elem.kind === "f64" ? "@scr_arr_get_f64" : "@scr_arr_get_bool");
// Holes, sparse and noncanonical indices keep the runtime state check,
// a read-only cold call, so loop-invariant header loads can be hoisted.
expect(ir).toMatch(
/call double @scr_arr_state\(.*\) cold memory\(read, inaccessiblemem: readwrite\)/,
);
expect(ir).toMatch(
elem.kind === "f64"
? /call double @scr_arr_get_f64\(.*\) cold memory\(read, inaccessiblemem: readwrite\)/
: /call zeroext i1 @scr_arr_get_bool\(.*\) cold memory\(read, inaccessiblemem: readwrite\)/,
);
if (consumer !== "local") expect(ir).toContain("local.array.dense");
}
});
@@ -14,6 +14,7 @@ import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
import { ReferenceEffects } from "./reference-effects.js";
import { SCR_ARR_FIELD, exactIndexLines } from "./common.js";
import { exactInteger, widenInteger } from "./integer-values.js";
import { READ_FALLBACK } from "./dense-array-access.js";
export interface LocalArrayRead {
type: IrType;
@@ -696,10 +697,15 @@ export function emitLocalArrayRead(
if (scalar) {
// Holes, present undefined, sparse and noncanonical indices: the
// runtime state decides presence, then the strict getter reads it.
// Both only read (the getter runs for a present element, so it does
// not trap), which lets LLVM keep the array's header loads out of a
// loop around an inlined read.
const state = B.tmp(),
present = B.tmp();
host.declare(`declare double @scr_arr_state(ptr, double)`);
B.line(`${state} = call double @scr_arr_state(ptr ${array.name}, double ${index.name})`);
B.line(
`${state} = call double @scr_arr_state(ptr ${array.name}, double ${index.name})${READ_FALLBACK}`,
);
B.line(`${present} = fcmp oeq double ${state}, 1.0`);
B.condBr(present, slowValue, no);
B.startBlock(slowValue);
@@ -707,12 +713,14 @@ export function emitLocalArrayRead(
bits = B.tmp();
if (read.element.kind === "f64") {
host.declare(`declare double @scr_arr_get_f64(ptr, double)`);
B.line(`${value} = call double @scr_arr_get_f64(ptr ${array.name}, double ${index.name})`);
B.line(
`${value} = call double @scr_arr_get_f64(ptr ${array.name}, double ${index.name})${READ_FALLBACK}`,
);
B.line(`${bits} = bitcast double ${value} to i64`);
} else {
host.declare(`declare zeroext i1 @scr_arr_get_bool(ptr, double)`);
B.line(
`${value} = call zeroext i1 @scr_arr_get_bool(ptr ${array.name}, double ${index.name})`,
`${value} = call zeroext i1 @scr_arr_get_bool(ptr ${array.name}, double ${index.name})${READ_FALLBACK}`,
);
B.line(`${bits} = zext i1 ${value} to i64`);
}
@@ -73,6 +73,9 @@ test("projection-only records live in frame storage and producers fill it", asyn
expect(projected).not.toMatch(/@sc_rnew_/);
expect(projected).toMatch(/call void @sc_\w*into_add\(/);
expect(projected).toMatch(/call void @sc_\w*into_shortest\(/);
// The result-slot entries are small and called per use: hinted inline.
expect(ir).toMatch(/^define internal void @sc_\w*into_add\([^)]*\) inlinehint /m);
expect(ir).toMatch(/^define internal void @sc_\w*into_shortest\([^)]*\) inlinehint /m);
// A literal stores every field: its storage is not zeroed first.
expect(projected).not.toContain("zeroinitializer");
// Stored values keep the heap path and the ordinary producer.
@@ -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<string, IrLibFn> = {
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),
);
}
@@ -0,0 +1,268 @@
import * as ts from "../../ts7/adapter.js";
import { type Lowerer } from "../lowerer.js";
import {
BOOL,
DYN,
F64,
STRING,
VOID,
arrayOf,
type IrExpr,
type IrStmt,
type IrType,
type SrcLoc,
} from "../../../ir/ir.js";
import { dynUndefinedExpr, numLit } from "../../../ir/build.js";
import { InternalCompilerError } from "../../../errors.js";
import { locOf } from "../../program.js";
import { checkForAwaitBinding } from "../lower-async-iteration.js";
import { builtinImportOf } from "./module-bindings.js";
/* fs.globSync, fs.glob and fs/promises glob (scr_glob.c, a port of Node's
* lib/internal/fs/glob.js). The pattern and options cross as dyn values so
* the runtime validates them in Node's order. The result form (strings or
* Dirent rows) comes from the call's mapped result type, so withFileTypes
* must be statically known; the runtime checks that the option agrees. */
/** The interned Dirent record array (type-mapper.ts), or null. */
function direntArray(lowerer: Lowerer, type: IrType | null | undefined): boolean {
if (type?.kind !== "array" || type.elem.kind !== "record") return false;
const shape = lowerer.shapes.get(type.elem.shapeId);
return (
!!shape &&
!shape.tuple &&
!shape.indexValue &&
shape.fields.length === 3 &&
shape.fields.some((f) => f.name === "%dtype")
);
}
/** An exclude function receives Node's Dirent objects when withFileTypes is
* set, which have no native representation here. */
function fenceDirentExclude(lowerer: Lowerer, call: ts.CallExpression, what: string): void {
const options = call.arguments[1];
if (!options) return;
const exclude = lowerer.checker.getPropertyOfType(lowerer.typeOf(options), "exclude");
if (!exclude) return;
const type = lowerer.checker.getTypeOfSymbolAtLocation(exclude, options);
if (lowerer.checker.getCallSignatures(type).length === 0) return;
lowerer.noLowering(
`${what} with an exclude function and withFileTypes`,
options,
"pass exclude as a string[] of glob patterns, or match on names without withFileTypes",
);
}
function globArgs(lowerer: Lowerer, call: ts.CallExpression, what: string): IrExpr[] {
if (call.arguments.some(ts.isSpreadElement)) {
lowerer.noLowering(`${what} with spread arguments`, call, "write the arguments positionally");
}
return call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN));
}
/** `globSync(pattern, options?)`: string[] or Dirent[]. */
export function lowerFsGlobSyncCall(
lowerer: Lowerer,
call: ts.CallExpression,
loc: SrcLoc,
): IrExpr {
const args = globArgs(lowerer, call, "fs.globSync");
const callType = lowerer.mapTypeOf(lowerer.typeOf(call));
const types = direntArray(lowerer, callType);
if (types) fenceDirentExclude(lowerer, call, "fs.globSync");
if (!types && !(callType?.kind === "array" && callType.elem.kind === "string")) {
lowerer.noLowering(
"fs.globSync where the result is not string[] or Dirent[]",
call,
"pass withFileTypes as a literal true or false so the result type is known",
);
}
return {
kind: "libCall",
fn: types ? "fs.globTypesSync" : "fs.globSync",
args: [args[0] ?? dynUndefinedExpr(loc), args[1] ?? dynUndefinedExpr(loc)],
type: types ? callType! : arrayOf(STRING),
loc,
};
}
/** `fs.glob(pattern[, options], callback)`: the argument vector goes to the
* runtime, which validates it and calls back with string[]. */
export function lowerFsGlobCallbackCall(
lowerer: Lowerer,
call: ts.CallExpression,
loc: SrcLoc,
): IrExpr {
const args = globArgs(lowerer, call, "fs.glob");
return {
kind: "libCall",
fn: "fs.callbackGlob",
args: [{ kind: "dynArrLit", elems: args, type: DYN, loc }],
type: VOID,
loc,
};
}
/** True when `node` calls fs/promises glob (any import spelling). */
export function isFspGlobCall(lowerer: Lowerer, node: ts.Expression): node is ts.CallExpression {
let expr = node;
while (ts.isParenthesizedExpression(expr)) expr = expr.expression;
if (!ts.isCallExpression(expr) || expr.questionDotToken) return false;
const callee = expr.expression;
const bi = ts.isIdentifier(callee)
? builtinImportOf(lowerer, callee)
: ts.isPropertyAccessExpression(callee) && !callee.questionDotToken
? lowerer.builtinMemberOf(callee)
: null;
return bi?.module === "fs/promises" && bi.member === "glob";
}
/** fs/promises `glob(pattern, options?)` as the head of a `for await` loop
* (lowerForOf marks the call): the matches as an array the loop awaits in
* order. The walk runs when the loop starts, like the other fs/promises
* operations, which complete before their continuations. Other uses of
* the async iterator have no lowering. */
export function lowerFspGlobCall(lowerer: Lowerer, call: ts.CallExpression, loc: SrcLoc): IrExpr {
if (lowerer.fspGlobHead !== call) {
lowerer.noLowering(
"fs/promises glob outside a for await loop",
call,
"iterate the matches directly with for await (const path of glob(pattern)), or use fs.globSync",
);
}
const args = globArgs(lowerer, call, "fs/promises glob");
// The async iterator's yielded type (its first type argument).
const yielded = lowerer.checker.getTypeArguments(lowerer.typeOf(call) as ts.TypeReference)[0];
const element = yielded ? lowerer.mapTypeOf(yielded) : null;
const rows: IrType | null = element ? { kind: "array", elem: element } : null;
const types = direntArray(lowerer, rows);
if (types) fenceDirentExclude(lowerer, call, "fs/promises glob");
if (!types && element?.kind !== "string") {
lowerer.noLowering(
"fs/promises glob where the yielded value is not a string or a Dirent",
call,
"pass withFileTypes as a literal true or false so the yielded type is known",
);
}
return {
kind: "libCall",
fn: types ? "fsp.globTypes" : "fsp.globStrings",
args: [args[0] ?? dynUndefinedExpr(loc), args[1] ?? dynUndefinedExpr(loc)],
type: types ? rows! : arrayOf(STRING),
loc,
};
}
/** `for await (const path of glob(pattern, options))`: the walk runs when
* the loop starts and its matches bind in order, one microtask hop per
* step. When the walk ended with an error (an exclude callback threw), the
* matches found before it bind first and the error is thrown when the loop
* reaches the end, as Node's async iterator yields them before rejecting;
* leaving the loop early with break or return skips it. */
export function lowerFspGlobForAwait(
lowerer: Lowerer,
stmt: ts.ForOfStatement,
call: ts.CallExpression,
): IrStmt {
const binding = checkForAwaitBinding(lowerer, stmt);
const loc = locOf(stmt);
lowerer.fspGlobHead = call;
let rowsExpr: IrExpr;
try {
rowsExpr = lowerer.lowerExpr(call);
} finally {
lowerer.fspGlobHead = null;
}
if (rowsExpr.type.kind !== "array") {
throw new InternalCompilerError("fs/promises glob head did not lower to an array");
}
const rowsT = rowsExpr.type;
const elemT = rowsT.elem;
lowerer.scopes.push(new Map());
try {
const rows = lowerer.declareHiddenLocal("%globRows", rowsT);
const error = lowerer.declareHiddenLocal("%globError", DYN);
const index = lowerer.declareHiddenLocal("%globIndex", F64);
index.mutable = true;
const item = lowerer.declareLocal(
binding.declaration.name,
binding.declaration.name.text,
elemT,
binding.mutable,
);
const rowsRef = (): IrExpr => ({ kind: "varRef", localId: rows.id, type: rowsT, loc });
const indexRef = (): IrExpr => ({ kind: "varRef", localId: index.id, type: F64, loc });
const errorRef = (): IrExpr => ({ kind: "varRef", localId: error.id, type: DYN, loc });
const body = lowerer.inCtl("loop", () => lowerer.lowerScopedBlock(stmt.statement));
const loop: IrStmt = {
kind: "while",
cond: { kind: "boolLit", value: true, type: BOOL, loc },
body: [
{
kind: "exprStmt",
expr: { kind: "libCall", fn: "async.hop", args: [], type: VOID, loc },
loc,
},
{
kind: "if",
cond: {
kind: "unary",
op: "!",
operand: { kind: "arrayHas", arr: rowsRef(), index: indexRef(), type: BOOL, loc },
type: BOOL,
loc,
},
then: [
{
kind: "if",
cond: {
kind: "unary",
op: "!",
operand: { kind: "dynTest", test: "undefined", value: errorRef(), type: BOOL, loc },
type: BOOL,
loc,
},
then: [{ kind: "throw", value: errorRef(), loc }],
else_: null,
loc,
},
{ kind: "break", loc },
],
else_: null,
loc,
},
{
kind: "varDecl",
localId: item.id,
init: { kind: "arrayGet", arr: rowsRef(), index: indexRef(), type: elemT, loc },
loc,
},
{
kind: "assign",
localId: index.id,
value: { kind: "bin", op: "+", left: indexRef(), right: numLit(1, loc), type: F64, loc },
loc,
},
...body,
],
loc,
};
return {
kind: "block",
body: [
{ kind: "varDecl", localId: rows.id, init: rowsExpr, loc },
{
kind: "varDecl",
localId: error.id,
init: { kind: "libCall", fn: "fsp.globError", args: [], type: DYN, loc },
loc,
},
{ kind: "varDecl", localId: index.id, init: numLit(0, loc), loc },
loop,
],
loc,
};
} finally {
lowerer.scopes.pop();
}
}
@@ -42,6 +42,7 @@ import {
lowerPathParseCall,
} from "./operating-system.js";
import { lowerQuerystringParseCall, lowerQuerystringStringifyCall } from "./querystring.js";
import { lowerFsGlobCallbackCall, lowerFsGlobSyncCall, lowerFspGlobCall } from "./fs-glob.js";
export function lowerBuiltinModuleCall(
lowerer: Lowerer,
@@ -59,6 +60,12 @@ export function lowerBuiltinModuleCall(
if (bi.module === "buffer" && ["isAscii", "isUtf8", "transcode"].includes(bi.member)) {
return lowerBufferEncodingCall(lowerer, expr, bi, fn, loc);
}
if (bi.module === "fs" && bi.member === "glob")
return lowerFsGlobCallbackCall(lowerer, expr, loc);
if (bi.module === "fs" && bi.member === "globSync")
return lowerFsGlobSyncCall(lowerer, expr, loc);
if (bi.module === "fs/promises" && bi.member === "glob")
return lowerFspGlobCall(lowerer, expr, loc);
if (fn.fn === "fs.callbackCall" && bi.member !== "rename") {
return lowerFsCallbackCall(lowerer, expr, bi, loc);
}
@@ -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<string, IrLibFn> = {
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);
@@ -279,7 +279,7 @@ function classAsyncIteratorPlan(
};
}
function checkForAwaitBinding(
export function checkForAwaitBinding(
lowerer: Lowerer,
stmt: ts.ForOfStatement,
): { declaration: ts.VariableDeclaration & { name: ts.Identifier }; mutable: boolean } {
@@ -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),
@@ -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'
@@ -129,6 +129,7 @@ import {
lowerServerCloseOverrideAssignment,
} from "./lower-server.js";
import { lowerUrlAssignment } from "./builtins/url.js";
import { isFspGlobCall, lowerFspGlobForAwait } from "./builtins/fs-glob.js";
import {
isNativeFfiRequire,
builtinMemberRequireDecl,
@@ -7828,6 +7829,11 @@ export function lowerForOf(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt {
// A synchronous array iterator is also valid in `for await`. Its values
// take the async-from-sync path below; other async sources retain their
// dedicated request protocols.
if (stmt.awaitModifier) {
let head: ts.Expression = stmt.expression;
while (ts.isParenthesizedExpression(head)) head = head.expression;
if (isFspGlobCall(lowerer, head)) return lowerFspGlobForAwait(lowerer, stmt, head);
}
const awaitArray =
!!stmt.awaitModifier && lowerer.mapTypeOf(lowerer.typeOf(stmt.expression))?.kind === "array";
if (stmt.awaitModifier && !awaitArray) {
@@ -1819,6 +1819,9 @@ export class Lowerer {
/** for-of loops over arrays that can hold neither holes nor undefined:
* each pass reads the element payload directly. */
readonly presentElementLoops = new Set<ts.ForOfStatement>();
/** The fs/promises glob() call lowerForOf is lowering as a for await
* head; other glob() calls have no lowering. */
fspGlobHead: ts.Node | null = null;
/** Capture entries and their origin share one mutable box. Normalize each
* entry to the origin so writes and flow proofs stay synchronized. */
readonly runtimeOptionalRoots = new Map<IrLocal, IrLocal>();
@@ -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;
@@ -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,
+19 -3
View File
@@ -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();
+28 -11
View File
@@ -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
@@ -523,6 +523,11 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"fs.rmdirSync",
"fs.readdirSync",
"fs.readdirTypesSync",
"fs.globSync",
"fs.globTypesSync",
"fs.callbackGlob",
"fsp.globStrings",
"fsp.globTypes",
"url.new",
"url.newBase",
"url.set",
@@ -444,6 +444,20 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
* sorts), the directory's order on Windows; no "."/"..". Throws
* Node's scandir errno error (may-throw seed set); fresh +1 array. */
"fs.readdirTypesSync": { argTypes: [STRING], result: VOID },
/** fs.globSync / fs.glob / fs/promises glob (scr_glob.c, Node's
* lib/internal/fs/glob.js walk): pattern and options are dyn values the
* runtime validates in Node's order. The string forms answer string[];
* the Dirent forms answer the call site's Dirent record array (validated
* in validate.ts) assembled from the snapshot; fs.callbackGlob takes the
* argument vector and calls back with string[]. */
"fs.globSync": { argTypes: [DYN, DYN], result: arrayOf(STRING) },
"fs.globTypesSync": { argTypes: [DYN, DYN], result: VOID },
"fs.callbackGlob": { argTypes: [DYN], result: VOID },
"fsp.globStrings": { argTypes: [DYN, DYN], result: arrayOf(STRING) },
"fsp.globTypes": { argTypes: [DYN, DYN], result: VOID },
/** The error an fs/promises glob walk ended with (undefined when none),
* read once right after fsp.globStrings / fsp.globTypes. */
"fsp.globError": { argTypes: [], result: DYN },
/** node:path (scr_path.c ports BOTH of Node's implementations
* function-by-function): the `path.*` family is posix, the
* `path.win32*` family is Node v24's path.win32 ported exactly — the
@@ -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
+9 -1
View File
@@ -44,6 +44,7 @@ export interface RuntimeFeatures {
const DYN_ASYNC_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"fs.callbackValue",
"fs.callbackCall",
"fs.callbackGlob",
"async.awaitDyn",
"timers.immediatePromise",
"crypto.native",
@@ -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 (
+8 -2
View File
@@ -4682,10 +4682,16 @@ function validateFunction(
}
return true;
}
if (e.fn === "fs.readdirTypesSync" || e.fn === "fsp.readdirTypes") {
if (
e.fn === "fs.readdirTypesSync" ||
e.fn === "fsp.readdirTypes" ||
e.fn === "fs.globTypesSync" ||
e.fn === "fsp.globTypes"
) {
// Result: the interned Dirent record array — {%dtype: f64,
// name: string, parentPath: string} rows (canonical field
// order; the structure lowerFsReaddirTypesCall pinned).
// order; the structure lowerFsReaddirTypesCall pinned). The glob
// forms answer the array itself.
const result =
e.fn === "fsp.readdirTypes"
? e.type.kind === "promise"
+12
View File
@@ -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",
@@ -0,0 +1,53 @@
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { expect, test } from "vitest";
import { compile } from "../src/index.js";
async function compileSource(source: string) {
const directory = mkdtempSync(join(tmpdir(), "scriptc-fs-glob-"));
try {
const entry = join(directory, "main.ts");
writeFileSync(entry, source);
return await compile(entry, {
outDir: directory,
outPath: join(directory, "program.ir"),
outputKind: "ir",
});
} finally {
rmSync(directory, { recursive: true, force: true });
}
}
// Forms whose result type or element values have no native representation
// fail during compilation.
test.each([
[
"fs/promises glob outside a for await loop",
'import { glob } from "node:fs/promises";\nconst matches = glob("*");\nvoid matches;\n',
],
])("%s fails during compilation", async (what, source) => {
const result = await compileSource(source);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("expected an fs.glob diagnostic");
expect(result.diagnostics).toEqual(
expect.arrayContaining([expect.objectContaining({ message: expect.stringContaining(what) })]),
);
expect(result.diagnostics.some((diagnostic) => diagnostic.code === "SC9001")).toBe(false);
});
test("the fs.glob forms compile", async () => {
const result = await compileSource(`import { globSync, glob as globCallback } from "node:fs";
import { glob } from "node:fs/promises";
console.log(globSync("**/*.ts", { cwd: ".", exclude: ["node_modules/**"] }).length);
console.log(globSync(["*.md"], { exclude: (path) => path.startsWith(".") }).length);
console.log(globSync("*", { withFileTypes: true }).map((d) => d.isFile()).length);
globCallback("*", (err, matches) => console.log(err, matches?.length));
async function main(): Promise<void> {
for await (const path of glob("*")) console.log(path);
for await (const entry of glob("*", { withFileTypes: true })) console.log(entry.name, entry.isDirectory());
}
void main();
`);
expect(result.ok, JSON.stringify(result.ok ? [] : result.diagnostics)).toBe(true);
});
+27 -8
View File
@@ -15,14 +15,18 @@ test("remainder uses integer proofs and preserves floating fallback and signed z
`
function remainder(x: number, y: number): number { return x % y; }
function constants(x: number): number {
let value = x % 8;
let value = x % 10;
value %= 3;
return value;
}
function powers(x: number): number {
return (x % 8) + (x % -(2 ** 32)) + (x % 1) + (x % 0.5);
}
function power(x: number, y: number): number { return x ** y; }
function signed(x: number): number { return (x | 0) % -7; }
function maybeZero(x: number, y: number): number { return (x | 0) % (y | 0); }
console.log(remainder(-9.5, 8), constants(17.25), power(2, 3), signed(-7), maybeZero(7, 0));
console.log(powers(-9.75));
// Fractional arguments keep the parameters arbitrary (no call-site proof).
console.log(remainder(5, 2.5), signed(-7.5), maybeZero(7.5, 2.5));
`,
@@ -36,25 +40,40 @@ console.log(remainder(5, 2.5), signed(-7.5), maybeZero(7.5, 2.5));
new RegExp(`^define internal [^\\n]*@sc_[bf]+_${name}(?:\\.i32)?\\([^]*?^}`, "m").exec(
ll,
)![0];
// Four source remainders keep a floating fallback behind a runtime
// integer check; ToUint32 coercion fallbacks use integer arithmetic.
expect(ll.match(/ = frem double /g)).toHaveLength(4);
// Five source remainders keep a floating fallback: four behind a
// runtime integer check and the one by 0.5. ToUint32 coercion
// fallbacks use integer arithmetic.
expect(ll.match(/ = frem double /g)).toHaveLength(5);
expect(ll).toContain("uint32.coerce.slow");
expect(ll.match(/ = srem i64 /g)).toHaveLength(5);
expect(ll).toMatch(/ = select i1 [^\n]+double [^\n]+double /);
// Unproven operands: both round-trip checks, the 2^53 dividend bound,
// Unproven operands: both round-trip checks, the 2^62 dividend bound,
// and a nonzero divisor guard the integer path.
const unproven = fn("remainder");
expect(unproven.match(/ = freeze i64 /g)).toHaveLength(2);
expect(unproven).toMatch(/ = add i64 [^\n]+, 9007199254740992$/m);
expect(unproven).toMatch(/ = icmp ule i64 [^\n]+, 18014398509481984$/m);
expect(unproven).toMatch(/ = add i64 [^\n]+, 4611686018427387904$/m);
expect(unproven).toMatch(/ = icmp sge i64 [^\n]+, 0$/m);
expect(unproven).toMatch(/ = icmp ne i64 [^\n]+, 0$/m);
expect(unproven).toContain("@llvm.expect.i1(");
expect(unproven).toContain("rem.float");
// A constant divisor needs only the dividend check.
const constant = fn("constants");
expect(constant).toMatch(/ = srem i64 [^\n]+, 8$/m);
expect(constant).toMatch(/ = srem i64 [^\n]+, 10$/m);
expect(constant).not.toMatch(/ = icmp ne i64 [^\n]+, 0$/m);
// Power-of-two divisors of at least 1 subtract the truncated multiple,
// with no integer check; the divisor's sign is dropped and 0.5 keeps
// frem.
const powers = fn("powers");
expect(powers).not.toContain("freeze");
expect(powers).not.toContain("srem");
expect(powers.match(/ = fdiv double [^\n]+, 0x/g)).toHaveLength(3);
expect(powers).toMatch(/ = fdiv double [^\n]+, 0x4020000000000000$/m);
expect(powers).toMatch(/ = fdiv double [^\n]+, 0x41F0000000000000$/m);
expect(powers).toMatch(/ = fdiv double [^\n]+, 0x3FF0000000000000$/m);
expect(
powers.match(/ = call double @llvm.copysign.f64\(double 0x0000000000000000, /g),
).toHaveLength(3);
expect(powers).toMatch(/ = frem double [^\n]+, 0x3FE0000000000000$/m);
// Proven integers with a nonzero divisor need no check or fallback.
const signed = fn("signed");
expect(signed).not.toContain("freeze");
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5333-record-boundary-nullable-argument.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5448-fs-glob-patterns.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5449-fs-glob-options.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5450-fs-glob-validation.js"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5451-fs-promises-glob.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5452-fs-glob-callback.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5455-power-of-two-remainder.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5456-remainder-large-integer-dividends.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5457-lcg-modulus.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5458-fractional-remainder.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5541-fs-stats-fields.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5552-string-appends.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5553-string-append-char-code.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5585-record-results-inlined.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5595-net-ref-unref.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/fixtures/node-types/fs-glob.ts"
],
"diags": []
}
+13 -4
View File
@@ -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
+9 -122
View File
@@ -23,13 +23,6 @@ static const ScrArrOps scr_bytes_io_bytes_ops = { scr_bytes_retain_v, scr_bytes_
#ifndef O_SYNC
#define O_SYNC 0 /* Windows CRT opens have no separate synchronous flag. */
#endif
/* Bionic added glob/globfree in API 28; Android packs target API 26. */
#if !defined(_WIN32) && !defined(__wasi__) && (!defined(__ANDROID__) || __ANDROID_API__ >= 28)
#include <glob.h>
#define SCR_FS_HAS_GLOB 1
#else
#define SCR_FS_HAS_GLOB 0
#endif
#ifdef __APPLE__
#include <sys/stat.h> /* lchmod(2) — the fs.lchmodSync ladder's real tail */
#endif
@@ -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
+3 -99
View File
@@ -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, &times, 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;
}
File diff suppressed because it is too large Load Diff
+1
View File
@@ -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). */
+4
View File
@@ -101,6 +101,10 @@ ScrStr *scr_fs_read_file_encoded(ScrStr *path, ScrStr *encoding);
ScrStr *scr_fs_read_fd_encoded(double fd, ScrStr *encoding);
ScrDyn *scr_fs_callback_value(ScrStr *member);
ScrDyn *scr_fs_callback_call(ScrStr *member, const ScrDyn *args);
/* The error-first callback queue (scr_bytes_io.c): a pending exception
* becomes [err], otherwise [null, result(, buffer)]; the callback runs on
* a later loop turn. result moves in. */
void scr_fs_cb_schedule(const ScrDyn *callback, ScrDyn *result, const ScrDyn *buffer);
ScrBytes *scr_fs_read_fd_bytes(double fd); /* +1; the fd form (scr_lib.c) */
void scr_fs_write_file_bytes(ScrStr *path, const ScrBytes *data);
void scr_fs_append_file_bytes(ScrStr *path, const ScrBytes *data);
+607 -92
View File
@@ -130,6 +130,17 @@ static ScrStr *scr_fs_win_utf8(const WCHAR *text, size_t length) {
#if defined(__linux__)
#include <sched.h> /* sched_getaffinity, CPU_COUNT (os.availableParallelism) */
#include <sys/prctl.h> /* PR_SET_NAME (process.title) */
#include <sys/syscall.h> /* 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 <sys/sysctl.h> /* 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, &times, 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.<name>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),
+24
View File
@@ -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
+27 -3
View File
@@ -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 };
+3
View File
@@ -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
+27
View File
@@ -61,5 +61,32 @@ ScrStr *scr_path_win32_format(ScrStr *root, ScrStr *dir, ScrStr *base, ScrStr *n
bool scr_path_matches_glob(ScrStr *path, ScrStr *pattern);
bool scr_path_win32_matches_glob(ScrStr *path, ScrStr *pattern);
bool scr_glob_match(ScrStr *path, ScrStr *pattern, bool windows, bool nocase);
/* fs.globSync / fs.glob / fs/promises glob (scr_glob.c): Node 24's
* lib/internal/fs/glob.js walk. The pattern (a string or string array) and
* options (exclude, cwd, followSymlinks, withFileTypes) arrive as dyn
* values and are validated in Node's order with Node's errors (borrowed).
* scr_fs_glob answers the result snapshot (+1, or NULL with the exception
* pending); mode bit 0 asks for Dirent rows (name, parentPath, UV_DIRENT
* kind) and must agree with withFileTypes, bit 1 is the fs/promises form,
* whose Dirents carry resolved parent paths. strings is the string[] form.
* callback takes fs.glob's argument vector, throws validation errors
* synchronously and delivers the result or the walk's error through the
* filesystem callback queue. */
typedef struct ScrDyn ScrDyn; /* full definition in scr_dyn.h (C11 repeat) */
typedef struct ScrFsGlob ScrFsGlob;
ScrFsGlob *scr_fs_glob(const ScrDyn *pattern, const ScrDyn *options, double mode);
size_t scr_fs_glob_count(const ScrFsGlob *g); /* NULL -> 0 */
ScrStr *scr_fs_glob_path(const ScrFsGlob *g, size_t i); /* +1 */
ScrStr *scr_fs_glob_name(const ScrFsGlob *g, size_t i); /* +1 */
ScrStr *scr_fs_glob_parent(const ScrFsGlob *g, size_t i); /* +1 */
double scr_fs_glob_type(const ScrFsGlob *g, size_t i);
void scr_fs_glob_free(ScrFsGlob *g); /* NULL-tolerant */
ScrArr *scr_fs_glob_strings(const ScrDyn *pattern, const ScrDyn *options);
void scr_fs_glob_callback(ScrDyn *args);
/* The fs/promises form: a walk error does not throw; the matches found
* before it are returned and scr_fsp_glob_error answers the error (+1 dyn,
* undefined when there is none) once, for the loop to throw after them. */
ScrArr *scr_fsp_glob_strings(const ScrDyn *pattern, const ScrDyn *options);
ScrDyn *scr_fsp_glob_error(void);
#endif /* SCR_PATH_H */
+22
View File
@@ -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). */
+4
View File
@@ -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. */
+9
View File
@@ -15,6 +15,15 @@
long scr_str_live_count(void); /* provided by scr_string.c */
/* fs.glob's callback form (scr_glob.c) schedules through the filesystem
* callback queue in scr_bytes_io.c, which this binary does not link; the
* oracle never calls it. */
void scr_fs_cb_schedule(const ScrDyn *callback, ScrDyn *result, const ScrDyn *buffer) {
(void)callback;
(void)buffer;
scr_dyn_release(result);
}
static int hex_val(char c) {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
@@ -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<string, Item>();
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<string>();
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<T>(xs: T[], index: number, value: T): T[] {
xs[index] = value;
return xs;
}
const sparse = grow<number>([], 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);
+82
View File
@@ -0,0 +1,82 @@
// fs.globSync over a fixed tree: globstars, braces, extglobs, classes, dot
// files, "." and ".." segments, trailing slashes, literal paths, missing
// directories, absolute patterns and pattern arrays. Results come out in
// Node's walk order over sorted directory listings.
import { globSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
const root = mkdtempSync(join(tmpdir(), "scriptc-glob-"));
for (const dir of ["a/b/c", "src/lib", ".hidden", "node_modules/x", "docs"]) {
mkdirSync(join(root, dir), { recursive: true });
}
const files = [
"a/one.ts",
"a/b/two.ts",
"a/b/c/three.js",
"src/index.ts",
"src/lib/util.ts",
"src/lib/util.test.ts",
"README.md",
".hidden/secret.ts",
".dot.ts",
"node_modules/x/index.ts",
"Upper.TS",
"docs/guide.md",
"docs/[draft].md",
];
for (const file of files) writeFileSync(join(root, file), "");
const show = (pattern: string | string[]): void => {
console.log(JSON.stringify(pattern), globSync(pattern, { cwd: root }).join(" "));
};
for (const pattern of [
"*.ts",
"*",
"**",
"**/*.ts",
"a/**",
"a/**/*.js",
"**/b",
"**/b/**",
"src/*",
"src/**/*.test.ts",
"{a,src}/*.ts",
"{a,src}/**/*.{ts,js}",
"a/{one,missing}.ts",
"?.ts",
"a/???.ts",
"[ab]/*",
"[!a]*",
"!(src|node_modules)/*.ts",
"@(a|src)/*.ts",
"+(a|b)/**",
"*.md",
"docs/\\[draft\\].md",
".*",
"**/.*",
".hidden/*",
"a/b/../one.ts",
"./a/*.ts",
"a/./b/*.ts",
"a/",
"a/b/",
"**/",
"a/b/c/three.js",
"missing/**",
"missing",
"**/node_modules/**",
"**/*.TS",
"",
]) {
show(pattern);
}
show(["*.md", "a/*.ts", "*.md"]);
show(["src/*", "src/**"]);
show([]);
// Absolute patterns answer absolute paths; print them relative to the root.
const absolute = globSync(join(root, "a", "**", "*.ts")).map((p) => p.slice(root.length));
console.log("absolute", absolute.join(" "));
rmSync(root, { recursive: true, force: true });
+84
View File
@@ -0,0 +1,84 @@
// fs.globSync options: cwd as an absolute path, a relative path and a file:
// URL; exclude as glob patterns (resolved against cwd, matched on whole
// paths and on directories with a trailing slash) or a function of the
// path; withFileTypes Dirents with name, parentPath (cwd-relative when cwd
// is relative) and the type probes; and followSymlinks.
import { type Dirent, globSync, mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, relative } from "node:path";
import { pathToFileURL } from "node:url";
const root = mkdtempSync(join(tmpdir(), "scriptc-glob-options-"));
for (const dir of ["pkg/src/lib", "pkg/test", "pkg/node_modules/dep", "pkg/dist"]) {
mkdirSync(join(root, dir), { recursive: true });
}
for (const file of [
"pkg/package.json",
"pkg/src/index.ts",
"pkg/src/lib/a.ts",
"pkg/src/lib/a.test.ts",
"pkg/test/main.test.ts",
"pkg/node_modules/dep/index.ts",
"pkg/dist/index.js",
]) {
writeFileSync(join(root, file), "{}");
}
let links = true;
try {
symlinkSync("src", join(root, "pkg", "linked"), "dir");
} catch {
links = false;
}
const pkg = join(root, "pkg");
console.log("absolute cwd", globSync("**/*.ts", { cwd: pkg }).join(" "));
process.chdir(root);
console.log("relative cwd", globSync("src/*.ts", { cwd: "pkg" }).join(" "));
console.log("url cwd", globSync("*.json", { cwd: pathToFileURL(pkg) }).join(" "));
console.log("default cwd", globSync("pkg/*.json").join(" "));
console.log(
"exclude patterns",
globSync("**/*.ts", { cwd: pkg, exclude: ["node_modules/**", "**/*.test.ts"] }).join(" "),
);
console.log("exclude directory", globSync("**", { cwd: pkg, exclude: ["dist/"] }).filter((p) => !p.includes("linked")).join(" "));
console.log(
"exclude function",
globSync("**/*.ts", { cwd: pkg, exclude: (path) => path.startsWith("node_modules") || path.endsWith(".test.ts") }).join(
" ",
),
);
const visited: string[] = [];
globSync("src/**", {
cwd: pkg,
exclude: (path) => {
visited.push(path);
return false;
},
});
console.log("exclude sees", visited.join(" "));
const describe = (list: Dirent[]): string =>
list
.map((d) => {
const kind = d.isSymbolicLink() ? "link" : d.isDirectory() ? "dir" : d.isFile() ? "file" : "other";
const parent = d.parentPath.startsWith(root) ? `<root>${d.parentPath.slice(root.length)}` : d.parentPath;
return `${d.name}:${parent}:${kind}`;
})
.join(" ");
console.log("dirents absolute", describe(globSync("src/**", { cwd: pkg, withFileTypes: true })));
console.log("dirents relative", describe(globSync("pkg/src/*", { withFileTypes: true })));
console.log("dirents exclude", describe(globSync("**/*.ts", { cwd: pkg, withFileTypes: true, exclude: ["node_modules/**"] })));
console.log("withFileTypes false", globSync("*", { cwd: pkg, withFileTypes: false }).join(" "));
if (links) {
console.log("links", globSync("**/*.ts", { cwd: pkg, exclude: ["node_modules/**"] }).join(" "));
console.log("links followed", globSync("**/*.ts", { cwd: pkg, followSymlinks: true, exclude: ["node_modules/**"] }).join(" "));
console.log("link entry", describe(globSync("linked", { cwd: pkg, withFileTypes: true })));
console.log("through link", globSync("linked/lib/*", { cwd: pkg }).join(" "));
} else {
console.log("links unavailable");
}
console.log("relative check", relative(root, pkg));
process.chdir(tmpdir());
rmSync(root, { recursive: true, force: true });
+80
View File
@@ -0,0 +1,80 @@
// fs.glob argument handling from untyped code: Node's ERR_INVALID_ARG_TYPE
// errors for patterns, options, exclude, followSymlinks, cwd and the
// callback, the ERR_INTERNAL_ASSERTION a non-string cwd with exclude
// patterns hits in Node, empty patterns, missing directories, exclude
// callbacks that throw or return non-booleans, and the errors fs.glob's
// callback and fs/promises glob deliver.
import { globSync, glob as globCallback, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { glob } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
const any = (value) => /** @type {any} */ (value);
const attempt = (label, run) => {
try {
console.log(label, run());
} catch (error) {
const e = /** @type {any} */ (error);
console.log(label, "threw", e.name, e.code, JSON.stringify(e.message));
}
};
const root = mkdtempSync(join(tmpdir(), "scriptc-glob-validation-"));
mkdirSync(join(root, "a"));
for (const file of ["a/one.ts", "a/two.ts", "top.md"]) writeFileSync(join(root, file), "");
process.chdir(root);
attempt("number pattern", () => globSync(any(5)));
attempt("null pattern", () => globSync(any(null)));
attempt("object pattern", () => globSync(any({})));
attempt("function pattern", () => globSync(any(() => "*")));
attempt("array with a number", () => globSync(any(["*", 5])));
attempt("string options", () => globSync("*", any("x")));
attempt("null options", () => globSync("*", any(null)));
attempt("array options", () => globSync("*", any([])));
attempt("number exclude", () => globSync("*", any({ exclude: 5 })));
attempt("exclude with a number", () => globSync("*", any({ exclude: ["a", 1] })));
attempt("number followSymlinks", () => globSync("*", any({ followSymlinks: 1 })));
attempt("null followSymlinks", () => globSync("*", any({ followSymlinks: null })).join(" "));
attempt("number cwd", () => globSync("*", any({ cwd: 5 })));
attempt("number cwd with exclude", () => globSync("*", any({ cwd: 5, exclude: ["x"] })));
attempt("Buffer cwd", () => globSync("*", any({ cwd: Buffer.from("a") })));
attempt("http URL cwd", () => globSync("*", any({ cwd: new URL("http://example.com/") })));
attempt("missing cwd", () => globSync("*", { cwd: join(root, "missing") }).length);
attempt("empty pattern", () => globSync("").length);
attempt("no patterns", () => globSync([]).length);
attempt("exclude throws", () =>
globSync("*", {
exclude: () => {
throw new RangeError("stop");
},
}),
);
attempt("exclude truthy values", () => globSync("**", { exclude: any((p) => (p.endsWith(".ts") ? 1 : "")) }).join(" "));
attempt("missing callback", () => globCallback("*", any(undefined)));
attempt("bad pattern with callback", () => globCallback(any(5), () => console.log("not reached")));
globCallback("a/*.ts", (err, files) => {
console.log("callback", err, files.join(" "));
globCallback("*", { cwd: "a" }, (err2, files2) => {
console.log("callback with options", err2, files2.join(" "));
globCallback("*", any({ cwd: 5 }), (error) => {
console.log("callback cwd error", error.name, error.code, error.message);
void loop();
});
});
});
async function loop() {
const seen = [];
for await (const path of glob("**/*.ts")) seen.push(path);
console.log("for await", seen.join(" "));
try {
for await (const path of glob(any(5))) console.log("not reached", path);
} catch (error) {
const e = /** @type {any} */ (error);
console.log("for await bad pattern", e.name, e.code);
}
process.chdir(tmpdir());
rmSync(root, { recursive: true, force: true });
}
+74
View File
@@ -0,0 +1,74 @@
// fs/promises glob as a for await source: string matches, Dirent matches
// (fs/promises Dirents carry the resolved parent directory), the named,
// namespace and fs.promises spellings, early exits with break and continue,
// nested loops, and an error from an exclude callback surfacing at the
// loop.
import * as fs from "node:fs";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import * as fsp from "node:fs/promises";
import { glob } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
const root = mkdtempSync(join(tmpdir(), "scriptc-fsp-glob-"));
for (const dir of ["one/deep", "two"]) mkdirSync(join(root, dir), { recursive: true });
for (const file of ["one/a.txt", "one/deep/b.txt", "two/c.txt", "two/d.md", "e.txt"]) {
writeFileSync(join(root, file), "");
}
async function main(): Promise<void> {
const all: string[] = [];
for await (const path of glob("**/*.txt", { cwd: root })) all.push(path);
console.log("named", all.join(" "));
const viaNamespace: string[] = [];
for await (const path of fsp.glob("*/*", { cwd: root, exclude: ["**/*.md"] })) viaNamespace.push(path);
console.log("namespace", viaNamespace.join(" "));
const viaFs: string[] = [];
for await (const path of fs.promises.glob("two/*", { cwd: root })) viaFs.push(path);
console.log("fs.promises", viaFs.join(" "));
const rows: string[] = [];
for await (const entry of glob("one/**", { cwd: root, withFileTypes: true })) {
rows.push(`${entry.name}|${entry.parentPath.slice(root.length)}|${entry.isDirectory()}`);
}
console.log("dirents", rows.join(" "));
let first = "";
for await (const path of glob("**/*.txt", { cwd: root })) {
first = path;
break;
}
console.log("break", first);
const skipped: string[] = [];
for await (const path of glob("**/*.txt", { cwd: root })) {
if (path.startsWith("one")) continue;
skipped.push(path);
}
console.log("continue", skipped.join(" "));
const pairs: string[] = [];
for await (const dir of glob("*/", { cwd: root })) {
for await (const file of glob(`${dir}/*.txt`, { cwd: root })) pairs.push(`${dir}>${file}`);
}
console.log("nested", pairs.join(" "));
try {
for await (const path of glob("**", {
cwd: root,
exclude: (p) => {
if (p === "two") throw new Error(`refused ${p}`);
return false;
},
})) {
console.log("visited", path);
}
} catch (error) {
console.log("caught", (error as Error).message);
}
rmSync(root, { recursive: true, force: true });
}
void main();
+44
View File
@@ -0,0 +1,44 @@
// fs.glob's error-first callback form: with and without options, pattern
// arrays, exclude patterns and functions, the error an exclude callback
// throws arriving at the callback. Each call starts after the previous
// callback ran, so the output order does not depend on I/O timing.
import { glob, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
const root = mkdtempSync(join(tmpdir(), "scriptc-glob-callback-"));
mkdirSync(join(root, "lib/inner"), { recursive: true });
for (const file of ["lib/a.js", "lib/b.ts", "lib/inner/c.ts", "top.json"]) writeFileSync(join(root, file), "");
process.chdir(root);
const steps: (() => void)[] = [
() => glob("lib/**/*.ts", (err, files) => next("no options", err, files)),
() => glob("**/*.ts", { cwd: "lib" }, (err, files) => next("cwd", err, files)),
() => glob(["*.json", "lib/*.js"], {}, (err, files) => next("array", err, files)),
() => glob("**/*", { exclude: ["lib/inner/**"] }, (err, files) => next("exclude patterns", err, files)),
() => glob("**/*", { exclude: (path) => path.endsWith(".js") }, (err, files) => next("exclude function", err, files)),
() =>
glob(
"**",
{
exclude: () => {
throw new Error("exclude failed");
},
},
(err, files) => next("exclude throws", err, files),
),
() => finish(),
];
function next(label: string, err: NodeJS.ErrnoException | null, files: string[] | undefined): void {
console.log(label, err ? `error ${err.message}` : null, files === undefined ? "no files" : files.join(" "));
steps.shift()?.();
}
function finish(): void {
process.chdir(tmpdir());
rmSync(root, { recursive: true, force: true });
console.log("done");
}
steps.shift()?.();
@@ -0,0 +1,66 @@
// `%` by a constant power of two of at least 1 matches Node for every kind
// of dividend: integers, fractions, subnormals, values beyond 2^53, -0,
// NaN and the infinities. The divisor's sign does not change the result,
// and a zero result takes the dividend's sign.
function show(label: string, v: number): void {
console.log(label, Object.is(v, -0) ? "-0" : String(v));
}
const dividends: number[] = [
0, -0, 1, -1, 3, -4, 7.25, -7.25, 0.75, -0.75, 1.5, -1.5, 5e-324, -5e-324,
2.2250738585072014e-308, -2.2250738585072014e-308, 1e-300, 2 ** 31 - 0.5, -(2 ** 31) + 0.5,
2 ** 32 + 0.25, 2 ** 52 + 1, -(2 ** 52 + 3), 2 ** 52 + 0.5, 2 ** 53, -(2 ** 53), 2 ** 53 + 2,
2 ** 60 + 1024, -(2 ** 60), 2 ** 63, 2 ** 64 + 4096, 1e21, -1e21, 1e300, -1e300,
Number.MAX_VALUE, -Number.MAX_VALUE, 123456789.123, -98765.4321, NaN, Infinity, -Infinity,
];
for (const x of dividends) {
show(`${x} % 1`, x % 1);
show(`${x} % -1`, x % -1);
show(`${x} % 2`, x % 2);
show(`${x} % 8`, x % 8);
show(`${x} % -16`, x % -16);
show(`${x} % 1024`, x % 1024);
show(`${x} % 2147483648`, x % 2147483648);
show(`${x} % -2147483648`, x % -2147483648);
show(`${x} % 4294967296`, x % 4294967296);
show(`${x} % 2 ** 32`, x % 2 ** 32);
show(`${x} % -(2 ** 40)`, x % -(2 ** 40));
show(`${x} % 2 ** 52`, x % 2 ** 52);
show(`${x} % 2 ** 53`, x % 2 ** 53);
show(`${x} % 2 ** 64`, x % 2 ** 64);
show(`${x} % 2 ** 1023`, x % 2 ** 1023);
show(`${x} % -(2 ** 1023)`, x % -(2 ** 1023));
}
// Divisors that are not powers of two of at least 1 keep the general path.
for (const x of dividends) {
show(`${x} % 0.5`, x % 0.5);
show(`${x} % 0.125`, x % 0.125);
show(`${x} % 3`, x % 3);
show(`${x} % 6`, x % 6);
show(`${x} % 2 ** 0.5`, x % 2 ** 0.5);
show(`${x} % 2 ** -1`, x % 2 ** -1);
}
// Module constants, compound assignment, record fields and closures.
const WORD = 4294967296;
const HALF_WORD = 65536;
interface Cell {
value: number;
}
const cell: Cell = { value: -12345.678 };
let total = 0;
for (let i = 0; i < 64; i++) {
cell.value = cell.value * -3.7 + i;
let v = cell.value;
v %= HALF_WORD;
total += (v % WORD) + (cell.value % 256);
}
show("total", total);
const wrap = (x: number): number => x % 2 ** 16;
show("wrap", wrap(-1e15 - 0.5) + wrap(2 ** 70) + wrap(65536.5));
let sum = 0;
for (let i = -20; i <= 20; i++) sum += (i * 0.37) % 1;
show("sum", sum);
@@ -0,0 +1,50 @@
// `%` with integer dividends beyond 2^53 (every double there is an integer)
// and integer divisors: constant, int32 and unproven. Results match Node,
// including the sign of zero and the bounds of the inline integer path at
// 2^62 and 2^63.
function show(label: string, v: number): void {
console.log(label, Object.is(v, -0) ? "-0" : String(v));
}
const dividends: number[] = [
2 ** 53, -(2 ** 53), 2 ** 53 + 2, -(2 ** 53 + 2), 2 ** 54 + 4, 2 ** 60 + 1024, -(2 ** 60 + 2048),
2 ** 61 + 3 * 2 ** 9, 2 ** 62 - 1024, -(2 ** 62 - 1024), 2 ** 62, -(2 ** 62), 2 ** 62 + 1024,
-(2 ** 62 + 1024), 2 ** 63 - 1024, -(2 ** 63 - 1024), 2 ** 63, -(2 ** 63), 2 ** 64, -(2 ** 64),
1e19, -1e19, 1e21, 9.999999999999999e22,
];
const divisors: number[] = [
1, -1, 2, 3, -3, 7, 10, 1000, 65535, 1000000007, 2147483647, -2147483647, 2 ** 31, 2 ** 32 - 1,
2 ** 53 - 1, 2 ** 53 + 2, 2 ** 62, -(2 ** 62), 2 ** 62 + 1024, 2 ** 63, -(2 ** 63), 2 ** 64, 0,
-0,
];
// Unproven dividends and divisors read from arrays.
let checksum = 0;
for (let i = 0; i < dividends.length; i++) {
for (let j = 0; j < divisors.length; j++) {
const r = dividends[i]! % divisors[j]!;
show(`${dividends[i]} % ${divisors[j]}`, r);
if (r === r) checksum = (checksum * 31 + (r % 1000003)) % 1000000007;
}
}
show("checksum", checksum);
// Constant divisors.
for (const x of dividends) {
show(`${x} % 3`, x % 3);
show(`${x} % -10`, x % -10);
show(`${x} % 1000000007`, x % 1000000007);
show(`${x} % 2147483647`, x % 2147483647);
show(`${x} % -1`, x % -1);
}
// Int32 divisors: loop counters and `| 0` values.
for (let k = 1; k <= 40; k += 13) {
for (const x of dividends) {
show(`${x} % ${k}`, x % k);
const m = (k * 7919) | 0;
show(`${x} % ${m}`, x % m);
show(`${x} % ${-m}`, x % -m);
}
}
+50
View File
@@ -0,0 +1,50 @@
// Linear congruential generators whose products exceed 2^53, reduced with
// `%` by powers of two and by other constants. The products round, so every
// step must reproduce Node's double arithmetic exactly.
let seed = 42;
function lcg31(): number {
seed = (seed * 1103515245 + 12345) % 2147483648;
return seed;
}
let prime = 42;
function parkMillerWide(): number {
prime = (prime * 1103515245 + 12345) % 2147483647;
return prime;
}
let word = 7;
function lcg32(): number {
word = (word * 6364136223846793 + 1442695040888963) % 2 ** 32;
return word;
}
let negative = -99;
function signedStep(): number {
negative = (negative * -1103515245 - 12345) % 2147483648;
return negative;
}
const seen = new Map<number, number>();
let a = 0;
let b = 0;
let c = 0;
let d = 0;
for (let i = 0; i < 20000; i++) {
const x = lcg31();
a = (a + x) % 1000000007;
b = (b * 31 + parkMillerWide()) % 1000000007;
c = (c + lcg32()) % 2 ** 40;
d = (d + signedStep()) % 1000000007;
if (i < 8) console.log(i, x, prime, word, negative);
seen.set(x, (seen.get(x) ?? 0) + 1);
}
console.log("sums", a, b, c, d, Object.is(d, -0));
console.log("distinct", seen.size);
// Bucket a stream into a small table with the low and high bits.
const buckets = new Int32Array(16);
for (let i = 0; i < 5000; i++) {
const x = lcg31();
buckets[x % 16]++;
buckets[Math.floor(x / 134217728) % 16]++;
}
console.log(Array.from(buckets).join(","));
+34
View File
@@ -0,0 +1,34 @@
// Fractional dividends with `%`: Weyl sequences and hashes reduced `% 1`,
// phases reduced by powers of two and by other divisors, and negative values
// whose remainders keep the dividend's sign.
function show(label: string, v: number): void {
console.log(label, Object.is(v, -0) ? "-0" : String(v));
}
let weyl = 0.5;
let hashSum = 0;
for (let i = 0; i < 2000; i++) {
weyl = (weyl + 0.6180339887498949) % 1;
const h = (weyl * 2654435761.5) % 1;
hashSum += h;
if (i < 6) show(`weyl ${i}`, weyl);
if (i < 6) show(`hash ${i}`, h);
}
show("hashSum", hashSum);
let phase = -3.75;
for (let i = 0; i < 12; i++) {
phase = phase * -2.5 + 0.125;
show(`phase ${i} % 4`, phase % 4);
show(`phase ${i} % 0.25`, phase % 0.25);
show(`phase ${i} % 360`, phase % 360);
show(`phase ${i} % (2 * Math.PI)`, phase % (2 * Math.PI));
}
// Fractional parts of negative values and of values that are integers.
for (const x of [-2.5, -2, -0.5, -0, 0, 0.5, 2, 2.5, -1e-310, 1e-310, 4503599627370495.5]) {
show(`${x} % 1`, x % 1);
show(`${x} % -1`, x % -1);
show(`${x} % 2`, x % 2);
}
+89
View File
@@ -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));
});
});
});
+94
View File
@@ -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);
@@ -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);
@@ -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);
+30
View File
@@ -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"));
+26
View File
@@ -0,0 +1,26 @@
/* fs.globSync, fs/promises glob and fs.glob under @types/node: the same
* native walk as under the bundled declarations. */
import { type Dirent, glob as globCallback, globSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { glob } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
const root = mkdtempSync(join(tmpdir(), "scriptc-node-types-glob-"));
mkdirSync(join(root, "src/util"), { recursive: true });
for (const file of ["src/a.ts", "src/util/b.ts", "src/util/b.test.ts", "notes.md"]) writeFileSync(join(root, file), "");
console.log(globSync("**/*.ts", { cwd: root, exclude: ["**/*.test.ts"] }).join(" "));
const entries: Dirent[] = globSync("src/*", { cwd: root, withFileTypes: true });
console.log(entries.map((d) => `${d.name}:${d.isDirectory()}`).join(" "));
async function main(): Promise<void> {
const found: string[] = [];
for await (const path of glob("src/**", { cwd: root })) found.push(path);
console.log(found.join(" "));
globCallback("*.md", { cwd: root }, (err, matches) => {
console.log(err, matches.join(" "));
rmSync(root, { recursive: true, force: true });
});
}
void main();
+1 -1
View File
@@ -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");
+14
View File
@@ -548,6 +548,20 @@ test("node-types: Blob and File match Node under @types/node", async () => {
expect(comparableStderr(native.stderr)).toBe(node.stderr);
});
test("node-types: fs.glob forms match Node under @types/node", async () => {
const outDir = outDirFor("node-fs-glob");
const entry = join(nodeTypesDir, "fs-glob.ts");
const result = await compile(entry, { outPath: join(outDir, "fs-glob"), outDir, sanitize });
expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics, null, 2) : "").toBe(true);
if (!result.ok) return;
const [native, node] = await Promise.all([
execFileAsync(result.binaryPath),
execFileAsync(process.execPath, ["--disable-warning=MODULE_TYPELESS_PACKAGE_JSON", entry]),
]);
expect(native.stdout).toBe(node.stdout);
expect(comparableStderr(native.stderr)).toBe(node.stderr);
});
test("node-types: util function helpers match Node under @types/node", async () => {
const outDir = outDirFor("node-util-callables");
const entry = join(nodeTypesDir, "util-callables.ts");