mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-04 08:04:37 +08:00
Host stdio: typed process.stdout.write slot, write-callback errors, drain, run-time-typed chunks (#2546)
This commit is contained in:
@@ -12,9 +12,11 @@ Stored global console values and detached output methods retain identity and acc
|
||||
Stored methods do not support format specifiers with substitution arguments.
|
||||
Other stored console APIs, custom `Console` instances, and console mutation are unsupported.
|
||||
|
||||
JavaScript can store `process.stdin`, `stdout`, and `stderr`, replace output `write` methods, and call saved methods with their stream receiver.
|
||||
JavaScript and TypeScript can store `process.stdin`, `stdout`, and `stderr`, replace output `write` methods, and call saved methods with their stream receiver.
|
||||
A replaced `write` method does not receive console's error-callback argument.
|
||||
Input supports data/end/error listeners, pause/resume, buffered reads, and TTY raw mode.
|
||||
Output writes are synchronous, with successful callbacks on the next-tick queue; queued backpressure is not modeled.
|
||||
Because no write is ever queued, `writableLength` is 0, `writableNeedDrain` is false, `writableHighWaterMark` is 65536, and `'drain'` never fires, so drain listeners register silently and `events.once(process.stdout, "drain")` stays pending, as it does in Node when no write returned false.
|
||||
Stored streams do not support piping, encoding changes, arbitrary properties, or other events.
|
||||
A TypeScript `NodeJS.WritableStream` parameter that only receives `process.stdout` or `process.stderr` supports `write(string)`.
|
||||
When other streams flow into the same parameter, the process streams join them as `Writable` instances with the full writable surface.
|
||||
|
||||
@@ -79697,6 +79697,7 @@
|
||||
"tests/corpus/process-builtin-values.js",
|
||||
"tests/corpus/process-hrtime.js",
|
||||
"tests/corpus/process-named-signals.cjs",
|
||||
"tests/corpus/process-stream-drain.ts",
|
||||
"tests/corpus/process-uncaught-handler-throws.cjs",
|
||||
"tests/corpus/process-uncaught-loop.cjs",
|
||||
"tests/corpus/process-uncaught-module.mjs",
|
||||
@@ -79704,9 +79705,11 @@
|
||||
"tests/corpus/process-uncaught-rejection.cjs",
|
||||
"tests/corpus/process-uncaught-sync.cjs",
|
||||
"tests/corpus/process-versions-storage.ts",
|
||||
"tests/corpus/process-write-dynamic-chunks.ts",
|
||||
"tests/corpus/stdio-values.mjs",
|
||||
"tests/corpus/stdio-write-errors.mjs",
|
||||
"tests/corpus/stdio-write-overrides.mjs",
|
||||
"tests/corpus/stdout-write-patch.ts",
|
||||
"tests/fixtures/node-types/env-files.ts",
|
||||
"tests/fixtures/node-types/util-callables.ts",
|
||||
"tests/harness/event-loop.test.ts",
|
||||
@@ -82065,6 +82068,9 @@
|
||||
"tests/corpus/stdio-values.mjs",
|
||||
"tests/corpus/stdio-write-overrides.mjs",
|
||||
"tests/corpus/stdio-write-errors.mjs",
|
||||
"tests/corpus/stdout-write-patch.ts",
|
||||
"tests/corpus/process-stream-drain.ts",
|
||||
"tests/corpus/process-write-dynamic-chunks.ts",
|
||||
"tests/harness/event-loop.test.ts"
|
||||
]
|
||||
},
|
||||
@@ -82107,6 +82113,9 @@
|
||||
"tests/corpus/stdio-values.mjs",
|
||||
"tests/corpus/stdio-write-overrides.mjs",
|
||||
"tests/corpus/stdio-write-errors.mjs",
|
||||
"tests/corpus/stdout-write-patch.ts",
|
||||
"tests/corpus/process-stream-drain.ts",
|
||||
"tests/corpus/process-write-dynamic-chunks.ts",
|
||||
"tests/harness/event-loop.test.ts"
|
||||
]
|
||||
},
|
||||
@@ -82149,6 +82158,9 @@
|
||||
"tests/corpus/stdio-values.mjs",
|
||||
"tests/corpus/stdio-write-overrides.mjs",
|
||||
"tests/corpus/stdio-write-errors.mjs",
|
||||
"tests/corpus/stdout-write-patch.ts",
|
||||
"tests/corpus/process-stream-drain.ts",
|
||||
"tests/corpus/process-write-dynamic-chunks.ts",
|
||||
"tests/harness/event-loop.test.ts"
|
||||
]
|
||||
},
|
||||
@@ -86951,7 +86963,8 @@
|
||||
"tests/corpus/5611-readable-from-class-iterables.ts",
|
||||
"tests/corpus/5612-readable-from-iterables.cjs",
|
||||
"tests/corpus/5617-pipeline-function-ends.ts",
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs"
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs",
|
||||
"tests/corpus/writable-error-callbacks.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "chapter"
|
||||
@@ -87077,7 +87090,8 @@
|
||||
"tests/corpus/5425-pipeline-generator-stage.ts",
|
||||
"tests/corpus/5428-pipeline-generator-stages.cjs",
|
||||
"tests/corpus/5617-pipeline-function-ends.ts",
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs"
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs",
|
||||
"tests/corpus/writable-error-callbacks.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -87111,7 +87125,8 @@
|
||||
"tests/corpus/5425-pipeline-generator-stage.ts",
|
||||
"tests/corpus/5428-pipeline-generator-stages.cjs",
|
||||
"tests/corpus/5617-pipeline-function-ends.ts",
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs"
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs",
|
||||
"tests/corpus/writable-error-callbacks.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -87145,7 +87160,8 @@
|
||||
"tests/corpus/5425-pipeline-generator-stage.ts",
|
||||
"tests/corpus/5428-pipeline-generator-stages.cjs",
|
||||
"tests/corpus/5617-pipeline-function-ends.ts",
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs"
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs",
|
||||
"tests/corpus/writable-error-callbacks.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -87202,7 +87218,8 @@
|
||||
"tests/corpus/5425-pipeline-generator-stage.ts",
|
||||
"tests/corpus/5428-pipeline-generator-stages.cjs",
|
||||
"tests/corpus/5617-pipeline-function-ends.ts",
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs"
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs",
|
||||
"tests/corpus/writable-error-callbacks.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -87236,7 +87253,8 @@
|
||||
"tests/corpus/5425-pipeline-generator-stage.ts",
|
||||
"tests/corpus/5428-pipeline-generator-stages.cjs",
|
||||
"tests/corpus/5617-pipeline-function-ends.ts",
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs"
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs",
|
||||
"tests/corpus/writable-error-callbacks.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -87270,7 +87288,8 @@
|
||||
"tests/corpus/5425-pipeline-generator-stage.ts",
|
||||
"tests/corpus/5428-pipeline-generator-stages.cjs",
|
||||
"tests/corpus/5617-pipeline-function-ends.ts",
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs"
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs",
|
||||
"tests/corpus/writable-error-callbacks.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -87666,7 +87685,8 @@
|
||||
"tests/corpus/5425-pipeline-generator-stage.ts",
|
||||
"tests/corpus/5428-pipeline-generator-stages.cjs",
|
||||
"tests/corpus/5617-pipeline-function-ends.ts",
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs"
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs",
|
||||
"tests/corpus/writable-error-callbacks.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -87723,7 +87743,8 @@
|
||||
"tests/corpus/5425-pipeline-generator-stage.ts",
|
||||
"tests/corpus/5428-pipeline-generator-stages.cjs",
|
||||
"tests/corpus/5617-pipeline-function-ends.ts",
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs"
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs",
|
||||
"tests/corpus/writable-error-callbacks.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -87757,7 +87778,8 @@
|
||||
"tests/corpus/5425-pipeline-generator-stage.ts",
|
||||
"tests/corpus/5428-pipeline-generator-stages.cjs",
|
||||
"tests/corpus/5617-pipeline-function-ends.ts",
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs"
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs",
|
||||
"tests/corpus/writable-error-callbacks.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -87814,7 +87836,8 @@
|
||||
"tests/corpus/5425-pipeline-generator-stage.ts",
|
||||
"tests/corpus/5428-pipeline-generator-stages.cjs",
|
||||
"tests/corpus/5617-pipeline-function-ends.ts",
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs"
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs",
|
||||
"tests/corpus/writable-error-callbacks.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "ancestor"
|
||||
@@ -87848,7 +87871,8 @@
|
||||
"tests/corpus/5425-pipeline-generator-stage.ts",
|
||||
"tests/corpus/5428-pipeline-generator-stages.cjs",
|
||||
"tests/corpus/5617-pipeline-function-ends.ts",
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs"
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs",
|
||||
"tests/corpus/writable-error-callbacks.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "ancestor"
|
||||
@@ -87882,7 +87906,8 @@
|
||||
"tests/corpus/5425-pipeline-generator-stage.ts",
|
||||
"tests/corpus/5428-pipeline-generator-stages.cjs",
|
||||
"tests/corpus/5617-pipeline-function-ends.ts",
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs"
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs",
|
||||
"tests/corpus/writable-error-callbacks.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "ancestor"
|
||||
@@ -88278,7 +88303,8 @@
|
||||
"tests/corpus/5425-pipeline-generator-stage.ts",
|
||||
"tests/corpus/5428-pipeline-generator-stages.cjs",
|
||||
"tests/corpus/5617-pipeline-function-ends.ts",
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs"
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs",
|
||||
"tests/corpus/writable-error-callbacks.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "ancestor"
|
||||
@@ -88381,7 +88407,8 @@
|
||||
"tests/corpus/5425-pipeline-generator-stage.ts",
|
||||
"tests/corpus/5428-pipeline-generator-stages.cjs",
|
||||
"tests/corpus/5617-pipeline-function-ends.ts",
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs"
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs",
|
||||
"tests/corpus/writable-error-callbacks.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -88441,7 +88468,8 @@
|
||||
"tests/corpus/5425-pipeline-generator-stage.ts",
|
||||
"tests/corpus/5428-pipeline-generator-stages.cjs",
|
||||
"tests/corpus/5617-pipeline-function-ends.ts",
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs"
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs",
|
||||
"tests/corpus/writable-error-callbacks.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -88475,7 +88503,8 @@
|
||||
"tests/corpus/5425-pipeline-generator-stage.ts",
|
||||
"tests/corpus/5428-pipeline-generator-stages.cjs",
|
||||
"tests/corpus/5617-pipeline-function-ends.ts",
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs"
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs",
|
||||
"tests/corpus/writable-error-callbacks.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -88509,7 +88538,8 @@
|
||||
"tests/corpus/5425-pipeline-generator-stage.ts",
|
||||
"tests/corpus/5428-pipeline-generator-stages.cjs",
|
||||
"tests/corpus/5617-pipeline-function-ends.ts",
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs"
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs",
|
||||
"tests/corpus/writable-error-callbacks.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -88566,7 +88596,8 @@
|
||||
"tests/corpus/5425-pipeline-generator-stage.ts",
|
||||
"tests/corpus/5428-pipeline-generator-stages.cjs",
|
||||
"tests/corpus/5617-pipeline-function-ends.ts",
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs"
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs",
|
||||
"tests/corpus/writable-error-callbacks.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -89159,7 +89190,8 @@
|
||||
"tests/corpus/5425-pipeline-generator-stage.ts",
|
||||
"tests/corpus/5428-pipeline-generator-stages.cjs",
|
||||
"tests/corpus/5617-pipeline-function-ends.ts",
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs"
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs",
|
||||
"tests/corpus/writable-error-callbacks.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -89193,7 +89225,8 @@
|
||||
"tests/corpus/5425-pipeline-generator-stage.ts",
|
||||
"tests/corpus/5428-pipeline-generator-stages.cjs",
|
||||
"tests/corpus/5617-pipeline-function-ends.ts",
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs"
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs",
|
||||
"tests/corpus/writable-error-callbacks.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -89227,7 +89260,8 @@
|
||||
"tests/corpus/5425-pipeline-generator-stage.ts",
|
||||
"tests/corpus/5428-pipeline-generator-stages.cjs",
|
||||
"tests/corpus/5617-pipeline-function-ends.ts",
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs"
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs",
|
||||
"tests/corpus/writable-error-callbacks.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -89261,7 +89295,8 @@
|
||||
"tests/corpus/5425-pipeline-generator-stage.ts",
|
||||
"tests/corpus/5428-pipeline-generator-stages.cjs",
|
||||
"tests/corpus/5617-pipeline-function-ends.ts",
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs"
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs",
|
||||
"tests/corpus/writable-error-callbacks.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -89295,7 +89330,8 @@
|
||||
"tests/corpus/5425-pipeline-generator-stage.ts",
|
||||
"tests/corpus/5428-pipeline-generator-stages.cjs",
|
||||
"tests/corpus/5617-pipeline-function-ends.ts",
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs"
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs",
|
||||
"tests/corpus/writable-error-callbacks.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -89329,7 +89365,8 @@
|
||||
"tests/corpus/5425-pipeline-generator-stage.ts",
|
||||
"tests/corpus/5428-pipeline-generator-stages.cjs",
|
||||
"tests/corpus/5617-pipeline-function-ends.ts",
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs"
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs",
|
||||
"tests/corpus/writable-error-callbacks.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -89363,7 +89400,8 @@
|
||||
"tests/corpus/5425-pipeline-generator-stage.ts",
|
||||
"tests/corpus/5428-pipeline-generator-stages.cjs",
|
||||
"tests/corpus/5617-pipeline-function-ends.ts",
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs"
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs",
|
||||
"tests/corpus/writable-error-callbacks.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -89397,7 +89435,8 @@
|
||||
"tests/corpus/5425-pipeline-generator-stage.ts",
|
||||
"tests/corpus/5428-pipeline-generator-stages.cjs",
|
||||
"tests/corpus/5617-pipeline-function-ends.ts",
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs"
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs",
|
||||
"tests/corpus/writable-error-callbacks.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -89431,7 +89470,8 @@
|
||||
"tests/corpus/5425-pipeline-generator-stage.ts",
|
||||
"tests/corpus/5428-pipeline-generator-stages.cjs",
|
||||
"tests/corpus/5617-pipeline-function-ends.ts",
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs"
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs",
|
||||
"tests/corpus/writable-error-callbacks.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -90850,7 +90890,8 @@
|
||||
"tests/corpus/5425-pipeline-generator-stage.ts",
|
||||
"tests/corpus/5428-pipeline-generator-stages.cjs",
|
||||
"tests/corpus/5617-pipeline-function-ends.ts",
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs"
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs",
|
||||
"tests/corpus/writable-error-callbacks.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -91114,7 +91155,8 @@
|
||||
"tests/corpus/5425-pipeline-generator-stage.ts",
|
||||
"tests/corpus/5428-pipeline-generator-stages.cjs",
|
||||
"tests/corpus/5617-pipeline-function-ends.ts",
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs"
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs",
|
||||
"tests/corpus/writable-error-callbacks.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "ancestor"
|
||||
@@ -91148,7 +91190,8 @@
|
||||
"tests/corpus/5425-pipeline-generator-stage.ts",
|
||||
"tests/corpus/5428-pipeline-generator-stages.cjs",
|
||||
"tests/corpus/5617-pipeline-function-ends.ts",
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs"
|
||||
"tests/corpus/5619-pipeline-function-ends.cjs",
|
||||
"tests/corpus/writable-error-callbacks.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "ancestor"
|
||||
|
||||
@@ -314,7 +314,7 @@
|
||||
{ "chapter": "process", "signatures": ["`process.hrtime([time])`", "`process.hrtime.bigint()`"], "status": "partial", "evidence": ["tests/corpus/process-hrtime.js"] },
|
||||
{ "chapter": "errors", "signatures": ["`stackTraceLimit` Type: {number}"], "status": "partial", "evidence": ["tests/corpus/error-stack-limit.ts"] },
|
||||
{ "chapter": "errors", "signatures": ["`stack` Type: {string}"], "status": "partial", "evidence": ["tests/corpus/error-stack-frames.ts", "tests/corpus/error-stack-limit.ts"] },
|
||||
{ "chapter": "process", "signatures": ["`stdin` Type: {Stream}", "`stdout` Type: {Stream}", "`stderr` Type: {Stream}"], "status": "partial", "evidence": ["tests/corpus/stdio-values.mjs", "tests/corpus/stdio-write-overrides.mjs", "tests/corpus/stdio-write-errors.mjs", "tests/harness/event-loop.test.ts"] },
|
||||
{ "chapter": "process", "signatures": ["`stdin` Type: {Stream}", "`stdout` Type: {Stream}", "`stderr` Type: {Stream}"], "status": "partial", "evidence": ["tests/corpus/stdio-values.mjs", "tests/corpus/stdio-write-overrides.mjs", "tests/corpus/stdio-write-errors.mjs", "tests/corpus/stdout-write-patch.ts", "tests/corpus/process-stream-drain.ts", "tests/corpus/process-write-dynamic-chunks.ts", "tests/harness/event-loop.test.ts"] },
|
||||
{ "chapter": "process", "signatures": ["Signal events"], "status": "partial", "evidence": ["tests/corpus/1443-signal-handlers.ts", "tests/corpus/process-named-signals.cjs"] },
|
||||
{ "chapter": "process", "signatures": ["Event: `'uncaughtException'`", "Event: `'uncaughtExceptionMonitor'`"], "status": "partial", "evidence": ["tests/corpus/process-uncaught-sync.cjs", "tests/corpus/process-uncaught-loop.cjs", "tests/corpus/process-uncaught-module.mjs", "tests/corpus/process-uncaught-rejection.cjs", "tests/corpus/process-uncaught-monitor.cjs", "tests/corpus/process-uncaught-handler-throws.cjs"] },
|
||||
{ "chapter": "process", "signatures": ["`process.getBuiltinModule(id)`"], "status": "partial", "evidence": ["tests/corpus/process-builtin-values.js", "packages/compiler/test/process-builtin-values.test.ts"] },
|
||||
@@ -478,7 +478,7 @@
|
||||
{ "symbols": ["StringDecoder", "stringDecoder.write", "stringDecoder.end"], "status": "partial", "evidence": ["tests/corpus/1662-string-decoder-encodings.ts", "tests/corpus/node24-string-decoder.ts", "tests/corpus/5882-string-decoder-runtime-encoding.ts"] },
|
||||
{ "symbols": ["setImmediate", "setInterval", "setTimeout", "clearImmediate", "clearInterval", "clearTimeout", "timersPromises.setTimeout", "timersPromises.setImmediate", "timersPromises.setInterval", "immediate.hasRef", "immediate.ref", "immediate.unref", "timeout.hasRef", "timeout.ref", "timeout.unref", "timeout.refresh"], "status": "partial", "evidence": ["tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", "tests/corpus/2093-timers-promises.ts", "tests/corpus/2852-timers-promises-interval.ts", "tests/corpus/5830-timers-promises-signal.ts"] },
|
||||
{ "symbols": ["timersPromises.scheduler.wait", "timersPromises.scheduler.yield"], "status": "partial", "evidence": ["tests/corpus/5839-timers-promises-scheduler.ts", "tests/corpus/5889-timers-scheduler-signal.ts"] },
|
||||
{ "symbols": ["stream.finished", "stream.pipeline", "stream.getDefaultHighWaterMark", "readable.push", "readable.unshift", "readable.read", "readable.pause", "readable.resume", "readable.isPaused", "readable.setEncoding", "readable.pipe", "readable.unpipe", "readable.destroy", "writable.write", "writable.end", "writable.cork", "writable.uncork", "writable.setDefaultEncoding"], "status": "partial", "evidence": ["tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", "tests/corpus/1814-stream-pipeline.cjs", "tests/corpus/5302-stream-promises-options.ts", "tests/corpus/5303-stream-finished-callback-opts.cjs", "tests/corpus/5425-pipeline-generator-stage.ts", "tests/corpus/5428-pipeline-generator-stages.cjs", "tests/corpus/5617-pipeline-function-ends.ts", "tests/corpus/5619-pipeline-function-ends.cjs"] },
|
||||
{ "symbols": ["stream.finished", "stream.pipeline", "stream.getDefaultHighWaterMark", "readable.push", "readable.unshift", "readable.read", "readable.pause", "readable.resume", "readable.isPaused", "readable.setEncoding", "readable.pipe", "readable.unpipe", "readable.destroy", "writable.write", "writable.end", "writable.cork", "writable.uncork", "writable.setDefaultEncoding"], "status": "partial", "evidence": ["tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", "tests/corpus/1814-stream-pipeline.cjs", "tests/corpus/5302-stream-promises-options.ts", "tests/corpus/5303-stream-finished-callback-opts.cjs", "tests/corpus/5425-pipeline-generator-stage.ts", "tests/corpus/5428-pipeline-generator-stages.cjs", "tests/corpus/5617-pipeline-function-ends.ts", "tests/corpus/5619-pipeline-function-ends.cjs", "tests/corpus/writable-error-callbacks.ts"] },
|
||||
{ "symbols": ["net.createServer", "net.connect", "net.createConnection", "net.getDefaultAutoSelectFamilyAttemptTimeout", "net.setDefaultAutoSelectFamilyAttemptTimeout", "server.address", "server.close", "server.listen", "socket.destroy", "socket.end", "socket.pause", "socket.resume", "socket.setEncoding", "socket.setTimeout", "socket.write"], "status": "partial", "evidence": ["tests/corpus/2500-net-autosel-timeout.ts", "tests/corpus/2596-net-arg-ladders.cjs"] },
|
||||
{ "symbols": ["dgram.createSocket", "socket.address", "socket.bind", "socket.close", "socket.connect", "socket.ref", "socket.remoteAddress", "socket.send", "socket.unref"], "status": "partial", "evidence": ["tests/corpus/2597-dgram-send-ladders.cjs"] },
|
||||
{ "symbols": ["net.SocketAddress", "net.BlockList"], "status": "partial", "evidence": ["tests/corpus/5659-net-blocklist-socketaddress.ts", "tests/fixtures/node-types/net-blocklist.ts"] },
|
||||
|
||||
@@ -1764,6 +1764,18 @@ declare module "tty" {
|
||||
/* The bytes queued behind the last write: the compiled binary's fd
|
||||
* 1/2 writes are synchronous, so it is always 0. */
|
||||
readonly writableLength: number;
|
||||
/* Node's default for the process streams (65536 for a pipe, a file and a
|
||||
* TTY): a constant, because nothing is ever queued. */
|
||||
readonly writableHighWaterMark: number;
|
||||
/* Always false: a synchronous write never returns false, so no 'drain'
|
||||
* is ever pending. */
|
||||
readonly writableNeedDrain: boolean;
|
||||
/* 'drain' and the other events by name. Node returns the stream. */
|
||||
on(event: string, listener: (...args: any[]) => void): this;
|
||||
once(event: string, listener: (...args: any[]) => void): this;
|
||||
addListener(event: string, listener: (...args: any[]) => void): this;
|
||||
off(event: string, listener: (...args: any[]) => void): this;
|
||||
removeListener(event: string, listener: (...args: any[]) => void): this;
|
||||
getWindowSize(): [number, number];
|
||||
}
|
||||
export function isatty(fd: number): boolean;
|
||||
|
||||
@@ -20,6 +20,8 @@ export function executableLinkFeatures(mod: IrModule, largePages = false): Nativ
|
||||
...(features.wasm ? { wasm: true } : {}),
|
||||
...(features.warningReplace ? { warningReplace: true } : {}),
|
||||
...(features.stdioExtended ? { stdioExtended: true } : {}),
|
||||
...(features.streamWriteErrors ? { streamWriteErrors: true } : {}),
|
||||
...(features.stdioWriteSlot ? { stdioWriteSlot: true } : {}),
|
||||
...(features.nodeIsland ? { nodeIsland: true } : {}),
|
||||
...(features.moduleNamespace ? { moduleNamespace: true } : {}),
|
||||
...(features.moduleNamespaceDyn ? { moduleNamespaceDyn: true } : {}),
|
||||
|
||||
@@ -391,9 +391,14 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"process.envPairs": "scr_env_pairs",
|
||||
"process.stdoutWrite": "scr_process_stdout_write",
|
||||
"process.stdio": "scr_process_stdio",
|
||||
"process.stdioSlot": "scr_process_stdio",
|
||||
"process.stdioWriteDyn": "scr_process_stdio_write_dyn",
|
||||
"process.stderrWrite": "scr_process_stderr_write",
|
||||
"process.isTTY": "scr_process_is_tty",
|
||||
"process.isTTYValue": "scr_process_is_tty_value",
|
||||
// A promise that never settles (events.once of an event a synchronous
|
||||
// stream never emits); a fresh pending promise is its owned result.
|
||||
"events.pendingForever": "scr_promise_new",
|
||||
"date.now": "scr_date_now",
|
||||
"date.newNow": "scr_date_now",
|
||||
"date.newMs": "scr_date_new_ms",
|
||||
@@ -1914,6 +1919,11 @@ export const USES_TIMERS_LIB_FNS = new Set<string>([
|
||||
"worker.receiveMessageOnPort",
|
||||
"worker.broadcastChannel",
|
||||
"process.stdio",
|
||||
// The typed write slot and the run-time-typed chunk write keep the loop
|
||||
// live for the write callbacks they queue: both reach the same stream
|
||||
// object as process.stdio and call back from a nextTick.
|
||||
"process.stdioSlot",
|
||||
"process.stdioWriteDyn",
|
||||
"fetch.start",
|
||||
"fetch.input",
|
||||
"fetch.function",
|
||||
@@ -1999,6 +2009,7 @@ export const USES_TIMERS_LIB_FNS = new Set<string>([
|
||||
"writable.writeU",
|
||||
"writable.writeDyn",
|
||||
"writable.writeEnc",
|
||||
"writable.writeEcb",
|
||||
"writable.end",
|
||||
"writable.uncork",
|
||||
"stream.destroy",
|
||||
|
||||
@@ -455,6 +455,44 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
|
||||
host.emitPendingCheck();
|
||||
return e.type.kind === "void" ? { name: "", type: e.type } : { name: t, type: e.type };
|
||||
}
|
||||
if (e.fn === "writable.writeEcb") {
|
||||
// (recv, mode, chunk, encoding, cb): mode 0 bytes, 1 string, 2 dyn. The
|
||||
// callback MOVES into an error-first wrapper (scr_stream_ecb_wrap); the
|
||||
// adapter turns the runtime's ScrError (NULL = success) into the
|
||||
// callback's Error | null argument, as fs.rename callbacks do.
|
||||
const modeArg = e.args[1]!;
|
||||
if (modeArg.kind !== "numLit")
|
||||
throw new InternalCompilerError("llvm emitter bug: writable.writeEcb mode not a literal");
|
||||
const cbT = e.args[4]!.type;
|
||||
if (cbT.kind !== "func")
|
||||
throw new InternalCompilerError("llvm emitter bug: writable.writeEcb callback not a func");
|
||||
const args = e.args.map((a) => host.emitExpr(a));
|
||||
host.toHeap(args[4]!);
|
||||
const adapter = host.fsRenameThunkFor(cbT);
|
||||
host.declare(`declare ptr @scr_stream_ecb_wrap(ptr, ptr)`);
|
||||
const wrapped = B.tmp();
|
||||
B.line(`${wrapped} = call ptr @scr_stream_ecb_wrap(ptr ${args[4]!.name}, ptr @${adapter})`);
|
||||
const entry =
|
||||
modeArg.value === 0
|
||||
? "scr_stream_write"
|
||||
: modeArg.value === 1
|
||||
? "scr_stream_write_str"
|
||||
: null;
|
||||
const t = B.tmp();
|
||||
if (entry !== null) {
|
||||
host.declare(`declare zeroext i1 @${entry}(ptr, ptr, ptr)`);
|
||||
B.line(
|
||||
`${t} = call zeroext i1 @${entry}(ptr ${args[0]!.name}, ptr ${args[2]!.name}, ptr ${wrapped})`,
|
||||
);
|
||||
} else {
|
||||
host.declare(`declare zeroext i1 @scr_stream_write_enc(ptr, ptr, ptr, ptr)`);
|
||||
B.line(
|
||||
`${t} = call zeroext i1 @scr_stream_write_enc(ptr ${args[0]!.name}, ptr ${args[2]!.name}, ptr ${args[3]!.name}, ptr ${wrapped})`,
|
||||
);
|
||||
}
|
||||
host.emitPendingCheck();
|
||||
return e.type.kind === "void" ? { name: "", type: e.type } : { name: t, type: e.type };
|
||||
}
|
||||
if (e.fn === "writable.end") {
|
||||
// (recv, flags[, chunk][, encoding][, cb]) — flags: 1 bytes chunk, 2
|
||||
// string chunk, 8 dyn chunk (write first, Node's end(chunk)
|
||||
|
||||
@@ -103,6 +103,7 @@ const INERT_LIB_CALLS = new Set([
|
||||
"json.parse",
|
||||
"process.stderrWrite",
|
||||
"process.stdoutWrite",
|
||||
"process.stdioWriteDyn",
|
||||
"process.envGet",
|
||||
"process.envPairs",
|
||||
"regex.groupEquals",
|
||||
|
||||
@@ -34,6 +34,12 @@ export interface NativeLinkFeatures {
|
||||
/** The program uses process.stdin.setEncoding, string 'data' listeners
|
||||
* or stdio 'resize' events: links the SCR_STDIO_EXTENDED events unit. */
|
||||
stdioExtended?: boolean;
|
||||
/** A write completion callback takes the error argument: links the
|
||||
* SCR_STREAM_WRITE_ERRORS build of the stream unit. */
|
||||
streamWriteErrors?: boolean;
|
||||
/** The program reaches the process output streams' write slot: links the
|
||||
* SCR_STDIO_WRITE_SLOT build of the events unit. */
|
||||
stdioWriteSlot?: boolean;
|
||||
/** Calls into Node.js (@scriptc/node): scr_node_island.c and the
|
||||
* SCR_NODE_ISLANDS variant of the event loop unit. */
|
||||
nodeIsland?: boolean;
|
||||
|
||||
@@ -161,6 +161,12 @@ export interface CcOptions {
|
||||
warningReplace?: boolean;
|
||||
/** SCR_STDIO_EXTENDED: stdin encoding, string data listeners, resize. */
|
||||
stdioExtended?: boolean;
|
||||
/** SCR_STREAM_WRITE_ERRORS: the stream unit publishes the error each
|
||||
* write completion callback receives (error-taking write callbacks). */
|
||||
streamWriteErrors?: boolean;
|
||||
/** SCR_STDIO_WRITE_SLOT: the events unit carries the typed write slot
|
||||
* (the write method as a value, its replacement, run-time-typed chunks). */
|
||||
stdioWriteSlot?: boolean;
|
||||
/** Calls into Node.js (@scriptc/node): compiles scr_node_island.c and
|
||||
* the event loop with SCR_NODE_ISLANDS (its helper slot). */
|
||||
nodeIsland?: boolean;
|
||||
|
||||
@@ -425,6 +425,11 @@ const RUNTIME_DEFINES: readonly {
|
||||
{ define: "SCR_JS_TIER", when: (opts) => opts.dynTier === true },
|
||||
{ define: "SCR_WARNING_REPLACE", when: (opts) => opts.warningReplace },
|
||||
{ define: "SCR_STDIO_EXTENDED", when: (opts) => opts.stdioExtended },
|
||||
// scr_stream.c's write-callback error publication, which the compiler's
|
||||
// error-first write adapter reads.
|
||||
{ define: "SCR_STREAM_WRITE_ERRORS", when: (opts) => opts.streamWriteErrors },
|
||||
// scr_events.c's typed process output write slot.
|
||||
{ define: "SCR_STDIO_WRITE_SLOT", when: (opts) => opts.stdioWriteSlot },
|
||||
// scr_async.c's native promise reactions, which the web streams unit uses,
|
||||
// scr_json.c's and scr_dyn_builtin_class.c's instanceof over its classes,
|
||||
// and scr_fetch.c's bodies of its streams.
|
||||
|
||||
@@ -101,6 +101,12 @@ export interface RuntimeFeatureSet extends NativeLinkFeatures {
|
||||
computedRequire: boolean;
|
||||
warningReplace: boolean;
|
||||
stdioExtended: boolean;
|
||||
/** The program's write completion callbacks take the error argument:
|
||||
* the SCR_STREAM_WRITE_ERRORS build of the stream unit. */
|
||||
streamWriteErrors: boolean;
|
||||
/** The program reaches the process output streams' write slot: the
|
||||
* SCR_STDIO_WRITE_SLOT build of the events unit. */
|
||||
stdioWriteSlot: boolean;
|
||||
nodeIsland: boolean;
|
||||
brotli: boolean;
|
||||
moduleNamespace: boolean;
|
||||
@@ -313,6 +319,8 @@ export function effectiveRuntimeFeatures(
|
||||
computedRequire: features.computedRequire === true,
|
||||
warningReplace: features.warningReplace === true,
|
||||
stdioExtended: features.stdioExtended === true,
|
||||
streamWriteErrors: features.streamWriteErrors === true,
|
||||
stdioWriteSlot: features.stdioWriteSlot === true,
|
||||
nodeIsland: features.nodeIsland === true,
|
||||
brotli: features.brotli === true,
|
||||
moduleNamespace: features.moduleNamespace === true,
|
||||
|
||||
@@ -370,6 +370,24 @@ export function sharedModuleDuplicatedDiag(
|
||||
};
|
||||
}
|
||||
|
||||
/** SC8020 — the program replaces a process output stream's write method
|
||||
* while a Node island exists: the island's `process.stdout` is its own
|
||||
* object, so the replacement and the island's writes cannot see each other.
|
||||
* H2 shares the native stream object with the islands and removes this. */
|
||||
export function stdioPatchAcrossIslandsDiag(
|
||||
stream: "stdout" | "stderr" | null,
|
||||
loc: SrcLoc,
|
||||
): ScrDiagnostic {
|
||||
const what =
|
||||
stream === null ? "process.stdout.write or process.stderr.write" : `process.${stream}.write`;
|
||||
return {
|
||||
code: "SC8020",
|
||||
message: `${what} is replaced while a Node island exists: the island's process output stream would not see this patch, and island writes would bypass it`,
|
||||
loc,
|
||||
hint: "the island's process.stdout would not see this patch",
|
||||
};
|
||||
}
|
||||
|
||||
/** SC8022 — island code extends a class of a module the program compiles.
|
||||
* A native instance cannot take an island subclass's overrides, so the
|
||||
* module is not owned by the program: a package runs in Node for both
|
||||
|
||||
@@ -65,6 +65,10 @@ export interface EarlyExecutableNativeFeatures {
|
||||
wasm?: boolean;
|
||||
warningReplace?: boolean;
|
||||
stdioExtended?: boolean;
|
||||
/** Error-taking write completion callbacks: SCR_STREAM_WRITE_ERRORS. */
|
||||
streamWriteErrors?: boolean;
|
||||
/** The process output write slot: SCR_STDIO_WRITE_SLOT. */
|
||||
stdioWriteSlot?: boolean;
|
||||
nodeIsland?: boolean;
|
||||
moduleNamespace?: boolean;
|
||||
moduleNamespaceDyn?: boolean;
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
npmStaticRefusalDiag,
|
||||
iceDiag,
|
||||
isCheckerPanic,
|
||||
stdioPatchAcrossIslandsDiag,
|
||||
type ScrDiagnostic,
|
||||
} from "../diagnostics/diagnostic.js";
|
||||
import type { IrModule, SrcLoc } from "../ir/ir.js";
|
||||
@@ -202,6 +203,14 @@ export function prepareExecutableModule(
|
||||
return fail(lowered.diagnostics);
|
||||
}
|
||||
|
||||
// A program that replaces a process output stream's write method has
|
||||
// nowhere to route an island's writes: the island's process.stdout is
|
||||
// its own object. Until islands share the native stream object (H2),
|
||||
// refuse the combination instead of silently missing the patch.
|
||||
if (islandDecision !== null && lowered.stdioPatches !== undefined) {
|
||||
return fail(lowered.stdioPatches.map((p) => stdioPatchAcrossIslandsDiag(p.stream, p.loc)));
|
||||
}
|
||||
|
||||
const validation = validateModule(lowered.module);
|
||||
timing("ir-validate");
|
||||
if (validation.length > 0) {
|
||||
|
||||
@@ -34,6 +34,7 @@ import { builtinFunctionValue } from "../lower-exprs.js";
|
||||
import { canonicalBuiltinModule } from "../../builtin-modules.js";
|
||||
import { lowerProcessIpcSend, ipcMessageListener, ipcDisconnectListener } from "./ipc.js";
|
||||
import { lowerDiagnosticsSubscriber, lowerTracingArguments } from "./async-context.js";
|
||||
import { isCheckerPanic } from "../../../diagnostics/diagnostic.js";
|
||||
|
||||
/** Whether the process has an IPC channel, Node's test for defining
|
||||
* process.send and process.disconnect: a channel this fork child opened
|
||||
@@ -173,6 +174,70 @@ export function programReadsExitCode(lowerer: Lowerer): boolean {
|
||||
return found;
|
||||
}
|
||||
|
||||
/** A write-method replacement on a process output stream, its source site.
|
||||
* `stream` is null when the receiver is a first-class alias, whose type does
|
||||
* not say which of the two output streams it holds. */
|
||||
export interface StdioPatchSite {
|
||||
stream: "stdout" | "stderr" | null;
|
||||
loc: SrcLoc;
|
||||
}
|
||||
|
||||
/** Whether any program file replaces a process output stream's write
|
||||
* method: `process.stdout.write = …` / `process.stderr.write = …`, or the
|
||||
* same assignment through a first-class alias of the stream (a value whose
|
||||
* type maps to the process stream). Such a program routes its typed
|
||||
* stdout/stderr writes through the stream object, so the replacement sees
|
||||
* them with their arguments, as in Node. Cached per program; the first
|
||||
* site found is recorded for the executable planner's SC8020 check. */
|
||||
export function programPatchesStdio(lowerer: Lowerer): boolean {
|
||||
if (lowerer.patchesStdio !== null) return lowerer.patchesStdio;
|
||||
let found = false;
|
||||
// undefined: not an output stream; null: a first-class alias of one.
|
||||
const streamOf = (recv: ts.Expression): "stdout" | "stderr" | null | undefined => {
|
||||
if (ts.isPropertyAccessExpression(recv)) {
|
||||
const direct = lowerer.stdlibGlobalMember(recv, "process");
|
||||
if (direct === "stdout" || direct === "stderr") return direct;
|
||||
}
|
||||
// An arbitrary receiver's mapping is a query, never a commit: a checker
|
||||
// panic (the type has no readable form) is not a patch site. The type
|
||||
// cannot say WHICH output stream an alias holds.
|
||||
try {
|
||||
return lowerer.mapTypeOf(lowerer.typeOf(recv))?.kind === "procStream" ? null : undefined;
|
||||
} catch (e) {
|
||||
if (!isCheckerPanic(e)) throw e;
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
const walk = (node: ts.Node): void => {
|
||||
if (found) return;
|
||||
if (
|
||||
ts.isBinaryExpression(node) &&
|
||||
node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
||||
ts.isPropertyAccessExpression(node.left) &&
|
||||
node.left.name.text === "write"
|
||||
) {
|
||||
const recv = node.left.expression;
|
||||
const direct = ts.isPropertyAccessExpression(recv)
|
||||
? lowerer.stdlibGlobalMember(recv, "process")
|
||||
: null;
|
||||
const stream = direct === "stdout" || direct === "stderr" ? direct : streamOf(recv);
|
||||
if (stream !== undefined) {
|
||||
found = true;
|
||||
lowerer.stdioPatchSites.push({ stream, loc: locOf(node) });
|
||||
return;
|
||||
}
|
||||
}
|
||||
ts.forEachChild(node, walk);
|
||||
};
|
||||
for (const sf of lowerer.program.getImplementationSourceFiles()) {
|
||||
if (sf.isDeclarationFile) continue;
|
||||
walk(sf);
|
||||
if (found) break;
|
||||
}
|
||||
lowerer.patchesStdio = found;
|
||||
return found;
|
||||
}
|
||||
|
||||
/** The pinned Node release the runtime reports as process.versions.node
|
||||
* (packages/runtime/src/scr_lib.c SCR_NODE_COMPAT_VERSION, kept equal to
|
||||
* internal/compatibility/node-v24.json by test/node-pin.test.ts). */
|
||||
@@ -341,6 +406,26 @@ export function lowerProcessStreamProperty(
|
||||
loc: locOf(expr),
|
||||
};
|
||||
}
|
||||
// The write method as a VALUE in TypeScript (`const original =
|
||||
// process.stdout.write`, `process.stdout.write.bind(process.stdout)`):
|
||||
// the stream object's current write slot, a checked-dynamic function
|
||||
// (the native method until a program replaces it).
|
||||
if (member === "write" && (direct === "stdout" || direct === "stderr")) {
|
||||
const loc = locOf(expr);
|
||||
return {
|
||||
kind: "dynKeyGet",
|
||||
value: {
|
||||
kind: "libCall",
|
||||
fn: "process.stdioSlot",
|
||||
args: [numLit(direct === "stdout" ? 1 : 2, loc)],
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
key: strLit("write", locOf(expr.name)),
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
// `if (process.stdin.setRawMode)`: Node's stdin has the method only
|
||||
// when it is a TTY. The runtime's stream object answers the same
|
||||
// presence (the method or undefined); calls keep their direct lowering.
|
||||
@@ -357,7 +442,9 @@ export function lowerProcessStreamProperty(
|
||||
member !== "isTTY" &&
|
||||
member !== "columns" &&
|
||||
member !== "rows" &&
|
||||
member !== "writableLength"
|
||||
member !== "writableLength" &&
|
||||
member !== "writableHighWaterMark" &&
|
||||
member !== "writableNeedDrain"
|
||||
)
|
||||
return null;
|
||||
let recv: ts.Expression = expr.expression;
|
||||
@@ -391,19 +478,31 @@ export function lowerProcessStreamProperty(
|
||||
// nothing is ever queued and Node's number is always 0. The other
|
||||
// members stay keyed reads — the runtime stream answers their live
|
||||
// values (isatty columns/rows, stdin readableLength).
|
||||
if (member === "writableLength") {
|
||||
if (
|
||||
member === "writableLength" ||
|
||||
member === "writableHighWaterMark" ||
|
||||
member === "writableNeedDrain"
|
||||
) {
|
||||
// The receiver still EVALUATES: a call or property read that hands
|
||||
// back the stream runs its own side effects — and throws where Node
|
||||
// throws — before the constant answers, exactly as the read of the
|
||||
// member would. A pure receiver (the alias above) folds away.
|
||||
const answer: IrExpr = { kind: "numLit", value: 0, type: F64, loc: locOf(expr) };
|
||||
const answer: IrExpr =
|
||||
member === "writableNeedDrain"
|
||||
? { kind: "boolLit", value: false, type: BOOL, loc: locOf(expr) }
|
||||
: {
|
||||
kind: "numLit",
|
||||
value: member === "writableLength" ? 0 : 65536,
|
||||
type: F64,
|
||||
loc: locOf(expr),
|
||||
};
|
||||
const effect = lowerer.lowerExpr(expr.expression);
|
||||
if (isSafeToDiscard(effect)) return answer;
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "exprStmt", expr: effect, loc: locOf(expr.expression) }],
|
||||
result: answer,
|
||||
type: F64,
|
||||
type: answer.type,
|
||||
loc: locOf(expr),
|
||||
};
|
||||
}
|
||||
@@ -435,6 +534,16 @@ export function lowerProcessStreamProperty(
|
||||
// true for the same reason). Backpressure probes read 0 and stay
|
||||
// correct; the write itself never defers.
|
||||
if (member === "writableLength") return { kind: "numLit", value: 0, type: F64, loc };
|
||||
// Node's default high-water mark for the process streams, and the
|
||||
// drain flag a synchronous write never sets.
|
||||
if (member === "writableHighWaterMark") {
|
||||
if (stream === "stdin") return null;
|
||||
return { kind: "numLit", value: 65536, type: F64, loc };
|
||||
}
|
||||
if (member === "writableNeedDrain") {
|
||||
if (stream === "stdin") return null;
|
||||
return { kind: "boolLit", value: false, type: BOOL, loc };
|
||||
}
|
||||
if (member === "isTTY") {
|
||||
const type = lowerer.withUndefinedArm(BOOL);
|
||||
return {
|
||||
@@ -920,6 +1029,58 @@ export function lowerProcessMethodCall(
|
||||
}
|
||||
return { kind: "libCall", fn: "process.stdinSetRawMode", args: [mode], type: VOID, loc };
|
||||
}
|
||||
// process.stdout/stderr.on("drain", fn) and its spellings: Node emits
|
||||
// 'drain' only after a write() returned false, and these writes are
|
||||
// synchronous (they never return false), so the listener can never run.
|
||||
// Registering it is therefore observably a no-op: the receiver and the
|
||||
// listener are evaluated for their effects and nothing is retained.
|
||||
// listenerCount/listeners/eventNames on the process streams stay
|
||||
// refused, so the missing registration cannot be observed.
|
||||
if (
|
||||
[
|
||||
"on",
|
||||
"once",
|
||||
"off",
|
||||
"addListener",
|
||||
"removeListener",
|
||||
"prependListener",
|
||||
"prependOnceListener",
|
||||
].includes(access.name.text) &&
|
||||
ts.isPropertyAccessExpression(access.expression) &&
|
||||
call.arguments.nodes.length === 2 &&
|
||||
!call.arguments.nodes.some(ts.isSpreadElement)
|
||||
) {
|
||||
const stream = lowerer.stdlibGlobalMember(access.expression, "process");
|
||||
const evT = lowerer.typeOf(call.arguments.nodes[0]!);
|
||||
if (
|
||||
(stream === "stdout" || stream === "stderr") &&
|
||||
evT.isStringLiteralType() &&
|
||||
evT.value === "drain"
|
||||
) {
|
||||
const loc = locOf(call);
|
||||
const parent = call.parent;
|
||||
const discarded =
|
||||
ts.isExpressionStatement(parent) || (ts.isArrowFunction(parent) && parent.body === call);
|
||||
if (!discarded) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
call,
|
||||
`using the result of process.${stream}.${access.name.text}("drain", ...) (register it as its own statement)`,
|
||||
);
|
||||
}
|
||||
const stmts: IrStmt[] = [];
|
||||
for (const arg of call.arguments.nodes) {
|
||||
const value = lowerer.lowerExprExpecting(arg, DYN);
|
||||
if (!isSafeToDiscard(value)) stmts.push({ kind: "exprStmt", expr: value, loc: value.loc });
|
||||
}
|
||||
// The call's value is discarded (statement position or a void arrow
|
||||
// body), so the lowering is the evaluated arguments alone; a bare
|
||||
// unitLit has no standalone IR value, so the no-op result is the
|
||||
// dynamic undefined a discarded call leaves.
|
||||
const result: IrExpr = dynUndefinedExpr(loc);
|
||||
return stmts.length === 0 ? result : { kind: "seqExpr", stmts, result, type: DYN, loc };
|
||||
}
|
||||
}
|
||||
// process.stdout.on("resize", cb) (and once/off/removeListener/
|
||||
// addListener, stderr too): the terminal resize event, registered on the
|
||||
// runtime's process stream object (a SIGWINCH listener that emits when a
|
||||
@@ -1078,6 +1239,33 @@ export function lowerProcessMethodCall(
|
||||
if (stream === "stdout" || stream === "stderr") {
|
||||
const loc = locOf(call);
|
||||
const args = call.arguments.nodes;
|
||||
// In a program that replaces a process stream's write, a write call is
|
||||
// what Node makes it: a call of the stream's CURRENT write property,
|
||||
// with the arguments as passed (the replacement sees byte chunks,
|
||||
// encodings and callbacks). The native method behind the slot is the
|
||||
// same synchronous write, so an unpatched stream behaves as before.
|
||||
if (programPatchesStdio(lowerer) && !args.some(ts.isSpreadElement)) {
|
||||
const handle: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: "process.stdioSlot",
|
||||
args: [numLit(stream === "stdout" ? 1 : 2, loc)],
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
const invoke: IrExpr = {
|
||||
kind: "dynInvoke",
|
||||
recv: handle,
|
||||
method: "write",
|
||||
calleeName: ts.getTextOfNode(access),
|
||||
args: args.map((arg) => lowerer.lowerExprExpecting(arg, DYN)),
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
// The declared boolean: a replacement returning something else
|
||||
// throws at a use that needs the boolean (types do not lie); a
|
||||
// discarded result is not converted.
|
||||
return resultIsDiscarded(call) ? invoke : lowerer.coerceToExpected(invoke, BOOL);
|
||||
}
|
||||
if (args.length < 1 || args.length > 3 || args.some(ts.isSpreadElement)) {
|
||||
lowerer.noLowering(
|
||||
`process.${stream}.write with ${args.length} arguments`,
|
||||
@@ -1174,6 +1362,24 @@ export function lowerProcessMethodCall(
|
||||
if (data.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(call))) {
|
||||
data = { kind: "dynCheck", value: data, type: STRING, loc };
|
||||
}
|
||||
// A chunk known only at run time (`any`, or a string | Uint8Array
|
||||
// union): the runtime dispatches on the value — a string or a
|
||||
// Uint8Array/Buffer writes, null throws ERR_STREAM_NULL_VALUES, any
|
||||
// other value throws ERR_INVALID_ARG_TYPE, as Node's Writable.write
|
||||
// does before writing anything. Only the one-argument form.
|
||||
if (
|
||||
args.length === 1 &&
|
||||
(data.type.kind === "dyn" || data.type.kind === "union") &&
|
||||
(data.type.kind === "dyn" || lowerer.dynConvertible(data.type))
|
||||
) {
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "process.stdioWriteDyn",
|
||||
args: [numLit(stream === "stdout" ? 1 : 2, loc), lowerer.coerceToExpected(data, DYN)],
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
const isBytes = data.type.kind === "bytes" && data.type.elem === "u8";
|
||||
if (!isBytes && data.type.kind !== "string") {
|
||||
lowerer.noLowering(
|
||||
|
||||
@@ -2557,6 +2557,43 @@ export function lowerEventsOnce(lowerer: Lowerer, call: ts.CallExpression, loc:
|
||||
const arrayT = arrayOf(DYN);
|
||||
const resultT: IrType = { kind: "promise", inner: arrayT };
|
||||
let receiver = lowerer.lowerExpr(args[0]!);
|
||||
// events.once(process.stdout, "drain"): the next 'drain' of a stream
|
||||
// whose writes are synchronous never comes (Node emits it only after a
|
||||
// write returned false), so the promise stays pending. A pending promise
|
||||
// holds nothing: the process exits when the loop is empty, as in Node.
|
||||
{
|
||||
let target = args[0]!;
|
||||
while (ts.isParenthesizedExpression(target)) target = target.expression;
|
||||
const stream = ts.isPropertyAccessExpression(target)
|
||||
? lowerer.stdlibGlobalMember(target, "process")
|
||||
: null;
|
||||
const nameT = lowerer.typeOf(args[1]!);
|
||||
if (
|
||||
(stream === "stdout" || stream === "stderr") &&
|
||||
nameT.isStringLiteralType() &&
|
||||
nameT.value === "drain" &&
|
||||
args.length === 2
|
||||
) {
|
||||
const pending: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: "events.pendingForever",
|
||||
args: [],
|
||||
type: resultT,
|
||||
loc,
|
||||
};
|
||||
let name = args[1]!;
|
||||
while (ts.isParenthesizedExpression(name)) name = name.expression;
|
||||
if (ts.isStringLiteralLike(name)) return pending;
|
||||
// A name expression typed "drain" still runs (`pick()`, `(log(), "drain")`).
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "exprStmt", expr: lowerer.lowerExpr(args[1]!), loc }],
|
||||
result: pending,
|
||||
type: resultT,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
}
|
||||
// A checked-dynamic NodeJS stream slot (dyn-streams.ts): a native stream
|
||||
// unboxes by identity; any other value throws the not-supported Error.
|
||||
if (
|
||||
|
||||
@@ -32,7 +32,7 @@ import {
|
||||
matchAllRowsReadByElements,
|
||||
storedMatchAllRowsReadByElements,
|
||||
} from "./containers/match-row-uses.js";
|
||||
import { dynUndefinedExpr, nodeThrowExpr, numLit, varRef } from "../../ir/build.js";
|
||||
import { dynUndefinedExpr, nodeThrowExpr, numLit, strLit, varRef } from "../../ir/build.js";
|
||||
import { runtimeKeyPatternRead } from "./expressions/keyed-field-targets.js";
|
||||
import { declaresHoistedBinding, staticCondition } from "./static-typeof.js";
|
||||
import { lowerCollectionSpread } from "./containers/collection-methods.js";
|
||||
@@ -9333,6 +9333,39 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm
|
||||
};
|
||||
}
|
||||
}
|
||||
// process.stdout.write = fn (and stderr): replace the stream object's
|
||||
// write slot. Every later write to the stream — console output, typed
|
||||
// write calls (programPatchesStdio), runtime writers — goes through it.
|
||||
if (ts.isPropertyAccessExpression(expr.left) && expr.left.name.text === "write") {
|
||||
const recv = expr.left.expression;
|
||||
const stream = ts.isPropertyAccessExpression(recv)
|
||||
? lowerer.stdlibGlobalMember(recv, "process")
|
||||
: null;
|
||||
if (stream === "stdout" || stream === "stderr") {
|
||||
const loc = locOf(expr);
|
||||
return {
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.keySet",
|
||||
args: [
|
||||
{
|
||||
kind: "libCall",
|
||||
fn: "process.stdioSlot",
|
||||
args: [numLit(stream === "stdout" ? 1 : 2, loc)],
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
strLit("write", locOf(expr.left.name)),
|
||||
lowerer.lowerExprExpecting(expr.right, DYN),
|
||||
],
|
||||
type: VOID,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
};
|
||||
}
|
||||
}
|
||||
if (!ts.isIdentifier(expr.left)) {
|
||||
lowerer.unsupported("SC1090", expr.left, "assignment to non-variables");
|
||||
}
|
||||
|
||||
@@ -6964,18 +6964,45 @@ export function lowerStreamMethodCall(
|
||||
const kind = chunkKind(args[0]!);
|
||||
if (kind === "null") lowerer.noLowering("write(null)", call, "end() finishes a writable");
|
||||
const lowerWriteCb = (cbArg: ts.Expression): IrExpr => {
|
||||
const cb = callbackDeclaresParameters(cbArg)
|
||||
? null
|
||||
: lowerer.lowerExprExpecting(cbArg, funcOf([], VOID));
|
||||
if (cb === null || cb.type.kind !== "func" || cb.type.params.length > 0) {
|
||||
// The stream ignores what the callback returns (`() => count++`, a
|
||||
// stored function returning a value): the value is discarded.
|
||||
const raw = lowerer.lowerExpr(cbArg);
|
||||
const cb =
|
||||
raw.type.kind === "func" && raw.type.ret.kind !== "void" && raw.type.params.length <= 1
|
||||
? lowerer.coerceToExpected(raw, funcOf(raw.type.params, VOID))
|
||||
: raw;
|
||||
if (cb.type.kind !== "func" || cb.type.ret.kind !== "void" || cb.type.params.length > 1) {
|
||||
lowerer.noLowering(
|
||||
"write completion callbacks taking the error argument",
|
||||
"write completion callbacks of this shape",
|
||||
cbArg,
|
||||
"() => void callbacks are supported (listen for 'error' to observe failures)",
|
||||
"callbacks take () or (error?: Error | null) and return nothing",
|
||||
);
|
||||
}
|
||||
return cb!;
|
||||
if (cb.type.params.length === 0) return cb;
|
||||
const param = cb.type.params[0]!;
|
||||
const def = param.kind === "union" ? lowerer.unions.get(param.unionId) : undefined;
|
||||
const ok =
|
||||
param.kind === "dyn" ||
|
||||
(!!def &&
|
||||
def.arms.some((a) => a.kind === "nullT") &&
|
||||
def.arms.some((a) => a.kind === "object" && a.className === "%Error") &&
|
||||
def.arms.every(
|
||||
(a) =>
|
||||
a.kind === "nullT" ||
|
||||
a.kind === "undefinedT" ||
|
||||
(a.kind === "object" && a.className === "%Error"),
|
||||
));
|
||||
if (!ok) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
cbArg,
|
||||
`write completion callbacks whose parameter is not 'Error | null' (got '${lowerer.fmt(param)}')`,
|
||||
);
|
||||
}
|
||||
return cb;
|
||||
};
|
||||
const takesError = (cb: IrExpr): boolean =>
|
||||
cb.type.kind === "func" && cb.type.params.length === 1;
|
||||
// write(chunk, encoding[, cb]), and write(value, cb): the encoding
|
||||
// argument reaches an object-mode side's _write as passed, null when
|
||||
// the callback took its place (a byte-mode side encodes a string
|
||||
@@ -6988,7 +7015,19 @@ export function lowerStreamMethodCall(
|
||||
const chunk = lowerer.lowerExprExpecting(args[0]!, DYN);
|
||||
const tail: IrExpr[] = [];
|
||||
if (encArg !== undefined) tail.push(lowerer.lowerExprExpecting(encArg, DYN));
|
||||
if (cbArg !== undefined) tail.push(lowerWriteCb(cbArg));
|
||||
if (cbArg !== undefined) {
|
||||
const cb = lowerWriteCb(cbArg);
|
||||
if (takesError(cb)) {
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "writable.writeEcb",
|
||||
args: [receiver, numLit(2, loc), chunk, tail[0] ?? dynUndefinedExpr(loc), cb],
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
tail.push(cb);
|
||||
}
|
||||
const flags = (encArg !== undefined ? 1 : 0) | (cbArg !== undefined ? 2 : 0);
|
||||
return {
|
||||
kind: "libCall",
|
||||
@@ -7055,6 +7094,15 @@ export function lowerStreamMethodCall(
|
||||
const fn = kind === "bytes" ? "writable.write" : "writable.writeStr";
|
||||
if (cbArg !== undefined) {
|
||||
const cb = lowerWriteCb(cbArg);
|
||||
if (takesError(cb)) {
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "writable.writeEcb",
|
||||
args: [receiver, numLit(kind === "bytes" ? 0 : 1, loc), chunk, dynUndefinedExpr(loc), cb],
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
return { kind: "libCall", fn, args: [receiver, chunk, cb], type: BOOL, loc };
|
||||
}
|
||||
return { kind: "libCall", fn, args: [receiver, chunk], type: BOOL, loc };
|
||||
|
||||
@@ -104,6 +104,8 @@ import { narrowStoredClassValue, narrowGenericClassValue } from "./class-unions.
|
||||
import {
|
||||
isOptionalProcessStreamProperty,
|
||||
isOptionalProcessVersionsRead,
|
||||
programPatchesStdio,
|
||||
type StdioPatchSite,
|
||||
} from "./builtins/process.js";
|
||||
import { isSettledIteratorValueRead } from "./lower-generators.js";
|
||||
import { isRebindableFunctionSymbol } from "./rebindable-functions.js";
|
||||
@@ -960,6 +962,11 @@ export interface LowerResult {
|
||||
dynamicFunctions?: DynamicFunction[];
|
||||
/** LowerOptions.tierReport: the report-only tier assignment. */
|
||||
tiers?: TierReport;
|
||||
/** The program replaces a process output stream's write method, with the
|
||||
* site: the executable planner refuses the combination with Node islands
|
||||
* (SC8020), whose process objects would not see the patch. Present only
|
||||
* when the build has islands. */
|
||||
stdioPatches?: StdioPatchSite[];
|
||||
}
|
||||
|
||||
export interface LowerOptions {
|
||||
@@ -2681,6 +2688,12 @@ export class Lowerer {
|
||||
streamPropMutatedSyms: Set<ts.Symbol> | null = null;
|
||||
/** Whether some source reads process.exitCode (processReadsExitCode). */
|
||||
readsExitCode: boolean | null = null;
|
||||
/** Whether some source replaces a process output stream's write method
|
||||
* (programPatchesStdio in builtins/process.ts). */
|
||||
patchesStdio: boolean | null = null;
|
||||
/** The write-replacement site programPatchesStdio found: the executable
|
||||
* planner refuses it beside Node islands (SC8020). */
|
||||
readonly stdioPatchSites: StdioPatchSite[] = [];
|
||||
requestInitPropMutatedSyms: Set<ts.Symbol> | null = null;
|
||||
emitterEventTable: Map<string, EventSig> | null = null;
|
||||
emitterComputedPatterns: ComputedEventPattern[] = [];
|
||||
@@ -4509,6 +4522,8 @@ export class Lowerer {
|
||||
"unixListenReachable",
|
||||
"streamPropMutatedSyms",
|
||||
"readsExitCode",
|
||||
"patchesStdio",
|
||||
"stdioPatchSites",
|
||||
"requestInitPropMutatedSyms",
|
||||
"classProtocols",
|
||||
// The whole-program prepass analyzeJsWidenedBindings computes this set
|
||||
@@ -9887,11 +9902,20 @@ export class Lowerer {
|
||||
// rewrite keeps every dynamic operation dynamic (each dyn operator
|
||||
// becomes its js twin), so coverage's dynamic statements are unchanged.
|
||||
if (module && this.dynamicFunctions.length > 0) promoteJsvals(module);
|
||||
// A program that replaces a process output stream's write method beside
|
||||
// Node islands is refused by the executable planner (SC8020): an
|
||||
// island's process.stdout is its own object. Only island builds pay for
|
||||
// the whole-program scan.
|
||||
const stdioPatches =
|
||||
this.nodeIslands !== null && programPatchesStdio(this)
|
||||
? [...this.stdioPatchSites]
|
||||
: undefined;
|
||||
return {
|
||||
module,
|
||||
diagnostics: this.diags,
|
||||
runtimeFences: this.runtimeFences,
|
||||
stats: this.stats,
|
||||
...(stdioPatches !== undefined && stdioPatches.length > 0 ? { stdioPatches } : {}),
|
||||
...(this.dynamicFunctions.length > 0 ? { dynamicFunctions: this.dynamicFunctions } : {}),
|
||||
...(this.statsByFile.size > 0 ? { statsByFile: this.statsByFile } : {}),
|
||||
...(this.provenanceElided.length > 0 ? { provenanceElided: this.provenanceElided } : {}),
|
||||
|
||||
@@ -5197,6 +5197,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
// AsyncLocalStorage.snapshot()'s generic runner is a dyn function value
|
||||
// (als.snapshot), called through the dyn call path.
|
||||
if (callSigs.length === 1 && isAsyncLocalStorageSnapshot(callSigs[0]!, ctx)) return DYN;
|
||||
// A process/net/stream `write` method taken as a value (`typeof
|
||||
// process.stdout.write`, the saved original of an output guard): its
|
||||
// overload set has no single compiled signature, and the value is the
|
||||
// stream's replaceable method slot, so it is the checked-dynamic function
|
||||
// the stream object hands out (scr_events.c ProcessStream).
|
||||
if (callSigs.length > 1 && isStdlibStreamWriteOverloads(callSigs, ctx)) return DYN;
|
||||
if (callSigs.length === 1) return mapCallSignature(callSigs[0]!, ctx);
|
||||
// Records: object types whose members are all data properties (shorthand
|
||||
// methods in type position count — they're func-typed fields) with
|
||||
@@ -5611,6 +5617,36 @@ function isAsyncLocalStorageSnapshot(sig: ts.Signature, ctx: TypeMapperCtx): boo
|
||||
);
|
||||
}
|
||||
|
||||
const STREAM_WRITE_OWNERS = new Set(["Socket", "Writable", "WriteStream", "WritableStream"]);
|
||||
|
||||
/** Every signature is a `write` method declared on a standard-library
|
||||
* stream interface or class (net.Socket, stream.Writable, tty.WriteStream,
|
||||
* NodeJS.WritableStream) — @types/node's or the fallback declarations'. */
|
||||
function isStdlibStreamWriteOverloads(
|
||||
sigs: readonly ts.Signature[],
|
||||
ctx: Pick<TypeMapperCtx, "isStdlibFile">,
|
||||
): boolean {
|
||||
return sigs.every((sig) => {
|
||||
const decl = sig.declaration;
|
||||
if (
|
||||
decl === undefined ||
|
||||
!(ts.isMethodSignatureDeclaration(decl) || ts.isMethodDeclaration(decl))
|
||||
)
|
||||
return false;
|
||||
if (!ts.isIdentifier(decl.name) || decl.name.text !== "write") return false;
|
||||
const owner = decl.parent;
|
||||
const ownerName =
|
||||
owner !== undefined && (ts.isInterfaceDeclaration(owner) || ts.isClassDeclaration(owner))
|
||||
? (owner.name?.text ?? null)
|
||||
: null;
|
||||
return (
|
||||
ownerName !== null &&
|
||||
STREAM_WRITE_OWNERS.has(ownerName) &&
|
||||
ctx.isStdlibFile(ts.getSourceFileOfNode(decl))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null {
|
||||
const { checker } = ctx;
|
||||
if (sig.getTypeParameters().length && ctx.erasingSignature !== sig) return null;
|
||||
|
||||
@@ -707,6 +707,8 @@ async function compileExecutableNative(
|
||||
...(features.wasm ? { wasm: true } : {}),
|
||||
...(features.warningReplace ? { warningReplace: true } : {}),
|
||||
...(features.stdioExtended ? { stdioExtended: true } : {}),
|
||||
...(features.streamWriteErrors ? { streamWriteErrors: true } : {}),
|
||||
...(features.stdioWriteSlot ? { stdioWriteSlot: true } : {}),
|
||||
...(features.nodeIsland ? { nodeIsland: true } : {}),
|
||||
...(features.moduleNamespace ? { moduleNamespace: true } : {}),
|
||||
...(features.moduleNamespaceDyn ? { moduleNamespaceDyn: true } : {}),
|
||||
|
||||
@@ -107,6 +107,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"ffi.argument",
|
||||
"ffi.memoryModule",
|
||||
"process.stdoutWrite",
|
||||
"process.stdioWriteDyn",
|
||||
"process.stderrWrite",
|
||||
"process.stdoutWriteBytes",
|
||||
"process.stderrWriteBytes",
|
||||
@@ -443,6 +444,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"writable.writeU",
|
||||
"writable.writeDyn",
|
||||
"writable.writeEnc",
|
||||
"writable.writeEcb",
|
||||
"writable.end",
|
||||
"writable.uncork",
|
||||
"writable.setDefaultEncoding",
|
||||
|
||||
@@ -3763,6 +3763,14 @@ function buildLibFnSignatures() {
|
||||
* never throws. */
|
||||
"process.stdoutWrite": { argTypes: [STRING], result: BOOL },
|
||||
"process.stdio": { argTypes: [F64], result: DYN },
|
||||
/** The same stream object, reached by the typed write-slot lowering
|
||||
* (the write method as a value, its replacement, a patched program's
|
||||
* typed writes): identical at run time, and the link switch for the
|
||||
* SCR_STDIO_WRITE_SLOT build of the events unit. */
|
||||
"process.stdioSlot": { argTypes: [F64], result: DYN },
|
||||
/** process.stdout/stderr.write(chunk) with a run-time chunk: Node's
|
||||
* Writable validation, then a synchronous write. */
|
||||
"process.stdioWriteDyn": { argTypes: [F64, DYN], result: BOOL },
|
||||
"process.stderrWrite": { argTypes: [STRING], result: BOOL },
|
||||
// error.new's result and the receiver slots are builtin-error classes —
|
||||
// program-dependent object types, checked in the libCall case.
|
||||
@@ -4671,6 +4679,10 @@ function buildLibFnSignatures() {
|
||||
"writable.writeU": { argTypes: [null, null], result: BOOL },
|
||||
"writable.writeDyn": { argTypes: [null, DYN], result: BOOL },
|
||||
"writable.writeEnc": { argTypes: [null, F64, DYN], result: BOOL },
|
||||
/** The error-first write form: (recv, mode, chunk, encoding, cb) where
|
||||
* mode 0 is a bytes chunk, 1 a string, 2 a run-time chunk, and the
|
||||
* callback takes the completion's error (Node's null on success). */
|
||||
"writable.writeEcb": { argTypes: [null, F64, null, DYN, null], result: BOOL },
|
||||
"writable.end": { argTypes: [null, F64], result: VOID },
|
||||
"writable.cork": { argTypes: [null], result: VOID },
|
||||
"writable.uncork": { argTypes: [null], result: VOID },
|
||||
@@ -5433,6 +5445,10 @@ function buildLibFnSignatures() {
|
||||
* undefined; tty.isatty exposes the boolean result directly. */
|
||||
"process.isTTY": { argTypes: [F64], result: BOOL },
|
||||
"process.isTTYValue": { argTypes: [DYN], result: BOOL },
|
||||
/** A promise that never settles (events.once of an event that cannot
|
||||
* occur, e.g. 'drain' on a synchronous process stream). The result is
|
||||
* events.once's own Promise<any[]> type. */
|
||||
"events.pendingForever": { argTypes: [], result: { kind: "promise", inner: arrayOf(DYN) } },
|
||||
// Like process.envGet: the result is the module's interned
|
||||
// `number | undefined` union — checked by arms in the libCall case.
|
||||
/** Terminal width over an fd literal (1/2 — process.stdout/stderr
|
||||
|
||||
@@ -647,6 +647,8 @@ const PROCESS_EVENT_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"process.onExit",
|
||||
"process.onSignal",
|
||||
"process.stdio",
|
||||
"process.stdioSlot",
|
||||
"process.stdioWriteDyn",
|
||||
"stdin.nextChunk",
|
||||
"stdin.onData",
|
||||
"stdin.onEnd",
|
||||
@@ -1208,6 +1210,18 @@ const RUNTIME_FEATURE_RULES = {
|
||||
(node.kind === "dynInvoke" && STDIO_EXTENDED_NAMES.has(node.method)) ||
|
||||
(node.kind === "strLit" && STDIO_EXTENDED_NAMES.has(node.value)),
|
||||
}),
|
||||
/** A write completion callback that takes the error argument lowers
|
||||
* through writable.writeEcb: the SCR_STREAM_WRITE_ERRORS stream unit
|
||||
* publishes the error the adapter delivers. */
|
||||
streamWriteErrors: rule({ fns: ["writable.writeEcb"] }),
|
||||
/** The program reaches the process output streams' write slot — the
|
||||
* write method as a value (process.stdioSlot), its replacement, a patched
|
||||
* program's typed writes, or a run-time-typed chunk
|
||||
* (process.stdioWriteDyn): the SCR_STDIO_WRITE_SLOT build of the events
|
||||
* unit carries the shared write body, the dynamic-chunk entry point and
|
||||
* Node's anonymous write function. Every other program links the default
|
||||
* object, byte for byte. */
|
||||
stdioWriteSlot: rule({ fns: ["process.stdioSlot", "process.stdioWriteDyn"] }),
|
||||
stream: rule({
|
||||
initial: (mod) => (mod.classes ?? []).some((c) => RUNTIME_STREAM_CLASSES.has(c.name)),
|
||||
fns: [...STREAM_LIB_FNS, "dyn.builtinStreamClass"],
|
||||
@@ -1336,6 +1350,8 @@ function noRuntimeFeatures(): RuntimeFeatures {
|
||||
searchParams: false,
|
||||
urlIdna: false,
|
||||
stdioExtended: false,
|
||||
stdioWriteSlot: false,
|
||||
streamWriteErrors: false,
|
||||
moduleNamespace: false,
|
||||
moduleNamespaceDyn: false,
|
||||
primitiveWrapper: false,
|
||||
|
||||
@@ -32,6 +32,8 @@ const features = {
|
||||
computedRequire: false,
|
||||
warningReplace: false,
|
||||
stdioExtended: false,
|
||||
streamWriteErrors: false,
|
||||
stdioWriteSlot: false,
|
||||
nodeIsland: false,
|
||||
brotli: false,
|
||||
moduleNamespace: false,
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/process-stream-drain.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/process-write-dynamic-chunks.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/stdout-write-patch.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/writable-error-callbacks.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/native-shim-monkeypatching.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/process-stream-listener-count.ts"
|
||||
],
|
||||
"diags": [
|
||||
{
|
||||
"code": "SC0001",
|
||||
"message": "Property 'listenerCount' does not exist on type 'WriteStream'.",
|
||||
"loc": {
|
||||
"file": "<repo>/tests/diagnostics/process-stream-listener-count.ts",
|
||||
"start": 269,
|
||||
"end": 282
|
||||
},
|
||||
"typeWorld": true,
|
||||
"hint": "the project has no @types/node, so scriptc types Node with its bundled declarations, which list only the members that compile natively — 'listenerCount' does not compile natively yet (https://scriptc.dev/limitations); installing @types/node turns this into a located refusal with a hint"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -376,6 +376,8 @@ async function build() {
|
||||
"SCR_TEXT_DECODER_LEGACY",
|
||||
"SCR_WARNING_REPLACE",
|
||||
"SCR_STDIO_EXTENDED",
|
||||
"SCR_STREAM_WRITE_ERRORS",
|
||||
"SCR_STDIO_WRITE_SLOT",
|
||||
"SCR_NATIVE_REACTIONS",
|
||||
"SCR_HANDLE_CLASSES",
|
||||
"SCR_YIN",
|
||||
|
||||
@@ -351,6 +351,29 @@ export function variantsFor(source) {
|
||||
when: { stdioExtended: true, nodeIsland: true },
|
||||
defines: ["SCR_STDIO_EXTENDED", "SCR_NODE_ISLANDS"],
|
||||
});
|
||||
// The typed process output write slot (the write method as a value, its
|
||||
// replacement, run-time-typed chunks): the shared write body, the
|
||||
// dynamic-chunk entry point and Node's anonymous write function. Every
|
||||
// other program keeps the default object, byte for byte.
|
||||
for (const [id, when, defines] of [
|
||||
["stdio-write-slot", { stdioWriteSlot: true }, ["SCR_STDIO_WRITE_SLOT"]],
|
||||
[
|
||||
"stdio-extended-write-slot",
|
||||
{ stdioExtended: true, stdioWriteSlot: true },
|
||||
["SCR_STDIO_EXTENDED", "SCR_STDIO_WRITE_SLOT"],
|
||||
],
|
||||
[
|
||||
"node-island-write-slot",
|
||||
{ nodeIsland: true, stdioWriteSlot: true },
|
||||
["SCR_NODE_ISLANDS", "SCR_STDIO_WRITE_SLOT"],
|
||||
],
|
||||
[
|
||||
"stdio-extended-node-island-write-slot",
|
||||
{ stdioExtended: true, nodeIsland: true, stdioWriteSlot: true },
|
||||
["SCR_STDIO_EXTENDED", "SCR_NODE_ISLANDS", "SCR_STDIO_WRITE_SLOT"],
|
||||
],
|
||||
])
|
||||
variants.push({ id, when, defines });
|
||||
}
|
||||
// Programs with Node islands link the event loop with the Node island slot;
|
||||
// every other program keeps the default object, byte for byte.
|
||||
@@ -361,6 +384,16 @@ export function variantsFor(source) {
|
||||
defines: ["SCR_NODE_ISLANDS"],
|
||||
});
|
||||
}
|
||||
// Error-taking write completion callbacks: the stream unit publishes the
|
||||
// error each callback receives; every other program keeps the default
|
||||
// object, byte for byte.
|
||||
if (source === "scr_stream.c") {
|
||||
variants.push({
|
||||
id: "write-errors",
|
||||
when: { streamWriteErrors: true },
|
||||
defines: ["SCR_STREAM_WRITE_ERRORS"],
|
||||
});
|
||||
}
|
||||
if (source === "scr_cycle.c") {
|
||||
// The young-in nursery (SCRIPTC_YOUNG_NURSERY=1): the collector with
|
||||
// nursery collections; programs built without it link the default.
|
||||
|
||||
@@ -545,6 +545,8 @@ async function build() {
|
||||
"SCR_TEXT_DECODER_LEGACY",
|
||||
"SCR_WARNING_REPLACE",
|
||||
"SCR_STDIO_EXTENDED",
|
||||
"SCR_STREAM_WRITE_ERRORS",
|
||||
"SCR_STDIO_WRITE_SLOT",
|
||||
"SCR_NATIVE_REACTIONS",
|
||||
"SCR_HANDLE_CLASSES",
|
||||
"SCR_YIN",
|
||||
|
||||
@@ -24,6 +24,38 @@ function selected(
|
||||
return matches[0];
|
||||
}
|
||||
|
||||
test("the typed stdio write slot selects the events build that carries it", () => {
|
||||
const plain = { stdioExtended: false, nodeIsland: false, workers: false };
|
||||
// Alone, the write slot selects its own build.
|
||||
expect(selected("scr_events.c", { ...plain, stdioWriteSlot: true }).defines).toEqual([
|
||||
"SCR_STDIO_WRITE_SLOT",
|
||||
]);
|
||||
// Beside the other events features it keeps each one's defines.
|
||||
expect(
|
||||
selected("scr_events.c", { ...plain, stdioExtended: true, stdioWriteSlot: true }).defines,
|
||||
).toEqual(["SCR_STDIO_EXTENDED", "SCR_STDIO_WRITE_SLOT"]);
|
||||
expect(
|
||||
selected("scr_events.c", { ...plain, nodeIsland: true, stdioWriteSlot: true }).defines,
|
||||
).toEqual(["SCR_NODE_ISLANDS", "SCR_STDIO_WRITE_SLOT"]);
|
||||
expect(
|
||||
selected("scr_events.c", {
|
||||
...plain,
|
||||
stdioExtended: true,
|
||||
nodeIsland: true,
|
||||
stdioWriteSlot: true,
|
||||
}).defines,
|
||||
).toEqual(["SCR_STDIO_EXTENDED", "SCR_NODE_ISLANDS", "SCR_STDIO_WRITE_SLOT"]);
|
||||
// A program that never reaches the write slot keeps the old object.
|
||||
expect(
|
||||
selected("scr_events.c", {
|
||||
...plain,
|
||||
stdioExtended: true,
|
||||
nodeIsland: true,
|
||||
stdioWriteSlot: false,
|
||||
}).defines,
|
||||
).toEqual(["SCR_STDIO_EXTENDED", "SCR_NODE_ISLANDS"]);
|
||||
});
|
||||
|
||||
test("web streams and Node islands select an async build that defines both", () => {
|
||||
const variant = selected("scr_async.c", { webStreams: true, nodeIsland: true, workers: false });
|
||||
expect(variant.defines).toEqual(
|
||||
|
||||
@@ -69,6 +69,10 @@ ScrDyn *scr_console_new(double out_fd, double err_fd, bool ignore_errors);
|
||||
void scr_console_bind(ScrDyn *console, double which, ScrStream *stream);
|
||||
void scr_console_inst_write(ScrDyn *console, ScrStr *text, bool to_stderr);
|
||||
ScrDyn *scr_process_stdio(double fd);
|
||||
/* process.stdout/stderr.write(chunk) with a run-time chunk: Node's Writable
|
||||
* validation, then a synchronous write. false with an exception pending when
|
||||
* the chunk is invalid. */
|
||||
bool scr_process_stdio_write_dyn(double fd, ScrDyn *chunk);
|
||||
bool scr_dyn_process_stdio_is(const ScrDyn *value);
|
||||
double scr_dyn_process_stdio_fd(const ScrDyn *value, const ScrDynPath *path);
|
||||
|
||||
|
||||
@@ -1311,6 +1311,57 @@ static void scr_stdio_resize_listener(ScrStdio *stream, ScrDyn *listener, bool r
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#ifdef SCR_STDIO_WRITE_SLOT
|
||||
/* stream.write(chunk[, encoding][, callback]) on a process output stream:
|
||||
* Node's Writable.write validation, then a synchronous descriptor write.
|
||||
* Returns +1 (true/false) or NULL with an exception pending. The typed
|
||||
* write-slot build factors it out of scr_stdio_method so the dynamic-chunk
|
||||
* entry point below shares it; every other build keeps the method's own
|
||||
* body, byte for byte. */
|
||||
static ScrDyn *scr_stdio_write_call(ScrStdio *stream, ScrDyn *const *args, size_t argc) {
|
||||
ScrDyn *arg = argc ? args[0] : scr_dyn_undefined();
|
||||
ScrDyn *result = NULL;
|
||||
ScrDyn *encoding = argc > 1 ? args[1] : scr_dyn_undefined();
|
||||
ScrDyn *callback = argc > 2 ? args[2] : scr_dyn_undefined();
|
||||
if (encoding->kind == SCR_DYN_FUNC) { callback = encoding; encoding = scr_dyn_undefined(); }
|
||||
if (callback->kind != SCR_DYN_UNDEF && callback->kind != SCR_DYN_NULL && callback->kind != SCR_DYN_FUNC) {
|
||||
scr_dyn_arg_type_fail("cb", "of type function", callback);
|
||||
return NULL;
|
||||
}
|
||||
if (arg->kind == SCR_DYN_NULL) {
|
||||
const char *m = "May not write null values to stream";
|
||||
scr_throw_error_msg_code(SCR_ERR_TYPE, m, strlen(m), "ERR_STREAM_NULL_VALUES");
|
||||
return NULL;
|
||||
}
|
||||
if (arg->kind == SCR_DYN_BYTES && arg->v.bytes->elem != SCR_BYTES_U8)
|
||||
return scr_stdio_refusal("write of a typed array other than Uint8Array");
|
||||
ScrBytes *bytes = NULL;
|
||||
if (arg->kind == SCR_DYN_STR) {
|
||||
if (encoding->kind != SCR_DYN_UNDEF && encoding->kind != SCR_DYN_NULL && encoding->kind != SCR_DYN_STR) {
|
||||
return scr_stdio_refusal("write encoding");
|
||||
}
|
||||
ScrStr *enc = encoding->kind == SCR_DYN_STR ? scr_str_retain(encoding->v.str) : scr_str_new("utf8", 4);
|
||||
bytes = scr_bytes_from_str(arg->v.str, enc);
|
||||
scr_str_release(enc);
|
||||
} else if (arg->kind == SCR_DYN_BYTES && arg->v.bytes->elem == SCR_BYTES_U8) {
|
||||
bytes = arg->v.bytes->shared ? scr_bytes_copy(arg->v.bytes) : scr_bytes_retain(arg->v.bytes);
|
||||
} else {
|
||||
scr_dyn_arg_type_fail("chunk", "of type string or an instance of Buffer, TypedArray, or DataView", arg);
|
||||
return NULL;
|
||||
}
|
||||
if (!bytes) return NULL;
|
||||
int error = scr_stdio_write_raw(stream->fd, bytes->data, bytes->len);
|
||||
scr_bytes_release(bytes);
|
||||
if (error) {
|
||||
scr_stdio_queue_write_error(stream, callback, error);
|
||||
return scr_dyn_new_bool(false);
|
||||
}
|
||||
if (callback->kind == SCR_DYN_FUNC) scr_next_tick(scr_stdio_callback(callback, (void *)scr_stdio_write_done));
|
||||
return scr_dyn_new_bool(true);
|
||||
}
|
||||
#endif
|
||||
|
||||
static ScrDyn *scr_stdio_method(ScrClosure *cb, ScrDyn *const *args, size_t argc) {
|
||||
int method = (int)scr_box_get_f64(cb->caps[0]);
|
||||
ScrDyn *self = scr_dyn_this_get();
|
||||
@@ -1328,6 +1379,9 @@ static ScrDyn *scr_stdio_method(ScrClosure *cb, ScrDyn *const *args, size_t argc
|
||||
scr_dyn_arr_push(result, columns);
|
||||
scr_dyn_arr_push(result, rows);
|
||||
} else if (method == STDIO_WRITE && stream->fd != 0) {
|
||||
#ifdef SCR_STDIO_WRITE_SLOT
|
||||
result = scr_stdio_write_call(stream, args, argc);
|
||||
#else
|
||||
ScrDyn *encoding = argc > 1 ? args[1] : scr_dyn_undefined();
|
||||
ScrDyn *callback = argc > 2 ? args[2] : scr_dyn_undefined();
|
||||
if (encoding->kind == SCR_DYN_FUNC) { callback = encoding; encoding = scr_dyn_undefined(); }
|
||||
@@ -1360,6 +1414,7 @@ static ScrDyn *scr_stdio_method(ScrClosure *cb, ScrDyn *const *args, size_t argc
|
||||
}
|
||||
if (callback->kind == SCR_DYN_FUNC) scr_next_tick(scr_stdio_callback(callback, (void *)scr_stdio_write_done));
|
||||
result = scr_dyn_new_bool(true);
|
||||
#endif
|
||||
} else if (method == STDIO_ON || method == STDIO_ONCE || method == STDIO_OFF) {
|
||||
ScrDyn *listener = argc > 1 ? args[1] : scr_dyn_undefined();
|
||||
if (listener->kind != SCR_DYN_FUNC) {
|
||||
@@ -1605,7 +1660,14 @@ ScrDyn *scr_process_stdio(double fd) {
|
||||
ScrClosure *cb = scr_closure_new(NULL, 1);
|
||||
cb->caps[0] = scr_box_new(SCR_BOX_F64);
|
||||
scr_box_set_f64(cb->caps[0], i);
|
||||
#ifdef SCR_STDIO_WRITE_SLOT
|
||||
// Node's process.stdout.write is an anonymous (bound) function:
|
||||
// .name is "" there, while the other stream methods carry their names.
|
||||
scr_stdio_methods[i] = scr_dyn_new_func(
|
||||
cb, scr_stdio_method, i == STDIO_WRITE ? 3 : 1, "", i == STDIO_WRITE ? "" : scr_stdio_names[i]);
|
||||
#else
|
||||
scr_stdio_methods[i] = scr_dyn_new_func(cb, scr_stdio_method, i == STDIO_WRITE ? 3 : 1, "", scr_stdio_names[i]);
|
||||
#endif
|
||||
}
|
||||
scr_stdio_console_handler = scr_dyn_new_func(scr_closure_new(NULL, 0), scr_stdio_console_error_handler, 1, "", "");
|
||||
scr_stdio_write_hook = scr_stdio_intercept;
|
||||
@@ -1622,6 +1684,21 @@ double scr_dyn_process_stdio_fd(const ScrDyn *value, const ScrDynPath *path) {
|
||||
return stream ? stream->fd : 0;
|
||||
}
|
||||
|
||||
#ifdef SCR_STDIO_WRITE_SLOT
|
||||
/* process.stdout/stderr.write(chunk) with a run-time chunk: the stream
|
||||
* method's own validation and write, without creating the stream value
|
||||
* or installing the write hook. False with an exception pending when the
|
||||
* chunk is invalid. */
|
||||
bool scr_process_stdio_write_dyn(double fd, ScrDyn *chunk) {
|
||||
ScrStdio *stream = &scr_stdio_streams[fd == 2 ? 2 : 1];
|
||||
ScrDyn *result = scr_stdio_write_call(stream, &chunk, 1);
|
||||
if (!result) return false;
|
||||
bool accepted = scr_dyn_truthy(result);
|
||||
scr_dyn_release(result);
|
||||
return accepted;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* ── the process 'exit' event ─────────────────────────────────────────
|
||||
* Listeners run SYNCHRONOUSLY when the process exits: at the end of a
|
||||
* normal run (the unit's atexit, registered by scr_events_install after
|
||||
|
||||
@@ -2200,6 +2200,59 @@ static void scr_stream_do_write_inner(ScrStream *s, ScrBytes *chunk /*moves*/, S
|
||||
st->w.wsync = false;
|
||||
}
|
||||
|
||||
#ifdef SCR_STREAM_WRITE_ERRORS
|
||||
/* The error the next user write callback receives (borrowed; NULL is
|
||||
* success, Node's null). Set immediately around each user write-callback
|
||||
* call below; read by the error-first wrapper scr_stream_ecb_fire. */
|
||||
static SCR_TL ScrError *scr_stream_cb_error = NULL;
|
||||
#define SCR_WCB_CALL(cb, err) \
|
||||
do { \
|
||||
ScrError *scr_wcb_saved = scr_stream_cb_error; \
|
||||
scr_stream_cb_error = (err); \
|
||||
((void (*)(ScrClosure *))(cb)->fn)(cb); \
|
||||
scr_stream_cb_error = scr_wcb_saved; \
|
||||
} while (0)
|
||||
/* The errorBuffer/destroyed-afterWrite form: the stream's own error, or
|
||||
* the fresh ERR_STREAM_DESTROYED Node passes (owned around the call). */
|
||||
#define SCR_WCB_CALL_ERROF(cb, st) \
|
||||
do { \
|
||||
ScrError *scr_wcb_err = (st)->errored \
|
||||
? scr_error_retain((st)->errored) \
|
||||
: scr_stream_mkerr("ERR_STREAM_DESTROYED", \
|
||||
"Cannot call write after a stream was destroyed"); \
|
||||
SCR_WCB_CALL(cb, scr_wcb_err); \
|
||||
scr_error_release(scr_wcb_err); \
|
||||
} while (0)
|
||||
|
||||
/* An error-first user callback behind the zero-argument write-callback ABI:
|
||||
* caps[0] holds the user's closure, caps[1] the compiler-built adapter
|
||||
* (ScrFsRenameFn: (cb, err-or-NULL) — NULL is Node's null). */
|
||||
static void *scr_stream_ecb_retain(void *p) { return p; }
|
||||
static void scr_stream_ecb_release(void *p) { (void)p; }
|
||||
static void scr_stream_ecb_trace(void *p, ScrTraceVisit visit, void *ctx) {
|
||||
(void)p;
|
||||
(void)visit;
|
||||
(void)ctx;
|
||||
}
|
||||
static void scr_stream_ecb_fire(ScrClosure *wrapper) {
|
||||
ScrClosure *user = (ScrClosure *)scr_box_get_ref(wrapper->caps[0]);
|
||||
ScrFsRenameFn fn = (ScrFsRenameFn)scr_box_get_ref(wrapper->caps[1]);
|
||||
fn(user, scr_stream_cb_error);
|
||||
scr_closure_release(user); /* scr_box_get_ref returned +1 */
|
||||
}
|
||||
ScrClosure *scr_stream_ecb_wrap(ScrClosure *cb /*moves*/, ScrFsRenameFn fn) {
|
||||
ScrClosure *w = scr_closure_new((void *)scr_stream_ecb_fire, 2);
|
||||
w->caps[0] = scr_box_new_obj(scr_closure_retain_v, scr_closure_release_v, scr_closure_trace_v);
|
||||
scr_box_set_ref(w->caps[0], cb);
|
||||
w->caps[1] = scr_box_new_obj(scr_stream_ecb_retain, scr_stream_ecb_release, scr_stream_ecb_trace);
|
||||
scr_box_set_ref(w->caps[1], (void *)fn);
|
||||
return w;
|
||||
}
|
||||
#else
|
||||
#define SCR_WCB_CALL(cb, err) ((void (*)(ScrClosure *))(cb)->fn)(cb)
|
||||
#define SCR_WCB_CALL_ERROF(cb, st) SCR_WCB_CALL(cb, NULL)
|
||||
#endif
|
||||
|
||||
/* errorBuffer: once destroyed, the buffered writes' callbacks and the
|
||||
* end(cb) callbacks run (Node passes them ERR_STREAM_DESTROYED or the
|
||||
* stream's error) — unless a write is in flight, whose completion runs
|
||||
@@ -2213,7 +2266,7 @@ static void scr_stream_error_buffer(ScrStream *s) {
|
||||
scr_stream_wchunk_release(st, e.chunk);
|
||||
if (e.enc) scr_dyn_release(e.enc);
|
||||
if (e.cb) {
|
||||
((void (*)(ScrClosure *))e.cb->fn)(e.cb);
|
||||
SCR_WCB_CALL_ERROF(e.cb, st);
|
||||
scr_closure_release(e.cb);
|
||||
}
|
||||
}
|
||||
@@ -2256,7 +2309,7 @@ static void scr_stream_after_write(ScrStream *s, ScrClosure *cb, size_t count) {
|
||||
if (cb) {
|
||||
/* afterWrite: the callback once per write the tick completes */
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
((void (*)(ScrClosure *))cb->fn)(cb);
|
||||
SCR_WCB_CALL(cb, NULL);
|
||||
if (scr_exc_pending()) break;
|
||||
}
|
||||
scr_closure_release(cb);
|
||||
@@ -7169,7 +7222,13 @@ static bool scr_stream_write_chunk_enc(ScrStream *s, ScrBytes *chunk /*borrowed*
|
||||
ScrError *e = st->has_w && !st->destroyed
|
||||
? scr_stream_mkerr("ERR_STREAM_WRITE_AFTER_END", "write after end")
|
||||
: scr_stream_mkerr("ERR_STREAM_DESTROYED", "Cannot call write after a stream was destroyed");
|
||||
if (cb) scr_st_tick(s, SCR_ST_WCB_ERR, NULL, cb);
|
||||
if (cb) {
|
||||
#ifdef SCR_STREAM_WRITE_ERRORS
|
||||
scr_st_tick(s, SCR_ST_WCB_ERR, scr_error_retain(e), cb);
|
||||
#else
|
||||
scr_st_tick(s, SCR_ST_WCB_ERR, NULL, cb);
|
||||
#endif
|
||||
}
|
||||
scr_stream_error_or_destroy(s, e);
|
||||
scr_error_release(e);
|
||||
return false;
|
||||
@@ -7421,7 +7480,7 @@ void scr_stream_write_done(ScrStream *s, ScrError *err /*moves*/) {
|
||||
return;
|
||||
}
|
||||
if (ucb) {
|
||||
((void (*)(ScrClosure *))ucb->fn)(ucb);
|
||||
SCR_WCB_CALL(ucb, err);
|
||||
scr_closure_release(ucb);
|
||||
}
|
||||
if (!scr_exc_pending()) scr_stream_error_or_destroy(s, err);
|
||||
@@ -8010,8 +8069,12 @@ static void scr_stream_run_tick(ScrStreamTick *t) {
|
||||
scr_stream_after_write(s, cb, t->count);
|
||||
} else {
|
||||
if (cb) {
|
||||
/* Node v24 afterWrite runs `cb(null)` even for a destroyed
|
||||
* stream (the destroy that landed after this tick was queued
|
||||
* does not poison the completions it already owns), then
|
||||
* errorBuffer. */
|
||||
for (size_t i = 0; i < t->count; i++) {
|
||||
((void (*)(ScrClosure *))cb->fn)(cb);
|
||||
SCR_WCB_CALL(cb, NULL);
|
||||
if (scr_exc_pending()) break;
|
||||
}
|
||||
scr_closure_release(cb);
|
||||
@@ -8105,7 +8168,7 @@ static void scr_stream_run_tick(ScrStreamTick *t) {
|
||||
ScrClosure *cb = t->cb;
|
||||
t->cb = NULL;
|
||||
if (cb) {
|
||||
((void (*)(ScrClosure *))cb->fn)(cb);
|
||||
SCR_WCB_CALL(cb, t->err);
|
||||
scr_closure_release(cb);
|
||||
}
|
||||
break;
|
||||
@@ -8114,7 +8177,7 @@ static void scr_stream_run_tick(ScrStreamTick *t) {
|
||||
ScrClosure *cb = t->cb;
|
||||
t->cb = NULL;
|
||||
if (cb) {
|
||||
((void (*)(ScrClosure *))cb->fn)(cb);
|
||||
SCR_WCB_CALL(cb, t->err);
|
||||
scr_closure_release(cb);
|
||||
}
|
||||
if (!scr_exc_pending()) scr_stream_error_or_destroy(s, t->err);
|
||||
|
||||
@@ -420,6 +420,14 @@ ScrStream *scr_stream_end(ScrStream *s, ScrBytes *chunk_b, ScrStr *chunk_s,
|
||||
void scr_stream_cork(ScrStream *s);
|
||||
void scr_stream_uncork(ScrStream *s);
|
||||
|
||||
#ifdef SCR_STREAM_WRITE_ERRORS
|
||||
/* An error-first write callback: wraps the user's zero-argument closure
|
||||
* (which MOVES) so the compiler-built adapter (an ScrFsRenameFn,
|
||||
* fn(cb, err-or-NULL)) receives the error this write completion carries
|
||||
* (NULL is Node's null). */
|
||||
ScrClosure *scr_stream_ecb_wrap(ScrClosure *cb /*moves*/, void (*fn)(ScrClosure *, ScrError *));
|
||||
#endif
|
||||
|
||||
/* Shared lifecycle. destroy's err is BORROWED (may be NULL). */
|
||||
ScrStream *scr_stream_destroy(ScrStream *s, ScrError *err); /* recv +1 */
|
||||
/* Readable async-iterator return(): destroy with Node's AbortError payload;
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
// process.stdout/stderr backpressure probes under the synchronous stream
|
||||
// model: every write reaches the descriptor before write() returns, so
|
||||
// nothing is ever queued. writableLength is 0, writableNeedDrain is false,
|
||||
// write() returns true, and 'drain' (emitted only after a write returned
|
||||
// false) never fires: drain listeners register and stay silent, and a
|
||||
// promise of the next drain never settles without holding the process.
|
||||
import { once } from "node:events";
|
||||
|
||||
console.log("hwm", process.stdout.writableHighWaterMark, process.stderr.writableHighWaterMark);
|
||||
console.log("needDrain", process.stdout.writableNeedDrain, process.stderr.writableNeedDrain);
|
||||
console.log("length", process.stdout.writableLength, process.stderr.writableLength);
|
||||
|
||||
let drained = 0;
|
||||
const onDrain = (): void => {
|
||||
drained++;
|
||||
};
|
||||
process.stdout.on("drain", onDrain);
|
||||
process.stdout.once("drain", () => {
|
||||
drained++;
|
||||
});
|
||||
process.stderr.addListener("drain", onDrain);
|
||||
process.stdout.off("drain", onDrain);
|
||||
process.stderr.removeListener("drain", onDrain);
|
||||
|
||||
const ok = process.stdout.write("chunk\n");
|
||||
console.log("write returned", ok);
|
||||
|
||||
// The flush idiom: wait for drain only when bytes are queued or the
|
||||
// write asked for it.
|
||||
async function flush(): Promise<void> {
|
||||
if (process.stdout.writableLength > 0) {
|
||||
await new Promise<void>((resolve) => process.stdout.once("drain", resolve));
|
||||
}
|
||||
if (!process.stdout.write("again\n")) await once(process.stdout, "drain");
|
||||
console.log("flushed");
|
||||
}
|
||||
void flush();
|
||||
|
||||
// A drain that cannot come: the promise stays pending, and the process
|
||||
// exits once the loop is empty.
|
||||
void (async () => {
|
||||
await once(process.stderr, "drain");
|
||||
console.log("never");
|
||||
})();
|
||||
|
||||
// A name expression typed "drain" still runs.
|
||||
let picks = 0;
|
||||
const drainName = (): "drain" => {
|
||||
picks++;
|
||||
return "drain";
|
||||
};
|
||||
void once(process.stdout, drainName());
|
||||
void once(process.stderr, (picks++, "drain"));
|
||||
console.log("picks", picks);
|
||||
|
||||
setTimeout(() => console.log("drained", drained), 5);
|
||||
@@ -0,0 +1,34 @@
|
||||
// process.stdout/stderr.write of chunks whose type is only known at run
|
||||
// time: `any` values (a child's 'data' chunk, a JSON field) and
|
||||
// string | Uint8Array unions. Strings and Uint8Array/Buffer values write
|
||||
// their bytes; null throws ERR_STREAM_NULL_VALUES and any other value
|
||||
// throws ERR_INVALID_ARG_TYPE, both TypeErrors raised synchronously before
|
||||
// anything is written.
|
||||
|
||||
function forward(data: any): boolean {
|
||||
return process.stdout.write(data);
|
||||
}
|
||||
|
||||
function pick(useBytes: boolean): string | Uint8Array {
|
||||
return useBytes ? new Uint8Array([98, 121, 116, 101, 115, 10]) : "text\n";
|
||||
}
|
||||
|
||||
console.log("string", forward("any string\n"));
|
||||
console.log("buffer", forward(Buffer.from("any buffer\n")));
|
||||
console.log("u8", forward(new TextEncoder().encode("any u8\n")));
|
||||
process.stdout.write(pick(false));
|
||||
process.stdout.write(pick(true));
|
||||
process.stderr.write(pick(false));
|
||||
const parsed: any = JSON.parse('{"chunk":"from json\\n","n":42}');
|
||||
forward(parsed.chunk);
|
||||
|
||||
for (const bad of [parsed.n, null, undefined, { a: 1 }]) {
|
||||
try {
|
||||
forward(bad);
|
||||
console.log("no throw");
|
||||
} catch (error) {
|
||||
if (error instanceof TypeError) {
|
||||
console.log((error as NodeJS.ErrnoException).code, error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// Replacing process.stdout.write from TypeScript: the output-guard idiom.
|
||||
// The real write methods are taken first with bind (they write straight
|
||||
// to the descriptor, past any later patch). The replacement then sees
|
||||
// every stdout write: console.log's lines, direct string and byte writes
|
||||
// with their encoding and callback arguments as passed. Assigning the
|
||||
// saved method back restores the stream.
|
||||
type RawWrite = (chunk: string, callback?: (error?: Error | null) => void) => boolean;
|
||||
|
||||
const rawStdout = process.stdout.write.bind(process.stdout) as RawWrite;
|
||||
const rawStderr = process.stderr.write.bind(process.stderr) as RawWrite;
|
||||
const original = process.stdout.write;
|
||||
const captured: string[] = [];
|
||||
|
||||
function text(chunk: string | Uint8Array): string {
|
||||
return typeof chunk === "string" ? chunk : new TextDecoder().decode(chunk);
|
||||
}
|
||||
|
||||
process.stdout.write = ((
|
||||
chunk: string | Uint8Array,
|
||||
encodingOrCallback?: BufferEncoding | ((error?: Error | null) => void),
|
||||
callback?: (error?: Error | null) => void,
|
||||
): boolean => {
|
||||
const done = typeof encodingOrCallback === "function" ? encodingOrCallback : callback;
|
||||
const encoding = typeof encodingOrCallback === "string" ? encodingOrCallback : "-";
|
||||
captured.push(`${typeof chunk === "string" ? "string" : "bytes"}/${encoding}:${text(chunk).trimEnd()}`);
|
||||
return rawStderr(`[redirected] ${text(chunk)}`, done);
|
||||
}) as typeof process.stdout.write;
|
||||
|
||||
console.log("from console.log", 42);
|
||||
process.stdout.write("direct string\n");
|
||||
process.stdout.write(new Uint8Array([104, 105, 10]));
|
||||
process.stdout.write("6869210a", "hex");
|
||||
process.stdout.write("with callback\n", (error) => {
|
||||
rawStderr(`callback error is null: ${error === null}\n`);
|
||||
});
|
||||
const accepted = process.stdout.write("result\n");
|
||||
rawStdout("raw stdout bypasses the patch\n");
|
||||
console.log("same method", process.stdout.write === original, accepted);
|
||||
|
||||
process.stdout.write = original;
|
||||
console.log("restored", process.stdout.write === original);
|
||||
console.log(captured.join(" | "));
|
||||
@@ -0,0 +1,66 @@
|
||||
// Writable.write completion callbacks that take the error argument: null
|
||||
// after a successful write, the _write failure, ERR_STREAM_WRITE_AFTER_END
|
||||
// after end(), and ERR_STREAM_DESTROYED after destroy(). Node orders the
|
||||
// callback and the 'error' event through its writable state machine; this
|
||||
// program prints both.
|
||||
import { Writable } from "node:stream";
|
||||
|
||||
function show(label: string): (error?: Error | null) => void {
|
||||
return (error) => {
|
||||
const code = error ? (error as NodeJS.ErrnoException).code : undefined;
|
||||
console.log(
|
||||
label,
|
||||
error === null ? "null" : error === undefined ? "undefined" : `${code ?? ""} ${error.message}`,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const sink = new Writable({
|
||||
write(chunk: Buffer, _encoding: string, callback: (error?: Error | null) => void): void {
|
||||
callback(chunk.toString() === "bad" ? new Error("rejected") : null);
|
||||
},
|
||||
});
|
||||
sink.on("error", (error) => console.log("error event", error.message));
|
||||
sink.write("good", show("good"));
|
||||
sink.write("bad", show("bad"));
|
||||
|
||||
const ended = new Writable({
|
||||
write(_chunk: Buffer, _encoding: string, callback: (error?: Error | null) => void): void {
|
||||
callback();
|
||||
},
|
||||
});
|
||||
ended.on("error", (error) =>
|
||||
console.log("ended error event", (error as NodeJS.ErrnoException).code),
|
||||
);
|
||||
ended.write("first", show("first"));
|
||||
ended.end();
|
||||
ended.write("late", show("late"));
|
||||
|
||||
const destroyed = new Writable({
|
||||
write(_chunk: Buffer, _encoding: string, callback: (error?: Error | null) => void): void {
|
||||
callback();
|
||||
},
|
||||
});
|
||||
destroyed.destroy();
|
||||
destroyed.write("after destroy", show("destroyed"));
|
||||
|
||||
// A callback declared without parameters keeps its current lowering.
|
||||
const plain = new Writable({
|
||||
write(_chunk: Buffer, _encoding: string, callback: () => void): void {
|
||||
callback();
|
||||
},
|
||||
});
|
||||
plain.write("x", () => console.log("plain done"));
|
||||
|
||||
// Callbacks that return a value: the stream discards it.
|
||||
let count = 0;
|
||||
const counted = (): number => ++count;
|
||||
plain.write("y", counted);
|
||||
const tally = (error?: Error | null): number => (error ? -1 : ++count);
|
||||
const counting = new Writable({
|
||||
write(_chunk: Buffer, _encoding: string, callback: (error?: Error | null) => void): void {
|
||||
callback(null);
|
||||
},
|
||||
});
|
||||
counting.write("tally", tally);
|
||||
setTimeout(() => console.log("count", count), 5);
|
||||
@@ -0,0 +1,20 @@
|
||||
// Patching a NATIVE host object: Node lets a library swap
|
||||
// `process.stdout.write`, hand out `process.stdout.write.bind(...)`, or
|
||||
// redefine a module member with Object.defineProperty. The process output
|
||||
// stream's write slot is the runtime stream object's, so the assignment and
|
||||
// the bind compile. A builtin module's exports are direct calls, so those
|
||||
// forms keep an explicit refusal instead of silently patching nothing. The
|
||||
// working split: use the module's functions directly.
|
||||
|
||||
import * as fs from "node:fs";
|
||||
|
||||
export function bindReadFileSync(): typeof fs.readFileSync {
|
||||
return fs.readFileSync.bind(fs);
|
||||
}
|
||||
|
||||
export function defineReadFileSync(): void {
|
||||
Object.defineProperty(fs, "readFileSync", { value: () => "" });
|
||||
}
|
||||
|
||||
bindReadFileSync();
|
||||
defineReadFileSync();
|
||||
@@ -0,0 +1,5 @@
|
||||
// Drain registration on the process streams lowers to nothing because a
|
||||
// synchronous write never asks for a drain; the listener table it would
|
||||
// fill is not observable, so reading it stays refused.
|
||||
process.stdout.once("drain", () => {});
|
||||
console.log(process.stdout.listenerCount("drain"));
|
||||
@@ -0,0 +1,26 @@
|
||||
native-shim-monkeypatching.ts:11:17 - error SC2007: values of type '{ (path: string | number | URL, encoding: BufferEncoding): string; (path: string | number | URL, options: { encoding: BufferEncoding; }): string; (path: string | ... 1 more ... | URL): Buffer<...>; (path: string | URL, options: unknown): string | Buffer<...>; }' cannot be compiled: the type declares multiple call signatures (overloads), and a compiled function value is always one concrete signature
|
||||
|
||||
10 |
|
||||
11 | export function bindReadFileSync(): typeof fs.readFileSync {
|
||||
| ^~~~~~~~~~~~~~~~
|
||||
12 | return fs.readFileSync.bind(fs);
|
||||
|
||||
hint: annotate the slot with the ONE signature the program actually calls (e.g. '(x: number) => string'), or wrap the overloaded function in a single-signature arrow
|
||||
|
||||
native-shim-monkeypatching.ts:16:3 - error SC2020: 'Object.defineProperty' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
15 | export function defineReadFileSync(): void {
|
||||
16 | Object.defineProperty(fs, "readFileSync", { value: () => "" });
|
||||
| ^~~~~~~~~~~~~~~~~~~~~
|
||||
17 | }
|
||||
|
||||
hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations)
|
||||
|
||||
native-shim-monkeypatching.ts:19:1 - error SC2004: uses of 'bindReadFileSync' inherit the blocker on its declaration
|
||||
|
||||
18 |
|
||||
19 | bindReadFileSync();
|
||||
| ^~~~~~~~~~~~~~~~
|
||||
20 | defineReadFileSync();
|
||||
|
||||
hint: the declaration of 'bindReadFileSync' did not compile — fix the diagnostic reported there and these sites clear with it
|
||||
@@ -0,0 +1,8 @@
|
||||
process-stream-listener-count.ts:5:28 - error SC0001: Property 'listenerCount' does not exist on type 'WriteStream'.
|
||||
|
||||
4 | process.stdout.once("drain", () => {});
|
||||
5 | console.log(process.stdout.listenerCount("drain"));
|
||||
| ^~~~~~~~~~~~~
|
||||
6 |
|
||||
|
||||
hint: the project has no @types/node, so scriptc types Node with its bundled declarations, which list only the members that compile natively — 'listenerCount' does not compile natively yet (https://scriptc.dev/limitations); installing @types/node turns this into a located refusal with a hint
|
||||
@@ -1,12 +1,3 @@
|
||||
stream-completion-callback-error.ts:10:17 - error SC2020: 'write completion callbacks taking the error argument' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
9 | });
|
||||
10 | sink.write("a", (err) => console.log("written", err));
|
||||
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
11 | sink.end((err?: Error | null) => console.log("ended", err));
|
||||
|
||||
hint: () => void callbacks are supported (listen for 'error' to observe failures)
|
||||
|
||||
stream-completion-callback-error.ts:11:10 - error SC2020: 'end callbacks with parameters' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
10 | sink.write("a", (err) => console.log("written", err));
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 5912, "san": 124235 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 4567, "san": 86527 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3200, "san": 2700 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 4567, "san": 86151 }
|
||||
@@ -116,7 +116,6 @@ const KNOWN: Record<string, Known> = {
|
||||
"package-imports": { island: true },
|
||||
"promise-bridge": { build: "SC1090" },
|
||||
"self-name": { build: "SC2013" },
|
||||
"stream-shims": { run: "SC2020" },
|
||||
"util-shims": { run: "SC2020" },
|
||||
// The package redeclares the var it exported: Node copies the first value.
|
||||
"var-export-redeclared": { island: true },
|
||||
|
||||
Reference in New Issue
Block a user