mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-22 05:59:31 +08:00
Merge main
This commit is contained in:
@@ -5,7 +5,7 @@ Some builtins can be stored and passed as functions:
|
||||
- `util.isDeepStrictEqual`, `styleText`, `stripVTControlCharacters`, `toUSVString`, `inherits`, `deprecate`, `callbackify`, `debuglog`, and `debug`.
|
||||
- `fs.existsSync`, `unlinkSync`, `chmodSync`, `chownSync`, `renameSync`, and `closeSync`, and `fs/promises.unlink`, `chmod`, and `rename`.
|
||||
|
||||
Other immutable aliases of table-backed Node builtins support direct calls, but cannot be passed elsewhere. `util.promisify` supports compile-time conversions of `child_process.execFile`, `child_process.exec`, `setTimeout` and `setImmediate` (to the `timers/promises` functions, as in Node), and the `fs` callback functions that have an `fs/promises` counterpart (`access`, `appendFile`, `chmod`, `chown`, `copyFile`, `lchown`, `link`, `lstat`, `lutimes`, `mkdir`, `mkdtemp`, `readFile`, `readdir`, `readlink`, `realpath`, `rename`, `rm`, `rmdir`, `stat`, `statfs`, `symlink`, `truncate`, `unlink`, `utimes`, and `writeFile`); calls through the converted function lower as the `fs/promises` call.
|
||||
Other immutable aliases of table-backed Node builtins support direct calls, but cannot be passed elsewhere. `util.promisify` supports compile-time conversions of `child_process.execFile`, `child_process.exec`, `setTimeout` and `setImmediate` (to the `timers/promises` functions, as in Node), `stream.finished` and `stream.pipeline` (to the `stream/promises` functions, as in Node), and the `fs` callback functions that have an `fs/promises` counterpart (`access`, `appendFile`, `chmod`, `chown`, `copyFile`, `lchown`, `link`, `lstat`, `lutimes`, `mkdir`, `mkdtemp`, `readFile`, `readdir`, `readlink`, `realpath`, `rename`, `rm`, `rmdir`, `stat`, `statfs`, `symlink`, `truncate`, `unlink`, `utimes`, and `writeFile`); calls through the converted function lower as the `fs/promises` call.
|
||||
|
||||
`util.styleText` supports standard formats and aliases, `#RGB` and `#RRGGBB` hex colors, arrays of formats, nested ANSI resets, and explicit color enabling with `{ validateStream: false }`. Native calls accept standard process streams; custom native streams and mutations to `util.inspect.colors` remain unsupported.
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
- **Async iteration:** Async `yield*` delegation, stored Web or Node iterator handles, and general structural async-iterator objects are unsupported. Typed async generators support lazy execution, `await`, direct `yield`, queued `.next()`/`.return()`/`.throw()` requests, and `for await`. `for await` also accepts Node `Readable` streams, Web `ReadableStream` values, and compiled class iterators whose zero-parameter `next()` returns a promise of a `{ value, done? }` record. Abrupt completion runs cleanup, including literal `destroyOnReturn` and `preventCancel` options. The `node:timers/promises` `setInterval(delay, value)` iterator compiles without AbortSignal options.
|
||||
- **Async iteration:** Async `yield*` delegation, stored Web or Node iterator handles, and general structural async-iterator objects are unsupported. Typed async generators support lazy execution, `await`, direct `yield`, queued `.next()`/`.return()`/`.throw()` requests, and `for await`. `for await` also accepts Node `Readable` streams, Web `ReadableStream` values, and compiled class iterators whose zero-parameter `next()` returns a promise of a `{ value, done? }` record. Abrupt completion runs cleanup, including literal `destroyOnReturn` and `preventCancel` options. The `node:timers/promises` `setInterval(delay, value)` iterator compiles without AbortSignal options. `Array.fromAsync` accepts arrays of values or of promises with an optional mapping function; async generator sources, a `thisArg`, and arrays that mix values and promises are unsupported.
|
||||
|
||||
@@ -1 +1 @@
|
||||
- **Resource disposal:** Top-level and switch-clause `using` declarations, and `DisposableStack` constructors are unsupported. Block- and function-scoped `using` and `await using` support compiled classes with zero-parameter disposal methods and supported FileHandle, timer, immediate, child-process, and readline handles. Array `for (using ... of ...)` loops also compile. Caught `SuppressedError` values expose `name` and `message`, but not `error` or `suppressed` payloads.
|
||||
- **Resource disposal:** Top-level and switch-clause `using` declarations, and `DisposableStack` constructors are unsupported. Block- and function-scoped `using` and `await using` support compiled classes with zero-parameter disposal methods, object literals and other `Disposable`- or `AsyncDisposable`-typed values, and supported FileHandle, timer, immediate, child-process, and readline handles. A class instance reached through a `Disposable`- or `unknown`-typed value throws an SC2020 error when its declaration runs; declare the variable with the class type. Array `for (using ... of ...)` loops also compile. `SuppressedError` values expose `error` and `suppressed`; `extends SuppressedError` and casting `unknown` to `SuppressedError` are unsupported (narrow with `instanceof`).
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
"schemaVersion": 3,
|
||||
"nodeVersion": "24.22.0",
|
||||
"nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93",
|
||||
"artifactVersion": "c79533e05530df7a2a01",
|
||||
"artifactVersion": "e8a11a8f3d91d2503aae",
|
||||
"rowCount": 3880
|
||||
}
|
||||
|
||||
@@ -164,9 +164,9 @@
|
||||
"apiEntries": 39,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 18,
|
||||
"partial": 17,
|
||||
"refused": 0,
|
||||
"not-implemented": 21,
|
||||
"not-implemented": 22,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
@@ -9312,9 +9312,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
"tierItemCount": 3472,
|
||||
"tiers": {
|
||||
"static": {
|
||||
"audit-partial": 1061,
|
||||
"verify-gap": 2284,
|
||||
"audit-partial": 1060,
|
||||
"verify-gap": 2285,
|
||||
"replace-refusal": 40,
|
||||
"classify": 2,
|
||||
"implement": 85
|
||||
@@ -3941,16 +3941,12 @@
|
||||
"priority": "high",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:child_process.child_process.exec",
|
||||
"tests": [
|
||||
"tests/corpus/5627-exec-callback.ts",
|
||||
"tests/fixtures/node-types/child-exec.ts",
|
||||
"tests/harness/project-config.test.ts"
|
||||
]
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:child_process",
|
||||
"tests": []
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -76140,6 +76136,7 @@
|
||||
"tests/fixtures/node-types/promisify-fs.ts",
|
||||
"tests/corpus/5628-promisify-timers.ts",
|
||||
"tests/fixtures/node-types/promisify-timers.ts",
|
||||
"tests/corpus/5629-promisify-stream.ts",
|
||||
"tests/harness/project-config.test.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -131,9 +131,9 @@
|
||||
"apiEntries": 39,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 18,
|
||||
"partial": 17,
|
||||
"refused": 0,
|
||||
"not-implemented": 21,
|
||||
"not-implemented": 22,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
@@ -11663,10 +11663,7 @@
|
||||
"tests/corpus/3127-spawn-runtime-options.ts",
|
||||
"tests/corpus/3130-child-generic-streams.ts",
|
||||
"tests/corpus/3131-child-rest-listeners.js",
|
||||
"tests/corpus/3132-child-generic-stream-completion.ts",
|
||||
"tests/corpus/5627-exec-callback.ts",
|
||||
"tests/fixtures/node-types/child-exec.ts",
|
||||
"tests/harness/project-config.test.ts"
|
||||
"tests/corpus/3132-child-generic-stream-completion.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "chapter"
|
||||
@@ -11735,13 +11732,8 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:child_process.child_process.exec",
|
||||
"tests": [
|
||||
"tests/corpus/5627-exec-callback.ts",
|
||||
"tests/fixtures/node-types/child-exec.ts",
|
||||
"tests/harness/project-config.test.ts"
|
||||
]
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:child_process"
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -100574,6 +100566,7 @@
|
||||
"tests/corpus/5625-promisify-exec.ts",
|
||||
"tests/corpus/5626-promisify-forms.ts",
|
||||
"tests/corpus/5628-promisify-timers.ts",
|
||||
"tests/corpus/5629-promisify-stream.ts",
|
||||
"tests/corpus/node24-deep-equality-properties.js",
|
||||
"tests/corpus/node24-deep-equality.cjs",
|
||||
"tests/corpus/node24-deep-equality.ts",
|
||||
@@ -102231,6 +102224,7 @@
|
||||
"tests/fixtures/node-types/promisify-fs.ts",
|
||||
"tests/corpus/5628-promisify-timers.ts",
|
||||
"tests/fixtures/node-types/promisify-timers.ts",
|
||||
"tests/corpus/5629-promisify-stream.ts",
|
||||
"tests/harness/project-config.test.ts"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -307,7 +307,6 @@
|
||||
{ "chapter": "packages", "symbols": ["--input-type"], "status": "not-applicable" },
|
||||
{ "chapter": "child_process", "signatures": ["`subprocess[Symbol.dispose]()`"], "status": "partial", "evidence": ["tests/corpus/2844-node-disposable-resources.ts"] },
|
||||
{ "chapter": "child_process", "signatures": ["`child_process.execFile(file[, args][, options][, callback])`"], "status": "partial", "evidence": ["tests/corpus/2911-execfile-callback.ts"] },
|
||||
{ "chapter": "child_process", "signatures": ["`child_process.exec(command[, options][, callback])`"], "status": "partial", "evidence": ["tests/corpus/5627-exec-callback.ts", "tests/fixtures/node-types/child-exec.ts", "tests/harness/project-config.test.ts"] },
|
||||
{ "chapter": "child_process", "signatures": ["`child_process.fork(modulePath[, args][, options])`", "`subprocess.send(message[, sendHandle[, options]][, callback])`", "`subprocess.disconnect()`", "`connected` Type: {boolean} Set to `false` after `subprocess.disconnect()` is called.", "Event: `'message'`", "Event: `'disconnect'`"], "status": "partial", "evidence": ["tests/corpus/2946-child-fork-ipc/main.ts"] },
|
||||
{ "chapter": "child_process", "signatures": ["Event: `'close'`"], "status": "partial", "evidence": ["tests/corpus/2939-child-shell-text.ts", "tests/corpus/3131-child-rest-listeners.js"] },
|
||||
{ "chapter": "child_process", "signatures": ["`stdout` Type: {stream.Readable|null|undefined}", "`stderr` Type: {stream.Readable|null|undefined}"], "status": "partial", "evidence": ["tests/corpus/1565-spawn-pipe-streams.ts", "tests/corpus/2939-child-shell-text.ts"] },
|
||||
@@ -343,7 +342,7 @@
|
||||
{ "chapter": "test", "parent": "Class: `MockFunctionContext`", "symbols": ["calls", "ctx.callCount", "ctx.mockImplementation", "ctx.mockImplementationOnce", "ctx.resetCalls", "ctx.restore"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/mock-fn.ts", "tests/harness/node-test.test.ts"] },
|
||||
{ "chapter": "test", "parent": "Class: `TestContext`", "symbols": ["context.before", "context.after", "context.beforeEach", "context.afterEach", "context.plan", "context.fullName"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/ctx-hooks.ts", "tests/fixtures/node-test/cases/ctx-plan.ts", "tests/harness/node-test.test.ts"] },
|
||||
{ "chapter": "test", "symbols": ["describe.skip", "describe.todo", "describe.only", "suite.skip", "suite.todo", "suite.only", "it.skip", "it.todo", "it.only"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/suite-variants.ts", "tests/fixtures/node-test/cases/only-nested.ts", "tests/fixtures/node-test/cases/todo-suites.ts", "tests/harness/node-test.test.ts"] },
|
||||
{ "chapter": "util", "signatures": ["`util.promisify(original)`"], "status": "partial", "evidence": ["tests/corpus/1473-promisify-execfile.ts", "tests/corpus/1573-promisify-execfile-env-spread.ts", "tests/corpus/2912-promisify-fs-readfile.ts", "tests/corpus/5625-promisify-exec.ts", "tests/corpus/5626-promisify-forms.ts", "tests/fixtures/node-types/promisify-fs.ts", "tests/corpus/5628-promisify-timers.ts", "tests/fixtures/node-types/promisify-timers.ts", "tests/harness/project-config.test.ts"] },
|
||||
{ "chapter": "util", "signatures": ["`util.promisify(original)`"], "status": "partial", "evidence": ["tests/corpus/1473-promisify-execfile.ts", "tests/corpus/1573-promisify-execfile-env-spread.ts", "tests/corpus/2912-promisify-fs-readfile.ts", "tests/corpus/5625-promisify-exec.ts", "tests/corpus/5626-promisify-forms.ts", "tests/fixtures/node-types/promisify-fs.ts", "tests/corpus/5628-promisify-timers.ts", "tests/fixtures/node-types/promisify-timers.ts", "tests/corpus/5629-promisify-stream.ts", "tests/harness/project-config.test.ts"] },
|
||||
{ "chapter": "typescript", "symbols": ["type"], "status": "supported", "evidence": ["tests/corpus/2702-type-import-link-static/main.ts", "tests/corpus/2703-type-import-link-static-reexport/main.ts", "tests/corpus/2704-type-import-link-static-success/main.ts", "tests/corpus/2707-type-import-link-static-default-reexport/main.ts", "tests/harness/errors.test.ts"] },
|
||||
{ "chapter": "zlib", "signatures": ["Class: `zlib.Deflate`", "Class: `zlib.Inflate`", "Class: `zlib.Gzip`", "Class: `zlib.Gunzip`", "Class: `zlib.DeflateRaw`", "Class: `zlib.InflateRaw`", "Class: `zlib.Unzip`", "Class: `zlib.ZlibBase`", "`zlib.close([callback])`", "`zlib.flush([kind, ]callback)`", "`zlib.params(level, strategy, callback)`", "`zlib.reset()`", "`bytesWritten` Type: {number}", "Class: `Options`"], "status": "partial", "evidence": ["tests/corpus/5370-zlib-stream-roundtrip.ts", "tests/corpus/5371-zlib-stream-chunking.cjs", "tests/corpus/5372-zlib-stream-errors.cjs", "tests/corpus/5373-zlib-stream-options.cjs", "tests/corpus/5374-zlib-stream-flush-params.ts", "tests/corpus/5375-zlib-stream-promises.ts", "tests/corpus/5377-zlib-stream-backpressure.cjs"] },
|
||||
{ "chapter": "zlib", "signatures": ["`zlib.constants`"], "status": "partial", "evidence": ["tests/corpus/5376-zlib-constants.cjs", "tests/corpus/5374-zlib-stream-flush-params.ts"] }
|
||||
|
||||
+13
-4
@@ -2971,10 +2971,10 @@ declare module "child_process" {
|
||||
args?: string[] | null,
|
||||
options?: ExecSyncOptions,
|
||||
): unknown;
|
||||
/* The async shell form. The callback forms lower like execFile's, with
|
||||
* the command line run by /bin/sh -c, and util.promisify recognizes it
|
||||
* statically, as it does execFile; reached calls without a callback
|
||||
* fence at their site. */
|
||||
/* The async shell form — declared surface without full lowering (the
|
||||
* type-level tolerance story): harness code on paths tests don't reach
|
||||
* must typecheck; a reached call fences at its site. util.promisify
|
||||
* recognizes it statically, as it does execFile. */
|
||||
export function exec(
|
||||
command: string,
|
||||
options?: ExecSyncOptions | ((error: Error | null, stdout: string, stderr: string) => void),
|
||||
@@ -3062,6 +3062,12 @@ declare module "util" {
|
||||
export type TextDecoder = globalThis.TextDecoder;
|
||||
export var TextEncoder: typeof globalThis.TextEncoder;
|
||||
export var TextDecoder: typeof globalThis.TextDecoder;
|
||||
/* stream.pipeline's (...streams: unknown[]) => unknown is assignable to
|
||||
* every other target's parameter type, so its overload comes first; no
|
||||
* other target is assignable to it. */
|
||||
export function promisify(
|
||||
fn: typeof import("node:stream").pipeline,
|
||||
): typeof import("node:stream/promises").pipeline;
|
||||
export function promisify(
|
||||
fn: typeof import("node:fs").readFile,
|
||||
): (path: string, encoding: "utf8" | "utf-8") => Promise<string>;
|
||||
@@ -3098,6 +3104,9 @@ declare module "util" {
|
||||
windowsHide?: boolean;
|
||||
},
|
||||
) => Promise<{ stdout: string; stderr: string }>;
|
||||
export function promisify(
|
||||
fn: typeof import("node:stream").finished,
|
||||
): typeof import("node:stream/promises").finished;
|
||||
/* Declared surface without full lowering (the type-level tolerance
|
||||
* story: guarded/diagnostic-path code must TYPECHECK; a reached use
|
||||
* without a lowering fences at its site). inspect's options carry the
|
||||
|
||||
@@ -18,13 +18,12 @@ export function emitChildProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr
|
||||
const argv = host.emitExpr(e.args[1]!);
|
||||
const cb = host.emitExpr(callbackArg);
|
||||
host.moveTemp(cb);
|
||||
const shell = host.emitExpr(e.args[3]!);
|
||||
const adapter = host.execFileThunkFor(cbT);
|
||||
host.usesTimers = true;
|
||||
host.declare(`declare ptr @scr_exec_file(ptr, ptr, ptr, ptr, i1)`);
|
||||
host.declare(`declare ptr @scr_exec_file(ptr, ptr, ptr, ptr)`);
|
||||
const out = B.tmp();
|
||||
B.line(
|
||||
`${out} = call ptr @scr_exec_file(ptr ${cmd.name}, ptr ${argv.name}, ptr ${cb.name}, ptr @${adapter}, i1 ${shell.name})`,
|
||||
`${out} = call ptr @scr_exec_file(ptr ${cmd.name}, ptr ${argv.name}, ptr ${cb.name}, ptr @${adapter})`,
|
||||
);
|
||||
return host.own({ name: out, type: e.type });
|
||||
}
|
||||
|
||||
@@ -135,6 +135,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"error.domCause": "scr_domex_cause",
|
||||
"error.domClone": "scr_domex_clone",
|
||||
"error.newAggregate": "scr_aggregate_new",
|
||||
"error.newSuppressed": "scr_suppressed_new",
|
||||
"error.aggregateSetErrors": "scr_aggregate_set_errors",
|
||||
"error.aggregateErrors": "scr_aggregate_errors",
|
||||
"dyn.errInstanceof": "scr_dyn_err_instanceof",
|
||||
@@ -1286,6 +1287,8 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"dyn.packPushSpreadIter": "scr_dyn_pack_push_spread_iter",
|
||||
"dyn.reflectApply": "scr_dyn_reflect_apply",
|
||||
"dyn.reflectGet": "scr_dyn_reflect_get",
|
||||
"dyn.usingMethod": "scr_dyn_using_method",
|
||||
"dyn.usingDispose": "scr_dyn_using_dispose",
|
||||
"dyn.reflectSet": "scr_dyn_reflect_set",
|
||||
"dyn.reflectDefine": "scr_dyn_reflect_define",
|
||||
"dyn.abstractEq": "scr_dyn_abstract_eq",
|
||||
|
||||
@@ -628,6 +628,8 @@ const ERROR_KIND_NAMES: readonly string[] = [
|
||||
"EvalError",
|
||||
"URIError",
|
||||
"AggregateError",
|
||||
"AssertionError",
|
||||
"SuppressedError",
|
||||
];
|
||||
|
||||
/** A short description of a node that seeds the may-throw fixpoint. */
|
||||
|
||||
@@ -205,12 +205,12 @@ export function generateSurfaceManifest(compilerVersion: string): SurfaceManifes
|
||||
[
|
||||
"using-declarations",
|
||||
"block- and function-scoped using declarations",
|
||||
"LIFO disposal on normal and abrupt scope exit through [Symbol.dispose]",
|
||||
"LIFO disposal on normal and abrupt scope exit through [Symbol.dispose], of class instances, object literals and Disposable-typed values; a failing disposal raises a SuppressedError with error and suppressed",
|
||||
],
|
||||
[
|
||||
"await-using-declarations",
|
||||
"block- and function-scoped await using declarations",
|
||||
"LIFO asynchronous disposal through [Symbol.asyncDispose], with synchronous fallback",
|
||||
"LIFO asynchronous disposal through [Symbol.asyncDispose], with synchronous fallback, of class instances, object literals and AsyncDisposable-typed values",
|
||||
],
|
||||
[
|
||||
"for-using-of",
|
||||
|
||||
@@ -56,8 +56,8 @@ export function lowerBuiltinCall(
|
||||
loc: SrcLoc,
|
||||
namespaceMember?: ts.MemberName,
|
||||
): IrExpr {
|
||||
if (bi.module === "child_process" && (bi.member === "execFile" || bi.member === "exec")) {
|
||||
return lowerer.lowerExecFileCall(expr, loc, bi.member === "exec");
|
||||
if (bi.module === "child_process" && bi.member === "execFile") {
|
||||
return lowerer.lowerExecFileCall(expr, loc);
|
||||
}
|
||||
if (bi.module === "events" && bi.member === "addAbortListener") {
|
||||
return lowerAddAbortListener(lowerer, expr, loc);
|
||||
|
||||
@@ -147,12 +147,19 @@ export function registerPromisifiedBuiltinDecl(
|
||||
module: "timers/promises",
|
||||
member: target.member,
|
||||
}
|
||||
: null;
|
||||
: target?.module === "stream" &&
|
||||
(target.member === "finished" || target.member === "pipeline")
|
||||
? {
|
||||
kind: "promisified-builtin" as const,
|
||||
module: "stream/promises",
|
||||
member: target.member,
|
||||
}
|
||||
: null;
|
||||
if (!projection) {
|
||||
lowerer.noLowering(
|
||||
"util.promisify of this target",
|
||||
argNode ?? e,
|
||||
"child_process.exec, child_process.execFile, setTimeout, setImmediate and the fs callback functions with an fs/promises counterpart are the supported targets",
|
||||
"child_process.exec, child_process.execFile, setTimeout, setImmediate, stream.finished, stream.pipeline and the fs callback functions with an fs/promises counterpart are the supported targets",
|
||||
);
|
||||
}
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(nameNode);
|
||||
|
||||
@@ -725,57 +725,35 @@ export function lowerForkCall(lowerer: Lowerer, expr: ts.CallExpression, loc: Sr
|
||||
}
|
||||
|
||||
/** `execFile(file[, args][, options], callback)` — the asynchronous callback
|
||||
* slice. With `shell`, `exec(command[, options], callback)`: the command
|
||||
* line runs with /bin/sh -c, as execSync runs it, and failure messages show
|
||||
* the command line as Node's do. The child starts immediately with
|
||||
* stdin/stdout/stderr piped; the runtime captures both outputs and invokes
|
||||
* the error-first callback after settlement. The callback may ignore a suffix of `(error, stdout,
|
||||
* slice. The child starts immediately with stdin/stdout/stderr piped; the
|
||||
* runtime captures both outputs and invokes the error-first callback after
|
||||
* settlement. The callback may ignore a suffix of `(error, stdout,
|
||||
* stderr)`, but every declared parameter must have the Node shape. Inline
|
||||
* `encoding: "utf8"` and a pure numeric `maxBuffer` are accepted; the latter
|
||||
* is not enforced by the growing native capture, as
|
||||
* with the synchronous and promisified capture forms. Other options remain
|
||||
* fenced until their lifecycle can share this asynchronous core. */
|
||||
export function lowerExecFileCall(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.CallExpression,
|
||||
loc: SrcLoc,
|
||||
shell = false,
|
||||
): IrExpr {
|
||||
const what = shell ? "exec" : "execFile";
|
||||
export function lowerExecFileCall(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr {
|
||||
if (
|
||||
expr.arguments.some(ts.isSpreadElement) ||
|
||||
expr.arguments.length < 2 ||
|
||||
expr.arguments.length > (shell ? 3 : 4)
|
||||
expr.arguments.length > 4
|
||||
) {
|
||||
lowerer.noLowering(
|
||||
`${what} with ${expr.arguments.length} arguments`,
|
||||
`execFile with ${expr.arguments.length} arguments`,
|
||||
expr,
|
||||
shell
|
||||
? "the supported callback forms are exec(command, callback) and exec(command, { encoding: 'utf8', maxBuffer: N }, callback)"
|
||||
: "the supported callback forms are execFile(file, callback), execFile(file, args, callback), and execFile(file, args, { encoding: 'utf8', maxBuffer: N }, callback)",
|
||||
"the supported callback forms are execFile(file, callback), execFile(file, args, callback), and execFile(file, args, { encoding: 'utf8', maxBuffer: N }, callback)",
|
||||
);
|
||||
}
|
||||
const first = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING);
|
||||
const cmd = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING);
|
||||
const argsNode = expr.arguments.length >= 3 ? expr.arguments[1] : undefined;
|
||||
const callbackNode = expr.arguments[expr.arguments.length - 1]!;
|
||||
// exec: /bin/sh -c <command>, execSync's lowering (the runtime reads the
|
||||
// command line back from argv[1] for failure messages); execFile: the
|
||||
// file and its args list.
|
||||
const cmd: IrExpr = shell ? { kind: "strLit", value: "/bin/sh", type: STRING, loc } : first;
|
||||
const argv: IrExpr = shell
|
||||
? {
|
||||
kind: "arrayLit",
|
||||
elems: [{ kind: "strLit", value: "-c", type: STRING, loc }, first],
|
||||
type: arrayOf(STRING),
|
||||
loc,
|
||||
}
|
||||
: lowerChildArgsArg(lowerer, expr.arguments.length >= 3 ? expr.arguments[1] : undefined, loc);
|
||||
const optionsNode =
|
||||
expr.arguments.length === (shell ? 3 : 4) ? expr.arguments[shell ? 1 : 2] : undefined;
|
||||
if (optionsNode) {
|
||||
const options = optionsNode;
|
||||
const argv = lowerChildArgsArg(lowerer, argsNode, loc);
|
||||
if (expr.arguments.length === 4) {
|
||||
const options = expr.arguments[2]!;
|
||||
if (!ts.isObjectLiteralExpression(options)) {
|
||||
lowerer.noLowering(
|
||||
`${what} with a non-literal options argument`,
|
||||
"execFile with a non-literal options argument",
|
||||
options,
|
||||
"pass encoding and maxBuffer in an inline object literal",
|
||||
);
|
||||
@@ -796,7 +774,7 @@ export function lowerExecFileCall(
|
||||
const member = optionMember(property);
|
||||
if (!member)
|
||||
lowerer.noLowering(
|
||||
`${what} with this options shape`,
|
||||
"execFile with this options shape",
|
||||
property,
|
||||
"use plain encoding and maxBuffer properties without spreads or computed keys",
|
||||
);
|
||||
@@ -806,7 +784,7 @@ export function lowerExecFileCall(
|
||||
(member.value.text !== "utf8" && member.value.text !== "utf-8")
|
||||
) {
|
||||
lowerer.noLowering(
|
||||
`${what} with a non-literal utf8 encoding`,
|
||||
"execFile with a non-literal utf8 encoding",
|
||||
member.value,
|
||||
"pass the literal 'utf8' or 'utf-8' (other expressions might have side effects)",
|
||||
);
|
||||
@@ -814,14 +792,14 @@ export function lowerExecFileCall(
|
||||
} else if (member.name === "maxBuffer") {
|
||||
if (!pureNumber(member.value)) {
|
||||
lowerer.noLowering(
|
||||
`${what} with a non-literal maxBuffer`,
|
||||
"execFile with a non-literal maxBuffer",
|
||||
member.value,
|
||||
"pass a numeric literal or literal arithmetic (the native capture grows without enforcing this limit)",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
lowerer.noLowering(
|
||||
`${what} option '${member.name}'`,
|
||||
`execFile option '${member.name}'`,
|
||||
property,
|
||||
"only encoding: 'utf8' and a pure numeric maxBuffer are supported in the callback form",
|
||||
);
|
||||
@@ -837,14 +815,14 @@ export function lowerExecFileCall(
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
callbackNode,
|
||||
`${what} callbacks take (error), (error, stdout), (error, stdout, stderr), or no parameters`,
|
||||
"execFile callbacks take (error), (error, stdout), (error, stdout, stderr), or no parameters",
|
||||
);
|
||||
}
|
||||
if (callback.type.ret.kind !== "void") {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
callbackNode,
|
||||
`${what} callbacks returning a value (make the callback body a block, or return nothing)`,
|
||||
"execFile callbacks returning a value (make the callback body a block, or return nothing)",
|
||||
);
|
||||
}
|
||||
const errorParam = callback.type.params[0];
|
||||
@@ -859,7 +837,7 @@ export function lowerExecFileCall(
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
callbackNode,
|
||||
`${what} callbacks whose first parameter is not 'Error | null' (got '${lowerer.fmt(errorParam)}')`,
|
||||
`execFile callbacks whose first parameter is not 'Error | null' (got '${lowerer.fmt(errorParam)}')`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -868,15 +846,9 @@ export function lowerExecFileCall(
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
callbackNode,
|
||||
`${what} callbacks whose ${i === 1 ? "stdout" : "stderr"} parameter is not 'string'`,
|
||||
`execFile callbacks whose ${i === 1 ? "stdout" : "stderr"} parameter is not 'string'`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "cp.execFile",
|
||||
args: [cmd, argv, callback, boolLit(shell, loc)],
|
||||
type: CHILD_T,
|
||||
loc,
|
||||
};
|
||||
return { kind: "libCall", fn: "cp.execFile", args: [cmd, argv, callback], type: CHILD_T, loc };
|
||||
}
|
||||
|
||||
@@ -21,28 +21,6 @@ const ASSERTION_ERROR_PROPERTIES: ReadonlySet<string> = new Set([
|
||||
* declaration is not stdlib). Reads only: writes keep their fence (no
|
||||
* compiled program constructs an errno error). Null for non-error
|
||||
* receivers and non-stdlib members, so the chain keeps trying. */
|
||||
/** True for child_process.ExecException, exec's callback error. Its
|
||||
* `code` is the child's numeric exit status (null after a signal), which
|
||||
* the runtime error's string code slot does not hold, so reads, `in` tests
|
||||
* and typeof tests of `code` refuse instead of answering from the slot. */
|
||||
export function isExecExceptionType(lowerer: Lowerer, t: ts.Type): boolean {
|
||||
const sym = t.getSymbol();
|
||||
return (
|
||||
sym?.name === "ExecException" &&
|
||||
lowerer.checker
|
||||
.declarationsOf(sym)
|
||||
.some((d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()))
|
||||
);
|
||||
}
|
||||
|
||||
export function refuseExecExceptionCode(lowerer: Lowerer, node: ts.Node): never {
|
||||
return lowerer.noLowering(
|
||||
"ExecException.code",
|
||||
node,
|
||||
"spawn reports the exit status through its 'close' event",
|
||||
);
|
||||
}
|
||||
|
||||
export function lowerErrorCodeProperty(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.PropertyAccessExpression,
|
||||
@@ -104,10 +82,12 @@ export function lowerErrorCodeProperty(
|
||||
};
|
||||
}
|
||||
// %AssertionError's actual, expected, operator, generatedMessage and
|
||||
// diff are properties of the error's dyn view, read at the declared type.
|
||||
// diff, and %SuppressedError's error and suppressed, are properties of
|
||||
// the error's dyn view, read at the declared type.
|
||||
if (
|
||||
recvT.className === "%AssertionError" &&
|
||||
ASSERTION_ERROR_PROPERTIES.has(expr.name.text) &&
|
||||
((recvT.className === "%AssertionError" && ASSERTION_ERROR_PROPERTIES.has(expr.name.text)) ||
|
||||
(recvT.className === "%SuppressedError" &&
|
||||
(expr.name.text === "error" || expr.name.text === "suppressed"))) &&
|
||||
lowerer.isStdlibMember(expr)
|
||||
) {
|
||||
const receiver = lowerer.lowerExpr(expr.expression);
|
||||
@@ -142,9 +122,6 @@ export function lowerErrorCodeProperty(
|
||||
!(recvT.className === "%Error" && lowerer.typeOf(expr.expression).isIntersectionType())
|
||||
)
|
||||
return null;
|
||||
if (expr.name.text === "code" && isExecExceptionType(lowerer, lowerer.typeOf(expr.expression))) {
|
||||
refuseExecExceptionCode(lowerer, expr);
|
||||
}
|
||||
const rawReceiver = lowerer.lowerExpr(expr.expression);
|
||||
const receiver =
|
||||
rawReceiver.type.kind === "dyn"
|
||||
|
||||
@@ -0,0 +1,338 @@
|
||||
import { boolLit, numLit, varRef } from "../../../ir/build.js";
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import type { Lowerer } from "../lowerer.js";
|
||||
import {
|
||||
BOOL,
|
||||
CAUGHT,
|
||||
F64,
|
||||
VOID,
|
||||
type IrExpr,
|
||||
type IrParam,
|
||||
type IrStmt,
|
||||
type IrType,
|
||||
type SrcLoc,
|
||||
typeEquals,
|
||||
} from "../../../ir/ir.js";
|
||||
import { locOf } from "../../program.js";
|
||||
import { arrayValueRead, arrayValueStore, arrayValueType } from "../array-values.js";
|
||||
import { newFnCtx } from "../function-context.js";
|
||||
import { lowerArrayCallback } from "./callback-arguments.js";
|
||||
|
||||
/** `Array.fromAsync(source, mapFn?)` over an array, as an async helper
|
||||
* with the spec's loop:
|
||||
*
|
||||
* const out = []; let k = 0;
|
||||
* for await (const v of source) out.push(mapFn ? await mapFn(v, k++) : v);
|
||||
* return out;
|
||||
*
|
||||
* An array iterates through the async-from-sync iterator: each element is
|
||||
* awaited (a promise element through its promise, any other value as one
|
||||
* resolved promise), and the iterator result is awaited again, which the
|
||||
* helper spells as the same microtask hops the `for await` array loop
|
||||
* takes. A throwing or rejecting mapFn, and a rejected element, close the
|
||||
* iterator before the returned promise rejects. Null when this is not an
|
||||
* `Array.fromAsync` call. */
|
||||
export function lowerArrayFromAsyncCall(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
access: ts.PropertyAccessExpression,
|
||||
): IrExpr | null {
|
||||
if (call.questionDotToken || access.questionDotToken) return null;
|
||||
if (!lowerer.isStdlibGlobal(access.expression, "Array")) return null;
|
||||
if (access.name.text !== "fromAsync") return null;
|
||||
const loc = locOf(call);
|
||||
const args = call.arguments;
|
||||
if (args.length < 1 || args.length > 2 || args.some(ts.isSpreadElement)) {
|
||||
lowerer.noLowering(
|
||||
`Array.fromAsync with ${args.length} arguments`,
|
||||
call,
|
||||
"pass the source and an optional mapping function",
|
||||
);
|
||||
}
|
||||
const resultT = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
if (resultT?.kind !== "promise" || resultT.inner.kind !== "array")
|
||||
lowerer.badType(call, lowerer.typeOf(call));
|
||||
const outT = resultT.inner;
|
||||
const source = lowerer.lowerExpr(args[0]!);
|
||||
if (source.type.kind !== "array") {
|
||||
if (source.type.kind === "generator" && source.type.async) {
|
||||
// An async generator's completion settles one microtask later here
|
||||
// than in Node, which would reorder the returned promise.
|
||||
lowerer.noLowering(
|
||||
"Array.fromAsync over an async generator",
|
||||
args[0]!,
|
||||
"collect its values with for await",
|
||||
);
|
||||
}
|
||||
lowerer.noLowering(
|
||||
`Array.fromAsync over a value of type '${lowerer.checker.typeToString(lowerer.typeOf(args[0]!))}'`,
|
||||
args[0]!,
|
||||
"an array is the supported source",
|
||||
);
|
||||
}
|
||||
const step = arraySourceStep(lowerer, args[0]!, source.type.elem);
|
||||
let mapper: (IrExpr & { type: IrType & { kind: "func" } }) | null = null;
|
||||
if (args[1]) {
|
||||
const callback = lowerArrayCallback(lowerer, args[1], [step.valueT], outT);
|
||||
if (callback.arity > 2)
|
||||
lowerer.noLowering("an Array.fromAsync mapping function with three parameters", args[1]);
|
||||
mapper = callback.fnArg;
|
||||
const ret = mapper.type.ret;
|
||||
if (ret.kind !== "promise" && !nonThenable(lowerer, ret)) {
|
||||
lowerer.noLowering(
|
||||
"an Array.fromAsync mapping function whose result may be a promise or thenable",
|
||||
args[1],
|
||||
"return a value or a Promise",
|
||||
);
|
||||
}
|
||||
}
|
||||
const name = `%array.fromAsync.${lowerer.liftedFns.length}`;
|
||||
const context = newFnCtx(false, null, null, outT);
|
||||
context.isAsync = true;
|
||||
lowerer.fnStack.push(context);
|
||||
try {
|
||||
const src = lowerer.declareHiddenLocal("source", source.type);
|
||||
const params: IrParam[] = [{ localId: src.id, name: "source", type: source.type }];
|
||||
let mapperRef: (IrExpr & { type: IrType & { kind: "func" } }) | null = null;
|
||||
if (mapper) {
|
||||
const local = lowerer.declareHiddenLocal("mapFn", mapper.type);
|
||||
params.push({ localId: local.id, name: "mapFn", type: mapper.type });
|
||||
mapperRef = { kind: "varRef", localId: local.id, type: mapper.type, loc };
|
||||
}
|
||||
const out = lowerer.declareHiddenLocal("%fromAsyncOut", outT);
|
||||
const k = lowerer.declareHiddenLocal("%fromAsyncIndex", F64);
|
||||
k.mutable = true;
|
||||
const kRef = varRef(k.id, F64, loc);
|
||||
const append = (value: IrExpr): IrStmt[] => {
|
||||
const stmts: IrStmt[] = [];
|
||||
let item = value;
|
||||
if (mapperRef) {
|
||||
const mapped: IrExpr = {
|
||||
kind: "callValue",
|
||||
callee: mapperRef,
|
||||
args: [value, kRef].slice(0, mapperRef.type.params.length),
|
||||
type: mapperRef.type.ret,
|
||||
loc,
|
||||
};
|
||||
if (mapped.type.kind === "promise") {
|
||||
item = { kind: "awaitExpr", value: mapped, type: mapped.type.inner, loc };
|
||||
} else {
|
||||
// Await of a value that is not a thenable: one resolved promise.
|
||||
const held = lowerer.declareHiddenLocal("%fromAsyncMapped", mapped.type);
|
||||
stmts.push({ kind: "varDecl", localId: held.id, init: mapped, loc }, hop(loc));
|
||||
item = varRef(held.id, held.type, loc);
|
||||
}
|
||||
}
|
||||
stmts.push(
|
||||
arrayValueStore(
|
||||
lowerer,
|
||||
varRef(out.id, outT, loc),
|
||||
kRef,
|
||||
// arrayValueStore stores an undefined arm (a hole) as undefined.
|
||||
typeEquals(item.type, outT.elem) || item.type.kind === "union"
|
||||
? item
|
||||
: lowerer.coerceToExpected(item, outT.elem),
|
||||
outT.elem,
|
||||
loc,
|
||||
),
|
||||
{
|
||||
kind: "assign",
|
||||
localId: k.id,
|
||||
value: { kind: "bin", op: "+", left: kRef, right: numLit(1, loc), type: F64, loc },
|
||||
loc,
|
||||
},
|
||||
);
|
||||
return stmts;
|
||||
};
|
||||
const body: IrStmt[] = [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: out.id,
|
||||
init: { kind: "arrayLit", elems: [], type: outT, loc },
|
||||
loc,
|
||||
},
|
||||
{ kind: "varDecl", localId: k.id, init: numLit(0, loc), loc },
|
||||
...step.loop(varRef(src.id, source.type, loc), append, loc),
|
||||
{ kind: "return", value: varRef(out.id, outT, loc), loc },
|
||||
];
|
||||
lowerer.liftedFns.push({
|
||||
name,
|
||||
params,
|
||||
returnType: outT,
|
||||
locals: context.locals,
|
||||
body,
|
||||
async: true,
|
||||
loc,
|
||||
});
|
||||
} finally {
|
||||
lowerer.fnStack.pop();
|
||||
}
|
||||
return {
|
||||
kind: "call",
|
||||
callee: name,
|
||||
args: mapper ? [source, mapper] : [source],
|
||||
type: resultT,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
function hop(loc: SrcLoc): IrStmt {
|
||||
return {
|
||||
kind: "exprStmt",
|
||||
expr: { kind: "libCall", fn: "async.hop", args: [], type: VOID, loc },
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** A value whose await takes one microtask without calling user code:
|
||||
* primitives, arrays and records without a `then` field. */
|
||||
function nonThenable(lowerer: Lowerer, type: IrType): boolean {
|
||||
if (type.kind === "union") {
|
||||
const arms = lowerer.unions.get(type.unionId)?.arms;
|
||||
return arms !== undefined && arms.every((arm) => nonThenable(lowerer, arm));
|
||||
}
|
||||
if (type.kind === "record") {
|
||||
const shape = lowerer.shapes.get(type.shapeId);
|
||||
return shape !== undefined && !shape.indexValue && !shape.fields.some((f) => f.name === "then");
|
||||
}
|
||||
return (
|
||||
type.kind === "f64" ||
|
||||
type.kind === "bigint" ||
|
||||
type.kind === "string" ||
|
||||
type.kind === "bool" ||
|
||||
type.kind === "symbol" ||
|
||||
type.kind === "array" ||
|
||||
type.kind === "undefinedT" ||
|
||||
type.kind === "nullT"
|
||||
);
|
||||
}
|
||||
|
||||
/** The `for await` array loop (lowerForOf's async-from-sync form): two
|
||||
* hops per element that is not a promise, the element's own await plus
|
||||
* one hop for a promise element, two hops before the final done result,
|
||||
* and one hop to close the iterator when the body completes abruptly. */
|
||||
function arraySourceStep(
|
||||
lowerer: Lowerer,
|
||||
node: ts.Expression,
|
||||
elem: IrType,
|
||||
): {
|
||||
/** The awaited value each element hands to the mapping function. */
|
||||
valueT: IrType;
|
||||
loop(source: IrExpr, append: (value: IrExpr) => IrStmt[], loc: SrcLoc): IrStmt[];
|
||||
} {
|
||||
const yieldedT = arrayValueType(lowerer, elem);
|
||||
let promiseTag: number | null = null;
|
||||
let valueT = yieldedT;
|
||||
const promiseElem = elem.kind === "promise" ? elem : null;
|
||||
if (
|
||||
promiseElem &&
|
||||
yieldedT.kind === "union" &&
|
||||
!["void", "union", "dyn", "undefinedT", "nullT"].includes(promiseElem.inner.kind)
|
||||
) {
|
||||
promiseTag = lowerer.armTag(yieldedT.unionId, promiseElem);
|
||||
valueT = lowerer.withUndefinedArm(promiseElem.inner);
|
||||
} else if (!nonThenable(lowerer, elem)) {
|
||||
lowerer.noLowering(
|
||||
"Array.fromAsync over array values that may be promises or thenables",
|
||||
node,
|
||||
"use an array of values or an array of promises",
|
||||
);
|
||||
}
|
||||
return {
|
||||
valueT,
|
||||
loop(source, append, loc) {
|
||||
const arr = lowerer.declareHiddenLocal("%fromAsyncArray", source.type);
|
||||
const cursor = lowerer.declareHiddenLocal("%fromAsyncCursor", F64);
|
||||
cursor.mutable = true;
|
||||
const needsClose = lowerer.declareHiddenLocal("%fromAsyncNeedsClose", BOOL);
|
||||
needsClose.mutable = true;
|
||||
const value = lowerer.declareHiddenLocal("%fromAsyncValue", valueT);
|
||||
value.mutable = true;
|
||||
const arrRef = varRef(arr.id, source.type, loc);
|
||||
const cursorRef = varRef(cursor.id, F64, loc);
|
||||
const read = arrayValueRead(lowerer, arrRef, cursorRef, elem, loc);
|
||||
const setClose = (on: boolean): IrStmt => ({
|
||||
kind: "assign",
|
||||
localId: needsClose.id,
|
||||
value: boolLit(on, loc),
|
||||
loc,
|
||||
});
|
||||
// A rejected element rejects the next result one reaction later, and
|
||||
// Node closes the iterator before the returned promise rejects.
|
||||
const awaitInto = (awaited: IrExpr): IrStmt => {
|
||||
const error = lowerer.declareHiddenLocal("%fromAsyncNextError", CAUGHT);
|
||||
return {
|
||||
kind: "tryCatch",
|
||||
tryBody: [{ kind: "assign", localId: value.id, value: awaited, loc }],
|
||||
catchBody: [hop(loc), hop(loc), { kind: "rethrow", localId: error.id, loc }],
|
||||
catchLocalId: error.id,
|
||||
finallyBody: null,
|
||||
loc,
|
||||
};
|
||||
};
|
||||
const next: IrStmt[] = [];
|
||||
if (promiseTag !== null) {
|
||||
next.push(
|
||||
awaitInto({ kind: "awaitUnionExpr", value: read, promiseTag, type: valueT, loc }),
|
||||
hop(loc),
|
||||
);
|
||||
} else {
|
||||
next.push({ kind: "assign", localId: value.id, value: read, loc }, hop(loc), hop(loc));
|
||||
}
|
||||
const length: IrExpr = {
|
||||
kind: "arrIntrinsic",
|
||||
method: "length",
|
||||
receiver: arrRef,
|
||||
args: [],
|
||||
type: F64,
|
||||
loc,
|
||||
};
|
||||
const loop: IrStmt = {
|
||||
kind: "for",
|
||||
init: { kind: "varDecl", localId: cursor.id, init: numLit(0, loc), loc },
|
||||
cond: boolLit(true, loc),
|
||||
update: {
|
||||
kind: "assign",
|
||||
localId: cursor.id,
|
||||
value: { kind: "bin", op: "+", left: cursorRef, right: numLit(1, loc), type: F64, loc },
|
||||
loc,
|
||||
},
|
||||
body: [
|
||||
setClose(false),
|
||||
{
|
||||
kind: "if",
|
||||
cond: { kind: "bin", op: ">=", left: cursorRef, right: length, type: BOOL, loc },
|
||||
then: [hop(loc), hop(loc), { kind: "break", loc }],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
...next,
|
||||
setClose(true),
|
||||
...append(varRef(value.id, valueT, loc)),
|
||||
],
|
||||
loc,
|
||||
};
|
||||
return [
|
||||
{ kind: "varDecl", localId: arr.id, init: source, loc },
|
||||
{ kind: "varDecl", localId: needsClose.id, init: boolLit(false, loc), loc },
|
||||
{ kind: "varDecl", localId: value.id, init: null, loc },
|
||||
{
|
||||
kind: "tryCatch",
|
||||
tryBody: [loop],
|
||||
catchBody: null,
|
||||
catchLocalId: null,
|
||||
finallyBody: [
|
||||
{
|
||||
kind: "if",
|
||||
cond: varRef(needsClose.id, BOOL, loc),
|
||||
then: [hop(loc)],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -68,6 +68,7 @@ import {
|
||||
lowerArrayFromCall,
|
||||
lowerArrayOfCall,
|
||||
} from "./containers/array-construction.js";
|
||||
import { lowerArrayFromAsyncCall } from "./containers/array-from-async.js";
|
||||
import { lowerDynArrayFilterCall, lowerDynArrayFlatMapCall } from "./containers/array-callbacks.js";
|
||||
import { lowerGroupByStaticCall } from "./containers/collection-grouping.js";
|
||||
import { lowerIteratorHelperCall } from "./containers/iterator-helpers.js";
|
||||
@@ -1392,6 +1393,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
|
||||
lowerObjectStaticCall(lowerer, expr, expr.expression) ??
|
||||
lowerObjectFromEntriesCall(lowerer, expr, expr.expression) ??
|
||||
lowerArrayFromCall(lowerer, expr, expr.expression) ??
|
||||
lowerArrayFromAsyncCall(lowerer, expr, expr.expression) ??
|
||||
lowerArrayOfCall(lowerer, expr, expr.expression) ??
|
||||
lowerer.lowerArrayMethodCall(expr, expr.expression) ??
|
||||
// Tuple reads preserve the receiver through argument evaluation;
|
||||
|
||||
@@ -2027,6 +2027,13 @@ export function collectClassShapeInner(
|
||||
"extending AssertionError (its constructor takes an options object the subclass constructor would have to forward — extend Error, or construct AssertionError directly)",
|
||||
);
|
||||
}
|
||||
if (lowerer.builtinErrorInfoOf(symbol)?.def.name === "%SuppressedError") {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
t,
|
||||
"extending SuppressedError (extend Error and keep error and suppressed in fields)",
|
||||
);
|
||||
}
|
||||
const named =
|
||||
(symbol && lowerer.classBySymbol.get(symbol)) ??
|
||||
lowerer.builtinErrorInfoOf(symbol) ??
|
||||
@@ -8499,6 +8506,21 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (errInfo && errInfo.def.name === "%SuppressedError") {
|
||||
const args = expr.arguments ?? [];
|
||||
if (args.length > 3 || args.some(ts.isSpreadElement)) {
|
||||
lowerer.noLowering(`new SuppressedError with this argument list`, expr);
|
||||
}
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "error.newSuppressed",
|
||||
args: [0, 1, 2].map((index) =>
|
||||
args[index] ? lowerer.lowerExprExpecting(args[index], DYN) : dynUndefinedExpr(loc),
|
||||
),
|
||||
type: { kind: "object", className: "%SuppressedError" },
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (errInfo && errInfo.def.name === "%AggregateError") {
|
||||
const args = expr.arguments ?? [];
|
||||
if (args.length > 3 || args.some(ts.isSpreadElement)) {
|
||||
|
||||
@@ -8,7 +8,6 @@ import { lowerWorkerMetadata } from "./builtins/workers.js";
|
||||
import { lowerReadlineProperty } from "./builtins/readline.js";
|
||||
import { lowerBlobInstanceof } from "./builtins/blob.js";
|
||||
import { lowerKeyObjectInstanceof } from "./builtins/key-object.js";
|
||||
import { isExecExceptionType, refuseExecExceptionCode } from "./builtins/errors.js";
|
||||
import { STATS_TYPE_TESTS } from "./builtins/filesystem.js";
|
||||
import { threadsImportOf } from "../threads-import.js";
|
||||
import { experimentalImportOf, lifetimeRegionMemberOf } from "../experimental-import.js";
|
||||
@@ -11119,7 +11118,6 @@ function lowerErrorCodeTypeofTest(
|
||||
recv = recv.expression;
|
||||
const errT = errorRootedObjectOf(lowerer, lowerer.typeOf(recv));
|
||||
if (!errT) continue;
|
||||
if (isExecExceptionType(lowerer, lowerer.typeOf(recv))) refuseExecExceptionCode(lowerer, prop);
|
||||
const caught = lowerer.caughtLocalOf(recv);
|
||||
let receiver: IrExpr;
|
||||
if (caught) {
|
||||
@@ -12716,9 +12714,6 @@ function lowerInExpression(lowerer: Lowerer, expr: ts.BinaryExpression, loc: Src
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (key === "code" && isExecExceptionType(lowerer, lowerer.typeOf(expr.right))) {
|
||||
refuseExecExceptionCode(lowerer, expr);
|
||||
}
|
||||
let recv = lowerer.lowerExpr(expr.right);
|
||||
const classInfo =
|
||||
recv.type.kind === "classval" ? lowerer.classes.get(recv.type.className) : undefined;
|
||||
|
||||
@@ -189,7 +189,7 @@ import { fenceNodeModuleMutation } from "./lower-node-module.js";
|
||||
import { fencePerfEntryKeyWalk } from "./builtins/performance.js";
|
||||
|
||||
interface UsingRegistration {
|
||||
acquire: IrStmt;
|
||||
acquire: IrStmt[];
|
||||
cleanup: IrStmt[];
|
||||
loc: SrcLoc;
|
||||
}
|
||||
@@ -403,14 +403,51 @@ function cleanupCall(
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The statements that dispose `value` at scope exit. `setup` receives
|
||||
* the statements that run at the declaration, after the value is bound. */
|
||||
function cleanupForValue(
|
||||
lowerer: Lowerer,
|
||||
node: ts.Identifier,
|
||||
value: IrExpr,
|
||||
awaited: boolean,
|
||||
setup: IrStmt[],
|
||||
): IrStmt[] {
|
||||
const loc = locOf(node);
|
||||
if (isUnitType(value.type)) return [];
|
||||
if (value.type.kind === "dyn") {
|
||||
// A dyn value (an object literal, a Disposable-typed result): the
|
||||
// declaration reads and checks its dispose method, and scope exit
|
||||
// calls it with the value as `this`.
|
||||
const method = lowerer.declareHiddenLocal("%dispose", DYN);
|
||||
setup.push({
|
||||
kind: "varDecl",
|
||||
localId: method.id,
|
||||
init: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.usingMethod",
|
||||
args: [value, { kind: "boolLit", value: awaited, type: BOOL, loc }],
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
});
|
||||
const call: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: "dyn.usingDispose",
|
||||
args: [{ kind: "varRef", localId: method.id, type: DYN, loc }, value],
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
return [
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: awaited
|
||||
? { kind: "libCall", fn: "async.awaitDyn", args: [call], type: DYN, loc }
|
||||
: call,
|
||||
loc,
|
||||
},
|
||||
];
|
||||
}
|
||||
if (value.type.kind === "union") {
|
||||
const def = lowerer.unions.get(value.type.unionId);
|
||||
if (!def)
|
||||
@@ -427,7 +464,9 @@ function cleanupForValue(
|
||||
type: arm,
|
||||
loc,
|
||||
};
|
||||
const body = cleanupForValue(lowerer, node, narrowed, awaited);
|
||||
if (arm.kind === "dyn")
|
||||
lowerer.unsupported("SC1090", node, "disposing a union with an 'unknown' arm");
|
||||
const body = cleanupForValue(lowerer, node, narrowed, awaited, setup);
|
||||
out.push({
|
||||
kind: "if",
|
||||
cond: {
|
||||
@@ -513,8 +552,9 @@ function lowerUsingStatement(lowerer: Lowerer, stmt: ts.VariableStatement): Usin
|
||||
if (!binding)
|
||||
throw new InternalCompilerError("lowerer bug: using declaration has no runtime binding");
|
||||
const value = varRef(binding.id, binding.type, locOf(decl.name));
|
||||
const cleanup = cleanupForValue(lowerer, decl.name, value, awaited);
|
||||
registrations.push({ acquire, cleanup, loc: locOf(decl) });
|
||||
const setup: IrStmt[] = [];
|
||||
const cleanup = cleanupForValue(lowerer, decl.name, value, awaited, setup);
|
||||
registrations.push({ acquire: [acquire, ...setup], cleanup, loc: locOf(decl) });
|
||||
}
|
||||
return registrations;
|
||||
}
|
||||
@@ -634,8 +674,8 @@ export function lowerStmts(lowerer: Lowerer, stmts: readonly ts.Statement[]): Ir
|
||||
const registrations = lowerUsingStatement(lowerer, stmt);
|
||||
const accounting: IrStmt[] = [];
|
||||
for (const registration of registrations) {
|
||||
out.push(registration.acquire);
|
||||
accounting.push(registration.acquire, ...registration.cleanup);
|
||||
out.push(...registration.acquire);
|
||||
accounting.push(...registration.acquire, ...registration.cleanup);
|
||||
if (registration.cleanup.length > 0) {
|
||||
usingMarkers.push({
|
||||
start: out.length,
|
||||
@@ -8612,8 +8652,10 @@ export function lowerForOf(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt {
|
||||
const body = lowerer.inCtl("loop", () => lowerer.lowerScopedBlock(stmt.statement), labels);
|
||||
if (!resourceLoop) return forValues(local.id, body);
|
||||
const resource = varRef(local.id, local.type, locOf(decl.name));
|
||||
const cleanup = cleanupForValue(lowerer, decl.name, resource, isAwaitUsing(list));
|
||||
const setup: IrStmt[] = [];
|
||||
const cleanup = cleanupForValue(lowerer, decl.name, resource, isAwaitUsing(list), setup);
|
||||
return forValues(local.id, [
|
||||
...setup,
|
||||
{
|
||||
kind: "tryCatch",
|
||||
tryBody: body,
|
||||
|
||||
@@ -11485,8 +11485,8 @@ export class Lowerer {
|
||||
return lowerSpawnCall(this, expr, loc);
|
||||
}
|
||||
|
||||
lowerExecFileCall(expr: ts.CallExpression, loc: SrcLoc, shell = false): IrExpr {
|
||||
return lowerExecFileCall(this, expr, loc, shell);
|
||||
lowerExecFileCall(expr: ts.CallExpression, loc: SrcLoc): IrExpr {
|
||||
return lowerExecFileCall(this, expr, loc);
|
||||
}
|
||||
|
||||
lowerExecSyncCall(expr: ts.CallExpression, shell: boolean, loc: SrcLoc): IrExpr {
|
||||
|
||||
@@ -2809,7 +2809,7 @@ export const BUILTIN_MODULE_FENCE_HINTS: Record<
|
||||
},
|
||||
util: {
|
||||
promisify:
|
||||
"the lowered shapes are const bindings over child_process.execFile, child_process.exec, setTimeout, setImmediate and the fs callback functions with an fs/promises counterpart; call the bound function directly",
|
||||
"the lowered shapes are const bindings over child_process.execFile, child_process.exec, setTimeout, setImmediate, stream.finished, stream.pipeline and the fs callback functions with an fs/promises counterpart; call the bound function directly",
|
||||
},
|
||||
"stream/consumers": {
|
||||
blob:
|
||||
|
||||
@@ -138,10 +138,11 @@ const FORCED_OPTIONS: ts.Ts7CompilerOptions = {
|
||||
// classification is intentionally independent below; this option is only
|
||||
// the binder's scope decision.
|
||||
moduleDetection: ts.ModuleDetectionKind.Force,
|
||||
// Explicit resource management is an ESNext library slice in TS 7 even
|
||||
// though Node 24 ships the runtime protocol. Include that focused lib
|
||||
// without widening the rest of the standard-library target past ES2025.
|
||||
lib: ["lib.es2025.d.ts", "lib.esnext.disposable.d.ts"],
|
||||
// Explicit resource management and Array.fromAsync are ESNext library
|
||||
// slices in TS 7 even though Node 24 ships them. Include those focused
|
||||
// libs without widening the rest of the standard-library target past
|
||||
// ES2025.
|
||||
lib: ["lib.es2025.d.ts", "lib.esnext.disposable.d.ts", "lib.esnext.array.d.ts"],
|
||||
types: [],
|
||||
allowImportingTsExtensions: true,
|
||||
allowJs: true,
|
||||
|
||||
@@ -1377,23 +1377,6 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
) {
|
||||
return { kind: "object", className: "%Error" };
|
||||
}
|
||||
// child_process.ExecException, exec's callback error, is an interface
|
||||
// extending Error with optional metadata members; the value is one
|
||||
// ordinary Error as well.
|
||||
const execException = type.getSymbol();
|
||||
if (
|
||||
execException?.name === "ExecException" &&
|
||||
checker
|
||||
.declarationsOf(execException)
|
||||
.some(
|
||||
(decl) =>
|
||||
ts.isInterfaceDeclaration(decl) &&
|
||||
ctx.isStdlibFile(decl.getSourceFile()) &&
|
||||
isDeclaredInAmbientModule(decl, "child_process"),
|
||||
)
|
||||
) {
|
||||
return { kind: "object", className: "%Error" };
|
||||
}
|
||||
if (isOsConstantsType(type, ctx) || isLegacyUrlType(type, ctx)) return DYN;
|
||||
// Immutable<T> (@scriptc/types) is a deep-readonly view: the value is
|
||||
// still the T it maps.
|
||||
@@ -2325,10 +2308,11 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
// The stdlib Console interface (the global console and node:console
|
||||
// Console instances) is a native handle carried by the checked-dynamic
|
||||
// representation and consumed by the console method lowerings. The
|
||||
// stdlib Disposable (events.addAbortListener's result) is a checked
|
||||
// object carrying its Symbol.dispose method.
|
||||
// stdlib Disposable (events.addAbortListener's result) and
|
||||
// AsyncDisposable are checked objects carrying their Symbol.dispose or
|
||||
// Symbol.asyncDispose method.
|
||||
if (
|
||||
(psym?.name === "Console" || psym?.name === "Disposable") &&
|
||||
(psym?.name === "Console" || psym?.name === "Disposable" || psym?.name === "AsyncDisposable") &&
|
||||
checker
|
||||
.declarationsOf(psym)
|
||||
.some((d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()))
|
||||
@@ -4900,6 +4884,7 @@ function mapErrorMetadataIntersection(type: ts.Type, ctx: TypeMapperCtx): IrType
|
||||
"TypeError",
|
||||
"URIError",
|
||||
"AggregateError",
|
||||
"SuppressedError",
|
||||
].includes(symbol.name) &&
|
||||
ctx.checker.declarationsOf(symbol).some((decl) => ctx.isStdlibFile(decl.getSourceFile()))
|
||||
);
|
||||
|
||||
@@ -31,6 +31,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"dyn.construct",
|
||||
"dyn.reflectApply",
|
||||
"dyn.reflectGet",
|
||||
"dyn.usingMethod",
|
||||
"dyn.usingDispose",
|
||||
"dyn.reflectSet",
|
||||
"dyn.reflectDefine",
|
||||
"dyn.abstractEq",
|
||||
@@ -347,6 +349,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"error.newOptions",
|
||||
"error.ctorOptions",
|
||||
"error.newAggregate",
|
||||
"error.newSuppressed",
|
||||
"error.toString",
|
||||
"error.stack",
|
||||
"error.cause",
|
||||
|
||||
@@ -2226,9 +2226,8 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
/** child_process.execFile's callback slice: starts an all-piped child,
|
||||
* captures stdout/stderr, and moves the error-first callback into the
|
||||
* child registry. The callback shape is program-dependent and checked
|
||||
* by the validator/backends. The last argument is exec's shell flag:
|
||||
* the argv is ["-c", command] and failure messages show the command. */
|
||||
"cp.execFile": { argTypes: [STRING, arrayOf(STRING), null, BOOL], result: CHILD_T },
|
||||
* by the validator/backends. */
|
||||
"cp.execFile": { argTypes: [STRING, arrayOf(STRING), null], result: CHILD_T },
|
||||
// spawn's options form: per-slot stdio modes (0 ignore / 1 inherit /
|
||||
// 2 fd) with the out/err fds for mode 2, detached, env replacement
|
||||
// pairs, cwd ("" = inherit).
|
||||
@@ -3030,6 +3029,13 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
},
|
||||
"error.aggregateSetErrors": { argTypes: [null, DYN], result: VOID },
|
||||
"error.aggregateErrors": { argTypes: [null], result: DYN },
|
||||
/** `new SuppressedError(error, suppressed, message?)` (scr_json.c): all
|
||||
* three dyn and borrowed; the message conversion can throw. Returns an
|
||||
* owned %SuppressedError. */
|
||||
"error.newSuppressed": {
|
||||
argTypes: [DYN, DYN, DYN],
|
||||
result: { kind: "object", className: "%SuppressedError" },
|
||||
},
|
||||
/** `d instanceof TypeError` (and the other BUILTIN error classes) on a
|
||||
* checked-dynamic value (scr_json.c): the from_error cache holds the
|
||||
* dyn↔error identity edge, so the test resolves the runtime error and
|
||||
@@ -3119,6 +3125,12 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
"dyn.packPushSpreadIter": { argTypes: [DYN, DYN], result: VOID },
|
||||
"dyn.reflectApply": { argTypes: [DYN, DYN, DYN], result: DYN },
|
||||
"dyn.reflectGet": { argTypes: [DYN, DYN, DYN], result: DYN },
|
||||
/** `using` / `await using` with a dyn value (scr_json.c): usingMethod
|
||||
* runs the declaration's checks and answers what usingDispose calls at
|
||||
* scope exit (undefined for a null or undefined value). The bool selects
|
||||
* `await using`. Both throw Node's TypeErrors or the method's error. */
|
||||
"dyn.usingMethod": { argTypes: [DYN, BOOL], result: DYN },
|
||||
"dyn.usingDispose": { argTypes: [DYN, DYN], result: DYN },
|
||||
"dyn.reflectSet": { argTypes: [DYN, DYN, DYN, DYN], result: BOOL },
|
||||
"dyn.reflectDefine": { argTypes: [DYN, DYN, DYN], result: BOOL },
|
||||
"dyn.abstractEq": { argTypes: [DYN, DYN], result: BOOL },
|
||||
|
||||
@@ -1388,6 +1388,10 @@ export const RUNTIME_ERROR_CLASSES: ReadonlyMap<
|
||||
// constructor. The Error layout; actual, expected, operator,
|
||||
// generatedMessage and diff are properties of the error's dyn view.
|
||||
["%AssertionError", { lib: "AssertionError", kind: 9, base: "%Error" }],
|
||||
// SuppressedError — the error a `using` disposal raises over a pending
|
||||
// error, and its own constructor. The Error layout; error and suppressed
|
||||
// are non-enumerable properties of the error's dyn view.
|
||||
["%SuppressedError", { lib: "SuppressedError", kind: 10, base: "%Error" }],
|
||||
]);
|
||||
|
||||
/** The runtime-provided node:events EventEmitter class (ScrEmitter /
|
||||
|
||||
@@ -348,7 +348,13 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run
|
||||
features.tls = true;
|
||||
}
|
||||
if (fn.startsWith("http2.") && !HTTP2_LEGACY_FNS.has(fn)) features.http2 = true;
|
||||
if (fn.startsWith("sym.") || fn === "util.isDeepStrictEqual") features.symbol = true;
|
||||
if (
|
||||
fn.startsWith("sym.") ||
|
||||
fn === "util.isDeepStrictEqual" ||
|
||||
fn === "dyn.usingMethod" ||
|
||||
fn === "dyn.usingDispose"
|
||||
)
|
||||
features.symbol = true;
|
||||
if (fn === "process.hrtimeValue") features.bigint = true;
|
||||
if (fn.startsWith("bigint.")) features.bigint = true;
|
||||
if (fn.startsWith("sp.") || fn === "url.searchParams") features.searchParams = true;
|
||||
|
||||
@@ -951,7 +951,6 @@ test("library callbacks retain child, specialized, and generic result diagnostic
|
||||
{ kind: "strLit", value: "tool", type: STRING, loc },
|
||||
{ kind: "arrayLit", elems: [], type: arrayOf(STRING), loc },
|
||||
{ kind: "boolLit", value: true, type: F64, loc },
|
||||
{ kind: "boolLit", value: false, type: BOOL, loc },
|
||||
],
|
||||
};
|
||||
expect(validateModule(expressionModule(expr, [])).map((error) => error.message)).toEqual([
|
||||
|
||||
@@ -2426,7 +2426,7 @@
|
||||
"name": "util.promisify",
|
||||
"status": "unsupported",
|
||||
"code": "SC2020",
|
||||
"note": "the lowered shapes are const bindings over child_process.execFile, child_process.exec, setTimeout, setImmediate and the fs callback functions with an fs/promises counterpart; call the bound function directly"
|
||||
"note": "the lowered shapes are const bindings over child_process.execFile, child_process.exec, setTimeout, setImmediate, stream.finished, stream.pipeline and the fs callback functions with an fs/promises counterpart; call the bound function directly"
|
||||
},
|
||||
{
|
||||
"id": "node-builtin.util.stripVTControlCharacters",
|
||||
@@ -5446,7 +5446,7 @@
|
||||
"kind": "syntax",
|
||||
"name": "block- and function-scoped await using declarations",
|
||||
"status": "static",
|
||||
"note": "LIFO asynchronous disposal through [Symbol.asyncDispose], with synchronous fallback"
|
||||
"note": "LIFO asynchronous disposal through [Symbol.asyncDispose], with synchronous fallback, of class instances, object literals and AsyncDisposable-typed values"
|
||||
},
|
||||
{
|
||||
"id": "syntax.bigint-literals",
|
||||
@@ -5621,7 +5621,7 @@
|
||||
"kind": "syntax",
|
||||
"name": "block- and function-scoped using declarations",
|
||||
"status": "static",
|
||||
"note": "LIFO disposal on normal and abrupt scope exit through [Symbol.dispose]"
|
||||
"note": "LIFO disposal on normal and abrupt scope exit through [Symbol.dispose], of class instances, object literals and Disposable-typed values; a failing disposal raises a SuppressedError with error and suppressed"
|
||||
},
|
||||
{
|
||||
"id": "syntax.with-statements",
|
||||
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5627-exec-callback.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5629-promisify-stream.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5850-using-disposable-values.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5851-using-suppressed-error-values.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5852-using-disposable-values-js.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5854-array-from-async.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5855-array-from-async-js.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/fixtures/node-types/child-exec-code-fences.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
Vendored
-6
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/fixtures/node-types/child-exec.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -2265,14 +2265,14 @@ static void scr_exec_file_fire(ScrChild *c) {
|
||||
}
|
||||
|
||||
ScrChild *scr_exec_file(ScrStr *cmd, ScrArr *args, ScrClosure *cb,
|
||||
ScrExecFileFn fn, bool shell) {
|
||||
ScrExecFileFn fn) {
|
||||
ScrStr *empty = scr_str_new("", 0);
|
||||
ScrChild *c = scr_spawn_opts(cmd, args, 3, 3, 3, 0, 0, false, false, false,
|
||||
NULL, empty);
|
||||
scr_str_release(empty);
|
||||
c->exec_cb = cb;
|
||||
c->exec_fn = fn;
|
||||
char *display = scr_exec_display(cmd, args, shell);
|
||||
char *display = scr_exec_display(cmd, args, false);
|
||||
c->exec_display = scr_str_new(display, strlen(display));
|
||||
free(display);
|
||||
scr_child_stream_capture_begin(c->out_stream);
|
||||
@@ -4905,14 +4905,14 @@ static void scr_exec_file_fire(ScrChild *c) {
|
||||
}
|
||||
|
||||
ScrChild *scr_exec_file(ScrStr *cmd, ScrArr *args, ScrClosure *cb,
|
||||
ScrExecFileFn fn, bool shell) {
|
||||
ScrExecFileFn fn) {
|
||||
ScrStr *empty = scr_str_new("", 0);
|
||||
ScrChild *c = scr_spawn_opts(cmd, args, 3, 3, 3, 0, 0, false, false, false,
|
||||
NULL, empty);
|
||||
scr_str_release(empty);
|
||||
c->exec_cb = cb;
|
||||
c->exec_fn = fn;
|
||||
char *display = scr_exec_display(cmd, args, shell);
|
||||
char *display = scr_exec_display(cmd, args, false);
|
||||
c->exec_display = scr_str_new(display, strlen(display));
|
||||
free(display);
|
||||
scr_child_stream_capture_begin(c->out_stream);
|
||||
|
||||
@@ -155,11 +155,9 @@ ScrChild *scr_spawn_dynamic(ScrStr *cmd, ScrArr *args, const ScrDyn *options);
|
||||
void scr_child_on_spawn(ScrChild *child, ScrClosure *cb); /* moves zero-argument callback */
|
||||
/* The callback slice: file + args, default options. The returned child has
|
||||
* all three stdio slots piped; stdout/stderr are captured internally and
|
||||
* the callback moves into the child registry until settlement. With shell
|
||||
* (exec), args is ["-c", command] for /bin/sh and failure messages show
|
||||
* the command, as for scr_exec_sync. */
|
||||
* the callback moves into the child registry until settlement. */
|
||||
ScrChild *scr_exec_file(ScrStr *cmd, ScrArr *args, ScrClosure *cb /*moves*/,
|
||||
ScrExecFileFn fn, bool shell);
|
||||
ScrExecFileFn fn);
|
||||
ScrChild *scr_child_retain(ScrChild *c);
|
||||
void scr_child_release(ScrChild *c);
|
||||
void *scr_child_retain_v(void *p);
|
||||
|
||||
@@ -603,6 +603,18 @@ ScrDyn *scr_dyn_new_symbol(ScrSym *value); /* borrows and retains value */
|
||||
ScrSym *scr_sym_well_known(ScrStr *name);
|
||||
ScrDyn *scr_dyn_symbol_get(const ScrDyn *value, const ScrStr *key);
|
||||
ScrDyn *scr_dyn_symbol_key_get(const ScrDyn *value, const ScrDyn *key, bool optional);
|
||||
/* `using` and `await using` with a dyn value. scr_dyn_using_method runs the
|
||||
* declaration's checks: null and undefined answer undefined (nothing to
|
||||
* dispose); any other value that is not an object or function throws
|
||||
* Node's TypeError, as does a missing or non-callable [Symbol.dispose].
|
||||
* With `async`, [Symbol.asyncDispose] is read first and [Symbol.dispose]
|
||||
* is the fallback, and the answer records which one was found.
|
||||
* scr_dyn_using_dispose calls that answer with `value` as `this` at scope
|
||||
* exit and returns the call's result (+1; undefined for a sync fallback,
|
||||
* whose result `await using` does not await). Both borrow their operands
|
||||
* and return NULL with the exception pending. */
|
||||
ScrDyn *scr_dyn_using_method(const ScrDyn *value, bool async);
|
||||
ScrDyn *scr_dyn_using_dispose(const ScrDyn *method, const ScrDyn *value);
|
||||
void scr_dyn_symbol_key_set(ScrDyn *value, ScrDyn *key, ScrDyn *stored);
|
||||
void scr_dyn_symbol_key_placeholder(ScrDyn *value, ScrDyn *key);
|
||||
bool scr_dyn_has_key_computed(const ScrDyn *value, const ScrDyn *key);
|
||||
|
||||
@@ -33,7 +33,8 @@ static const char *const scr_error_names[SCR_ERR_COUNT] = {
|
||||
/* DOMException's DEFAULT name is "Error" too (WebIDL: the name
|
||||
* parameter defaults to "Error"); the class identity lives in the
|
||||
* vtable, never the name. */
|
||||
"Error", "ReferenceError", "EvalError", "URIError", "AggregateError", "AssertionError"};
|
||||
"Error", "ReferenceError", "EvalError", "URIError", "AggregateError", "AssertionError",
|
||||
"SuppressedError"};
|
||||
|
||||
void scr_error_trace(void *e, ScrTraceVisit visit, void *ctx) {
|
||||
/* name/message are strings — never collector-headered; nothing to visit. */
|
||||
@@ -140,6 +141,28 @@ static void scr_aggregate_destroy(void *obj) {
|
||||
scr_error_destroy(obj);
|
||||
}
|
||||
|
||||
/* SuppressedError teardown: the ScrError fields plus the disposal
|
||||
* snapshots scr_exc_suppress recorded for its `error` and `suppressed`. */
|
||||
static void scr_suppressed_gcfree(void *obj) {
|
||||
scr_exc_suppressed_forget(obj);
|
||||
scr_error_gcfree(obj);
|
||||
}
|
||||
|
||||
static void scr_suppressed_destroy(void *obj) {
|
||||
scr_exc_suppressed_forget(obj);
|
||||
scr_error_destroy(obj);
|
||||
}
|
||||
|
||||
static void scr_suppressed_reld(void *obj) {
|
||||
ScrError *e = (ScrError *)obj;
|
||||
if (--e->rc == 0) {
|
||||
if (scr_error_traced) scr_cyc_dead(e);
|
||||
scr_rc_destroy(obj, scr_suppressed_destroy);
|
||||
} else if (scr_error_traced) {
|
||||
scr_cyc_released(e);
|
||||
}
|
||||
}
|
||||
|
||||
static void scr_aggregate_reld(void *obj) {
|
||||
ScrAggregateError *a = (ScrAggregateError *)obj;
|
||||
if (--a->base.rc == 0) {
|
||||
@@ -163,6 +186,7 @@ SCR_TL ScrVt scr_error_vts[SCR_ERR_COUNT] = {
|
||||
{7, 7, &scr_error_reld}, /* URIError */
|
||||
{8, 8, &scr_aggregate_reld}, /* AggregateError */
|
||||
{9, 9, &scr_error_reld}, /* AssertionError */
|
||||
{10, 10, &scr_suppressed_reld}, /* SuppressedError */
|
||||
};
|
||||
|
||||
ScrError *scr_error_retain(ScrError *e) {
|
||||
@@ -208,11 +232,13 @@ static ScrError *scr_error_alloc(int kind) {
|
||||
static ScrCycType error_type = { &scr_error_trace, &scr_error_gcfree, SCR_CYC_KIND_ERROR };
|
||||
static ScrCycType domex_type = { &scr_error_trace, &scr_domex_gcfree, SCR_CYC_KIND_DOMEXCEPTION };
|
||||
static ScrCycType aggregate_type = { &scr_error_trace, &scr_aggregate_gcfree, SCR_CYC_KIND_ERROR };
|
||||
static ScrCycType suppressed_type = { &scr_error_trace, &scr_suppressed_gcfree, SCR_CYC_KIND_ERROR };
|
||||
ScrError *e;
|
||||
if (scr_error_traced) {
|
||||
e = scr_cyc_alloc(size, kind == SCR_ERR_DOMEX ? &domex_type
|
||||
: kind == SCR_ERR_AGGREGATE ? &aggregate_type
|
||||
: &error_type);
|
||||
: kind == SCR_ERR_AGGREGATE ? &aggregate_type
|
||||
: kind == SCR_ERR_SUPPRESSED ? &suppressed_type
|
||||
: &error_type);
|
||||
} else {
|
||||
e = calloc(1, size);
|
||||
if (!e) scr_error_oom();
|
||||
|
||||
@@ -119,7 +119,8 @@ enum {
|
||||
SCR_ERR_URI = 7,
|
||||
SCR_ERR_AGGREGATE = 8, /* AggregateError — ScrAggregateError, the wider layout */
|
||||
SCR_ERR_ASSERTION = 9, /* node:assert's AssertionError — the Error layout */
|
||||
SCR_ERR_COUNT = 10,
|
||||
SCR_ERR_SUPPRESSED = 10, /* SuppressedError — the Error layout; error and suppressed are view properties */
|
||||
SCR_ERR_COUNT = 11,
|
||||
};
|
||||
|
||||
extern SCR_TL ScrVt scr_error_vts[SCR_ERR_COUNT]; /* indexed by SCR_ERR_*; main() stamps pre/post */
|
||||
@@ -201,6 +202,14 @@ ScrError *scr_aggregate_new(const struct ScrDyn *message, const struct ScrDyn *o
|
||||
void scr_aggregate_set_errors(ScrError *e, struct ScrDyn *errors); /* both borrowed */
|
||||
struct ScrDyn *scr_aggregate_errors(ScrError *e); /* +1 */
|
||||
|
||||
/* new SuppressedError(error, suppressed, message) (scr_json.c): the message
|
||||
* as in scr_error_new_options, then `error` and `suppressed` as
|
||||
* non-enumerable properties of the error's dyn view. All arguments are
|
||||
* borrowed (dyn undefined when absent). Returns +1, or NULL with an
|
||||
* exception pending when the message conversion throws. */
|
||||
ScrError *scr_suppressed_new(const struct ScrDyn *error, const struct ScrDyn *suppressed,
|
||||
const struct ScrDyn *message);
|
||||
|
||||
/* Switch runtime-side error allocation to collector-headered (called from
|
||||
* main() when the compiler's cycle fixpoint marks the hierarchy). */
|
||||
void scr_error_set_traced(void);
|
||||
|
||||
@@ -354,10 +354,41 @@ void scr_rethrow(const ScrCaught *c) {
|
||||
}
|
||||
}
|
||||
|
||||
/* Explicit resource management's error merge. The full JavaScript object
|
||||
* exposes `.error` and `.suppressed`; the static catch surface currently
|
||||
* exposes Error name/message, so retain Node's externally visible identity
|
||||
* and ordering while consuming both snapshots exactly once. */
|
||||
/* Explicit resource management's error merge: a SuppressedError whose
|
||||
* `error` is the disposal error and whose `suppressed` is the body error.
|
||||
* The snapshots wait in this table for the error's dyn view, so programs
|
||||
* that never read them link no checked-dynamic code. */
|
||||
typedef struct ScrSuppressedEntry {
|
||||
const void *error_object;
|
||||
ScrCaught *error;
|
||||
ScrCaught *suppressed;
|
||||
} ScrSuppressedEntry;
|
||||
static SCR_TL ScrSuppressedEntry *scr_suppressed_entries = NULL;
|
||||
static SCR_TL size_t scr_suppressed_len = 0;
|
||||
static SCR_TL size_t scr_suppressed_cap = 0;
|
||||
|
||||
bool scr_exc_suppressed_values(const void *error_object, const ScrCaught **error,
|
||||
const ScrCaught **suppressed) {
|
||||
for (size_t i = 0; i < scr_suppressed_len; i++) {
|
||||
if (scr_suppressed_entries[i].error_object != error_object) continue;
|
||||
*error = scr_suppressed_entries[i].error;
|
||||
*suppressed = scr_suppressed_entries[i].suppressed;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void scr_exc_suppressed_forget(const void *error_object) {
|
||||
for (size_t i = 0; i < scr_suppressed_len; i++) {
|
||||
if (scr_suppressed_entries[i].error_object != error_object) continue;
|
||||
ScrSuppressedEntry entry = scr_suppressed_entries[i];
|
||||
scr_suppressed_entries[i] = scr_suppressed_entries[--scr_suppressed_len];
|
||||
scr_caught_release(entry.error);
|
||||
scr_caught_release(entry.suppressed);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void scr_exc_suppress(ScrCaught *suppressed) {
|
||||
if (suppressed->kind == SCR_EXC_GENRET) {
|
||||
/* A generator .return() is a return completion, not a thrown error.
|
||||
@@ -371,11 +402,22 @@ void scr_exc_suppress(ScrCaught *suppressed) {
|
||||
scr_caught_release(suppressed);
|
||||
return;
|
||||
}
|
||||
scr_caught_release(error);
|
||||
scr_caught_release(suppressed);
|
||||
scr_throw_error_named(
|
||||
scr_str_new("SuppressedError", sizeof("SuppressedError") - 1),
|
||||
scr_str_new("An error was suppressed during disposal", sizeof("An error was suppressed during disposal") - 1));
|
||||
static const char text[] = "An error was suppressed during disposal";
|
||||
ScrStr *message = scr_str_new(text, sizeof text - 1);
|
||||
ScrError *e = scr_error_new(SCR_ERR_SUPPRESSED, message);
|
||||
scr_str_release(message);
|
||||
if (scr_suppressed_len == scr_suppressed_cap) {
|
||||
size_t cap = scr_suppressed_cap ? scr_suppressed_cap * 2 : 4;
|
||||
ScrSuppressedEntry *entries = realloc(scr_suppressed_entries, cap * sizeof *entries);
|
||||
if (!entries) {
|
||||
fputs("scriptc: out of memory\n", stderr);
|
||||
abort();
|
||||
}
|
||||
scr_suppressed_entries = entries;
|
||||
scr_suppressed_cap = cap;
|
||||
}
|
||||
scr_suppressed_entries[scr_suppressed_len++] = (ScrSuppressedEntry){e, error, suppressed};
|
||||
scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg());
|
||||
}
|
||||
|
||||
bool scr_caught_instanceof(const ScrCaught *c, size_t pre, size_t post) {
|
||||
|
||||
@@ -165,8 +165,16 @@ ScrCaught *scr_caught_retain(ScrCaught *c);
|
||||
void scr_caught_release(ScrCaught *c); /* NULL-tolerant */
|
||||
void scr_caught_trace_v(void *c, ScrTraceVisit visit, void *ctx);
|
||||
/* Replace the pending disposal error with a SuppressedError and consume the
|
||||
* original body-error snapshot. The current exception cell must be pending. */
|
||||
* original body-error snapshot. The current exception cell must be pending.
|
||||
* The SuppressedError keeps both snapshots in a side table until its
|
||||
* teardown: scr_exc_suppressed_values answers them (borrowed) for the
|
||||
* error's `error` (the disposal error) and `suppressed` (the body error)
|
||||
* when scr_json.c builds its dyn view, and scr_exc_suppressed_forget
|
||||
* releases them. */
|
||||
void scr_exc_suppress(ScrCaught *suppressed);
|
||||
bool scr_exc_suppressed_values(const void *error_object, const ScrCaught **error,
|
||||
const ScrCaught **suppressed);
|
||||
void scr_exc_suppressed_forget(const void *error_object);
|
||||
/* Borrows the box; re-raises the saved exception exactly (payload RETAINED
|
||||
* — the binding stays live until its scope exits). */
|
||||
void scr_rethrow(const ScrCaught *c);
|
||||
|
||||
@@ -5677,6 +5677,16 @@ ScrDyn *scr_error_dyn_fields(const ScrError *e) {
|
||||
scr_dyn_obj_set(d, "cause", 5, scr_domex_cause((ScrError *)e));
|
||||
}
|
||||
}
|
||||
/* A disposal SuppressedError's non-enumerable `error` and `suppressed`
|
||||
* (scr_exc_suppress); a constructed one sets them on its view. */
|
||||
const ScrCaught *suppressed_error = NULL, *suppressed_value = NULL;
|
||||
if (e->vt == &scr_error_vts[SCR_ERR_SUPPRESSED] &&
|
||||
scr_exc_suppressed_values(e, &suppressed_error, &suppressed_value)) {
|
||||
scr_dyn_obj_set(d, "error", 5, scr_caught_to_dyn(suppressed_error));
|
||||
d->v.obj.entries[d->v.obj.len - 1].enumerable = false;
|
||||
scr_dyn_obj_set(d, "suppressed", 10, scr_caught_to_dyn(suppressed_value));
|
||||
d->v.obj.entries[d->v.obj.len - 1].enumerable = false;
|
||||
}
|
||||
/* AggregateError's own non-enumerable `errors` list, shared with the
|
||||
* typed read so both spellings answer the same array. */
|
||||
if (e->vt == &scr_error_vts[SCR_ERR_AGGREGATE]) {
|
||||
@@ -10888,6 +10898,24 @@ ScrError *scr_error_new_options(int kind, const ScrDyn *message, const ScrDyn *o
|
||||
return e;
|
||||
}
|
||||
|
||||
/* SuppressedError's `error` and `suppressed`: non-enumerable own
|
||||
* properties of the error's dyn view. Both values move in. */
|
||||
static void scr_suppressed_attach(ScrError *e, ScrDyn *error, ScrDyn *suppressed) {
|
||||
ScrDyn *view = scr_dyn_from_error(e);
|
||||
scr_dyn_obj_set(view, "error", 5, error);
|
||||
view->v.obj.entries[view->v.obj.len - 1].enumerable = false;
|
||||
scr_dyn_obj_set(view, "suppressed", 10, suppressed);
|
||||
view->v.obj.entries[view->v.obj.len - 1].enumerable = false;
|
||||
scr_dyn_release(view);
|
||||
}
|
||||
|
||||
ScrError *scr_suppressed_new(const ScrDyn *error, const ScrDyn *suppressed, const ScrDyn *message) {
|
||||
ScrError *e = scr_error_new_options(SCR_ERR_SUPPRESSED, message, NULL);
|
||||
if (!e) return NULL;
|
||||
scr_suppressed_attach(e, scr_dyn_retain((ScrDyn *)error), scr_dyn_retain((ScrDyn *)suppressed));
|
||||
return e;
|
||||
}
|
||||
|
||||
ScrDyn *scr_error_cause(ScrError *e) {
|
||||
size_t hit = scr_errdyn_find(e, true);
|
||||
if (hit != SIZE_MAX) {
|
||||
|
||||
@@ -356,3 +356,104 @@ void scr_dyn_dirent_finish(ScrDyn *obj) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* The well-known symbol `Symbol.<name>` as a dyn key (+1). */
|
||||
static ScrDyn *scr_using_key(const char *name, size_t len) {
|
||||
ScrStr *text = scr_str_new(name, len);
|
||||
ScrSym *symbol = scr_sym_well_known(text);
|
||||
scr_str_release(text);
|
||||
ScrDyn *key = scr_dyn_new_symbol(symbol);
|
||||
scr_sym_release(symbol);
|
||||
return key;
|
||||
}
|
||||
|
||||
static bool scr_using_typeof_is(const ScrDyn *value, const char *name, size_t len) {
|
||||
ScrStr *type = scr_dyn_typeof(value);
|
||||
bool match = type->len == len && !memcmp(type->data, name, len);
|
||||
scr_str_release(type);
|
||||
return match;
|
||||
}
|
||||
|
||||
/* The [Symbol.<name>] method of a `using` value: NULL for an absent member
|
||||
* (undefined or null), the callable member (+1), or NULL with a pending
|
||||
* TypeError when the member is not callable. */
|
||||
static ScrDyn *scr_using_get_method(const ScrDyn *value, const char *name, size_t len, bool *absent) {
|
||||
ScrDyn *key = scr_using_key(name, len);
|
||||
ScrDyn *method = scr_dyn_symbol_key_get(value, key, false);
|
||||
scr_dyn_release(key);
|
||||
*absent = false;
|
||||
if (!method) return NULL;
|
||||
if (method->kind == SCR_DYN_UNDEF || method->kind == SCR_DYN_NULL) {
|
||||
scr_dyn_release(method);
|
||||
*absent = true;
|
||||
return NULL;
|
||||
}
|
||||
if (!scr_using_typeof_is(method, "function", 8)) {
|
||||
scr_dyn_release(method);
|
||||
ScrJsonBuf message;
|
||||
scr_jb_init(&message);
|
||||
scr_jb_puts(&message, "Symbol(Symbol.");
|
||||
scr_jb_write(&message, name, len);
|
||||
scr_jb_puts(&message, ") is not a function");
|
||||
scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&message));
|
||||
return NULL;
|
||||
}
|
||||
return method;
|
||||
}
|
||||
|
||||
ScrDyn *scr_dyn_using_method(const ScrDyn *value, bool async) {
|
||||
if (value->kind == SCR_DYN_NULL || value->kind == SCR_DYN_UNDEF) return scr_dyn_retain(scr_dyn_undefined());
|
||||
if (!scr_using_typeof_is(value, "object", 6) && !scr_using_typeof_is(value, "function", 8)) {
|
||||
static const char message[] = "An object is expected with `using` declarations";
|
||||
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
|
||||
return NULL;
|
||||
}
|
||||
bool absent = false;
|
||||
if (async) {
|
||||
ScrDyn *method = scr_using_get_method(value, "asyncDispose", 12, &absent);
|
||||
if (!method && !absent) return NULL;
|
||||
if (method) {
|
||||
ScrDyn *record = scr_dyn_new_arr();
|
||||
scr_dyn_arr_push(record, method);
|
||||
scr_dyn_arr_push(record, scr_dyn_new_bool(false));
|
||||
return record;
|
||||
}
|
||||
}
|
||||
ScrDyn *method = scr_using_get_method(value, "dispose", 7, &absent);
|
||||
if (!method) {
|
||||
if (absent && value->kind == SCR_DYN_TYPED_REF && value->v.typed_ref.type_key_len > 7 &&
|
||||
!memcmp(value->v.typed_ref.type_key, "object:", 7)) {
|
||||
/* A class instance seen through an `unknown` or Disposable-typed
|
||||
* value: its dyn view carries no methods. */
|
||||
static const char message[] =
|
||||
"disposing a class instance through a Disposable- or unknown-typed value has no lowering "
|
||||
"yet (declare the variable with the class type)";
|
||||
scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020");
|
||||
} else if (absent) {
|
||||
static const char message[] = "Symbol(Symbol.dispose) is not a function";
|
||||
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
if (!async) return method;
|
||||
ScrDyn *record = scr_dyn_new_arr();
|
||||
scr_dyn_arr_push(record, method);
|
||||
scr_dyn_arr_push(record, scr_dyn_new_bool(true));
|
||||
return record;
|
||||
}
|
||||
|
||||
ScrDyn *scr_dyn_using_dispose(const ScrDyn *method, const ScrDyn *value) {
|
||||
if (method->kind == SCR_DYN_UNDEF) return scr_dyn_retain(scr_dyn_undefined());
|
||||
bool sync_fallback = false;
|
||||
const ScrDyn *callee = method;
|
||||
if (method->kind == SCR_DYN_ARR) {
|
||||
callee = method->v.arr.items[0];
|
||||
sync_fallback = method->v.arr.items[1]->v.b;
|
||||
}
|
||||
scr_dyn_this_push_dyn(value);
|
||||
ScrDyn *result = scr_dyn_call(callee, NULL, 0, "[Symbol.dispose]");
|
||||
scr_dyn_this_pop();
|
||||
if (!result || !sync_fallback) return result;
|
||||
scr_dyn_release(result);
|
||||
return scr_dyn_retain(scr_dyn_undefined());
|
||||
}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
// child_process.exec's callback forms: the command line runs with
|
||||
// /bin/sh -c, the call returns a live ChildProcess immediately, utf8
|
||||
// stdout/stderr are captured, and the error-first callback fires after the
|
||||
// current turn. A non-zero exit (a missing command is the shell's 127)
|
||||
// reports "Command failed: <command>\n<stderr>".
|
||||
import { exec } from "node:child_process";
|
||||
import * as childProcess from "node:child_process";
|
||||
|
||||
const child = exec("printf out; printf err >&2", (error, stdout, stderr) => {
|
||||
console.log("ok", error === null, JSON.stringify(stdout), JSON.stringify(stderr));
|
||||
exec("printf 'a\\nb\\nc\\n' | wc -l", (pipeError, lines) => {
|
||||
console.log("pipe", pipeError === null, lines.trim());
|
||||
exec(`printf '%s|' "a b" 'c d' e`, { encoding: "utf8", maxBuffer: 1024 * 1024 }, (_, quoted) => {
|
||||
console.log("quoting", quoted);
|
||||
exec("printf partial; printf failed >&2; exit 3", (failed, failedOut, failedErr) => {
|
||||
console.log("fail", failed !== null, JSON.stringify(failedOut), JSON.stringify(failedErr));
|
||||
console.log("fail-message", JSON.stringify(failed === null ? "" : failed.message));
|
||||
exec("exit 1", (silent) => {
|
||||
console.log("silent", JSON.stringify(silent === null ? "" : silent.message));
|
||||
exec("scriptc-definitely-not-a-command", (missing, missingOut) => {
|
||||
const message = missing === null ? "" : missing.message;
|
||||
console.log("missing", missing !== null, JSON.stringify(missingOut));
|
||||
console.log("missing-message", message.split("\n")[0]);
|
||||
exec("true", () => {
|
||||
console.log("zero-parameter callback");
|
||||
childProcess.exec("printf namespace", (_, out) => console.log(out));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
console.log("returned", child.pid !== undefined, child.stdout !== null, child.stderr !== null);
|
||||
console.log("scheduled");
|
||||
@@ -20,11 +20,15 @@ async function main(): Promise<void> {
|
||||
console.log("module timeout:", await sleepFromModule(1, 42));
|
||||
console.log("module immediate:", await immediateFromModule(true));
|
||||
|
||||
// Timers armed together settle in deadline order however late the loop
|
||||
// reaches its timers phase. (An immediate against a short timer is not
|
||||
// ordered: the timer fires first once its delay has passed when the loop
|
||||
// reaches the timers phase, in Node too.)
|
||||
const order: string[] = [];
|
||||
const slow = sleep(30, "slow").then((v) => order.push(v));
|
||||
const fast = sleep(1, "fast").then((v) => order.push(v));
|
||||
const now = immediate("now").then((v) => order.push(v));
|
||||
await Promise.all([slow, fast, now]);
|
||||
const middle = sleep(10, "middle").then((v) => order.push(v));
|
||||
await Promise.all([slow, fast, middle]);
|
||||
console.log("order:", order.join(","));
|
||||
|
||||
const started = Date.now();
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
// util.promisify(stream.finished) and util.promisify(stream.pipeline) are
|
||||
// Node's custom promisified forms, stream/promises' finished and pipeline:
|
||||
// fulfilled when the stream (or the last stage) closes cleanly, rejected
|
||||
// with the stream's error or ERR_STREAM_PREMATURE_CLOSE. The named imports
|
||||
// and the namespace members are both recognized.
|
||||
import * as stream from "node:stream";
|
||||
import { PassThrough, Readable, Transform, Writable, finished } from "node:stream";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
const finishedAsync = promisify(finished);
|
||||
const pipelineAsync = promisify(stream.pipeline);
|
||||
|
||||
function source(items: string[]): Readable {
|
||||
let i = 0;
|
||||
return new Readable({
|
||||
read() {
|
||||
this.push(i < items.length ? items[i++]! : null);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const clean = source(["a", "b"]);
|
||||
const cleanDone = finishedAsync(clean);
|
||||
clean.resume();
|
||||
await cleanDone;
|
||||
console.log("finished: clean");
|
||||
|
||||
const torn = new Readable({ read() {} });
|
||||
const tornDone = finishedAsync(torn);
|
||||
torn.destroy(new Error("torn"));
|
||||
try {
|
||||
await tornDone;
|
||||
console.log("SHOULD NOT PRINT");
|
||||
} catch (e) {
|
||||
if (e instanceof Error) console.log("finished: rejected", e.message);
|
||||
}
|
||||
|
||||
const early = new Readable({ read() {} });
|
||||
const earlyDone = finishedAsync(early);
|
||||
early.destroy();
|
||||
try {
|
||||
await earlyDone;
|
||||
console.log("SHOULD NOT PRINT");
|
||||
} catch (e) {
|
||||
if (e instanceof Error) console.log("finished: premature", (e as NodeJS.ErrnoException).code);
|
||||
}
|
||||
|
||||
const out: string[] = [];
|
||||
const sink = new Writable({
|
||||
write(chunk: Buffer, _enc: string, cb: () => void) {
|
||||
out.push(chunk.toString());
|
||||
cb();
|
||||
},
|
||||
});
|
||||
await pipelineAsync(source(["x", "y", "z"]), new PassThrough(), sink);
|
||||
console.log("pipeline:", out.join(","));
|
||||
|
||||
const failing = new Transform({
|
||||
transform(_chunk: Buffer, _enc: string, cb: (err?: Error | null) => void) {
|
||||
cb(new Error("boom"));
|
||||
},
|
||||
});
|
||||
const drain = new Writable({
|
||||
write(_c: Buffer, _e: string, cb: () => void) {
|
||||
cb();
|
||||
},
|
||||
});
|
||||
try {
|
||||
await pipelineAsync(source(["q"]), failing, drain);
|
||||
console.log("SHOULD NOT PRINT");
|
||||
} catch (e) {
|
||||
if (e instanceof Error) console.log("pipeline: rejected", e.message, drain.destroyed);
|
||||
}
|
||||
}
|
||||
main();
|
||||
@@ -0,0 +1,57 @@
|
||||
// 'using' and 'await using' with Disposable- and AsyncDisposable-typed values:
|
||||
// object literals and function results, the declaration's TypeErrors, LIFO
|
||||
// disposal, and the sync fallback of 'await using'. Node.js prints the same.
|
||||
function attempt(name: string, f: () => void): void {
|
||||
try {
|
||||
f();
|
||||
console.log(name, "ok");
|
||||
} catch (e) {
|
||||
console.log(name, (e as Error).name, (e as Error).message);
|
||||
}
|
||||
}
|
||||
const values: unknown[] = [1, "s", true, {}, { [Symbol.dispose]: 5 }, null, undefined];
|
||||
for (const value of values) {
|
||||
attempt(String(typeof value), () => {
|
||||
using x = value as Disposable;
|
||||
console.log("body");
|
||||
});
|
||||
}
|
||||
function make(label: string): Disposable {
|
||||
return { [Symbol.dispose]() { console.log("dispose", label); } };
|
||||
}
|
||||
attempt("order", () => {
|
||||
using a = make("a");
|
||||
using b = make("b");
|
||||
console.log("body");
|
||||
});
|
||||
async function main(): Promise<void> {
|
||||
const asyncValues: unknown[] = [5, {}, { [Symbol.asyncDispose]: 3 }];
|
||||
for (const value of asyncValues) {
|
||||
try {
|
||||
await using x = value as AsyncDisposable;
|
||||
console.log("abody");
|
||||
} catch (e) {
|
||||
console.log("async", (e as Error).name, (e as Error).message);
|
||||
}
|
||||
}
|
||||
{
|
||||
await using y = { [Symbol.dispose]() { console.log("sync in await using"); return 7; } } as Disposable;
|
||||
await using z = { async [Symbol.asyncDispose]() { await null; console.log("async disposed"); } } as AsyncDisposable;
|
||||
console.log("block");
|
||||
}
|
||||
console.log("after");
|
||||
}
|
||||
main();
|
||||
|
||||
const literal = { [Symbol.dispose]() { console.log("literal disposed"); } };
|
||||
function scope(): void {
|
||||
using a = literal;
|
||||
using b = make("b2");
|
||||
using c = null;
|
||||
console.log("scope body");
|
||||
}
|
||||
scope();
|
||||
for (const label of ["p", "q"]) {
|
||||
using r = make(label);
|
||||
console.log("loop", label);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// SuppressedError: the error a 'using' disposal raises over a pending error,
|
||||
// with its error and suppressed values, nested disposals, and the constructor.
|
||||
// Node.js prints the same.
|
||||
class Bad implements Disposable {
|
||||
label: string;
|
||||
constructor(label: string) {
|
||||
this.label = label;
|
||||
}
|
||||
[Symbol.dispose](): void {
|
||||
throw new Error("dispose " + this.label);
|
||||
}
|
||||
}
|
||||
try {
|
||||
using x = new Bad("x");
|
||||
throw new Error("in body");
|
||||
} catch (e) {
|
||||
const se = e as SuppressedError;
|
||||
console.log(se.name, se.message, e instanceof SuppressedError, e instanceof Error);
|
||||
console.log((se.error as Error).message, (se.suppressed as Error).message);
|
||||
console.log(String(se));
|
||||
}
|
||||
try {
|
||||
using a = new Bad("a");
|
||||
using b = new Bad("b");
|
||||
throw new RangeError("body");
|
||||
} catch (e) {
|
||||
if (e instanceof SuppressedError) {
|
||||
const inner = e.suppressed;
|
||||
if (inner instanceof SuppressedError)
|
||||
console.log((e.error as Error).message, inner.name, (inner.error as Error).message, (inner.suppressed as Error).name);
|
||||
}
|
||||
}
|
||||
try {
|
||||
using a = new Bad("only");
|
||||
} catch (e) {
|
||||
console.log(e instanceof SuppressedError, (e as Error).message);
|
||||
}
|
||||
const made = new SuppressedError(1, "two", "msg");
|
||||
console.log(made.error, made.suppressed, made.message, made.name, made instanceof SuppressedError);
|
||||
const bare = new SuppressedError(new TypeError("t"), undefined);
|
||||
console.log(JSON.stringify(bare.message), (bare.error as Error).name, bare.suppressed);
|
||||
try {
|
||||
using v = { [Symbol.dispose]() { throw 42; } } as Disposable;
|
||||
throw "body";
|
||||
} catch (e) {
|
||||
const se = e as SuppressedError;
|
||||
console.log(se.error, se.suppressed);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// 'using' with object literals and function results in JavaScript, and the
|
||||
// SuppressedError properties. Node.js prints the same.
|
||||
function res(label, fail) {
|
||||
return { [Symbol.dispose]() { console.log("close", label); if (fail) throw new Error("dispose " + label); } };
|
||||
}
|
||||
{
|
||||
using a = res("a");
|
||||
using b = res("b");
|
||||
console.log("body");
|
||||
}
|
||||
try {
|
||||
using a = res("a", true);
|
||||
using b = res("b", true);
|
||||
throw new Error("body");
|
||||
} catch (e) {
|
||||
console.log(e.name, e.message, e.error.message, e.suppressed.name, e.suppressed.error.message, e.suppressed.suppressed.message);
|
||||
console.log(e instanceof SuppressedError, e instanceof Error, Object.keys(e), String(e));
|
||||
}
|
||||
const se = new SuppressedError(1, "two", "msg");
|
||||
console.log(se.error, se.suppressed, se.message, se.name);
|
||||
async function main() {
|
||||
{
|
||||
await using y = { [Symbol.dispose]() { console.log("sync in await using"); } };
|
||||
await using z = { async [Symbol.asyncDispose]() { await null; console.log("async disposed"); } };
|
||||
console.log("block");
|
||||
}
|
||||
console.log("after");
|
||||
}
|
||||
main();
|
||||
@@ -0,0 +1,56 @@
|
||||
// Array.fromAsync over arrays of values and of promises, with and without a
|
||||
// mapping function, interleaved with other promise jobs: every result,
|
||||
// rejection and iterator close lands on the microtask Node uses. Node.js prints
|
||||
// the same.
|
||||
const log: string[] = [];
|
||||
async function ticker(n: number): Promise<void> {
|
||||
for (let i = 0; i < n; i++) {
|
||||
log.push("t" + i);
|
||||
await null;
|
||||
}
|
||||
}
|
||||
function report(label: string, job: Promise<number[]>): Promise<number> {
|
||||
return job.then(
|
||||
(a) => log.push(label + " " + a.join(",")),
|
||||
(e) => log.push(label + " caught " + (e as Error).message),
|
||||
);
|
||||
}
|
||||
async function main(): Promise<void> {
|
||||
const tick = ticker(24);
|
||||
const jobs = [
|
||||
report("values", Array.fromAsync([1, 2, 3])),
|
||||
report("promises", Array.fromAsync([Promise.resolve(4), Promise.resolve(5)])),
|
||||
report("empty", Array.fromAsync([] as number[])),
|
||||
report("map", Array.fromAsync([1, 2], (x: number, i: number) => x * 2 + i)),
|
||||
report("async map", Array.fromAsync([1, 2], async (x: number) => x + 1)),
|
||||
report("promise map", Array.fromAsync([3, 4], (x: number) => Promise.resolve(x * x))),
|
||||
report("promises async map", Array.fromAsync([Promise.resolve(1), Promise.resolve(2)], async (x: number) => x * 5)),
|
||||
report(
|
||||
"map throws",
|
||||
Array.fromAsync([1, 2], (x: number) => {
|
||||
if (x === 2) throw new Error("map failed");
|
||||
return x;
|
||||
}),
|
||||
),
|
||||
report(
|
||||
"async map rejects",
|
||||
Array.fromAsync([1, 2, 3], async (x: number) => {
|
||||
if (x === 2) throw new Error("async map failed");
|
||||
return x;
|
||||
}),
|
||||
),
|
||||
report("first rejected", Array.fromAsync([Promise.reject(new Error("first")), Promise.resolve(1)])),
|
||||
report("second rejected", Array.fromAsync([Promise.resolve(1), Promise.reject(new Error("second"))])),
|
||||
report(
|
||||
"later",
|
||||
Array.fromAsync([new Promise<number>((resolve) => setTimeout(() => resolve(7), 1)), Promise.resolve(8)]),
|
||||
),
|
||||
];
|
||||
log.push("sync end");
|
||||
await Promise.all(jobs);
|
||||
await tick;
|
||||
console.log(log.join("\n"));
|
||||
const strings = await Array.fromAsync(["a", "b"], async (s: string, i: number) => s.repeat(i + 1));
|
||||
console.log(strings.join("|"), strings.length);
|
||||
}
|
||||
main();
|
||||
@@ -0,0 +1,14 @@
|
||||
// Array.fromAsync in JavaScript over arrays of values and promises, with a
|
||||
// mapping function, beside other promise jobs. Node.js prints the same.
|
||||
const order = [];
|
||||
const job = Array.fromAsync([Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)], (x, i) => x * 10 + i);
|
||||
Promise.resolve()
|
||||
.then(() => order.push("a"))
|
||||
.then(() => order.push("b"))
|
||||
.then(() => order.push("c"))
|
||||
.then(() => order.push("d"));
|
||||
job.then((values) => {
|
||||
order.push("values " + values.join(","));
|
||||
console.log(order.join(" "));
|
||||
});
|
||||
Array.fromAsync(["x", "y"]).then((values) => console.log(values.join("+")));
|
||||
@@ -1,8 +0,0 @@
|
||||
import { exec } from "node:child_process";
|
||||
|
||||
exec("exit 3", (error) => {
|
||||
if (error === null) return;
|
||||
console.log(error.code);
|
||||
console.log("code" in error);
|
||||
console.log(typeof error.code === "number");
|
||||
});
|
||||
-9
@@ -1,9 +0,0 @@
|
||||
import { exec } from "node:child_process";
|
||||
|
||||
exec("printf typed-exec; printf err >&2", (error, stdout, stderr) => {
|
||||
if (error !== null) throw error;
|
||||
console.log(stdout, stderr);
|
||||
exec("exit 2", { encoding: "utf8" }, (failed) => {
|
||||
console.log(JSON.stringify(failed === null ? "" : failed.message));
|
||||
});
|
||||
});
|
||||
+1
-1
@@ -11,6 +11,6 @@ console.log(await sleepFromModule(1, true));
|
||||
const order: string[] = [];
|
||||
await Promise.all([
|
||||
sleep(20, "slow").then((v) => order.push(v)),
|
||||
immediate("now").then((v) => order.push(v)),
|
||||
sleep(1, "fast").then((v) => order.push(v)),
|
||||
]);
|
||||
console.log(order.join(","));
|
||||
|
||||
@@ -426,40 +426,6 @@ test("node-types: unsupported child stdin forms remain named fences", async () =
|
||||
]);
|
||||
});
|
||||
|
||||
test("node-types: callback exec uses the typed ExecException overloads", async () => {
|
||||
const dir = outDirFor("node-child-exec");
|
||||
const entry = join(nodeTypesDir, "child-exec.ts");
|
||||
const result = await compile(entry, {
|
||||
outPath: join(dir, "child-exec"),
|
||||
outDir: dir,
|
||||
sanitize,
|
||||
});
|
||||
expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics, null, 2) : "").toBe(true);
|
||||
if (!result.ok) return;
|
||||
const { stdout } = await execFileAsync(result.binaryPath);
|
||||
expect(stdout).toBe('typed-exec err\n"Command failed: exit 2\\n"\n');
|
||||
});
|
||||
|
||||
test("node-types: exec's numeric ExecException.code remains a named fence", async () => {
|
||||
const dir = outDirFor("node-child-exec-code-fences");
|
||||
const entry = join(nodeTypesDir, "child-exec-code-fences.ts");
|
||||
const result = await compile(entry, {
|
||||
outPath: join(dir, "child-exec-code-fences"),
|
||||
outDir: dir,
|
||||
sanitize,
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) return;
|
||||
expect(result.diagnostics.map((diagnostic) => diagnostic.code)).toEqual([
|
||||
"SC2020",
|
||||
"SC2020",
|
||||
"SC2020",
|
||||
]);
|
||||
for (const diagnostic of result.diagnostics) {
|
||||
expect(diagnostic.message).toContain("ExecException.code");
|
||||
}
|
||||
});
|
||||
|
||||
test("node-types: path and os lower statically under @types/node's shapes", async () => {
|
||||
const outDir = outDirFor("node-path-os");
|
||||
const result = await compile(join(nodeTypesDir, "path-os.ts"), {
|
||||
|
||||
@@ -149,8 +149,6 @@ const WINDOWS_SKIPS: Record<string, string> = {
|
||||
"posix-shaped: the uncaught sh ENOENT throw crashes both sides, rendered differently (1645 covers env here)",
|
||||
"1464-env-writes.ts":
|
||||
'posix-shaped: the observing child is ENOENT on Windows, exposing the documented spawn-failure ""-vs-null stance',
|
||||
"5627-exec-callback.ts":
|
||||
"posix-shaped: exec runs /bin/sh -c (execSync's static shell lowering); Windows Node runs cmd.exe",
|
||||
// Two programs the events unit's win32 arm surfaced (they compiled for
|
||||
// the first time once the events gate lifted): both drive their signal
|
||||
// listeners through a /bin/sh child, ENOENT on Windows Node too.
|
||||
|
||||
Reference in New Issue
Block a user