mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-25 06:24:27 +08:00
Merge remote-tracking branch 'origin/opt/builtins-npm2' into opt/builtins-npm2
This commit is contained in:
@@ -140,7 +140,6 @@ Never hand-edit these files:
|
||||
- `internal/compatibility/generated/node-v24-internal.json`
|
||||
- `internal/compatibility/generated/node-v24-backlog.json`
|
||||
- `docs/src/generated/node-v24-compatibility.json`
|
||||
- `docs/src/generated/node-v24-compatibility-meta.json`
|
||||
|
||||
Change compiler decision sources or the implementation-owned compatibility manifests, then regenerate. Use `pnpm manifest` when compiler decision tables change and `pnpm node-compat` whenever the Node pin, compatibility manifests, compiler surface manifest, changes.
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
- **Iterator helpers:** `map`, `filter`, `take`, `drop`, `flatMap`, `toArray`, `forEach`, `reduce`, `some`, `every` and `find` compile on generators, on array, Map and Set iterators, and on the objects the helpers return. Helper objects support `.next()`, `for...of`, spreads, `Array.from` and `yield*`, pull their source lazily, and close it (run its `return()`) at the same points as Node: when `take` reaches its count, when a short-circuiting method finds its answer, when a callback throws, and when the loop consuming the helper exits early. `flatMap` callbacks return arrays or generators. Iterator helpers on string iterators, `Iterator.from`, the `Iterator` global, calling `return()` on a helper object in TypeScript, printing helper objects, and helper calls on untyped iterators in JavaScript are unsupported.
|
||||
- **Iterator helpers:** `map`, `filter`, `take`, `drop`, `flatMap`, `toArray`, `forEach`, `reduce`, `some`, `every` and `find` compile on generators, on array, typed array, string, Map and Set iterators, and on the objects the helpers return. Helper objects support `.next()`, `.return()`, `for...of`, spreads, `Array.from` and `yield*`, pull their source lazily, and close it (run its `return()`) at the same points as Node: when `take` reaches its count, when a short-circuiting method finds its answer, when a callback throws, and when the loop consuming the helper exits early. In TypeScript, `h.return!()` and `h.return?.()` compile when `h` is a helper call or a `const` initialized with one. `flatMap` callbacks return arrays, generators, Sets or Maps; a string result throws Node's TypeError. `Iterator.from` accepts arrays, strings, typed arrays, Sets, Maps and iterator objects such as generators. In JavaScript, untyped array, string, Map and Set iterators answer the same methods. `Iterator.from` over an object with its own `next` method or over an untyped value, the `Iterator` global as a value, and printing typed helper objects are unsupported.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
`process.argv[0]` is `"scriptc"`, `process.argv[1]` is the binary's path, and program arguments begin at index 2. In programs that use `node:test`, a leading `--test-update-snapshots` argument is Node's snapshot update flag: it is kept out of `process.argv` and appears in `process.execArgv`, which is otherwise `[]`. `process.version` and `process.versions.node` report the Node.js release the runtime is verified against.
|
||||
|
||||
Uncaught exceptions print `Uncaught <value>` to stderr instead of Node.js's source excerpt and stack-trace block; thrown strings and numbers print their value. Exit codes and output written before the exception are preserved. Runtime errors expose `message` and Node's `code`, but not `errno`, `syscall`, or `path`.
|
||||
Uncaught exceptions print `Uncaught <value>` to stderr instead of Node.js's source excerpt and stack-trace block; thrown strings and numbers print their value. Exit codes and output written before the exception are preserved. Runtime errors expose `message` and Node's `code` as typed members. The other own properties Node gives them, such as a system error's `errno`, `syscall`, and `path`, a child process error's `status`, `stdout`, and `stderr`, or an assertion error's `actual` and `expected`, are readable through an untyped view of the error (for example a cast of a caught value) and appear in `util.inspect` output.
|
||||
|
||||
Errors carry no stack frames. `console.log` and `util.inspect` print an Error in Node's stackless form, `[TypeError: message]`, where Node prints `TypeError: message` followed by stack frames; the two agree when `Error.stackTraceLimit` is `0`.
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { createGeistdocs } from "@vercel/geistdocs/next";
|
||||
|
||||
const withGeistdocs = createGeistdocs();
|
||||
const legacySlugs = ["quickstart", "coverage", "dependencies", "ffi", "native-objects", "wasm", "platforms", "cli", "how-it-works", "limitations"];
|
||||
|
||||
// The compatibility page fetches its data under a content hash of the
|
||||
// committed artifact, so a deploy never serves a cached older copy, and
|
||||
// checks the row count it expects. Both are derived here at build time.
|
||||
const compatibilityArtifact = readFileSync(new URL("./src/generated/node-v24-compatibility.json", import.meta.url), "utf8");
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
env: {
|
||||
COMPATIBILITY_ARTIFACT_VERSION: createHash("sha256").update(compatibilityArtifact).digest("hex").slice(0, 20),
|
||||
COMPATIBILITY_ROW_COUNT: String(JSON.parse(compatibilityArtifact).rows.length),
|
||||
},
|
||||
agentRules: false,
|
||||
allowedDevOrigins: ["127.0.0.1"],
|
||||
distDir: process.env.NEXT_DIST_DIR || ".next",
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
// @refresh reset
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import compatibilityMeta from "@/generated/node-v24-compatibility-meta.json";
|
||||
|
||||
// Set by next.config.mjs from the committed artifact.
|
||||
const artifactVersion = process.env.COMPATIBILITY_ARTIFACT_VERSION;
|
||||
const rowCount = Number(process.env.COMPATIBILITY_ROW_COUNT);
|
||||
|
||||
type Status =
|
||||
| "supported"
|
||||
@@ -211,14 +214,14 @@ export function NodeCompatibilityMatrix() {
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
fetch(`/compatibility/data?v=${compatibilityMeta.artifactVersion}`, { cache: "no-store", signal: controller.signal })
|
||||
fetch(`/compatibility/data?v=${artifactVersion}`, { cache: "no-store", signal: controller.signal })
|
||||
.then((response) => {
|
||||
if (!response.ok) throw new Error(`compatibility data: ${response.status}`);
|
||||
return response.json() as Promise<Snapshot>;
|
||||
})
|
||||
.then((data) => {
|
||||
if (!Array.isArray(data.rows) || data.rows.length !== compatibilityMeta.rowCount) {
|
||||
throw new Error(`compatibility data is stale or malformed (expected ${compatibilityMeta.rowCount} rows)`);
|
||||
if (!Array.isArray(data.rows) || data.rows.length !== rowCount) {
|
||||
throw new Error(`compatibility data is stale or malformed (expected ${rowCount} rows)`);
|
||||
}
|
||||
setSnapshot(data);
|
||||
})
|
||||
@@ -281,7 +284,7 @@ export function NodeCompatibilityMatrix() {
|
||||
return <div className="flex h-full items-center justify-center p-6"><div className="rounded-xl border border-red-500/40 bg-red-100 p-4 text-sm text-red-900">The compatibility dataset could not be loaded.</div></div>;
|
||||
}
|
||||
if (!snapshot) {
|
||||
return <div className="flex h-full items-center justify-center p-6 text-sm text-gray-900">Loading {compatibilityMeta.rowCount.toLocaleString()} Node.js API rows…</div>;
|
||||
return <div className="flex h-full items-center justify-center p-6 text-sm text-gray-900">Loading {rowCount.toLocaleString()} Node.js API rows…</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -151,8 +151,11 @@ test("compatibility page and data endpoints keep their existing contracts", asyn
|
||||
const response = await get("/compatibility/data");
|
||||
assert.equal(response.status, 200);
|
||||
const dataset = await response.json();
|
||||
const metadata = JSON.parse(await readFile(new URL("../src/generated/node-v24-compatibility-meta.json", import.meta.url), "utf8"));
|
||||
assert.equal(dataset.rows.length, metadata.rowCount);
|
||||
const artifact = JSON.parse(await readFile(new URL("../src/generated/node-v24-compatibility.json", import.meta.url), "utf8"));
|
||||
assert.equal(dataset.rows.length, artifact.rows.length);
|
||||
// The row count next.config.mjs derives reaches the page (React separates
|
||||
// adjacent text nodes with empty comments).
|
||||
assert.ok(html.replaceAll("<!-- -->", "").includes(`Loading ${artifact.rows.length.toLocaleString()} Node.js API rows`));
|
||||
assert.equal((await get("/node-compatibility")).status, 308);
|
||||
assert.equal((await get("/node-compatibility/data")).status, 308);
|
||||
});
|
||||
|
||||
@@ -24,7 +24,6 @@ const root = fileURLToPath(new URL("../../../", import.meta.url));
|
||||
const internalOutputPath = `${packageRoot}generated/node-v24-internal.json`;
|
||||
const backlogOutputPath = `${packageRoot}generated/node-v24-backlog.json`;
|
||||
const publicOutputPath = `${root}docs/src/generated/node-v24-compatibility.json`;
|
||||
const publicMetaOutputPath = `${root}docs/src/generated/node-v24-compatibility-meta.json`;
|
||||
const compilerManifestPath = `${root}packages/compiler/surface-manifest.json`;
|
||||
const compilerCompatPath = `${packageRoot}static-support.json`;
|
||||
const pinPath = `${packageRoot}node-v24.json`;
|
||||
@@ -1009,17 +1008,6 @@ function publicSnapshot(snapshot) {
|
||||
};
|
||||
}
|
||||
|
||||
function publicMetadata(publicRendered, snapshot) {
|
||||
const matrix = projectPublicMatrix(snapshot);
|
||||
return {
|
||||
schemaVersion: 3,
|
||||
nodeVersion: snapshot.nodeVersion,
|
||||
nodeCommit: snapshot.nodeCommit,
|
||||
artifactVersion: sha(publicRendered).slice(0, 20),
|
||||
rowCount: matrix.rows.length,
|
||||
};
|
||||
}
|
||||
|
||||
function checkLocal() {
|
||||
const snapshot = JSON.parse(readFileSync(internalOutputPath, "utf8"));
|
||||
if (snapshot.nodeVersion !== pin.version || snapshot.nodeCommit !== pin.commit) {
|
||||
@@ -1071,11 +1059,8 @@ function checkLocal() {
|
||||
if (readFileSync(backlogOutputPath, "utf8") !== render(buildBacklog(snapshot))) {
|
||||
throw new Error("the Node compatibility backlog is stale; run 'pnpm node-compat'");
|
||||
}
|
||||
const publicRendered = render(publicSnapshot(snapshot));
|
||||
if (
|
||||
readFileSync(publicMetaOutputPath, "utf8") !== render(publicMetadata(publicRendered, snapshot))
|
||||
) {
|
||||
throw new Error("the docs compatibility metadata is stale; run 'pnpm node-compat'");
|
||||
if (readFileSync(publicOutputPath, "utf8") !== render(publicSnapshot(snapshot))) {
|
||||
throw new Error("the docs compatibility artifact is stale; run 'pnpm node-compat'");
|
||||
}
|
||||
for (const row of snapshot.rows) {
|
||||
for (const key of ["static"]) {
|
||||
@@ -1141,7 +1126,6 @@ async function main() {
|
||||
const internalRendered = render(snapshot);
|
||||
const backlogRendered = render(buildBacklog(snapshot));
|
||||
const publicRendered = render(publicSnapshot(snapshot));
|
||||
const publicMetaRendered = render(publicMetadata(publicRendered, snapshot));
|
||||
|
||||
if (args.has("--check")) {
|
||||
if (readFileSync(internalOutputPath, "utf8") !== internalRendered)
|
||||
@@ -1150,8 +1134,6 @@ async function main() {
|
||||
throw new Error("Node compatibility backlog is stale; run 'pnpm node-compat'");
|
||||
if (readFileSync(publicOutputPath, "utf8") !== publicRendered)
|
||||
throw new Error("public Node compatibility snapshot is stale; run 'pnpm node-compat'");
|
||||
if (readFileSync(publicMetaOutputPath, "utf8") !== publicMetaRendered)
|
||||
throw new Error("public Node compatibility metadata is stale; run 'pnpm node-compat'");
|
||||
console.log(
|
||||
`Node compatibility snapshot matches ${pin.tag} (${snapshot.rows.length} API rows, commit ${pin.commit.slice(0, 12)})`,
|
||||
);
|
||||
@@ -1160,7 +1142,6 @@ async function main() {
|
||||
writeFileSync(internalOutputPath, internalRendered);
|
||||
writeFileSync(backlogOutputPath, backlogRendered);
|
||||
writeFileSync(publicOutputPath, publicRendered);
|
||||
writeFileSync(publicMetaOutputPath, publicMetaRendered);
|
||||
console.log(
|
||||
`wrote compatibility ledger, backlog, and public artifact (${snapshot.rows.length} source rows, ${projectPublicMatrix(snapshot).rows.length} public matrix rows, ${pin.tag})`,
|
||||
);
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { expect, test } from "vitest";
|
||||
import { DYN_HANDLE_KINDS } from "../../ir/ir.js";
|
||||
import { DYN_HANDLE_TAG_NUM } from "./dyn.js";
|
||||
|
||||
// ScrDynHandleTag in scr_dyn.h lists its tags by name, each with an explicit
|
||||
// value, so changes that add tags edit different lines. These checks stand
|
||||
// in for what the enum's order used to guarantee.
|
||||
const header = readFileSync(new URL("../../../../runtime/src/scr_dyn.h", import.meta.url), "utf8");
|
||||
const body = /typedef enum \{\n([\s\S]*?)\} ScrDynHandleTag;/.exec(header)![1]!;
|
||||
const tags = [...body.matchAll(/^\s*(SCR_DYNH_[A-Z0-9_]+) = (\d+),/gm)].map(([, name, value]) => ({
|
||||
name: name!,
|
||||
value: Number(value),
|
||||
}));
|
||||
const capacity = Number(/#define SCR_DYNH_CAPACITY (\d+)/.exec(header)![1]);
|
||||
|
||||
test("every ScrDynHandleTag has an explicit value, in name order", () => {
|
||||
const lines = body.split("\n").filter((line) => line.trim().startsWith("SCR_DYNH_"));
|
||||
expect(tags.length).toBe(lines.length);
|
||||
expect(tags.map((tag) => tag.name)).toEqual(tags.map((tag) => tag.name).toSorted());
|
||||
});
|
||||
|
||||
test("ScrDynHandleTag values are unique and below SCR_DYNH_CAPACITY", () => {
|
||||
const owners = new Map<number, string>();
|
||||
for (const { name, value } of tags) {
|
||||
expect(owners.get(value), `${name} = ${value}`).toBeUndefined();
|
||||
owners.set(value, name);
|
||||
expect(value, name).toBeLessThan(capacity);
|
||||
}
|
||||
});
|
||||
|
||||
test("the backend's handle tag numbers match scr_dyn.h", () => {
|
||||
const values = new Map(tags.map((tag) => [tag.name, tag.value]));
|
||||
for (const [kind, num] of Object.entries(DYN_HANDLE_TAG_NUM)) {
|
||||
const tag = DYN_HANDLE_KINDS.get(kind)?.tag;
|
||||
expect(tag, kind).toBeDefined();
|
||||
expect(values.get(tag!), `${kind} (${tag})`).toBe(num);
|
||||
}
|
||||
});
|
||||
@@ -72,8 +72,8 @@ export function recordDynKeepsIdentity(shape: IrRecordShape): boolean {
|
||||
}
|
||||
|
||||
/** ScrDynKind values (scr_runtime.h). */
|
||||
/** ScrDynHandleTag numeric values (scr_runtime.h's enum order). */
|
||||
const DYN_HANDLE_TAG_NUM: Record<string, number> = {
|
||||
/** ScrDynHandleTag values (scr_dyn.h's explicit values; dyn-handle-tags.test.ts checks them). */
|
||||
export const DYN_HANDLE_TAG_NUM: Record<string, number> = {
|
||||
httpReq: 0,
|
||||
httpRes: 1,
|
||||
netSocket: 2,
|
||||
|
||||
@@ -321,6 +321,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"date.getUTCDate": "scr_date_get_date_utc",
|
||||
"date.getDay": "scr_date_get_day_local",
|
||||
"date.getUTCDay": "scr_date_get_day_utc",
|
||||
"date.toUTCString": "scr_date_to_utc_string",
|
||||
"date.getHours": "scr_date_get_hours_local",
|
||||
"date.getUTCHours": "scr_date_get_hours_utc",
|
||||
"date.getMinutes": "scr_date_get_minutes_local",
|
||||
@@ -828,6 +829,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
// ambient-this read never throw. The fs dyn read is the sync-fs story.
|
||||
"fetch.start": "scr_fetch_static",
|
||||
"fetch.responseNew": "scr_fetch_response_new",
|
||||
"fetch.responseJsonStatic": "scr_fetch_response_json_static",
|
||||
"fetch.input": "scr_fetch_input",
|
||||
"abort.controllerNew": "scr_fetch_abort_controller_new",
|
||||
"event.targetNew": "scr_event_target_new",
|
||||
|
||||
@@ -518,6 +518,15 @@ export const NODE24_FETCH_COMPAT_PROFILE = {
|
||||
evidence: [fixture("static")],
|
||||
}),
|
||||
),
|
||||
{
|
||||
id: "stdlib.response.static.json",
|
||||
name: "Response.json",
|
||||
kind: "static-method",
|
||||
facets: ["webidl-conversion", "error-shape"],
|
||||
scope:
|
||||
"data that crosses the checked-dynamic boundary, serialized with JSON.stringify (toJSON hooks included); ResponseInit as for the constructor",
|
||||
evidence: [fixture("static-response-json")],
|
||||
},
|
||||
...["json", "text", "bytes", "arrayBuffer"].map((member): FetchCompatOperation => ({
|
||||
id: `stdlib.response.${member}`,
|
||||
name: `Response.${member}`,
|
||||
@@ -681,7 +690,8 @@ export const NODE24_FETCH_COMPAT_PROFILE = {
|
||||
),
|
||||
|
||||
staticEntry("stdlib.response.constructor", "Response", "constructor", "constructor"),
|
||||
...["error", "json", "redirect"].map((member) =>
|
||||
staticEntry("stdlib.response.static.json", "Response", "json", "static"),
|
||||
...["error", "redirect"].map((member) =>
|
||||
unsupportedEntry(
|
||||
`stdlib.response.static.${member}`,
|
||||
"Response",
|
||||
|
||||
@@ -102,6 +102,8 @@ export function lowerExecSyncCall(
|
||||
let stdoutMode = 1;
|
||||
let stderrMode = 0;
|
||||
let stdinInherit = false;
|
||||
// Without an encoding option, a failure's error carries Buffer outputs.
|
||||
let encodingGiven = false;
|
||||
// A conditional env spread (`...(c ? { env: ... } : {})`): the call
|
||||
// itself splits into a ternary of the two env-nesses at the tail.
|
||||
let condEnvSpread: { cond: IrExpr; pairs: IrExpr; whenTrue: boolean } | null = null;
|
||||
@@ -134,6 +136,8 @@ export function lowerExecSyncCall(
|
||||
runtime.timeout,
|
||||
runtime.stdoutMode,
|
||||
runtime.stderrMode,
|
||||
// The runtime options carry a utf8 encoding (others throw).
|
||||
boolLit(false, loc),
|
||||
],
|
||||
type: STRING,
|
||||
loc,
|
||||
@@ -251,6 +255,7 @@ export function lowerExecSyncCall(
|
||||
'outputs are captured as utf8 — pass { encoding: "utf8" }',
|
||||
);
|
||||
}
|
||||
encodingGiven = true;
|
||||
break;
|
||||
}
|
||||
case "cwd":
|
||||
@@ -395,6 +400,7 @@ export function lowerExecSyncCall(
|
||||
timeout,
|
||||
numLit(stdoutMode + (stdinInherit ? 4 : 0), loc),
|
||||
numLit(stderrMode, loc),
|
||||
boolLit(!encodingGiven, loc),
|
||||
],
|
||||
type: STRING,
|
||||
loc,
|
||||
|
||||
@@ -34,7 +34,7 @@ const DATE_GETTER_FNS: Readonly<Record<string, IrLibFn>> = {
|
||||
};
|
||||
|
||||
const DATE_METHOD_HINT =
|
||||
"getTime(), valueOf(), toISOString(), toJSON(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering";
|
||||
"getTime(), valueOf(), toISOString(), toUTCString(), toJSON(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering";
|
||||
|
||||
/** The Date slice: statics plus read-only Date values backed by one
|
||||
* TimeClip'd millisecond scalar. Construction lives in lowerNew;
|
||||
@@ -113,6 +113,9 @@ export function lowerDateCall(
|
||||
if (name === "toISOString") {
|
||||
return { kind: "libCall", fn: "date.toISOStringValue", args: [receiver], type: STRING, loc };
|
||||
}
|
||||
if (name === "toUTCString") {
|
||||
return { kind: "libCall", fn: "date.toUTCString", args: [receiver], type: STRING, loc };
|
||||
}
|
||||
const fn = own(DATE_GETTER_FNS, name);
|
||||
if (fn !== undefined) {
|
||||
return { kind: "libCall", fn, args: [receiver], type: F64, loc };
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
isUnitType,
|
||||
} from "../../../ir/ir.js";
|
||||
import { optionalStringTags } from "./arguments.js";
|
||||
import { constituentTypes } from "../../ts7/checker.js";
|
||||
|
||||
/** JSON parse produces checked-dynamic data; a native reviver walks it
|
||||
* bottom-up before any checked cast. Stringify without a callback keeps
|
||||
@@ -152,9 +153,18 @@ export function lowerJsonMethodCall(
|
||||
}
|
||||
const optionalString = lowerOptionalStringifyRoot(lowerer, value, indent, loc);
|
||||
if (optionalString) return optionalString;
|
||||
// A program class instance whose class (or a base) declares toJSON:
|
||||
// JSON.stringify serializes whatever toJSON(key) returns, so the
|
||||
// instance crosses into the runtime walker, which calls the hook
|
||||
// through the checked-dynamic class view.
|
||||
const toJsonClass =
|
||||
value.type.kind === "object" &&
|
||||
lowerer.findMethodOn(lowerer.classes.get(value.type.className) ?? null, "toJSON") !== null &&
|
||||
lowerer.dynConvertible(value.type);
|
||||
if (
|
||||
value.type.kind === "url" ||
|
||||
value.type.kind === "dyn" ||
|
||||
toJsonClass ||
|
||||
isJsonStringifyDynamicType(
|
||||
value.type,
|
||||
(id) => lowerer.shapes.get(id),
|
||||
@@ -191,9 +201,11 @@ export function lowerJsonMethodCall(
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
// Preserve the existing dyn-root string ABI, including its textual
|
||||
// "undefined" result, while letting the runtime honor toJSON hooks.
|
||||
return value.type.kind === "dyn"
|
||||
// A dyn root can serialize to undefined (undefined, a function, a
|
||||
// symbol). Where the program can observe that (jsonResultObserved),
|
||||
// keep the actual undefined; elsewhere the dyn-root string ABI
|
||||
// stays, whose undefined result prints the same text.
|
||||
return (value.type.kind === "dyn" || toJsonClass) && !jsonResultObserved(lowerer, call)
|
||||
? { kind: "toString", operand: raw, type: STRING, loc }
|
||||
: { kind: "dynCheck", value: raw, type: lowerer.withUndefinedArm(STRING), loc };
|
||||
}
|
||||
@@ -290,6 +302,106 @@ function lowerOptionalStringifyRoot(
|
||||
return { kind: "call", callee: helper, args: [value], type: resultT, loc };
|
||||
}
|
||||
|
||||
/** True when a `JSON.stringify` result is used where JavaScript can tell
|
||||
* undefined from the text "undefined": compared with undefined/null,
|
||||
* `typeof`, `??`, `?.`, truth tests, a return from a function declaring
|
||||
* an undefined result, or an unannotated local binding whose references
|
||||
* do one of these. The lib types say `string`, so other uses (concatenation,
|
||||
* printing, string parameters) cannot tell the difference. */
|
||||
function jsonResultObserved(lowerer: Lowerer, call: ts.CallExpression): boolean {
|
||||
let node: ts.Node = call;
|
||||
while (ts.isParenthesizedExpression(node.parent)) node = node.parent;
|
||||
if (observesUndefined(node as ts.Expression)) return true;
|
||||
const parent = node.parent;
|
||||
if (ts.isReturnStatement(parent)) {
|
||||
let fn: ts.Node | undefined = parent.parent;
|
||||
while (fn && !ts.isFunctionLike(fn)) fn = fn.parent;
|
||||
const annotation = fn && ts.isFunctionLike(fn) ? fn.type : undefined;
|
||||
if (!annotation) return false;
|
||||
const type = lowerer.checker.getTypeAtLocation(annotation);
|
||||
const parts = type.flags & ts.TypeFlags.Union ? constituentTypes(type) : [type];
|
||||
return parts.some((part) => (part.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Void)) !== 0);
|
||||
}
|
||||
// Module-scope bindings are globals whose slot type comes from the
|
||||
// checker's `string`; only function- and block-scoped locals adopt the
|
||||
// initializer's checked `string | undefined`.
|
||||
if (
|
||||
!ts.isVariableDeclaration(parent) ||
|
||||
parent.initializer !== node ||
|
||||
parent.type ||
|
||||
!ts.isIdentifier(parent.name) ||
|
||||
(ts.isVariableDeclarationList(parent.parent) &&
|
||||
ts.isVariableStatement(parent.parent.parent) &&
|
||||
ts.isSourceFile(parent.parent.parent.parent))
|
||||
)
|
||||
return false;
|
||||
const name = parent.name;
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(name);
|
||||
if (!symbol) return false;
|
||||
let scope: ts.Node = parent;
|
||||
while (!ts.isBlock(scope.parent) && !ts.isSourceFile(scope.parent) && scope.parent)
|
||||
scope = scope.parent;
|
||||
let observed = false;
|
||||
const visit = (current: ts.Node): void => {
|
||||
if (observed) return;
|
||||
if (ts.isIdentifier(current)) {
|
||||
if (
|
||||
current !== name &&
|
||||
current.text === name.text &&
|
||||
lowerer.checker.getSymbolAtLocation(current) === symbol &&
|
||||
observesUndefined(current)
|
||||
)
|
||||
observed = true;
|
||||
return;
|
||||
}
|
||||
ts.forEachChild(current, visit);
|
||||
};
|
||||
visit(scope.parent ?? scope);
|
||||
return observed;
|
||||
}
|
||||
|
||||
function observesUndefined(expr: ts.Expression): boolean {
|
||||
let node: ts.Node = expr;
|
||||
while (ts.isParenthesizedExpression(node.parent)) node = node.parent;
|
||||
const parent = node.parent;
|
||||
if (ts.isTypeOfExpression(parent)) return true;
|
||||
if (ts.isPrefixUnaryExpression(parent) && parent.operator === ts.SyntaxKind.ExclamationToken)
|
||||
return true;
|
||||
if (
|
||||
(ts.isPropertyAccessExpression(parent) || ts.isElementAccessExpression(parent)) &&
|
||||
parent.expression === node &&
|
||||
parent.questionDotToken
|
||||
)
|
||||
return true;
|
||||
if (
|
||||
(ts.isIfStatement(parent) || ts.isWhileStatement(parent) || ts.isDoStatement(parent)) &&
|
||||
parent.expression === node
|
||||
)
|
||||
return true;
|
||||
if (ts.isConditionalExpression(parent) && parent.condition === node) return true;
|
||||
if (ts.isBinaryExpression(parent)) {
|
||||
const op = parent.operatorToken.kind;
|
||||
if (op === ts.SyntaxKind.QuestionQuestionToken) return parent.left === node;
|
||||
if (op === ts.SyntaxKind.AmpersandAmpersandToken || op === ts.SyntaxKind.BarBarToken)
|
||||
return parent.left === node;
|
||||
if (
|
||||
op === ts.SyntaxKind.EqualsEqualsEqualsToken ||
|
||||
op === ts.SyntaxKind.ExclamationEqualsEqualsToken ||
|
||||
op === ts.SyntaxKind.EqualsEqualsToken ||
|
||||
op === ts.SyntaxKind.ExclamationEqualsToken
|
||||
) {
|
||||
let other: ts.Node = parent.left === node ? parent.right : parent.left;
|
||||
while (ts.isParenthesizedExpression(other)) other = other.expression;
|
||||
return (
|
||||
other.kind === ts.SyntaxKind.NullKeyword ||
|
||||
(ts.isIdentifier(other) && other.text === "undefined") ||
|
||||
ts.isVoidExpression(other)
|
||||
);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** True when a class field type holds a RegExp or a Promise outside a
|
||||
* nested class instance (whose own fields are checked separately): Node
|
||||
* writes both as {}, and the runtime JSON traversal has no writer for them. */
|
||||
|
||||
@@ -13,7 +13,13 @@ import {
|
||||
typeEquals,
|
||||
} from "../../ir/ir.js";
|
||||
import { isJsSourceFile, isNodeEsmFile } from "../program.js";
|
||||
import { genResultRecord, isAnyArrayType, jsOpenObjectType } from "../type-mapper.js";
|
||||
import {
|
||||
genResultRecord,
|
||||
isAnyArrayType,
|
||||
isUnitOnlyTsType,
|
||||
jsOpenObjectType,
|
||||
unitOnlyUnion,
|
||||
} from "../type-mapper.js";
|
||||
import { dynFallbackType } from "./lowerer.js";
|
||||
import { builtinModuleFnOf } from "./surfaces.js";
|
||||
import { mixinFnShapeOf } from "./lower-mixins.js";
|
||||
@@ -380,7 +386,7 @@ export function paramShapes(
|
||||
const tsType = symbol
|
||||
? lowerer.checker.getTypeOfSymbol(symbol)
|
||||
: lowerer.typeOf(declParam.name);
|
||||
const mapped = jsOpenObjectType(
|
||||
let mapped = jsOpenObjectType(
|
||||
declParam,
|
||||
lowerer.runtimeOptionalBindingType(
|
||||
declParam.name,
|
||||
@@ -389,6 +395,11 @@ export function paramShapes(
|
||||
lowerer.shapes,
|
||||
lowerer.unions,
|
||||
);
|
||||
// An instantiation with a literal `undefined` argument (`id(undefined)`
|
||||
// binds T = undefined): standalone undefined maps to VOID (the return
|
||||
// convention), and a parameter is a value position, so it takes the
|
||||
// unit-only slot like a declared `v: undefined` parameter.
|
||||
if (mapped?.kind === "void" && isUnitOnlyTsType(tsType)) mapped = unitOnlyUnion(lowerer.unions);
|
||||
if (!mapped || mapped.kind === "void")
|
||||
lowerer.badType(blameOf?.(declParam, i) ?? declParam.name, tsType);
|
||||
if (declParam.dotDotDotToken) {
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
} from "../optional-arguments.js";
|
||||
import { lowerCheckedArrayFrom } from "./array-construction.js";
|
||||
import { lowerToNumberArgument } from "../lower-exprs.js";
|
||||
import { lowerBytesIteratorValue } from "../iterator-adapters.js";
|
||||
|
||||
/** TypedArray.prototype.sort and toSorted. The receiver and comparator
|
||||
* expressions are evaluated before entering the helper (buildBytesSortFn).
|
||||
@@ -463,6 +464,21 @@ export function lowerBytesMethodCall(
|
||||
if (name === "sort" || name === "toSorted") {
|
||||
return lowerBytesSortCall(lowerer, call, access, receiverIr);
|
||||
}
|
||||
// values()/keys()/entries() as iterator objects (for...of heads and
|
||||
// stored numeric iterators lower before this).
|
||||
if ((name === "values" || name === "keys" || name === "entries") && nArgs === 0) {
|
||||
const type = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
if (type?.kind === "generator") {
|
||||
const value = lowerBytesIteratorValue(
|
||||
lowerer,
|
||||
lowerer.lowerExpr(access.expression),
|
||||
name,
|
||||
type,
|
||||
loc,
|
||||
);
|
||||
if (value) return value;
|
||||
}
|
||||
}
|
||||
if (name === "toReversed") {
|
||||
if (nArgs !== 0) {
|
||||
lowerer.noLowering(`.toReversed with ${nArgs} arguments on typed arrays`, call);
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { Lowerer } from "../lowerer.js";
|
||||
import {
|
||||
BOOL,
|
||||
CAUGHT,
|
||||
DYN,
|
||||
F64,
|
||||
type IrMapIntrinsicMethod,
|
||||
type IrSetIntrinsicMethod,
|
||||
@@ -28,12 +29,18 @@ import { concatStrings, throwTypeError } from "./dynamic-receivers.js";
|
||||
import {
|
||||
type GenType,
|
||||
generatorLoopBinding,
|
||||
resultRecordOf,
|
||||
genStep,
|
||||
genValue,
|
||||
stepDrivable,
|
||||
} from "../lower-generators.js";
|
||||
import { exitBeforeReturns } from "./for-of.js";
|
||||
import { iteratorSlotAdapter } from "../iterator-adapters.js";
|
||||
import {
|
||||
iteratorSlotAdapter,
|
||||
lowerArrayIteratorValue,
|
||||
lowerBytesIteratorValue,
|
||||
lowerStringIteratorValue,
|
||||
} from "../iterator-adapters.js";
|
||||
import { lowerArrayCallback } from "./callback-arguments.js";
|
||||
|
||||
/* ── iterator helpers (ES2025) ─────────────────────────────────────────── */
|
||||
@@ -189,6 +196,24 @@ export function lowerGeneratorHelperCall(
|
||||
collectionSeed(lowerer, source),
|
||||
);
|
||||
}
|
||||
// Iterator.from(arr) walks the array by index.
|
||||
const arrayRoot = arrayIteratorCall(lowerer, cur);
|
||||
if (arrayRoot !== null) {
|
||||
const items = lowerer.lowerExpr(arrayRoot.receiver);
|
||||
if (items.type.kind !== "array")
|
||||
lowerer.badType(arrayRoot.receiver, lowerer.typeOf(arrayRoot.receiver));
|
||||
const elem =
|
||||
arrayRoot.proj === "entries" ? entryPairType(lowerer, items.type.elem) : items.type.elem;
|
||||
return lowerFusedChain(
|
||||
lowerer,
|
||||
call,
|
||||
name,
|
||||
stages,
|
||||
{ kind: "array", type: items.type, proj: arrayRoot.proj },
|
||||
items,
|
||||
elem,
|
||||
);
|
||||
}
|
||||
const src = lowerHelperSource(lowerer, cur);
|
||||
return lowerFusedChain(
|
||||
lowerer,
|
||||
@@ -201,6 +226,128 @@ export function lowerGeneratorHelperCall(
|
||||
);
|
||||
}
|
||||
|
||||
/** `h.return!()` or `h.return?.()` on a helper object. The lib declares
|
||||
* return() as optional on iterator objects, and a helper object always
|
||||
* has one, so both spellings are the call: the helper completes and closes
|
||||
* its source, and the result is `{ value: undefined, done: true }`. The
|
||||
* receiver must be provably a helper object — a helper stage call, or a
|
||||
* const binding initialized with one — since other iterator objects (the
|
||||
* array, string, Map and Set iterators) have no return(). Null otherwise,
|
||||
* and for the plain `h.return()` spelling (lowerGenMethodCall's). */
|
||||
export function lowerHelperReturnCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null {
|
||||
let callee: ts.Expression = call.expression;
|
||||
const nonNull = ts.isNonNullExpression(callee);
|
||||
if (nonNull) callee = (callee as ts.NonNullExpression).expression;
|
||||
if (!nonNull && call.questionDotToken === undefined) return null;
|
||||
if (!ts.isPropertyAccessExpression(callee) || callee.questionDotToken) return null;
|
||||
if (callee.name.text !== "return" || call.arguments.length !== 0) return null;
|
||||
if (!isHelperObject(lowerer, callee.expression) || !lowerer.isStdlibMember(callee)) return null;
|
||||
const callT = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
const gen = lowerer.lowerExpr(callee.expression);
|
||||
if (gen.type.kind !== "generator" || gen.type.async) return null;
|
||||
const loc = locOf(call);
|
||||
const recT = resultRecordOf(lowerer, gen.type);
|
||||
const resume: IrExpr = { kind: "genResume", mode: "return", gen, arg: null, type: recT, loc };
|
||||
// `?.` types the call `IteratorResult | undefined`, which has no mapping
|
||||
// as a whole; the result is never undefined, so it is the record itself.
|
||||
if (callT === null || typeEquals(callT, recT)) return resume;
|
||||
const result = lowerer.coerceToExpected(resume, callT);
|
||||
if (!typeEquals(result.type, callT)) lowerer.badType(call, lowerer.typeOf(call));
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Whether `expr` is a helper stage call, or a const binding initialized
|
||||
* with one. */
|
||||
function isHelperObject(lowerer: Lowerer, expr: ts.Expression): boolean {
|
||||
while (ts.isParenthesizedExpression(expr)) expr = expr.expression;
|
||||
if (isStageCall(lowerer, expr)) return true;
|
||||
if (!ts.isIdentifier(expr)) return false;
|
||||
const symbol = lowerer.resolveValueSymbol(expr);
|
||||
const decl = symbol ? lowerer.checker.valueDeclarationOf(symbol) : undefined;
|
||||
if (
|
||||
!decl ||
|
||||
!ts.isVariableDeclaration(decl) ||
|
||||
decl.initializer === undefined ||
|
||||
!ts.isVariableDeclarationList(decl.parent) ||
|
||||
(decl.parent.flags & ts.NodeFlags.Const) === 0
|
||||
)
|
||||
return false;
|
||||
let init: ts.Expression = decl.initializer;
|
||||
while (ts.isParenthesizedExpression(init)) init = init.expression;
|
||||
return isStageCall(lowerer, init);
|
||||
}
|
||||
|
||||
/** `Iterator.from(value)` over a typed built-in iterable: the value's own
|
||||
* iterator, which already inherits from Iterator.prototype, so Node returns
|
||||
* it rather than a wrapper. A generator (including a helper object or an
|
||||
* array iterator) is itself; an array, string or typed array gives its
|
||||
* values iterator; a Set its values and a Map its entries, adapted into a
|
||||
* generator over the live collection. The result converts into the call's
|
||||
* IteratorObject type. Null for other arguments (objects with their own
|
||||
* next method, untyped values), which keep the Iterator fence. */
|
||||
export function lowerIteratorFromCall(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
access: ts.PropertyAccessExpression,
|
||||
): IrExpr | null {
|
||||
if (access.name.text !== "from" || call.arguments.length !== 1) return null;
|
||||
const argNode = call.arguments[0]!;
|
||||
if (ts.isSpreadElement(argNode)) return null;
|
||||
const resultT = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
const argT = lowerer.mapTypeOf(lowerer.typeOf(argNode));
|
||||
if (resultT?.kind !== "generator" || resultT.async || argT === null) return null;
|
||||
const iterableKinds = ["generator", "array", "string", "bytes", "set", "map"];
|
||||
if (!iterableKinds.includes(argT.kind) || (argT.kind === "generator" && argT.async)) return null;
|
||||
const loc = locOf(call);
|
||||
const value = lowerer.lowerExpr(argNode);
|
||||
const own = (yieldT: IrType): GenType => ({
|
||||
kind: "generator",
|
||||
yieldT,
|
||||
retT: VOID,
|
||||
nextT: UNDEFINED_T,
|
||||
});
|
||||
let iterator: IrExpr | null = null;
|
||||
const t = value.type;
|
||||
if (t.kind === "generator") iterator = value;
|
||||
else if (t.kind === "array")
|
||||
iterator = lowerArrayIteratorValue(lowerer, value, "values", own(t.elem), loc);
|
||||
else if (t.kind === "string")
|
||||
iterator = lowerStringIteratorValue(lowerer, value, own(STRING), loc);
|
||||
else if (t.kind === "bytes")
|
||||
iterator = lowerBytesIteratorValue(lowerer, value, "values", own(F64), loc);
|
||||
else if (t.kind === "set" || t.kind === "map") {
|
||||
const elem = t.kind === "set" ? t.elem : entryPairTypeOf(lowerer, t.key, t.value);
|
||||
const native: IrExpr = {
|
||||
kind: "dynInvoke",
|
||||
recv: lowerer.coerceToExpected(value, DYN),
|
||||
method: t.kind === "set" ? "values" : "entries",
|
||||
calleeName: t.kind === "set" ? "set.values" : "map.entries",
|
||||
args: [],
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
iterator = iteratorSlotAdapter(lowerer, native, own(elem));
|
||||
}
|
||||
if (iterator === null) lowerer.badType(argNode, lowerer.typeOf(argNode));
|
||||
const result = lowerer.coerceToExpected(iterator, resultT);
|
||||
if (!typeEquals(result.type, resultT)) lowerer.badType(call, lowerer.typeOf(call));
|
||||
return result;
|
||||
}
|
||||
|
||||
/** The interned [K, V] pair a Map's entries yield. */
|
||||
function entryPairTypeOf(lowerer: Lowerer, keyT: IrType, valueT: IrType): IrType {
|
||||
return {
|
||||
kind: "record",
|
||||
shapeId: lowerer.shapes.intern(
|
||||
[
|
||||
{ name: "0", type: keyT },
|
||||
{ name: "1", type: valueT },
|
||||
],
|
||||
true,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
const ITER_STAGES = new Set(["map", "filter", "take", "drop", "flatMap"]);
|
||||
const ITER_TERMINALS = new Set(["toArray", "forEach", "reduce", "some", "every", "find"]);
|
||||
/** Native iterators that ride checked runtime values and adapt into
|
||||
@@ -353,12 +500,30 @@ export function lowerForOfHelperChain(
|
||||
}
|
||||
}
|
||||
|
||||
/** `arr.values()` or `arr.entries()` on an array value, as the root of a
|
||||
* helper chain. */
|
||||
/** The argument of an `Iterator.from(x)` call, or null. As a chain's root,
|
||||
* the call is the argument's own iterator, so the chain can walk the
|
||||
* argument directly. */
|
||||
function iteratorFromArgument(lowerer: Lowerer, expr: ts.Expression): ts.Expression | null {
|
||||
if (!ts.isCallExpression(expr) || !ts.isPropertyAccessExpression(expr.expression)) return null;
|
||||
if (expr.questionDotToken || expr.expression.questionDotToken || expr.arguments.length !== 1)
|
||||
return null;
|
||||
const arg = expr.arguments[0]!;
|
||||
if (ts.isSpreadElement(arg) || expr.expression.name.text !== "from") return null;
|
||||
if (!lowerer.isStdlibGlobal(expr.expression.expression, "Iterator")) return null;
|
||||
return lowerer.isStdlibMember(expr.expression) ? arg : null;
|
||||
}
|
||||
|
||||
/** `arr.values()`, `arr.entries()` or `Iterator.from(arr)` on an array value,
|
||||
* as the root of a helper chain. */
|
||||
function arrayIteratorCall(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.Expression,
|
||||
): { receiver: ts.Expression; proj: "values" | "entries" } | null {
|
||||
const fromArg = iteratorFromArgument(lowerer, expr);
|
||||
if (fromArg !== null)
|
||||
return lowerer.mapTypeOf(lowerer.typeOf(fromArg))?.kind === "array"
|
||||
? { receiver: fromArg, proj: "values" }
|
||||
: null;
|
||||
if (!ts.isCallExpression(expr) || !ts.isPropertyAccessExpression(expr.expression)) return null;
|
||||
if (expr.questionDotToken || expr.expression.questionDotToken || expr.arguments.length !== 0)
|
||||
return null;
|
||||
@@ -401,6 +566,17 @@ function collectionIteratorCall(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.Expression,
|
||||
): { receiver: ts.Expression; proj: "keys" | "values" | "entries" } | null {
|
||||
// Iterator.from(set) is the Set's values iterator, Iterator.from(map) the
|
||||
// Map's entries iterator.
|
||||
const fromArg = iteratorFromArgument(lowerer, expr);
|
||||
if (fromArg !== null) {
|
||||
const kind = lowerer.mapTypeOf(lowerer.typeOf(fromArg))?.kind;
|
||||
return kind === "set"
|
||||
? { receiver: fromArg, proj: "values" }
|
||||
: kind === "map"
|
||||
? { receiver: fromArg, proj: "entries" }
|
||||
: null;
|
||||
}
|
||||
if (!ts.isCallExpression(expr) || !ts.isPropertyAccessExpression(expr.expression)) return null;
|
||||
if (expr.questionDotToken || expr.expression.questionDotToken || expr.arguments.length !== 0)
|
||||
return null;
|
||||
@@ -480,6 +656,11 @@ function lowerHelperSource(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.Expression,
|
||||
): { gen: IrExpr & { type: GenType }; elem: IrType } {
|
||||
// Iterator.from(gen) is the generator itself; the call's IteratorObject
|
||||
// type would add an adapter generator over it.
|
||||
const fromArg = iteratorFromArgument(lowerer, expr);
|
||||
if (fromArg !== null && lowerer.mapTypeOf(lowerer.typeOf(fromArg))?.kind === "generator")
|
||||
return lowerHelperSource(lowerer, fromArg);
|
||||
const tsType = lowerer.typeOf(expr);
|
||||
const mapped = lowerer.mapTypeOf(tsType);
|
||||
const arg = lowerer.checker.getTypeArguments(tsType as ts.TypeReference)[0];
|
||||
@@ -577,10 +758,17 @@ function lowerStage(
|
||||
if (ret.kind === "array") return { stage: { kind: "flatMap", fn }, out: ret.elem };
|
||||
if (ret.kind === "generator" && ret.async !== true && stepDrivable(ret))
|
||||
return { stage: { kind: "flatMap", fn }, out: ret.yieldT };
|
||||
// A Set flattens its values and a Map its entries, walked live. A string
|
||||
// is rejected at run time with Node's TypeError (strings are not
|
||||
// iterator objects).
|
||||
if (ret.kind === "set") return { stage: { kind: "flatMap", fn }, out: ret.elem };
|
||||
if (ret.kind === "map")
|
||||
return { stage: { kind: "flatMap", fn }, out: entryPairTypeOf(lowerer, ret.key, ret.value) };
|
||||
if (ret.kind === "string") return { stage: { kind: "flatMap", fn }, out: STRING };
|
||||
lowerer.noLowering(
|
||||
`.flatMap over a callback returning '${lowerer.fmt(ret)}'`,
|
||||
s.argNode,
|
||||
"callbacks returning an array or a generator are the supported forms",
|
||||
"callbacks returning an array, a generator, a Set or a Map are the supported forms",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1245,8 +1433,9 @@ function walkCollection(
|
||||
}
|
||||
|
||||
/** A flatMap stage's inner walk over the callback's result in local
|
||||
* `innerId`: an array by index, or a generator driven (and closed on
|
||||
* early exit) like the source. `stop` ends the walk early. */
|
||||
* `innerId`: an array by index, a Set's values or a Map's entries walked
|
||||
* live, or a generator driven (and closed on early exit) like the source.
|
||||
* A string result throws Node's TypeError. `stop` ends the walk early. */
|
||||
function walkInner(
|
||||
lowerer: Lowerer,
|
||||
frame: ChainFrame,
|
||||
@@ -1257,6 +1446,27 @@ function walkInner(
|
||||
loc: SrcLoc,
|
||||
): IrStmt[] {
|
||||
if (innerT.kind === "array") return [walkArray(frame, innerId, innerT, stop, body, loc)];
|
||||
if (innerT.kind === "set" || innerT.kind === "map") {
|
||||
const source: ChainSource = {
|
||||
kind: "collection",
|
||||
type: innerT,
|
||||
proj: innerT.kind === "set" ? "values" : "entries",
|
||||
};
|
||||
return walkCollection(lowerer, frame, innerId, source, stop ?? boolLit(false, loc), body, loc);
|
||||
}
|
||||
if (innerT.kind === "string") {
|
||||
return [
|
||||
throwTypeError(
|
||||
{
|
||||
kind: "strLit",
|
||||
value: "Iterator.prototype.flatMap called on non-object",
|
||||
type: STRING,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
),
|
||||
];
|
||||
}
|
||||
if (innerT.kind !== "generator")
|
||||
throw new InternalCompilerError("lowerer bug: flatMap over a non-iterable result");
|
||||
const pre: IrStmt[] =
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import type { IrExpr, IrFunction } from "../../ir/ir.js";
|
||||
import {
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
type IrType,
|
||||
callSiteReturnType,
|
||||
typeEquals,
|
||||
} from "../../ir/ir.js";
|
||||
import { everyStmtList, transformStmtList } from "../../ir/traverse.js";
|
||||
import type { ClassInfo } from "./lower-classes.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
@@ -56,6 +62,9 @@ export function devirtualizeVirtualCalls(
|
||||
|
||||
class ConstructedClasses {
|
||||
private readonly lowered = new Set<string>();
|
||||
/** Lowered bodies' return types: an override may return a subclass
|
||||
* (`set(): this` resolves per declaring class). */
|
||||
private readonly returnTypes = new Map<string, IrType>();
|
||||
/** Classes some retained IR allocates or exposes as a class value. */
|
||||
private readonly allocated = new Set<string>();
|
||||
private readonly exportedNames: ReadonlySet<string>;
|
||||
@@ -74,6 +83,7 @@ class ConstructedClasses {
|
||||
};
|
||||
for (const fn of functions) {
|
||||
this.lowered.add(fn.name);
|
||||
this.returnTypes.set(fn.name, callSiteReturnType(fn));
|
||||
everyStmtList(fn.body, {
|
||||
stmt: () => true,
|
||||
expr: (e) => {
|
||||
@@ -102,7 +112,7 @@ class ConstructedClasses {
|
||||
this.facts.reachable.has(`%${candidate.def.name}.${e.method}`),
|
||||
)
|
||||
)
|
||||
return this.direct(e, info, nearest.declarer);
|
||||
return this.direct(e, info, nearest.declarer) ?? e;
|
||||
if (this.open(info)) return e;
|
||||
const targets: ClassInfo[] = [];
|
||||
for (const candidate of this.descendants(info)) {
|
||||
@@ -117,7 +127,7 @@ class ConstructedClasses {
|
||||
const callee = `%${target.def.name}.${e.method}`;
|
||||
if (!this.facts.reachable.has(callee) || !this.lowered.has(callee)) return e;
|
||||
}
|
||||
if (targets.length === 1) return this.direct(e, info, targets[0]!);
|
||||
if (targets.length === 1) return this.direct(e, info, targets[0]!) ?? e;
|
||||
// A small closed set: the backend tests the loaded slot against each
|
||||
// implementation and calls it directly (no indirect fallback; every
|
||||
// instance that can reach this site resolves to one of them).
|
||||
@@ -125,7 +135,19 @@ class ConstructedClasses {
|
||||
return { ...e, targets: targets.map((t) => t.def.name) };
|
||||
}
|
||||
|
||||
private direct(e: VirtualCall, info: ClassInfo, target: ClassInfo): IrExpr {
|
||||
/** The direct call, or null when the target's return type cannot stand
|
||||
* for the site's: an override returning a subclass object (a polymorphic
|
||||
* `this` return) upcasts to the site's class; any other mismatch keeps
|
||||
* the virtual call. */
|
||||
private direct(e: VirtualCall, info: ClassInfo, target: ClassInfo): IrExpr | null {
|
||||
const callee = `%${target.def.name}.${e.method}`;
|
||||
const ret = this.returnTypes.get(callee) ?? e.type;
|
||||
const upcast =
|
||||
!typeEquals(ret, e.type) &&
|
||||
ret.kind === "object" &&
|
||||
e.type.kind === "object" &&
|
||||
this.lowerer.isSubclassOf(ret.className, e.type.className);
|
||||
if (!typeEquals(ret, e.type) && !upcast) return null;
|
||||
const receiver = e.args[0]!;
|
||||
const typed: IrExpr =
|
||||
target === info || this.lowerer.isSubclassOf(info.def.name, target.def.name)
|
||||
@@ -139,13 +161,16 @@ class ConstructedClasses {
|
||||
type: { kind: "object", className: target.def.name },
|
||||
loc: receiver.loc,
|
||||
};
|
||||
return {
|
||||
const call: IrExpr = {
|
||||
kind: "call",
|
||||
callee: `%${target.def.name}.${e.method}`,
|
||||
callee,
|
||||
args: [typed, ...e.args.slice(1)],
|
||||
type: e.type,
|
||||
type: ret,
|
||||
loc: e.loc,
|
||||
};
|
||||
return upcast && e.type.kind === "object"
|
||||
? this.lowerer.upcastTo(call, e.type.className)
|
||||
: call;
|
||||
}
|
||||
|
||||
/** The class and every class below it, in the isSubclassOf sense
|
||||
|
||||
@@ -823,7 +823,7 @@ export function lowerObjectLiteral(
|
||||
// `k?: ...` — which desugar to one conditional field at the
|
||||
// spread's own position (any position: they introduce one fresh
|
||||
// name, collision-fenced below). Everything else stays fenced.
|
||||
const cs = conditionalSpreadOf(prop.expression);
|
||||
const cs = conditionalSpreadOf(prop.expression) ?? andSpreadOf(lowerer, prop.expression);
|
||||
if (cs === "unsupported" || (cs && cs.props.length !== 1)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
@@ -1264,6 +1264,7 @@ export function lowerObjectLiteral(
|
||||
expr.properties.length >= 2 &&
|
||||
ts.isSpreadAssignment(expr.properties[0]!) &&
|
||||
!conditionalSpreadOf(expr.properties[0]!.expression) &&
|
||||
!andSpreadOf(lowerer, expr.properties[0]!.expression) &&
|
||||
expr.properties
|
||||
.slice(1)
|
||||
.every(
|
||||
@@ -1340,7 +1341,7 @@ export function lowerObjectLiteral(
|
||||
let mergeable = true;
|
||||
for (const prop of expr.properties) {
|
||||
if (ts.isSpreadAssignment(prop)) {
|
||||
const cs = conditionalSpreadOf(prop.expression);
|
||||
const cs = conditionalSpreadOf(prop.expression) ?? andSpreadOf(lowerer, prop.expression);
|
||||
if (cs === "unsupported" || (cs && cs.props.length !== 1)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
@@ -1481,7 +1482,12 @@ export function lowerObjectLiteral(
|
||||
!shape.tuple &&
|
||||
shape.fields.length > 0 &&
|
||||
expr.properties.length > 0 &&
|
||||
expr.properties.every((p) => ts.isSpreadAssignment(p) && !conditionalSpreadOf(p.expression)) &&
|
||||
expr.properties.every(
|
||||
(p) =>
|
||||
ts.isSpreadAssignment(p) &&
|
||||
!conditionalSpreadOf(p.expression) &&
|
||||
!andSpreadOf(lowerer, p.expression),
|
||||
) &&
|
||||
expr.properties.some((p) => {
|
||||
const t = lowerer.mapTypeOf(lowerer.typeOf((p as ts.SpreadAssignment).expression));
|
||||
return (
|
||||
@@ -1599,7 +1605,7 @@ export function lowerObjectLiteral(
|
||||
}
|
||||
}
|
||||
if (ts.isSpreadAssignment(prop)) {
|
||||
const cs = conditionalSpreadOf(prop.expression);
|
||||
const cs = conditionalSpreadOf(prop.expression) ?? andSpreadOf(lowerer, prop.expression);
|
||||
if (cs && cs !== "unsupported") {
|
||||
// `...(c ? { k: v } : {})` — ONE conditional field at the
|
||||
// spread's own position: cond evaluates exactly once, `v` only
|
||||
@@ -1728,7 +1734,8 @@ export function lowerObjectLiteral(
|
||||
const laterNames = new Set<string>();
|
||||
for (const later of expr.properties.slice(expr.properties.indexOf(prop) + 1)) {
|
||||
if (ts.isSpreadAssignment(later)) {
|
||||
if (conditionalSpreadOf(later.expression)) continue;
|
||||
if (conditionalSpreadOf(later.expression) || andSpreadOf(lowerer, later.expression))
|
||||
continue;
|
||||
const lt = laterSpreadType(lowerer, later.expression);
|
||||
if (lt?.kind === "record") {
|
||||
// A field the later source may lack defines nothing.
|
||||
@@ -1963,7 +1970,8 @@ export function lowerObjectLiteral(
|
||||
const laterNames = new Set<string>();
|
||||
for (const later of expr.properties.slice(expr.properties.indexOf(prop) + 1)) {
|
||||
if (ts.isSpreadAssignment(later)) {
|
||||
if (conditionalSpreadOf(later.expression)) continue;
|
||||
if (conditionalSpreadOf(later.expression) || andSpreadOf(lowerer, later.expression))
|
||||
continue;
|
||||
const lt = laterSpreadType(lowerer, later.expression);
|
||||
if (lt?.kind === "record") {
|
||||
// A field the later source may lack defines nothing.
|
||||
@@ -2517,6 +2525,54 @@ function lowerUnionRecordClone(lowerer: Lowerer, expr: ts.ObjectLiteralExpressio
|
||||
};
|
||||
}
|
||||
|
||||
/** `...(c && { k: v })`: spreading a falsy primitive (false, 0, "", NaN)
|
||||
* copies nothing, so the idiom is `...(c ? { k: v } : {})` with `c`
|
||||
* evaluated once. Recognized only where the spread's own type mixes the
|
||||
* record with primitive arms; a record or record-or-unit spread keeps the
|
||||
* ordinary source-copy path. */
|
||||
export function andSpreadOf(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.Expression,
|
||||
): { cond: ts.Expression; props: CondSpreadProp[]; whenTrue: boolean } | "unsupported" | null {
|
||||
let e: ts.Expression = expr;
|
||||
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
if (!ts.isBinaryExpression(e) || e.operatorToken.kind !== ts.SyntaxKind.AmpersandAmpersandToken)
|
||||
return null;
|
||||
let right: ts.Expression = e.right;
|
||||
while (ts.isParenthesizedExpression(right)) right = right.expression;
|
||||
if (!ts.isObjectLiteralExpression(right)) return null;
|
||||
const spreadType = lowerer.mapTypeOf(lowerer.typeOf(e));
|
||||
if (spreadType?.kind !== "union") return null;
|
||||
const arms = lowerer.unions.get(spreadType.unionId)?.arms ?? [];
|
||||
if (
|
||||
arms.filter((arm) => arm.kind === "record").length !== 1 ||
|
||||
!arms.some((arm) => arm.kind === "bool" || arm.kind === "f64" || arm.kind === "string") ||
|
||||
!arms.every(
|
||||
(arm) =>
|
||||
arm.kind === "record" ||
|
||||
arm.kind === "bool" ||
|
||||
arm.kind === "f64" ||
|
||||
arm.kind === "string" ||
|
||||
isUnitType(arm),
|
||||
)
|
||||
)
|
||||
return null;
|
||||
if (right.properties.length === 0) return "unsupported";
|
||||
const props: CondSpreadProp[] = [];
|
||||
for (const p of right.properties) {
|
||||
if (
|
||||
(ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) &&
|
||||
p.name &&
|
||||
(ts.isIdentifier(p.name) || ts.isStringLiteral(p.name))
|
||||
) {
|
||||
props.push(p as CondSpreadProp);
|
||||
} else {
|
||||
return "unsupported";
|
||||
}
|
||||
}
|
||||
return { cond: e.left, props, whenTrue: true };
|
||||
}
|
||||
|
||||
/** A conditional spread's carrier property: `name: value` or shorthand,
|
||||
* with an identifier/string-literal name. */
|
||||
export type CondSpreadProp = (ts.PropertyAssignment | ts.ShorthandPropertyAssignment) & {
|
||||
|
||||
@@ -4,6 +4,8 @@ import { DYN, STRING, UNDEFINED_T, VOID, isUnitType, typeEquals } from "../../..
|
||||
import type { IrExpr, IrStmt, SrcLoc } from "../../../ir/ir.js";
|
||||
import { isNodeEsmFile, locOf } from "../../program.js";
|
||||
import type { Lowerer } from "../lowerer.js";
|
||||
import { tryLowerExpression } from "./try-lower-expression.js";
|
||||
import { constituentTypes } from "../../ts7/checker.js";
|
||||
|
||||
/** One optional-chain STEP: `a?.b`, `a?.m(...)`, `a?.[i]` (the token on
|
||||
* the member access) and `f?.()` (the token on the call). The receiver
|
||||
@@ -135,6 +137,31 @@ export function hasOptionalChainGuard(expr: ts.Expression): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
/** `recv.m` where `m` is a required method the standard library declares
|
||||
* and `recv` itself is a non-optional, never-nullish typed receiver. */
|
||||
function alwaysPresentStdlibMethod(lowerer: Lowerer, node: ts.Expression): boolean {
|
||||
if (!ts.isPropertyAccessExpression(node) || node.questionDotToken) return false;
|
||||
if (hasOptionalChainGuard(node.expression)) return false;
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(node.name);
|
||||
if (!symbol || !(symbol.flags & ts.SymbolFlags.Method) || symbol.flags & ts.SymbolFlags.Optional)
|
||||
return false;
|
||||
if (!lowerer.isStdlibSymbol(symbol)) return false;
|
||||
const nullable =
|
||||
ts.TypeFlags.Any |
|
||||
ts.TypeFlags.Unknown |
|
||||
ts.TypeFlags.Undefined |
|
||||
ts.TypeFlags.Null |
|
||||
ts.TypeFlags.Void;
|
||||
const receiverType = lowerer.checker.getTypeAtLocation(node.expression);
|
||||
const calleeType = lowerer.checker.getTypeAtLocation(node);
|
||||
const parts = (type: ts.Type): readonly ts.Type[] =>
|
||||
type.flags & ts.TypeFlags.Union ? constituentTypes(type) : [type];
|
||||
return (
|
||||
parts(receiverType).every((part) => !(part.flags & nullable)) &&
|
||||
parts(calleeType).every((part) => !(part.flags & nullable))
|
||||
);
|
||||
}
|
||||
|
||||
export function lowerOptionalChain(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.CallExpression | ts.PropertyAccessExpression | ts.ElementAccessExpression,
|
||||
@@ -181,7 +208,25 @@ export function lowerOptionalChain(
|
||||
dotNode = tail;
|
||||
recvNode = tail.expression;
|
||||
}
|
||||
const loweredReceiver = lowerer.lowerExpr(recvNode);
|
||||
// `h.getSetCookie?.()`: a required standard-library METHOD on a
|
||||
// never-nullish receiver always exists, so `?.()` is the plain call.
|
||||
// Native handle methods have no first-class value to guard; when the
|
||||
// method read itself has no lowering, re-dispatch the plain call (which
|
||||
// reports its own diagnostics) instead of failing on the value read.
|
||||
let loweredReceiver: IrExpr | null;
|
||||
if (dotNode === expr && alwaysPresentStdlibMethod(lowerer, recvNode)) {
|
||||
loweredReceiver = tryLowerExpression(lowerer, recvNode);
|
||||
if (loweredReceiver === null) {
|
||||
lowerer.chainHandled.add(dotNode);
|
||||
try {
|
||||
return lowerer.lowerExpr(expr);
|
||||
} finally {
|
||||
lowerer.chainHandled.delete(dotNode);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
loweredReceiver = lowerer.lowerExpr(recvNode);
|
||||
}
|
||||
const receiver = lowerer.runtimeOptionalSourceValue(recvNode, loweredReceiver) ?? loweredReceiver;
|
||||
if (receiver.type.kind === "dyn") {
|
||||
if (dotNode === expr && ts.isCallExpression(expr)) {
|
||||
|
||||
@@ -265,6 +265,222 @@ function dynIteratorAdapter(
|
||||
return { kind: "call", callee: name, args: [expr], type: expected, loc };
|
||||
}
|
||||
|
||||
/** `ta.values()`, `ta.keys()` or `ta.entries()` on a typed array as a
|
||||
* value: an interned generator over the indices, reading the length and
|
||||
* the element at each step, as the typed array iterator does. Null when
|
||||
* `type` is not the matching number (or [number, number]) generator. */
|
||||
export function lowerBytesIteratorValue(
|
||||
lowerer: Lowerer,
|
||||
receiver: IrExpr,
|
||||
method: "values" | "keys" | "entries",
|
||||
type: IrType,
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
const bytesT = receiver.type;
|
||||
if (bytesT.kind !== "bytes" || type.kind !== "generator") return null;
|
||||
if (type.retT.kind !== "void" || type.nextT.kind !== "undefinedT") return null;
|
||||
const pair = type.yieldT.kind === "record" ? lowerer.shapes.get(type.yieldT.shapeId) : undefined;
|
||||
const pairOk =
|
||||
pair?.tuple === true &&
|
||||
pair.fields.length === 2 &&
|
||||
pair.fields.every((f) => (f.name === "0" || f.name === "1") && f.type.kind === "f64");
|
||||
if (method === "entries" ? !pairOk : type.yieldT.kind !== "f64") return null;
|
||||
const resultType = genResultRecord(type.yieldT, type.retT, lowerer.shapes, lowerer.unions);
|
||||
if (!resultType) return null;
|
||||
const key = `bytesIter:${method}:${typeKey(bytesT)}:${typeKey(type)}`;
|
||||
let name = lowerer.valueHelpers.get(key);
|
||||
if (!name) {
|
||||
name = `%bytes.iter.${lowerer.valueHelpers.size}`;
|
||||
lowerer.valueHelpers.set(key, name);
|
||||
const list = varRef("list.0", bytesT, loc);
|
||||
const index = varRef("index.0", F64, loc);
|
||||
const element: IrExpr = {
|
||||
kind: "bytesIntrinsic",
|
||||
method: "get",
|
||||
receiver: list,
|
||||
args: [index],
|
||||
type: F64,
|
||||
loc,
|
||||
};
|
||||
const yielded: IrExpr =
|
||||
method === "keys"
|
||||
? index
|
||||
: method === "values"
|
||||
? element
|
||||
: {
|
||||
kind: "recordLit",
|
||||
fields: [
|
||||
{ name: "0", value: index },
|
||||
{ name: "1", value: element },
|
||||
],
|
||||
type: type.yieldT,
|
||||
loc,
|
||||
};
|
||||
lowerer.liftedFns.push({
|
||||
name,
|
||||
params: [{ localId: "list.0", name: "list", type: bytesT }],
|
||||
returnType: VOID,
|
||||
locals: [
|
||||
{ id: "list.0", name: "list", type: bytesT, mutable: false },
|
||||
{ id: "index.0", name: "index", type: F64, mutable: true },
|
||||
],
|
||||
body: [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "index.0",
|
||||
init: { kind: "numLit", value: 0, type: F64, loc },
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "while",
|
||||
cond: {
|
||||
kind: "bin",
|
||||
op: "<",
|
||||
left: index,
|
||||
right: {
|
||||
kind: "bytesIntrinsic",
|
||||
method: "length",
|
||||
receiver: list,
|
||||
args: [],
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
body: [
|
||||
{ kind: "exprStmt", expr: { kind: "yieldExpr", value: yielded, type: VOID, loc }, loc },
|
||||
{
|
||||
kind: "assign",
|
||||
localId: "index.0",
|
||||
value: {
|
||||
kind: "bin",
|
||||
op: "+",
|
||||
left: index,
|
||||
right: { kind: "numLit", value: 1, type: F64, loc },
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
},
|
||||
{ kind: "return", value: null, loc },
|
||||
],
|
||||
generator: { yieldT: type.yieldT, nextT: UNDEFINED_T, resultType },
|
||||
loc,
|
||||
});
|
||||
}
|
||||
return { kind: "call", callee: name, args: [receiver], type, loc };
|
||||
}
|
||||
|
||||
/** `s[Symbol.iterator]()` on a string value: an interned generator over
|
||||
* its code points, the string iterator's steps (a lone surrogate is one
|
||||
* step). Null when `type` is not the plain string-yielding generator. */
|
||||
export function lowerStringIteratorValue(
|
||||
lowerer: Lowerer,
|
||||
receiver: IrExpr,
|
||||
type: IrType,
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
if (receiver.type.kind !== "string" || type.kind !== "generator") return null;
|
||||
if (
|
||||
type.yieldT.kind !== "string" ||
|
||||
type.retT.kind !== "void" ||
|
||||
type.nextT.kind !== "undefinedT"
|
||||
)
|
||||
return null;
|
||||
const resultType = genResultRecord(type.yieldT, type.retT, lowerer.shapes, lowerer.unions);
|
||||
if (!resultType) return null;
|
||||
const key = `strIter:${typeKey(type)}`;
|
||||
let name = lowerer.valueHelpers.get(key);
|
||||
if (!name) {
|
||||
name = `%str.iter.${lowerer.valueHelpers.size}`;
|
||||
lowerer.valueHelpers.set(key, name);
|
||||
const text = varRef("text.0", STRING, loc);
|
||||
const index = varRef("index.0", F64, loc);
|
||||
const point = varRef("point.0", STRING, loc);
|
||||
lowerer.liftedFns.push({
|
||||
name,
|
||||
params: [{ localId: "text.0", name: "text", type: STRING }],
|
||||
returnType: VOID,
|
||||
locals: [
|
||||
{ id: "text.0", name: "text", type: STRING, mutable: false },
|
||||
{ id: "index.0", name: "index", type: F64, mutable: true },
|
||||
{ id: "point.0", name: "point", type: STRING, mutable: false },
|
||||
],
|
||||
body: [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "index.0",
|
||||
init: { kind: "numLit", value: 0, type: F64, loc },
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "while",
|
||||
cond: {
|
||||
kind: "bin",
|
||||
op: "<",
|
||||
left: index,
|
||||
right: {
|
||||
kind: "strIntrinsic",
|
||||
method: "length",
|
||||
receiver: text,
|
||||
args: [],
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
body: [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "point.0",
|
||||
init: {
|
||||
kind: "strIntrinsic",
|
||||
method: "cpAt",
|
||||
receiver: text,
|
||||
args: [index],
|
||||
type: STRING,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "assign",
|
||||
localId: "index.0",
|
||||
value: {
|
||||
kind: "bin",
|
||||
op: "+",
|
||||
left: index,
|
||||
right: {
|
||||
kind: "strIntrinsic",
|
||||
method: "length",
|
||||
receiver: point,
|
||||
args: [],
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
{ kind: "exprStmt", expr: { kind: "yieldExpr", value: point, type: VOID, loc }, loc },
|
||||
],
|
||||
loc,
|
||||
},
|
||||
{ kind: "return", value: null, loc },
|
||||
],
|
||||
generator: { yieldT: type.yieldT, nextT: UNDEFINED_T, resultType },
|
||||
loc,
|
||||
});
|
||||
}
|
||||
return { kind: "call", callee: name, args: [receiver], type, loc };
|
||||
}
|
||||
|
||||
/** A generator flowing into a slot with other channel types (a
|
||||
* `Generator<number, void, undefined>` returned through an
|
||||
* `IterableIterator<number>` annotation). The interned adapter generator
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { guardNullProto, nullProtoRecordShape } from "./null-prototype.js";
|
||||
import { lowerUnionEquality, tagEqualityMayMissAlias } from "./strict-equality.js";
|
||||
import { lowerHashStreamCall } from "./builtins/hash-stream.js";
|
||||
import { lowerResponseStaticJson } from "./lower-stdlib.js";
|
||||
import { lowerArrayCallbackCall } from "./containers/array-callback-call.js";
|
||||
/** Dispatch source calls, builtin members and receiver-specific operations.
|
||||
* Signature collection, argument completion and function bodies have their
|
||||
@@ -93,6 +94,8 @@ import { lowerDynArrayFilterCall, lowerDynArrayFlatMapCall } from "./containers/
|
||||
import { lowerGroupByStaticCall } from "./containers/collection-grouping.js";
|
||||
import {
|
||||
lowerGeneratorHelperCall,
|
||||
lowerHelperReturnCall,
|
||||
lowerIteratorFromCall,
|
||||
lowerIteratorHelperCall,
|
||||
} from "./containers/iterator-helpers.js";
|
||||
import {
|
||||
@@ -227,6 +230,7 @@ import {
|
||||
completeArgs,
|
||||
} from "./call-arguments.js";
|
||||
import { isThisParameter, type ParamShape } from "./call-signatures.js";
|
||||
import { lowerStringIteratorValue } from "./iterator-adapters.js";
|
||||
import { checkedArmExtraction } from "./coercions/unions.js";
|
||||
import { maybePromiseReturnStmt, promiseArmedUnionOf } from "./maybe-promise.js";
|
||||
import {
|
||||
@@ -369,6 +373,9 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
|
||||
// machinery (the member always exists on a POSIX target, so the
|
||||
// optional call IS the call; `process.getuid` itself has no value
|
||||
// lowering for the chain to guard).
|
||||
// `h.return!()` / `h.return?.()` on a helper object: the method exists.
|
||||
const helperReturn = lowerHelperReturnCall(lowerer, expr);
|
||||
if (helperReturn) return helperReturn;
|
||||
const processOptional = lowerer.lowerProcessOptionalMethodCall(expr);
|
||||
if (processOptional) return processOptional;
|
||||
const reqStreamOptional = lowerCompatReqStreamOptionalCall(lowerer, expr);
|
||||
@@ -1469,6 +1476,8 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
|
||||
// compile-time-known, so a literal key folds to a constant.
|
||||
lowerClassHasOwnPropertyCall(lowerer, expr, expr.expression) ??
|
||||
lowerObjectOwnMethodCall(lowerer, expr, expr.expression) ??
|
||||
// Static Response.json(data, init?) builds a native Response.
|
||||
lowerResponseStaticJson(lowerer, expr) ??
|
||||
// Refuse Response constructor operations before generic-call
|
||||
// fallbacks can report an unrelated SC1090 diagnostic.
|
||||
lowerer.fenceUnsupportedFetchConstructorMember(expr.expression) ??
|
||||
@@ -1621,6 +1630,17 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
|
||||
lowerer.unsupported("SC1090", expr, `method calls like '${expr.expression.getText()}'`);
|
||||
}
|
||||
|
||||
// `s[Symbol.iterator]()` on a string: the string iterator as a value.
|
||||
if (
|
||||
ts.isElementAccessExpression(expr.expression) &&
|
||||
!expr.expression.questionDotToken &&
|
||||
!expr.questionDotToken &&
|
||||
expr.arguments.length === 0
|
||||
) {
|
||||
const iterator = lowerStringIteratorCall(lowerer, expr, expr.expression);
|
||||
if (iterator) return iterator;
|
||||
}
|
||||
|
||||
// The element spelling of an unsupported native Web method must fence
|
||||
// before lowering the callee into a checked-dynamic keyed read.
|
||||
if (ts.isElementAccessExpression(expr.expression)) {
|
||||
@@ -3162,10 +3182,32 @@ function lowerOptionalStringNumber(
|
||||
return { kind: "call", callee: helper, args: [arg], type: F64, loc };
|
||||
}
|
||||
|
||||
/** Iterator types with checked runtime receivers: Map/Set iterators'
|
||||
* helper calls lower in lowerGeneratorHelperCall; the string iterator's
|
||||
* have no lowering. */
|
||||
const UNLOWERED_ITERATOR_TYPES = new Set(["MapIterator", "SetIterator", "StringIterator"]);
|
||||
/** `s[Symbol.iterator]()` on a string-typed receiver: the generator over
|
||||
* its code points. Null for other element calls. */
|
||||
function lowerStringIteratorCall(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
access: ts.ElementAccessExpression,
|
||||
): IrExpr | null {
|
||||
let key = access.argumentExpression;
|
||||
while (ts.isParenthesizedExpression(key)) key = key.expression;
|
||||
if (
|
||||
!ts.isPropertyAccessExpression(key) ||
|
||||
lowerer.stdlibGlobalMember(key, "Symbol") !== "iterator"
|
||||
)
|
||||
return null;
|
||||
if (lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind !== "string") return null;
|
||||
const type = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
if (type?.kind !== "generator") return null;
|
||||
const receiver = lowerer.lowerExpr(access.expression);
|
||||
const value = lowerStringIteratorValue(lowerer, receiver, type, locOf(call));
|
||||
if (!value) lowerer.unsupported("SC1090", call, "this string iterator type");
|
||||
return value;
|
||||
}
|
||||
|
||||
/** Iterator types with checked runtime receivers, whose helper calls lower
|
||||
* in lowerGeneratorHelperCall. */
|
||||
const CHECKED_ITERATOR_TYPES = new Set(["MapIterator", "SetIterator"]);
|
||||
const ITERATOR_HELPER_METHODS = new Set([
|
||||
"map",
|
||||
"filter",
|
||||
@@ -3225,25 +3267,17 @@ function lowerDynReceiverMethodCall(
|
||||
const recvSymbol = recvTs.getSymbol();
|
||||
if (
|
||||
recvSymbol &&
|
||||
UNLOWERED_ITERATOR_TYPES.has(recvSymbol.name) &&
|
||||
CHECKED_ITERATOR_TYPES.has(recvSymbol.name) &&
|
||||
lowerer.isStdlibSymbol(recvSymbol) &&
|
||||
ITERATOR_HELPER_METHODS.has(access.name.text) &&
|
||||
lowerer.isStdlibMember(access)
|
||||
) {
|
||||
if (recvSymbol.name === "MapIterator" || recvSymbol.name === "SetIterator") {
|
||||
const helper = lowerGeneratorHelperCall(lowerer, call, access);
|
||||
if (helper) return helper;
|
||||
lowerer.noLowering(
|
||||
`collection iterator .${access.name.text}()`,
|
||||
call,
|
||||
"stored Map/Set iterators support .next() and iteration; iterator helper methods have no lowering",
|
||||
);
|
||||
}
|
||||
// The checked runtime's string iterator has no helper methods.
|
||||
const helper = lowerGeneratorHelperCall(lowerer, call, access);
|
||||
if (helper) return helper;
|
||||
lowerer.noLowering(
|
||||
`${recvSymbol.name}.${access.name.text}`,
|
||||
`collection iterator .${access.name.text}()`,
|
||||
call,
|
||||
"iterator helpers lower on generators and on array, Map and Set iterators — spread the string first: [...s].values()",
|
||||
"iterator helpers on Map/Set iterators are called directly on the iterator, without optional chaining",
|
||||
);
|
||||
}
|
||||
const arrayReceiver = lowerer.checker.isArrayType(recvTs);
|
||||
@@ -4378,8 +4412,15 @@ function lowerUnionToStringCall(
|
||||
(a.kind === "bytes" && a.elem === "u8"),
|
||||
);
|
||||
if (!stringable) return null;
|
||||
const operand = lowerer.lowerExpr(access.expression);
|
||||
let operand = lowerer.lowerExpr(access.expression);
|
||||
if (operand.type.kind !== "union") return null;
|
||||
// A flow-narrowed read keeps its wider storage union (`string | number`
|
||||
// after `!Array.isArray(h.host)` over a `string | number | string[]`
|
||||
// slot): retag it to the proven union before the per-union helper.
|
||||
if (operand.type.unionId !== recvT.unionId) {
|
||||
operand = lowerer.coerceInto(access.expression, operand, recvT);
|
||||
if (operand.type.kind !== "union" || operand.type.unionId !== recvT.unionId) return null;
|
||||
}
|
||||
return { kind: "toString", operand, type: STRING, loc: locOf(call) };
|
||||
}
|
||||
|
||||
@@ -6733,10 +6774,10 @@ function recordAssignHelper(
|
||||
return helper;
|
||||
}
|
||||
|
||||
/** The `Iterator` global's statics (ES2025 — Iterator.from, and the
|
||||
* abstract constructor as a value) have no lowering: every member fences
|
||||
* with the working spelling named instead of the generic-method fence's
|
||||
* monomorphization wording. */
|
||||
/** The `Iterator` global's statics (ES2025): `Iterator.from` over a typed
|
||||
* built-in iterable lowers (lowerIteratorFromCall); every other member,
|
||||
* and other arguments, fence with the working spelling named instead of
|
||||
* the generic-method fence's monomorphization wording. */
|
||||
function lowerIteratorStaticFence(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
@@ -6744,11 +6785,15 @@ function lowerIteratorStaticFence(
|
||||
): IrExpr | null {
|
||||
if (!lowerer.isStdlibGlobal(access.expression, "Iterator")) return null;
|
||||
if (!lowerer.isStdlibMember(access)) return null;
|
||||
const from = lowerIteratorFromCall(lowerer, call, access);
|
||||
if (from) return from;
|
||||
lowerer.noLowering(
|
||||
`Iterator.${access.name.text}`,
|
||||
call,
|
||||
"iterator helpers apply directly to generators and to array, Map and Set iterators: " +
|
||||
"arr.values().map(f).take(n).toArray()",
|
||||
access.name.text === "from"
|
||||
? "Iterator.from accepts arrays, strings, typed arrays, Sets, Maps and iterator objects such as generators"
|
||||
: "iterator helpers apply directly to generators and to array, Map and Set iterators: " +
|
||||
"arr.values().map(f).take(n).toArray()",
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2661,9 +2661,15 @@ export function collectClassShapeInner(
|
||||
? String(Number(member.name.text))
|
||||
: member.name.text;
|
||||
const fieldName = symbolKey?.fieldName ?? stringKey;
|
||||
// A TypeScript field without an initializer takes its declared
|
||||
// type and the identifier fields' definite-assignment rule (an
|
||||
// unconditional `this[key] = ...` at the constructor's top
|
||||
// level), checked with the unguarded fields below.
|
||||
const declaredOnly =
|
||||
!member.initializer && !isJsSourceFile(member.getSourceFile()) && !!member.type;
|
||||
if (
|
||||
fieldName === null ||
|
||||
(!member.initializer && !isJsSourceFile(member.getSourceFile()))
|
||||
(!member.initializer && !isJsSourceFile(member.getSourceFile()) && !declaredOnly)
|
||||
) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
@@ -2676,9 +2682,10 @@ export function collectClassShapeInner(
|
||||
member.type || !member.initializer
|
||||
? declared
|
||||
: lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(member.initializer));
|
||||
const mapped = member.initializer
|
||||
? (lowerer.mapTypeOf(inferred) ?? dynFallbackType(lowerer, member, inferred))
|
||||
: DYN;
|
||||
const mapped =
|
||||
member.initializer || declaredOnly
|
||||
? (lowerer.mapTypeOf(inferred) ?? dynFallbackType(lowerer, member, inferred))
|
||||
: DYN;
|
||||
let type =
|
||||
mapped?.kind === "record" && isJsSourceFile(member.getSourceFile()) && !member.type
|
||||
? DYN
|
||||
@@ -2690,7 +2697,7 @@ export function collectClassShapeInner(
|
||||
type.kind === "void" ||
|
||||
(type.kind === "dyn" &&
|
||||
!isJsSourceFile(member.getSourceFile()) &&
|
||||
!lowerer.classImplementedProtocol(inferred))
|
||||
!lowerer.classFieldStoresDyn(inferred))
|
||||
)
|
||||
lowerer.badType(member, inferred);
|
||||
if (isJsSourceFile(member.getSourceFile())) {
|
||||
@@ -2698,7 +2705,24 @@ export function collectClassShapeInner(
|
||||
if (base?.fields.get(fieldName)?.kind === "dyn") type = DYN;
|
||||
if (base && !fields.has(fieldName) && lowerer.dynConvertible(type)) type = DYN;
|
||||
}
|
||||
const undefinedInitializer = !member.initializer ? locOf(member) : undefined;
|
||||
const undefinedInitializer =
|
||||
!member.initializer && (!declaredOnly || type.kind === "dyn")
|
||||
? locOf(member)
|
||||
: undefined;
|
||||
if (
|
||||
declaredOnly &&
|
||||
type.kind !== "dyn" &&
|
||||
!(
|
||||
type.kind === "union" &&
|
||||
lowerer.unions.get(type.unionId)?.arms.some((a) => a.kind === "undefinedT")
|
||||
)
|
||||
) {
|
||||
unguardedFields.push({
|
||||
node: member.name,
|
||||
name: fieldName,
|
||||
why: `initializer-less computed fields not assigned at the constructor's top level (nothing guarantees '${symbolKey ? symbolFieldDisplayName(fieldName) : fieldName}' is assigned before a read — assign it unconditionally at the top of the constructor, give it an initializer, or include undefined in its type)`,
|
||||
});
|
||||
}
|
||||
const previous = fields.get(fieldName);
|
||||
if (previous) {
|
||||
if (symbolKey && !symbolFields.has(symbolKey.identity)) {
|
||||
@@ -3458,7 +3482,17 @@ export function collectClassShapeInner(
|
||||
}
|
||||
let sig: { params: ParamShape[]; ret: IrType };
|
||||
if (isGet) {
|
||||
const ret = lowerer.declaredReturnType(member, member.name);
|
||||
let ret = lowerer.declaredReturnType(member, member.name);
|
||||
// A getter that never returns a value: typed `never` (it only
|
||||
// throws, Next.js's removed `page`/`ua` getters) or, unannotated
|
||||
// with no return statement, inferred `void`. A read answers
|
||||
// undefined or throws, so the property takes the checked-dynamic
|
||||
// slot instead of VOID.
|
||||
if (
|
||||
ret.kind === "void" &&
|
||||
(lowerer.typeOf(member.name).flags & (ts.TypeFlags.Never | ts.TypeFlags.Void)) !== 0
|
||||
)
|
||||
ret = DYN;
|
||||
if (ret.kind === "void") lowerer.badType(member.name, lowerer.typeOf(member.name));
|
||||
sig = { params: [], ret };
|
||||
} else {
|
||||
@@ -3605,6 +3639,16 @@ export function collectClassShapeInner(
|
||||
stmt.expression.left.expression.kind === ts.SyntaxKind.ThisKeyword
|
||||
) {
|
||||
topAssigned.add(stmt.expression.left.name.text);
|
||||
} else if (
|
||||
ts.isExpressionStatement(stmt) &&
|
||||
ts.isBinaryExpression(stmt.expression) &&
|
||||
stmt.expression.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
||||
ts.isElementAccessExpression(stmt.expression.left) &&
|
||||
stmt.expression.left.expression.kind === ts.SyntaxKind.ThisKeyword
|
||||
) {
|
||||
const key = stmt.expression.left.argumentExpression;
|
||||
const name = classSymbolKeyOf(lowerer, key)?.fieldName ?? lowerer.foldedStringKeyOf(key);
|
||||
if (name !== null) topAssigned.add(name);
|
||||
}
|
||||
}
|
||||
for (const f of unguardedFields) {
|
||||
|
||||
@@ -147,6 +147,7 @@ import {
|
||||
} from "../../diagnostics/diagnostic.js";
|
||||
import { PoisonError, dynFallbackType, jsFuncNameOf, neverTaintedJsType, own } from "./lowerer.js";
|
||||
import { lowerCollectionSpread } from "./containers/collection-methods.js";
|
||||
import { lowerHelperReturnCall } from "./containers/iterator-helpers.js";
|
||||
import {
|
||||
arrayNumberRead,
|
||||
arrayValueRead,
|
||||
@@ -1931,6 +1932,10 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
if (ts.isPostfixUnaryExpression(expr)) return lowerIncDec(lowerer, expr, false);
|
||||
if (ts.isBinaryExpression(expr)) return lowerer.lowerBinary(expr);
|
||||
if (ts.isCallExpression(expr)) {
|
||||
// `h.return?.()` on a helper object: the method exists, so the
|
||||
// optional call is the call.
|
||||
const helperReturn = lowerHelperReturnCall(lowerer, expr);
|
||||
if (helperReturn) return helperReturn;
|
||||
// The tail of an optional chain whose `?.` sits deeper
|
||||
// (`x?.trim().toLowerCase()`): the whole tail short-circuits together.
|
||||
if (isOptionalChainTail(lowerer, expr)) return lowerer.lowerOptionalChain(expr);
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
type IrExpr,
|
||||
type IrStmt,
|
||||
type IrType,
|
||||
type SrcLoc,
|
||||
STRING,
|
||||
VOID,
|
||||
canConvertToDyn,
|
||||
@@ -1932,11 +1933,39 @@ export function lowerResponseNew(lowerer: Lowerer, expr: ts.NewExpression): IrEx
|
||||
if (stdlibGlobalNameOf(lowerer, expr.expression) !== "Response") return null;
|
||||
const args = expr.arguments ?? [];
|
||||
if (args.some(ts.isSpreadElement)) return null;
|
||||
const loc = locOf(expr);
|
||||
return lowerResponseConstruction(lowerer, args, locOf(expr), false);
|
||||
}
|
||||
|
||||
/** Static `Response.json(data, init?)`: the data crosses into the runtime's
|
||||
* checked-dynamic JSON serializer (toJSON hooks included) after ResponseInit
|
||||
* converts, then the response initializes as the constructor does. */
|
||||
export function lowerResponseStaticJson(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null {
|
||||
if (call.questionDotToken || !ts.isPropertyAccessExpression(call.expression)) return null;
|
||||
const access = call.expression;
|
||||
if (access.questionDotToken || access.name.text !== "json") return null;
|
||||
if (!lowerer.isStdlibGlobal(access.expression, "Response")) return null;
|
||||
if (call.arguments.some(ts.isSpreadElement) || call.arguments.length === 0) return null;
|
||||
return lowerResponseConstruction(lowerer, call.arguments, locOf(call), true);
|
||||
}
|
||||
|
||||
function lowerResponseConstruction(
|
||||
lowerer: Lowerer,
|
||||
args: readonly ts.Expression[],
|
||||
loc: SrcLoc,
|
||||
json: boolean,
|
||||
): IrExpr {
|
||||
const toDyn = (node: ts.Expression | undefined, value: IrExpr, what: string): IrExpr => {
|
||||
if (node === undefined) return dynUndefinedExpr(loc);
|
||||
if (value.type.kind === "dyn") return value;
|
||||
if (what === "JSON data") {
|
||||
const boxed = lowerer.coerceToExpected(value, DYN);
|
||||
if (boxed.type.kind === "dyn") return boxed;
|
||||
lowerer.noLowering(
|
||||
`Response.json of '${lowerer.fmt(value.type)}' values`,
|
||||
node,
|
||||
"the data must cross the checked-dynamic boundary (number, string, boolean, null, records, arrays, and 'unknown')",
|
||||
);
|
||||
}
|
||||
/* A user class can satisfy ResponseInit structurally through plain
|
||||
* fields. Project just the declared dictionary members into a record,
|
||||
* then use the ordinary checked-dynamic copy. objRecordWidthHelper
|
||||
@@ -2059,8 +2088,11 @@ export function lowerResponseNew(lowerer: Lowerer, expr: ts.NewExpression): IrEx
|
||||
}
|
||||
const result: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: "fetch.responseNew",
|
||||
args: [toDyn(bodyNode, bodyRef, "a body"), toDyn(initNode, initRef, "an init value")],
|
||||
fn: json ? "fetch.responseJsonStatic" : "fetch.responseNew",
|
||||
args: [
|
||||
toDyn(bodyNode, bodyRef, json ? "JSON data" : "a body"),
|
||||
toDyn(initNode, initRef, "an init value"),
|
||||
],
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { dynUndefinedExpr, numLit, varRef } from "../../ir/build.js";
|
||||
import { lowerCollectionSpread } from "./containers/collection-methods.js";
|
||||
import { lowerEventObjectPropertyWrite } from "./lower-event-target.js";
|
||||
import { lowerDynObjectLiteral } from "./expressions/object-literals.js";
|
||||
import { importMetaBindingSource } from "./import-meta.js";
|
||||
@@ -2517,6 +2518,39 @@ export function lowerBindingPattern(
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Map and Set sources (`const [[k, v], ...rest] = map`): the iterator
|
||||
// yields the collection's entries (Map pairs) or values in order, so
|
||||
// the source snapshots ONCE into that array — the spread's own
|
||||
// machinery — and the pattern lowers as an array pattern over it,
|
||||
// like the string case above. Element defaults keep a fence: they
|
||||
// run between iterator steps and could mutate the live collection.
|
||||
if (srcType.kind === "map" || srcType.kind === "set") {
|
||||
const defaulted = pattern.elements.find(
|
||||
(el): el is ts.BindingElement =>
|
||||
ts.isBindingElement(el) && el.initializer !== undefined && !el.dotDotDotToken,
|
||||
);
|
||||
if (defaulted)
|
||||
lowerer.unsupported("SC1031", defaulted, "binding defaults over Map/Set sources");
|
||||
const snapshot = lowerCollectionSpread(
|
||||
lowerer,
|
||||
srcRef(),
|
||||
pattern as unknown as ts.Expression,
|
||||
);
|
||||
if (snapshot && snapshot.type.kind === "array") {
|
||||
const loc = locOf(pattern);
|
||||
const snapshotT = snapshot.type;
|
||||
const entries = lowerer.declareHiddenLocal("%dentries", snapshotT);
|
||||
out.push({ kind: "varDecl", localId: entries.id, init: snapshot, loc });
|
||||
lowerer.lowerBindingPattern(
|
||||
pattern,
|
||||
() => ({ kind: "varRef", localId: entries.id, type: snapshotT, loc }),
|
||||
snapshotT,
|
||||
isLet,
|
||||
out,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (srcType.kind !== "array") {
|
||||
lowerer.unsupported(
|
||||
"SC1031",
|
||||
|
||||
@@ -242,6 +242,7 @@ import {
|
||||
isOverloadedCallableMemberType,
|
||||
isUnitOnlyTsType,
|
||||
isWorkerHandleType,
|
||||
isFetchHandleType,
|
||||
jsOpenObjectType,
|
||||
mapType,
|
||||
ShapeRegistry,
|
||||
@@ -3115,12 +3116,14 @@ export class Lowerer {
|
||||
/** Declared class fields may store a DYN value only when its static type
|
||||
* is `unknown` or `object` (also with `null` or `undefined`, or an array
|
||||
* of those), names checked protocol identity, or is an owned runtime
|
||||
* handle (a Worker or MessagePort); `any` fields stay refused. */
|
||||
* handle (a Worker or MessagePort, or a fetch Headers/Request/Response);
|
||||
* `any` fields stay refused. */
|
||||
classFieldStoresDyn(type: ts.Type): boolean {
|
||||
return (
|
||||
this.opaqueValueType(type) ||
|
||||
this.classImplementedProtocol(type) ||
|
||||
isWorkerHandleType(type, this.typeCtx)
|
||||
isWorkerHandleType(type, this.typeCtx) ||
|
||||
isFetchHandleType(type, this.typeCtx)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2227,6 +2227,13 @@ export const AMBIENT_SURFACE_FNS: readonly AmbientSurfaceRow[] = [
|
||||
fns: ["date.toISOString", "date.toISOStringValue"],
|
||||
note: "UTC ISO formatting over constructed and stored Date values",
|
||||
},
|
||||
{
|
||||
id: "stdlib.date.toUTCString",
|
||||
kind: "stdlib",
|
||||
name: "Date.prototype.toUTCString",
|
||||
fns: ["date.toUTCString"],
|
||||
note: "RFC 7231 IMF-fixdate formatting over stored Date values",
|
||||
},
|
||||
{
|
||||
id: "stdlib.date.getFullYear",
|
||||
kind: "stdlib",
|
||||
|
||||
@@ -1336,6 +1336,40 @@ export function isWorkerHandleType(
|
||||
return handle;
|
||||
}
|
||||
|
||||
/** The native fetch handles (Headers, Request, Response), optionally in
|
||||
* arrays or with null/undefined: checked-dynamic handles wherever they are
|
||||
* stored, like the worker handles above. */
|
||||
export function isFetchHandleType(
|
||||
type: ts.Type,
|
||||
ctx: Pick<TypeMapperCtx, "checker" | "isStdlibFile">,
|
||||
): boolean {
|
||||
const members = type.isUnionType() ? ts.constituentTypes(type) : [type];
|
||||
let handle = false;
|
||||
for (const member of members) {
|
||||
if (member.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) continue;
|
||||
if (ctx.checker.isArrayType(member)) {
|
||||
const element = ctx.checker.getTypeArguments(member as ts.TypeReference)[0];
|
||||
if (element === undefined || !isFetchHandleType(element, ctx)) return false;
|
||||
} else {
|
||||
const symbol = member.getSymbol();
|
||||
if (
|
||||
!symbol ||
|
||||
(symbol.name !== "Headers" && symbol.name !== "Request" && symbol.name !== "Response") ||
|
||||
!ctx.checker
|
||||
.declarationsOf(symbol)
|
||||
.some(
|
||||
(d) =>
|
||||
(ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) &&
|
||||
ctx.isStdlibFile(d.getSourceFile()),
|
||||
)
|
||||
)
|
||||
return false;
|
||||
}
|
||||
handle = true;
|
||||
}
|
||||
return handle;
|
||||
}
|
||||
|
||||
export function mapType(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
if (mapTypeDepth >= MAP_TYPE_MAX_DEPTH) return null;
|
||||
// These intrinsic domains cannot depend on generic bindings, declaration
|
||||
@@ -3233,8 +3267,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
// Native collection iterators use checked runtime iterator objects.
|
||||
if (isStdlibInterface("MapIterator") || isStdlibInterface("SetIterator")) return DYN;
|
||||
// Array iterators (`list.values()` and friends) are generators over the
|
||||
// live array: each step reads the current length and element.
|
||||
if (isStdlibInterface("ArrayIterator")) {
|
||||
// live array: each step reads the current length and element. String
|
||||
// iterators (`s[Symbol.iterator]()`) are generators over the code points.
|
||||
if (isStdlibInterface("ArrayIterator") || isStdlibInterface("StringIterator")) {
|
||||
const arg = checker.getTypeArguments(widened as ts.TypeReference)[0];
|
||||
const yieldT = arg ? mapType(arg, ctx) : null;
|
||||
if (!yieldT || yieldT.kind === "void" || isUnitType(yieldT)) return null;
|
||||
|
||||
@@ -119,6 +119,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"fetch.requestNew",
|
||||
"fetch.headersNew",
|
||||
"fetch.responseNew",
|
||||
"fetch.responseJsonStatic",
|
||||
"fetch.abortTimeout",
|
||||
"fetch.abortAny",
|
||||
"abort.timeout",
|
||||
|
||||
@@ -64,6 +64,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
* are opaque checked-dynamic handles. */
|
||||
"fetch.start": { argTypes: [STRING, DYN], result: { kind: "promise", inner: DYN } },
|
||||
"fetch.responseNew": { argTypes: [DYN, DYN], result: DYN },
|
||||
"fetch.responseJsonStatic": { argTypes: [DYN, DYN], result: DYN },
|
||||
"fetch.input": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } },
|
||||
"abort.controllerNew": { argTypes: [], result: DYN },
|
||||
/** EventTarget / Event / CustomEvent construction (scr_event_target.c):
|
||||
@@ -2299,11 +2300,13 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
result: VOID,
|
||||
},
|
||||
/** execFileSync/execSync as ONE entry (args: cmd, argv, shell, input,
|
||||
* cwd, hasEnv, envPairs, timeoutMs, stdoutMode, stderrMode — see
|
||||
* scr_runtime.h). Throws Node's exact errors: "Command failed: <cmd>"
|
||||
* cwd, hasEnv, envPairs, timeoutMs, stdoutMode, stderrMode, buffers —
|
||||
* see scr_child.h). Throws Node's exact errors: "Command failed: <cmd>"
|
||||
* (+ captured stderr) on non-zero exit or signal death, "spawnSync
|
||||
* <file> ENOENT" on spawn failure, "spawnSync <file> ETIMEDOUT" after
|
||||
* the SIGTERM timeout. Result is the captured utf8 stdout (+1). */
|
||||
* the SIGTERM timeout, with Node's status, signal, output, pid, stdout
|
||||
* and stderr properties (Buffer outputs when buffers is set: no
|
||||
* encoding option). Result is the captured utf8 stdout (+1). */
|
||||
"cp.execSync": {
|
||||
argTypes: [
|
||||
STRING,
|
||||
@@ -2317,6 +2320,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
F64,
|
||||
F64,
|
||||
F64,
|
||||
BOOL,
|
||||
],
|
||||
result: STRING,
|
||||
},
|
||||
@@ -4219,6 +4223,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
"date.valueOf": { argTypes: [DATE_T], result: F64 },
|
||||
"date.toISOString": { argTypes: [F64], result: STRING },
|
||||
"date.toISOStringValue": { argTypes: [DATE_T], result: STRING },
|
||||
/** Date.prototype.toUTCString over a stored Date ("Invalid Date" for
|
||||
* NaN; never throws). */
|
||||
"date.toUTCString": { argTypes: [DATE_T], result: STRING },
|
||||
"date.getFullYear": { argTypes: [DATE_T], result: F64 },
|
||||
"date.getUTCFullYear": { argTypes: [DATE_T], result: F64 },
|
||||
"date.getMonth": { argTypes: [DATE_T], result: F64 },
|
||||
|
||||
@@ -879,6 +879,33 @@ export function typeKey(t: IrType): string {
|
||||
}
|
||||
}
|
||||
|
||||
/** The type a CALL SITE (or function value) receives from a module
|
||||
* function: an async body's returnType is the promise's INNER type, so
|
||||
* calls and closures over it see Promise<T>; a generator body's is the
|
||||
* TReturn channel, so call sites see the generator type. The one place
|
||||
* the body/call-site split is spelled out, for the validator and lowering. */
|
||||
export function callSiteReturnType(fn: IrFunction): IrType {
|
||||
if (fn.async && fn.generator !== undefined) {
|
||||
return {
|
||||
kind: "generator",
|
||||
async: true,
|
||||
yieldT: fn.generator.yieldT,
|
||||
retT: fn.returnType,
|
||||
nextT: fn.generator.nextT,
|
||||
};
|
||||
}
|
||||
if (fn.async) return { kind: "promise", inner: fn.returnType };
|
||||
if (fn.generator !== undefined) {
|
||||
return {
|
||||
kind: "generator",
|
||||
yieldT: fn.generator.yieldT,
|
||||
retT: fn.returnType,
|
||||
nextT: fn.generator.nextT,
|
||||
};
|
||||
}
|
||||
return fn.returnType;
|
||||
}
|
||||
|
||||
export function typeEquals(a: IrType, b: IrType): boolean {
|
||||
// Dispatch once: a chain of negative narrowing checks repeatedly retags
|
||||
// the remaining variants when this comparator runs in the native compiler.
|
||||
|
||||
@@ -56,7 +56,7 @@ import {
|
||||
maybePromiseUnionArmsOk,
|
||||
VOID,
|
||||
} from "./ir.js";
|
||||
import { DYN_CLASS_PROPERTIES } from "./ir.js";
|
||||
import { DYN_CLASS_PROPERTIES, callSiteReturnType } from "./ir.js";
|
||||
import { unionWideningTags } from "./analysis.js";
|
||||
import { alwaysReturns } from "./control-flow.js";
|
||||
import { dynamicOperationOf, functionTier } from "./tiers.js";
|
||||
@@ -85,33 +85,6 @@ export interface IrValidationError {
|
||||
loc: SrcLoc;
|
||||
}
|
||||
|
||||
/** The type a CALL SITE (or function value) receives from a module
|
||||
* function: an async body's returnType is the promise's INNER type, so
|
||||
* calls and closures over it see Promise<T>; a generator body's is the
|
||||
* TReturn channel, so call sites see the generator type. The one place
|
||||
* the body/call-site split is spelled out in the validator. */
|
||||
function callSiteReturnType(fn: IrFunction): IrType {
|
||||
if (fn.async && fn.generator !== undefined) {
|
||||
return {
|
||||
kind: "generator",
|
||||
async: true,
|
||||
yieldT: fn.generator.yieldT,
|
||||
retT: fn.returnType,
|
||||
nextT: fn.generator.nextT,
|
||||
};
|
||||
}
|
||||
if (fn.async) return { kind: "promise", inner: fn.returnType };
|
||||
if (fn.generator !== undefined) {
|
||||
return {
|
||||
kind: "generator",
|
||||
yieldT: fn.generator.yieldT,
|
||||
retT: fn.returnType,
|
||||
nextT: fn.generator.nextT,
|
||||
};
|
||||
}
|
||||
return fn.returnType;
|
||||
}
|
||||
|
||||
interface VirtualCallInfo {
|
||||
declared: boolean;
|
||||
hasOverride: boolean;
|
||||
|
||||
@@ -4229,6 +4229,13 @@
|
||||
"status": "static",
|
||||
"note": "UTC ISO formatting over constructed and stored Date values"
|
||||
},
|
||||
{
|
||||
"id": "stdlib.date.toUTCString",
|
||||
"kind": "stdlib",
|
||||
"name": "Date.prototype.toUTCString",
|
||||
"status": "static",
|
||||
"note": "RFC 7231 IMF-fixdate formatting over stored Date values"
|
||||
},
|
||||
{
|
||||
"id": "stdlib.date.valueOf",
|
||||
"kind": "stdlib",
|
||||
@@ -5306,9 +5313,8 @@
|
||||
"id": "stdlib.response.static.json",
|
||||
"kind": "stdlib",
|
||||
"name": "Response.json",
|
||||
"status": "unsupported",
|
||||
"code": "SC2020",
|
||||
"note": "Node 24.22.0 / Undici 7.29.1; Response static constructor-object operations have no compiler lowering"
|
||||
"status": "static",
|
||||
"note": "Node 24.22.0 / Undici 7.29.1; facets: webidl-conversion, error-shape; supported scope: data that crosses the checked-dynamic boundary, serialized with JSON.stringify (toJSON hooks included); ResponseInit as for the constructor; differential evidence: fixture:static-response-json"
|
||||
},
|
||||
{
|
||||
"id": "stdlib.response.static.redirect",
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5634-error-extra-props-inspect.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5635-error-extra-props-retry-loop.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5636-child-error-props.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6071-iterator-sources.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6072-iterator-sources-js.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/class-devirtualized-this-return.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/class-fetch-handle-fields.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/class-never-getter.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/class-symbol-field-constructor-assigned.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/date-to-utc-string.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/destructure-map-set-iterables.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/fetch-headers-set-cookie-iteration.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/fetch-invalid-url-cause.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/fetch-request-set-cookie-wire.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/generic-undefined-argument.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/json-stringify-class-tojson.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/json-stringify-unknown-undefined-root.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/object-spread-and-conditional.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/optional-call-native-method.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/union-narrowed-tostring.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/string-iterator-helpers.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -167,19 +167,16 @@ static ScrDyn *scr_assert_dyn_text(const char *text) {
|
||||
return d;
|
||||
}
|
||||
|
||||
/* Sets the properties on the error's view in Node's order. The values
|
||||
* move in. */
|
||||
/* Sets the error's own properties in Node's order. The values move in. */
|
||||
static void scr_assert_attach_values(ScrError *e, bool generated, ScrDyn *actual, ScrDyn *expected,
|
||||
ScrDyn *op, ScrDyn *diff) {
|
||||
ScrDyn *view = scr_dyn_from_error(e);
|
||||
scr_dyn_obj_set(view, "generatedMessage", 16, scr_dyn_new_bool(generated));
|
||||
scr_error_set_prop(e, "generatedMessage", 16, scr_dyn_new_bool(generated));
|
||||
scr_error_set_code(e, "ERR_ASSERTION");
|
||||
scr_dyn_obj_set(view, "code", 4, scr_assert_dyn_text("ERR_ASSERTION"));
|
||||
scr_dyn_obj_set(view, "actual", 6, actual);
|
||||
scr_dyn_obj_set(view, "expected", 8, expected);
|
||||
scr_dyn_obj_set(view, "operator", 8, op);
|
||||
scr_dyn_obj_set(view, "diff", 4, diff);
|
||||
scr_dyn_release(view);
|
||||
scr_error_set_prop(e, "code", 4, scr_assert_dyn_text("ERR_ASSERTION"));
|
||||
scr_error_set_prop(e, "actual", 6, actual);
|
||||
scr_error_set_prop(e, "expected", 8, expected);
|
||||
scr_error_set_prop(e, "operator", 8, op);
|
||||
scr_error_set_prop(e, "diff", 4, diff);
|
||||
}
|
||||
|
||||
static ScrError *scr_assert_error(ScrStr *message, bool generated, const char *diff) {
|
||||
|
||||
@@ -1036,11 +1036,9 @@ void scr_process_emit_warning(ScrDyn *args) {
|
||||
e->name = type ? scr_str_retain((ScrStr *)type) : scr_str_new("Warning", 7);
|
||||
e->name_present = true;
|
||||
if (code) e->code = scr_str_retain((ScrStr *)code);
|
||||
if (detail) scr_error_set_prop(e, "detail", 6, scr_dyn_new_str((ScrStr *)detail)); /* retains */
|
||||
ScrDyn *w = scr_dyn_from_error(e);
|
||||
scr_error_release(e);
|
||||
if (detail) {
|
||||
scr_dyn_obj_set(w, "detail", 6, scr_dyn_new_str((ScrStr *)detail)); /* retains */
|
||||
}
|
||||
scr_warning_emit_deferred(w);
|
||||
scr_dyn_release(w);
|
||||
}
|
||||
|
||||
@@ -62,6 +62,118 @@ enum {
|
||||
SCR_CHILD_DATA_STRING = 2,
|
||||
};
|
||||
|
||||
/* ── error properties (lib/child_process.js) ──────────────────────────
|
||||
* The own properties Node attaches to child_process errors, set on the
|
||||
* error's checked-dynamic node (scr_error_set_prop) in Node's key order. */
|
||||
|
||||
static ScrDyn *scr_child_dyn_cstr(const char *text) {
|
||||
ScrStr *s = scr_str_new(text, strlen(text));
|
||||
ScrDyn *d = scr_dyn_new_str(s);
|
||||
scr_str_release(s);
|
||||
return d;
|
||||
}
|
||||
|
||||
static ScrDyn *scr_child_dyn_text_or_null(const char *text) {
|
||||
return text != NULL ? scr_child_dyn_cstr(text) : scr_dyn_new_null();
|
||||
}
|
||||
|
||||
/* A spawn failure's errnoException fields: errno, code (already stamped),
|
||||
* syscall "<verb> <file>" and path come from the native system fields,
|
||||
* then spawnargs (the arguments after the file). Set before any other
|
||||
* property, so the node orders them first. */
|
||||
static void scr_child_error_spawn_fields(ScrError *e, double uv_errno, const char *verb,
|
||||
ScrStr *file, ScrArr *args) {
|
||||
size_t cap = strlen(verb) + 1 + file->len + 1;
|
||||
char *call = malloc(cap);
|
||||
if (!call) scr_trap("scriptc: out of memory\n");
|
||||
int len = snprintf(call, cap, "%s %s", verb, file->data);
|
||||
e->system_errno = uv_errno;
|
||||
e->system_call = scr_str_new(call, (size_t)len);
|
||||
free(call);
|
||||
e->system_path = scr_str_retain(file);
|
||||
ScrDyn *spawnargs = scr_dyn_new_arr();
|
||||
size_t n = args != NULL ? (size_t)scr_arr_len(args) : 0;
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
ScrStr *arg = (ScrStr *)scr_arr_get_ref(args, (double)i);
|
||||
scr_dyn_arr_push(spawnargs, scr_dyn_new_str(arg));
|
||||
scr_str_release(arg);
|
||||
}
|
||||
scr_error_set_prop(e, "spawnargs", 9, spawnargs);
|
||||
}
|
||||
|
||||
/* exec/execFile's command failure: code (the exit status, or null after a
|
||||
* signal), killed and signal, before cmd. */
|
||||
static void scr_child_error_exit_fields(ScrError *e, bool has_code, double code,
|
||||
const char *signal, bool killed) {
|
||||
scr_error_set_prop(e, "code", 4, has_code ? scr_dyn_new_num(code) : scr_dyn_new_null());
|
||||
scr_error_set_prop(e, "killed", 6, scr_dyn_new_bool(killed));
|
||||
scr_error_set_prop(e, "signal", 6, scr_child_dyn_text_or_null(signal));
|
||||
}
|
||||
|
||||
/* The sync forms' spawn failure and timeout carry the error itself as
|
||||
* `error` (Node assigns the spawnSync result, whose error it is). */
|
||||
static void scr_child_error_self(ScrError *e) {
|
||||
scr_error_set_prop(e, "error", 5, scr_dyn_from_error(e));
|
||||
}
|
||||
|
||||
static void scr_child_error_cmd(ScrError *e, const char *cmd) {
|
||||
scr_error_set_prop(e, "cmd", 3, scr_child_dyn_cstr(cmd));
|
||||
}
|
||||
|
||||
/* The promisified forms add the captured outputs. Borrows. */
|
||||
static void scr_child_error_outputs(ScrError *e, ScrStr *out, ScrStr *err) {
|
||||
scr_error_set_prop(e, "stdout", 6, scr_dyn_new_str(out));
|
||||
scr_error_set_prop(e, "stderr", 6, scr_dyn_new_str(err));
|
||||
}
|
||||
|
||||
/* A captured sync output as Node's result holds it: a Buffer, the utf8
|
||||
* string with an encoding option, or null for a stream that was not
|
||||
* piped. +1. */
|
||||
static ScrDyn *scr_child_sync_output(const char *data, size_t len, bool piped, bool buffers) {
|
||||
if (!piped) return scr_dyn_new_null();
|
||||
if (!buffers) {
|
||||
ScrStr *s = scr_str_new(data, len);
|
||||
ScrDyn *d = scr_dyn_new_str(s);
|
||||
scr_str_release(s);
|
||||
return d;
|
||||
}
|
||||
ScrBytes *b = scr_bytes_new(SCR_BYTES_U8, (double)len);
|
||||
b->is_buffer = true;
|
||||
if (len) memcpy(b->data, data, len);
|
||||
ScrDyn *d = scr_dyn_new_buffer(b);
|
||||
scr_bytes_release(b);
|
||||
return d;
|
||||
}
|
||||
|
||||
/* execSync/execFileSync: the spawnSync result's status, signal, output,
|
||||
* pid, stdout and stderr. out/err move in (null for a stream that was not
|
||||
* piped); output is [null, stdout, stderr]. A child that never started
|
||||
* has a null output and undefined outputs. */
|
||||
static void scr_child_error_sync_fields(ScrError *e, bool has_status, double status,
|
||||
const char *signal, double pid, bool started,
|
||||
ScrDyn *out, ScrDyn *err) {
|
||||
if (!started) {
|
||||
scr_dyn_release(out);
|
||||
scr_dyn_release(err);
|
||||
out = scr_dyn_retain(scr_dyn_undefined());
|
||||
err = scr_dyn_retain(scr_dyn_undefined());
|
||||
}
|
||||
scr_error_set_prop(e, "status", 6, has_status ? scr_dyn_new_num(status) : scr_dyn_new_null());
|
||||
scr_error_set_prop(e, "signal", 6, scr_child_dyn_text_or_null(signal));
|
||||
if (started) {
|
||||
ScrDyn *output = scr_dyn_new_arr();
|
||||
scr_dyn_arr_push(output, scr_dyn_new_null());
|
||||
scr_dyn_arr_push(output, scr_dyn_retain(out));
|
||||
scr_dyn_arr_push(output, scr_dyn_retain(err));
|
||||
scr_error_set_prop(e, "output", 6, output);
|
||||
} else {
|
||||
scr_error_set_prop(e, "output", 6, scr_dyn_new_null());
|
||||
}
|
||||
scr_error_set_prop(e, "pid", 3, scr_dyn_new_num(pid));
|
||||
scr_error_set_prop(e, "stdout", 6, out);
|
||||
scr_error_set_prop(e, "stderr", 6, err);
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
/* ── Windows arm: real children via CreateProcessW ────────────────────
|
||||
* The POSIX arm below owns the corresponding implementation; this arm
|
||||
@@ -766,6 +878,7 @@ typedef struct {
|
||||
const char *spawn_errname; /* NULL = the child ran (and was reaped) */
|
||||
bool timed_out; /* the deadline TerminateProcess fired */
|
||||
DWORD exit_code;
|
||||
DWORD pid; /* the child's process id (0 when it never ran) */
|
||||
ScrCapBuf out, err; /* always initialized; caller frees */
|
||||
} ScrWinSyncRes;
|
||||
|
||||
@@ -780,6 +893,7 @@ static void scr_win_run_sync(ScrStr *cmd, ScrArr *args, const ScrStr *input,
|
||||
res->spawn_errname = NULL;
|
||||
res->timed_out = false;
|
||||
res->exit_code = 0;
|
||||
res->pid = 0;
|
||||
scr_cap_init(&res->out);
|
||||
scr_cap_init(&res->err);
|
||||
|
||||
@@ -842,6 +956,7 @@ static void scr_win_run_sync(ScrStr *cmd, ScrArr *args, const ScrStr *input,
|
||||
res->spawn_errname = sp.errname;
|
||||
return;
|
||||
}
|
||||
res->pid = sp.pid;
|
||||
|
||||
/* Pump: drain both captures, feed stdin (PIPE_NOWAIT — a full pipe
|
||||
* writes short, never blocks), watch the deadline. The 1ms wait rides
|
||||
@@ -1082,8 +1197,8 @@ static char *scr_exec_display(ScrStr *cmd, ScrArr *args, bool shell) {
|
||||
return out;
|
||||
}
|
||||
|
||||
static void scr_exec_throw_failed(const char *display, const ScrCapBuf *err_cap,
|
||||
bool with_stderr, bool always_nl) {
|
||||
static ScrError *scr_exec_failed_error(const char *display, const ScrCapBuf *err_cap,
|
||||
bool with_stderr, bool always_nl) {
|
||||
size_t dlen = strlen(display);
|
||||
size_t elen = with_stderr ? err_cap->len : 0;
|
||||
size_t cap = 16 + dlen + 1 + elen + 1;
|
||||
@@ -1095,18 +1210,33 @@ static void scr_exec_throw_failed(const char *display, const ScrCapBuf *err_cap,
|
||||
memcpy(msg + at, err_cap->data, elen);
|
||||
at += elen;
|
||||
}
|
||||
scr_throw_error_msg(SCR_ERR_ERROR, msg, at);
|
||||
ScrStr *message = scr_str_new(msg, at);
|
||||
free(msg);
|
||||
ScrError *e = scr_error_new(SCR_ERR_ERROR, message);
|
||||
scr_str_release(message);
|
||||
return e;
|
||||
}
|
||||
|
||||
static void scr_exec_throw_spawn(ScrStr *cmd, const char *errname, bool async_shape) {
|
||||
/* Node's spawn-failure/timeout error with its errnoException fields
|
||||
* (errno is libuv's code for the name). +1. */
|
||||
static ScrError *scr_exec_spawn_error(ScrStr *cmd, ScrArr *args, const char *errname,
|
||||
bool async_shape) {
|
||||
const char *verb = async_shape ? "spawn" : "spawnSync";
|
||||
size_t cap = 10 + cmd->len + 1 + strlen(errname) + 1;
|
||||
char *msg = malloc(cap);
|
||||
if (!msg) scr_child_oom();
|
||||
int len = snprintf(msg, cap, "%s %s %s", async_shape ? "spawn" : "spawnSync",
|
||||
cmd->data, errname);
|
||||
scr_throw_error_msg_code(SCR_ERR_ERROR, msg, (size_t)len, errname);
|
||||
int len = snprintf(msg, cap, "%s %s %s", verb, cmd->data, errname);
|
||||
ScrStr *message = scr_str_new(msg, (size_t)len);
|
||||
free(msg);
|
||||
ScrError *e = scr_error_new(SCR_ERR_ERROR, message);
|
||||
scr_str_release(message);
|
||||
scr_error_set_code(e, errname);
|
||||
scr_child_error_spawn_fields(e, (double)scr_win_uv_errno(errname), verb, cmd, args);
|
||||
return e;
|
||||
}
|
||||
|
||||
static void scr_exec_throw(ScrError *e /* moves */) {
|
||||
scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg());
|
||||
}
|
||||
|
||||
/* The shared exec body (sync + promisified shapes — the POSIX arm's
|
||||
@@ -1115,7 +1245,7 @@ static ScrStr *scr_exec_run(ScrStr *cmd, ScrArr *args, bool shell,
|
||||
const ScrStr *input, const ScrStr *cwd,
|
||||
ScrArr *env_pairs, double timeout_ms,
|
||||
int stdout_mode, int stderr_mode, bool async_shape,
|
||||
ScrStr **stderr_out) {
|
||||
bool buffers, ScrStr **stderr_out) {
|
||||
/* stdio "inherit"'s stdin rides bit 4 of stdout_mode (scr_runtime.h). */
|
||||
bool stdin_inherit = (stdout_mode & 4) != 0;
|
||||
stdout_mode &= 3;
|
||||
@@ -1129,31 +1259,67 @@ static ScrStr *scr_exec_run(ScrStr *cmd, ScrArr *args, bool shell,
|
||||
if (w.spawn_errname != NULL) {
|
||||
free(w.out.data);
|
||||
free(w.err.data);
|
||||
scr_exec_throw_spawn(cmd, w.spawn_errname, async_shape);
|
||||
ScrError *e = scr_exec_spawn_error(cmd, args, w.spawn_errname, async_shape);
|
||||
if (async_shape) {
|
||||
char *display = scr_exec_display(cmd, args, shell);
|
||||
scr_child_error_cmd(e, display);
|
||||
free(display);
|
||||
ScrStr *empty = scr_str_new("", 0);
|
||||
scr_child_error_outputs(e, empty, empty);
|
||||
scr_str_release(empty);
|
||||
} else {
|
||||
scr_child_error_self(e);
|
||||
scr_child_error_sync_fields(e, false, 0, NULL, 0, false, NULL, NULL);
|
||||
}
|
||||
scr_exec_throw(e);
|
||||
return NULL;
|
||||
}
|
||||
if (w.timed_out && !async_shape) {
|
||||
/* The SYNC forms throw Node's spawnSync ETIMEDOUT; the async shape
|
||||
* reports the kill's exit-1 death as an ordinary command failure
|
||||
* below, exactly Node's promisified execFile. */
|
||||
free(w.out.data);
|
||||
free(w.err.data);
|
||||
scr_exec_throw_spawn(cmd, "ETIMEDOUT", false);
|
||||
return NULL;
|
||||
}
|
||||
bool failed = w.exit_code != 0;
|
||||
/* Node's inheritStderr: no stdio option given → the captured stderr
|
||||
* echoes to the parent's stderr AFTER completion, success or failure. */
|
||||
* echoes to the parent's stderr AFTER completion, success, failure or
|
||||
* timeout. */
|
||||
if (stderr_mode == 0 && w.err.len > 0) {
|
||||
fflush(stdout);
|
||||
fwrite(w.err.data, 1, w.err.len, stderr);
|
||||
}
|
||||
if (w.timed_out && !async_shape) {
|
||||
/* The SYNC forms throw Node's spawnSync ETIMEDOUT; the async shape
|
||||
* reports the kill's death as an ordinary command failure below,
|
||||
* exactly Node's promisified execFile. */
|
||||
ScrError *e = scr_exec_spawn_error(cmd, args, "ETIMEDOUT", false);
|
||||
scr_child_error_self(e);
|
||||
scr_child_error_sync_fields(e, false, 0, "SIGTERM", (double)w.pid, true,
|
||||
scr_child_sync_output(w.out.data, w.out.len, cap_out, buffers),
|
||||
scr_child_sync_output(w.err.data, w.err.len, cap_err, buffers));
|
||||
free(w.out.data);
|
||||
free(w.err.data);
|
||||
scr_exec_throw(e);
|
||||
return NULL;
|
||||
}
|
||||
bool failed = w.exit_code != 0 || w.timed_out;
|
||||
if (failed) {
|
||||
char *display = scr_exec_display(cmd, args, shell);
|
||||
scr_exec_throw_failed(display, &w.err, cap_err, async_shape);
|
||||
ScrError *e = scr_exec_failed_error(display, &w.err, cap_err, async_shape);
|
||||
/* libuv's kill emulation: a timed-out child died to the kill signal. */
|
||||
bool exited = !w.timed_out;
|
||||
if (async_shape) {
|
||||
scr_child_error_exit_fields(e, exited, (double)w.exit_code, exited ? NULL : "SIGTERM",
|
||||
w.timed_out);
|
||||
scr_child_error_cmd(e, display);
|
||||
ScrStr *out_s = scr_str_new(w.out.data, w.out.len);
|
||||
ScrStr *err_s = scr_str_new(w.err.data, w.err.len);
|
||||
scr_child_error_outputs(e, out_s, err_s);
|
||||
scr_str_release(out_s);
|
||||
scr_str_release(err_s);
|
||||
} else {
|
||||
scr_child_error_sync_fields(e, exited, (double)w.exit_code, exited ? NULL : "SIGTERM",
|
||||
(double)w.pid, true,
|
||||
scr_child_sync_output(w.out.data, w.out.len, cap_out, buffers),
|
||||
scr_child_sync_output(w.err.data, w.err.len, cap_err, buffers));
|
||||
}
|
||||
free(display);
|
||||
free(w.out.data);
|
||||
free(w.err.data);
|
||||
scr_exec_throw(e);
|
||||
return NULL;
|
||||
}
|
||||
if (async_shape && stderr_out != NULL) {
|
||||
@@ -1167,10 +1333,11 @@ static ScrStr *scr_exec_run(ScrStr *cmd, ScrArr *args, bool shell,
|
||||
|
||||
ScrStr *scr_exec_sync(ScrStr *cmd, ScrArr *args, bool shell, ScrStr *input,
|
||||
bool has_input, ScrStr *cwd, bool has_env, ScrArr *env_pairs,
|
||||
double timeout_ms, double stdout_mode, double stderr_mode) {
|
||||
double timeout_ms, double stdout_mode, double stderr_mode,
|
||||
bool buffers) {
|
||||
return scr_exec_run(cmd, args, shell, has_input ? input : NULL,
|
||||
cwd->len > 0 ? cwd : NULL, has_env ? env_pairs : NULL,
|
||||
timeout_ms, (int)stdout_mode, (int)stderr_mode, false, NULL);
|
||||
timeout_ms, (int)stdout_mode, (int)stderr_mode, false, buffers, NULL);
|
||||
}
|
||||
|
||||
ScrSpawnRes *scr_exec_capture(ScrStr *cmd, ScrArr *args, bool shell, ScrStr *cwd,
|
||||
@@ -1178,7 +1345,7 @@ ScrSpawnRes *scr_exec_capture(ScrStr *cmd, ScrArr *args, bool shell, ScrStr *cwd
|
||||
ScrStr *err_s = NULL;
|
||||
ScrStr *out_s = scr_exec_run(cmd, args, shell, NULL, cwd->len > 0 ? cwd : NULL,
|
||||
has_env ? env_pairs : NULL, timeout_ms, 1, 1,
|
||||
true, &err_s);
|
||||
true, false, &err_s);
|
||||
if (out_s == NULL) return NULL; /* exception pending; dummy result */
|
||||
return scr_spawn_res_new(true, 0, out_s, err_s);
|
||||
}
|
||||
@@ -1302,6 +1469,9 @@ struct ScrChild {
|
||||
ScrClosure *exec_cb;
|
||||
ScrExecFileFn exec_fn;
|
||||
ScrStr *exec_display;
|
||||
ScrStr *exec_file; /* the spawned file and its arguments (+1 each), for */
|
||||
ScrArr *exec_args; /* a spawn failure's path and spawnargs */
|
||||
void (*exec_props)(struct ScrChild *c, ScrError *error); /* Node's error properties */
|
||||
ScrChildStream *out_stream;
|
||||
ScrChildStream *err_stream;
|
||||
ScrChildWriter *in_stream;
|
||||
@@ -1356,6 +1526,8 @@ void scr_child_release(ScrChild *c) {
|
||||
scr_child_drop_listeners(c); /* only reachable pre-settle via leaks */
|
||||
scr_str_release(c->err_msg);
|
||||
scr_str_release(c->exec_display);
|
||||
scr_str_release(c->exec_file);
|
||||
if (c->exec_args) scr_arr_release(c->exec_args);
|
||||
scr_dyn_release(c->out_dyn);
|
||||
scr_dyn_release(c->err_dyn);
|
||||
scr_dyn_release(c->in_dyn);
|
||||
@@ -2236,10 +2408,24 @@ ScrChild *scr_fork(double target, ScrArr *args, double in_mode,
|
||||
return child;
|
||||
}
|
||||
|
||||
/* Node's properties on the callback form's error. Reached only through
|
||||
* the hook scr_exec_file installs, so programs without the callback form
|
||||
* link none of the property code (the settle path is shared by every
|
||||
* child). */
|
||||
static void scr_exec_file_error_props(ScrChild *c, ScrError *error) {
|
||||
if (c->state == SCR_CHILD_SPAWN_FAILED) {
|
||||
scr_child_error_spawn_fields(error, (double)c->spawn_uv_errno, "spawn", c->exec_file, c->exec_args);
|
||||
} else {
|
||||
scr_child_error_exit_fields(error, c->has_code, c->code, c->exit_signal, c->killed);
|
||||
}
|
||||
scr_child_error_cmd(error, c->exec_display->data);
|
||||
}
|
||||
|
||||
static ScrError *scr_exec_file_error(ScrChild *c, ScrStr *stderr_value) {
|
||||
if (c->state == SCR_CHILD_SPAWN_FAILED) {
|
||||
ScrError *error = scr_error_new(0, c->err_msg);
|
||||
scr_error_set_code(error, c->spawn_errname);
|
||||
if (c->exec_props) c->exec_props(c, error);
|
||||
return error;
|
||||
}
|
||||
if (c->has_code && c->code == 0) return NULL;
|
||||
@@ -2254,6 +2440,7 @@ static ScrError *scr_exec_file_error(ScrChild *c, ScrStr *stderr_value) {
|
||||
free(msg);
|
||||
ScrError *error = scr_error_new(0, message);
|
||||
scr_str_release(message);
|
||||
if (c->exec_props) c->exec_props(c, error);
|
||||
return error;
|
||||
}
|
||||
|
||||
@@ -2273,6 +2460,9 @@ ScrChild *scr_exec_file(ScrStr *cmd, ScrArr *args, ScrClosure *cb,
|
||||
scr_str_release(empty);
|
||||
c->exec_cb = cb;
|
||||
c->exec_fn = fn;
|
||||
c->exec_file = scr_str_retain(cmd);
|
||||
c->exec_args = scr_arr_retain(args);
|
||||
c->exec_props = &scr_exec_file_error_props;
|
||||
char *display = scr_exec_display(cmd, args, shell);
|
||||
c->exec_display = scr_str_new(display, strlen(display));
|
||||
free(display);
|
||||
@@ -3099,9 +3289,10 @@ ScrSpawnRes *scr_spawn_sync_stdio_str(ScrStr *cmd, ScrArr *args, double timeout_
|
||||
* the child completes (Node's inheritStderr flag — the capture is
|
||||
* unconditional); an explicit stdio option turns that off, and an
|
||||
* "ignore" stderr slot discards it entirely (nothing in the message).
|
||||
* - The thrown error carries the MESSAGE only: Node's status/stdout/
|
||||
* stderr properties don't exist here (SEMANTICS.md — nothing the
|
||||
* supported catch forms can read anyway).
|
||||
* - The thrown error carries Node's own properties: status, signal,
|
||||
* output, pid, stdout and stderr (Buffers, or strings with an encoding
|
||||
* option), after the errnoException fields on a spawn failure or a
|
||||
* timeout.
|
||||
* The result is the captured utf8 stdout (+1). */
|
||||
|
||||
typedef struct {
|
||||
@@ -3121,6 +3312,8 @@ typedef struct {
|
||||
* lands and the death reports as an ordinary command failure, exactly
|
||||
* Node's promisified behavior. */
|
||||
bool async_shape;
|
||||
/* The sync forms' error outputs are Buffers (no encoding option). */
|
||||
bool buffers;
|
||||
/* async_shape success: the captured stderr string (+1) lands here. */
|
||||
ScrStr **stderr_out;
|
||||
} ScrExecOpts;
|
||||
@@ -3187,12 +3380,12 @@ static void scr_exec_envp_free(char **envp) {
|
||||
free(envp);
|
||||
}
|
||||
|
||||
/* The Command-failed thrower (message only — see the block comment).
|
||||
/* The Command-failed error (+1; the caller adds its properties).
|
||||
* always_nl is the async shape: Node's promisified execFile appends
|
||||
* "\n<stderr>" unconditionally (a trailing newline on empty stderr),
|
||||
* where the sync form appends only when stderr is non-empty. */
|
||||
static void scr_exec_throw_failed(const char *display, const ScrCapBuf *err_cap,
|
||||
bool with_stderr, bool always_nl) {
|
||||
static ScrError *scr_exec_failed_error(const char *display, const ScrCapBuf *err_cap,
|
||||
bool with_stderr, bool always_nl) {
|
||||
size_t dlen = strlen(display);
|
||||
size_t elen = with_stderr ? err_cap->len : 0;
|
||||
size_t cap = 16 + dlen + 1 + elen + 1;
|
||||
@@ -3204,22 +3397,34 @@ static void scr_exec_throw_failed(const char *display, const ScrCapBuf *err_cap,
|
||||
memcpy(msg + at, err_cap->data, elen);
|
||||
at += elen;
|
||||
}
|
||||
scr_throw_error_msg(SCR_ERR_ERROR, msg, at);
|
||||
ScrStr *message = scr_str_new(msg, at);
|
||||
free(msg);
|
||||
ScrError *e = scr_error_new(SCR_ERR_ERROR, message);
|
||||
scr_str_release(message);
|
||||
return e;
|
||||
}
|
||||
|
||||
/* "spawnSync <file> <ERRNAME>" — Node's uv-error message for spawn
|
||||
* failures and timeouts, with `code` = the errno name (Node's
|
||||
* errnoException shape; the Command-failed error stays code-less — Node
|
||||
* puts the exit STATUS there, in properties this surface doesn't carry). */
|
||||
static void scr_exec_throw_spawn(ScrStr *cmd, const char *errname, bool async_shape) {
|
||||
/* "spawnSync <file> <ERRNAME>" — Node's uv-error for spawn failures and
|
||||
* timeouts (+1): `code` is the errno name, and the errnoException fields
|
||||
* (errno, syscall, path, spawnargs) follow. */
|
||||
static ScrError *scr_exec_spawn_error(ScrStr *cmd, ScrArr *args, int errnum, bool async_shape) {
|
||||
const char *errname = scr_errname(errnum);
|
||||
const char *verb = async_shape ? "spawn" : "spawnSync";
|
||||
size_t cap = 10 + cmd->len + 1 + strlen(errname) + 1;
|
||||
char *msg = malloc(cap);
|
||||
if (!msg) scr_child_oom();
|
||||
int len = snprintf(msg, cap, "%s %s %s", async_shape ? "spawn" : "spawnSync",
|
||||
cmd->data, errname);
|
||||
scr_throw_error_msg_code(SCR_ERR_ERROR, msg, (size_t)len, errname);
|
||||
int len = snprintf(msg, cap, "%s %s %s", verb, cmd->data, errname);
|
||||
ScrStr *message = scr_str_new(msg, (size_t)len);
|
||||
free(msg);
|
||||
ScrError *e = scr_error_new(SCR_ERR_ERROR, message);
|
||||
scr_str_release(message);
|
||||
scr_error_set_code(e, errname);
|
||||
scr_child_error_spawn_fields(e, -(double)errnum, verb, cmd, args);
|
||||
return e;
|
||||
}
|
||||
|
||||
static void scr_exec_throw(ScrError *e /* moves */) {
|
||||
scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg());
|
||||
}
|
||||
|
||||
ScrStr *scr_exec_sync_core(ScrStr *cmd, ScrArr *args, const ScrExecOpts *o) {
|
||||
@@ -3286,7 +3491,19 @@ ScrStr *scr_exec_sync_core(ScrStr *cmd, ScrArr *args, const ScrExecOpts *o) {
|
||||
if (has_input) close(infd[1]);
|
||||
/* execSync's ENOENT is the SHELL missing — can't happen; the file
|
||||
* form reports Node's spawnSync error. */
|
||||
scr_exec_throw_spawn(cmd, scr_errname(spawn_err), o->async_shape);
|
||||
ScrError *e = scr_exec_spawn_error(cmd, args, spawn_err, o->async_shape);
|
||||
if (o->async_shape) {
|
||||
char *display = scr_exec_display(cmd, args, o->shell);
|
||||
scr_child_error_cmd(e, display);
|
||||
free(display);
|
||||
ScrStr *empty = scr_str_new("", 0);
|
||||
scr_child_error_outputs(e, empty, empty);
|
||||
scr_str_release(empty);
|
||||
} else {
|
||||
scr_child_error_self(e);
|
||||
scr_child_error_sync_fields(e, false, 0, NULL, 0, false, NULL, NULL);
|
||||
}
|
||||
scr_exec_throw(e);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -3379,29 +3596,53 @@ ScrStr *scr_exec_sync_core(ScrStr *cmd, ScrArr *args, const ScrExecOpts *o) {
|
||||
} while (reaped < 0 && errno == EINTR);
|
||||
}
|
||||
|
||||
char *display = NULL;
|
||||
if (timed_out && !o->async_shape) {
|
||||
/* The SYNC forms throw Node's spawnSync ETIMEDOUT; the async shape
|
||||
* reports the SIGTERM death as an ordinary command failure below,
|
||||
* exactly Node's promisified execFile. */
|
||||
free(out.data);
|
||||
free(err.data);
|
||||
scr_exec_throw_spawn(cmd, "ETIMEDOUT", false);
|
||||
return NULL;
|
||||
}
|
||||
bool failed = !WIFEXITED(st) || WEXITSTATUS(st) != 0;
|
||||
/* Node's inheritStderr: no stdio option given → the captured stderr
|
||||
* echoes to the parent's stderr AFTER completion, success or failure. */
|
||||
* echoes to the parent's stderr AFTER completion, success, failure or
|
||||
* timeout. */
|
||||
if (o->stderr_mode == 0 && err.len > 0) {
|
||||
fflush(stdout);
|
||||
fwrite(err.data, 1, err.len, stderr);
|
||||
}
|
||||
bool exited = WIFEXITED(st) && !(timed_out && !o->async_shape);
|
||||
const char *signal = WIFSIGNALED(st) ? scr_signal_name(WTERMSIG(st)) : NULL;
|
||||
if (timed_out && !o->async_shape) {
|
||||
/* The SYNC forms throw Node's spawnSync ETIMEDOUT (signal = the kill
|
||||
* signal even when the child raced it); the async shape reports the
|
||||
* SIGTERM death as an ordinary command failure below, exactly Node's
|
||||
* promisified execFile. */
|
||||
ScrError *e = scr_exec_spawn_error(cmd, args, ETIMEDOUT, false);
|
||||
scr_child_error_self(e);
|
||||
scr_child_error_sync_fields(e, false, 0, signal != NULL ? signal : "SIGTERM", (double)pid,
|
||||
true, scr_child_sync_output(out.data, out.len, cap_out, o->buffers),
|
||||
scr_child_sync_output(err.data, err.len, cap_err, o->buffers));
|
||||
free(out.data);
|
||||
free(err.data);
|
||||
scr_exec_throw(e);
|
||||
return NULL;
|
||||
}
|
||||
bool failed = !exited || WEXITSTATUS(st) != 0;
|
||||
if (failed) {
|
||||
display = scr_exec_display(cmd, args, o->shell);
|
||||
scr_exec_throw_failed(display, &err, cap_err, o->async_shape);
|
||||
char *display = scr_exec_display(cmd, args, o->shell);
|
||||
ScrError *e = scr_exec_failed_error(display, &err, cap_err, o->async_shape);
|
||||
if (o->async_shape) {
|
||||
scr_child_error_exit_fields(e, exited, exited ? (double)WEXITSTATUS(st) : 0, signal,
|
||||
timed_out);
|
||||
scr_child_error_cmd(e, display);
|
||||
ScrStr *out_s = scr_str_new(out.data, out.len);
|
||||
ScrStr *err_s = scr_str_new(err.data, err.len);
|
||||
scr_child_error_outputs(e, out_s, err_s);
|
||||
scr_str_release(out_s);
|
||||
scr_str_release(err_s);
|
||||
} else {
|
||||
scr_child_error_sync_fields(e, exited, exited ? (double)WEXITSTATUS(st) : 0, signal,
|
||||
(double)pid, true,
|
||||
scr_child_sync_output(out.data, out.len, cap_out, o->buffers),
|
||||
scr_child_sync_output(err.data, err.len, cap_err, o->buffers));
|
||||
}
|
||||
free(display);
|
||||
free(out.data);
|
||||
free(err.data);
|
||||
scr_exec_throw(e);
|
||||
return NULL;
|
||||
}
|
||||
if (o->async_shape && o->stderr_out != NULL) {
|
||||
@@ -3421,7 +3662,8 @@ ScrStr *scr_exec_sync_core(ScrStr *cmd, ScrArr *args, const ScrExecOpts *o) {
|
||||
* it means an empty environment). */
|
||||
ScrStr *scr_exec_sync(ScrStr *cmd, ScrArr *args, bool shell, ScrStr *input,
|
||||
bool has_input, ScrStr *cwd, bool has_env, ScrArr *env_pairs,
|
||||
double timeout_ms, double stdout_mode, double stderr_mode) {
|
||||
double timeout_ms, double stdout_mode, double stderr_mode,
|
||||
bool buffers) {
|
||||
ScrExecOpts o = {
|
||||
.shell = shell,
|
||||
.input = has_input ? input : NULL,
|
||||
@@ -3433,6 +3675,7 @@ ScrStr *scr_exec_sync(ScrStr *cmd, ScrArr *args, bool shell, ScrStr *input,
|
||||
.stdout_mode = (int)stdout_mode & 3,
|
||||
.stderr_mode = (int)stderr_mode,
|
||||
.stdin_inherit = ((int)stdout_mode & 4) != 0,
|
||||
.buffers = buffers,
|
||||
};
|
||||
return scr_exec_sync_core(cmd, args, &o);
|
||||
}
|
||||
@@ -3634,6 +3877,9 @@ struct ScrChild {
|
||||
ScrClosure *exec_cb;
|
||||
ScrExecFileFn exec_fn;
|
||||
ScrStr *exec_display;
|
||||
ScrStr *exec_file; /* the spawned file and its arguments (+1 each), for */
|
||||
ScrArr *exec_args; /* a spawn failure's path and spawnargs */
|
||||
void (*exec_props)(struct ScrChild *c, ScrError *error); /* Node's error properties */
|
||||
/* Piped stdio (stdio mode 3): the stream handles child.stdout /
|
||||
* child.stderr answer — owned (+1 each), NULL when not piped. */
|
||||
ScrChildStream *out_stream;
|
||||
@@ -3828,6 +4074,8 @@ void scr_child_release(ScrChild *c) {
|
||||
scr_child_drop_listeners(c); /* only reachable pre-settle via leaks */
|
||||
scr_str_release(c->err_msg);
|
||||
scr_str_release(c->exec_display);
|
||||
scr_str_release(c->exec_file);
|
||||
if (c->exec_args) scr_arr_release(c->exec_args);
|
||||
scr_dyn_release(c->out_dyn);
|
||||
scr_dyn_release(c->err_dyn);
|
||||
scr_dyn_release(c->in_dyn);
|
||||
@@ -3858,6 +4106,7 @@ static const char *scr_errname(int err) {
|
||||
case EAGAIN: return "EAGAIN";
|
||||
case EIO: return "EIO";
|
||||
case EINVAL: return "EINVAL";
|
||||
case ETIMEDOUT: return "ETIMEDOUT";
|
||||
default: return "EUNKNOWN";
|
||||
}
|
||||
}
|
||||
@@ -4881,10 +5130,24 @@ ScrChild *scr_fork(double target, ScrArr *args, double in_mode,
|
||||
return child;
|
||||
}
|
||||
|
||||
/* Node's properties on the callback form's error. Reached only through
|
||||
* the hook scr_exec_file installs, so programs without the callback form
|
||||
* link none of the property code (the settle path is shared by every
|
||||
* child). */
|
||||
static void scr_exec_file_error_props(ScrChild *c, ScrError *error) {
|
||||
if (c->state == SCR_CHILD_SPAWN_FAILED) {
|
||||
scr_child_error_spawn_fields(error, -(double)c->spawn_errno, "spawn", c->exec_file, c->exec_args);
|
||||
} else {
|
||||
scr_child_error_exit_fields(error, c->has_code, c->code, c->exit_signal, c->killed);
|
||||
}
|
||||
scr_child_error_cmd(error, c->exec_display->data);
|
||||
}
|
||||
|
||||
static ScrError *scr_exec_file_error(ScrChild *c, ScrStr *stderr_value) {
|
||||
if (c->state == SCR_CHILD_SPAWN_FAILED) {
|
||||
ScrError *error = scr_error_new(0, c->err_msg);
|
||||
scr_error_set_code(error, scr_errname(c->spawn_errno));
|
||||
if (c->exec_props) c->exec_props(c, error);
|
||||
return error;
|
||||
}
|
||||
if (c->has_code && c->code == 0) return NULL;
|
||||
@@ -4899,6 +5162,7 @@ static ScrError *scr_exec_file_error(ScrChild *c, ScrStr *stderr_value) {
|
||||
free(msg);
|
||||
ScrError *error = scr_error_new(0, message);
|
||||
scr_str_release(message);
|
||||
if (c->exec_props) c->exec_props(c, error);
|
||||
return error;
|
||||
}
|
||||
|
||||
@@ -4918,6 +5182,9 @@ ScrChild *scr_exec_file(ScrStr *cmd, ScrArr *args, ScrClosure *cb,
|
||||
scr_str_release(empty);
|
||||
c->exec_cb = cb;
|
||||
c->exec_fn = fn;
|
||||
c->exec_file = scr_str_retain(cmd);
|
||||
c->exec_args = scr_arr_retain(args);
|
||||
c->exec_props = &scr_exec_file_error_props;
|
||||
char *display = scr_exec_display(cmd, args, shell);
|
||||
c->exec_display = scr_str_new(display, strlen(display));
|
||||
free(display);
|
||||
|
||||
@@ -44,18 +44,22 @@ ScrSpawnRes *scr_spawn_sync_stdio_str(ScrStr *cmd, ScrArr *args, double timeout_
|
||||
* 0 captures AND echoes to the parent stderr after completion (Node's
|
||||
* no-stdio-option default), 1 captures only, 2 ignores, 3 inherits.
|
||||
* Result is the captured utf8 stdout (+1) — "" when nothing captures
|
||||
* (Node answers null there; discarded results cannot tell). All args
|
||||
* (Node answers null there; discarded results cannot tell). A failure's
|
||||
* error carries Node's status, signal, output, pid, stdout and stderr;
|
||||
* `buffers` (no encoding option) makes those outputs Buffers. All args
|
||||
* borrowed. */
|
||||
ScrStr *scr_exec_sync(ScrStr *cmd, ScrArr *args, bool shell, ScrStr *input,
|
||||
bool has_input, ScrStr *cwd, bool has_env, ScrArr *env_pairs,
|
||||
double timeout_ms, double stdout_mode, double stderr_mode);
|
||||
double timeout_ms, double stdout_mode, double stderr_mode,
|
||||
bool buffers);
|
||||
/* The promisified-execFile/exec capture (cp.execCapture; shell as for
|
||||
* scr_exec_sync — the /bin/sh -c argv): the same core in the
|
||||
* async shape — both streams captured (no echo), Node's ASYNC error
|
||||
* messages on the throw paths ("Command failed: <cmd>\n<stderr>" with the
|
||||
* unconditional newline, "spawn <file> <ERR>" with .code, timeouts as
|
||||
* ordinary SIGTERM command failures). +1 ScrSpawnRes on success (stdout +
|
||||
* stderr; status unused). All args borrowed. */
|
||||
* ordinary SIGTERM command failures), each error with Node's code, killed,
|
||||
* signal, cmd, stdout and stderr properties. +1 ScrSpawnRes on success
|
||||
* (stdout + stderr; status unused). All args borrowed. */
|
||||
ScrSpawnRes *scr_exec_capture(ScrStr *cmd, ScrArr *args, bool shell, ScrStr *cwd,
|
||||
bool has_env, ScrArr *env_pairs, double timeout_ms);
|
||||
ScrSpawnRes *scr_spawn_res_retain(ScrSpawnRes *r);
|
||||
|
||||
@@ -159,22 +159,20 @@ static void sc_throw_openssl(const char *packed, const char *reason, const char
|
||||
snprintf(message, sizeof message, "error:%s:Provider routines::%s", packed, reason);
|
||||
ScrStr *text = scr_str_new(message, strlen(message));
|
||||
ScrError *e = scr_error_new(SCR_ERR_ERROR, text);
|
||||
ScrDyn *view = scr_dyn_from_error(e);
|
||||
ScrDyn *stack = scr_dyn_new_arr();
|
||||
scr_dyn_arr_push(stack, scr_dyn_new_str(text));
|
||||
scr_str_release(text);
|
||||
scr_dyn_obj_set(view, "opensslErrorStack", 17, stack);
|
||||
scr_error_set_prop(e, "opensslErrorStack", 17, stack);
|
||||
ScrStr *library = scr_str_new("Provider routines", 17);
|
||||
scr_dyn_obj_set(view, "library", 7, scr_dyn_new_str(library));
|
||||
scr_error_set_prop(e, "library", 7, scr_dyn_new_str(library));
|
||||
scr_str_release(library);
|
||||
ScrStr *reason_text = scr_str_new(reason, strlen(reason));
|
||||
scr_dyn_obj_set(view, "reason", 6, scr_dyn_new_str(reason_text));
|
||||
scr_error_set_prop(e, "reason", 6, scr_dyn_new_str(reason_text));
|
||||
scr_str_release(reason_text);
|
||||
scr_error_set_code(e, code);
|
||||
ScrStr *code_text = scr_str_new(code, strlen(code));
|
||||
scr_dyn_obj_set(view, "code", 4, scr_dyn_new_str(code_text));
|
||||
scr_error_set_prop(e, "code", 4, scr_dyn_new_str(code_text));
|
||||
scr_str_release(code_text);
|
||||
scr_dyn_release(view);
|
||||
scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg());
|
||||
}
|
||||
|
||||
|
||||
@@ -1297,9 +1297,9 @@ static ScrDyn *scr_dns_exception(int rc, const char *syscall, const ScrStr *host
|
||||
scr_error_set_code(e, code);
|
||||
e->system_call = scr_str_new(syscall, strlen(syscall));
|
||||
e->system_errno = errnum;
|
||||
scr_error_set_prop(e, "hostname", 8, scr_dyn_new_str((ScrStr *)host));
|
||||
ScrDyn *d = scr_dyn_from_error(e);
|
||||
scr_error_release(e);
|
||||
scr_dyn_obj_set(d, "hostname", 8, scr_dyn_new_str((ScrStr *)host));
|
||||
return d;
|
||||
}
|
||||
|
||||
|
||||
@@ -117,76 +117,81 @@ typedef enum {
|
||||
/* The handle-type tags the checked-dynamic tree can carry. The set is deliberately the
|
||||
* HANDLE kinds whose member surfaces already have complete static
|
||||
* lowerings (the http/net receiver surface); new kinds join by adding a
|
||||
* tag + an ops registration in their owning unit. */
|
||||
* tag + an ops registration in their owning unit.
|
||||
*
|
||||
* Tags are sorted by name and every value is explicit and permanent: the
|
||||
* LLVM backend hardcodes some of them (DYN_HANDLE_TAG_NUM in
|
||||
* packages/compiler/src/backend/llvm/dyn.ts). A new tag goes in its sorted
|
||||
* place with the next value after the largest one in use, below
|
||||
* SCR_DYNH_CAPACITY. packages/compiler/src/backend/llvm/dyn-handle-tags.test.ts
|
||||
* checks that values are unique and in range and that the backend's
|
||||
* numbers match. */
|
||||
typedef enum {
|
||||
SCR_DYNH_HTTP_REQ, /* ScrHttpReq — IncomingMessage (server req & client res) */
|
||||
SCR_DYNH_HTTP_RES, /* ScrHttpRes — ServerResponse */
|
||||
SCR_DYNH_NET_SOCKET, /* ScrNetSocket — net.Socket */
|
||||
SCR_DYNH_NET_SERVER, /* ScrNetServer — net.Server (http.Server rides the same handle) */
|
||||
SCR_DYNH_H2_SESSION, /* ScrH2Session — Http2Session (client & server) */
|
||||
SCR_DYNH_H2_STREAM, /* ScrH2Stream — Http2Stream (client & server) */
|
||||
SCR_DYNH_HTTP_CLIENT, /* ScrHttpClientReq — http.ClientRequest */
|
||||
SCR_DYNH_HTTP_AGENT, /* ScrHttpAgent — http.Agent / https.Agent */
|
||||
SCR_DYNH_ABORT_SIGNAL, /* native static-fetch AbortSignal */
|
||||
SCR_DYNH_WEB_STREAM, /* native WHATWG ReadableStream */
|
||||
SCR_DYNH_WEB_READER, /* native ReadableStreamDefaultReader */
|
||||
SCR_DYNH_WEB_CONTROLLER, /* native ReadableStreamDefaultController */
|
||||
SCR_DYNH_FETCH_RESPONSE, /* native static-fetch Response */
|
||||
SCR_DYNH_FETCH_HEADERS, /* native static-fetch response Headers */
|
||||
SCR_DYNH_EVENT, /* ScrEvent — new Event() and AbortSignal's abort events */
|
||||
SCR_DYNH_ABORT_CONTROLLER, /* native static-fetch AbortController */
|
||||
SCR_DYNH_CHILD, /* child_process.ChildProcess */
|
||||
SCR_DYNH_FILE_HANDLE, /* fs/promises.FileHandle */
|
||||
SCR_DYNH_ARRAY_BUFFER, /* fixed-length ArrayBuffer storage */
|
||||
SCR_DYNH_SEGMENTER, /* default Unicode grapheme segmentation */
|
||||
SCR_DYNH_SEGMENTS, /* iterable immutable segmentation result */
|
||||
SCR_DYNH_BUILTIN_MODULE, /* native builtin export object */
|
||||
SCR_DYNH_GLOBAL, /* native global object identity */
|
||||
SCR_DYNH_WEAK_MAP, /* native weak-key metadata */
|
||||
SCR_DYNH_WEAK_SET, /* native weak membership */
|
||||
SCR_DYNH_SET, /* native Set<unknown>, shared backing map */
|
||||
SCR_DYNH_REGEXP, /* native RegExp, shared compiled pattern */
|
||||
SCR_DYNH_STDIO, /* stable process stdin/stdout/stderr values */
|
||||
SCR_DYNH_URL, /* native WHATWG URL */
|
||||
SCR_DYNH_MAP, /* native Map<unknown, unknown>, shared backing */
|
||||
SCR_DYNH_DATE, /* JavaScript Date with stable native identity */
|
||||
SCR_DYNH_ITERATOR, /* live native array/string/byte iterator */
|
||||
SCR_DYNH_CONSOLE, /* stored global console value */
|
||||
SCR_DYNH_SEARCH_PARAMS, /* live WHATWG URLSearchParams */
|
||||
SCR_DYNH_FETCH_REQUEST,
|
||||
SCR_DYNH_WORKER,
|
||||
SCR_DYNH_MESSAGE_PORT,
|
||||
SCR_DYNH_SHARED_ARRAY_BUFFER,
|
||||
SCR_DYNH_CONSOLE_INSTANCE, /* node:console Console instance */
|
||||
SCR_DYNH_EVENT_TARGET, /* ScrEventTarget — new EventTarget() */
|
||||
SCR_DYNH_CUSTOM_EVENT, /* ScrEvent with detail — new CustomEvent() */
|
||||
SCR_DYNH_BLOB, /* ScrBlob — new Blob() and Blob#slice() */
|
||||
SCR_DYNH_FILE, /* ScrBlob with a name — new File() */
|
||||
SCR_DYNH_KEY_OBJECT, /* node:crypto secret KeyObject (scr_lib.c) */
|
||||
SCR_DYNH_DIR, /* fs.Dir (scr_lib.c) */
|
||||
SCR_DYNH_CIPHER, /* node:crypto Cipheriv (scr_cipher.c) */
|
||||
SCR_DYNH_DECIPHER, /* node:crypto Decipheriv (scr_cipher.c) */
|
||||
SCR_DYNH_NET_SOCKET_ADDRESS, /* net.SocketAddress (scr_net.c) */
|
||||
SCR_DYNH_NET_BLOCK_LIST, /* net.BlockList (scr_net.c) */
|
||||
SCR_DYNH_DISPOSABLE_STACK, /* DisposableStack (scr_symbol.c) */
|
||||
SCR_DYNH_MESSAGE_EVENT, /* ScrEvent with data — BroadcastChannel's 'message' */
|
||||
SCR_DYNH_BROADCAST_CHANNEL, /* worker_threads BroadcastChannel (scr_worker_events.c) */
|
||||
SCR_DYNH_HASH, /* node:crypto Hash (scr_lib.c) */
|
||||
SCR_DYNH_HMAC, /* node:crypto Hmac (scr_lib.c) */
|
||||
SCR_DYNH_ITERATOR_HELPER, /* an iterator helper object over a checked iterator (scr_json.c) */
|
||||
SCR_DYNH_COUNT,
|
||||
SCR_DYNH_ABORT_CONTROLLER = 15, /* native static-fetch AbortController */
|
||||
SCR_DYNH_ABORT_SIGNAL = 8, /* native static-fetch AbortSignal */
|
||||
SCR_DYNH_ARRAY_BUFFER = 18, /* fixed-length ArrayBuffer storage */
|
||||
SCR_DYNH_BLOB = 41, /* ScrBlob — new Blob() and Blob#slice() */
|
||||
SCR_DYNH_BROADCAST_CHANNEL = 51, /* worker_threads BroadcastChannel (scr_worker_events.c) */
|
||||
SCR_DYNH_BUILTIN_MODULE = 21, /* native builtin export object */
|
||||
SCR_DYNH_CHILD = 16, /* child_process.ChildProcess */
|
||||
SCR_DYNH_CIPHER = 45, /* node:crypto Cipheriv (scr_cipher.c) */
|
||||
SCR_DYNH_CONSOLE = 32, /* stored global console value */
|
||||
SCR_DYNH_CONSOLE_INSTANCE = 38, /* node:console Console instance */
|
||||
SCR_DYNH_CUSTOM_EVENT = 40, /* ScrEvent with detail — new CustomEvent() */
|
||||
SCR_DYNH_DATE = 30, /* JavaScript Date with stable native identity */
|
||||
SCR_DYNH_DECIPHER = 46, /* node:crypto Decipheriv (scr_cipher.c) */
|
||||
SCR_DYNH_DIR = 44, /* fs.Dir (scr_lib.c) */
|
||||
SCR_DYNH_DISPOSABLE_STACK = 49, /* DisposableStack (scr_symbol.c) */
|
||||
SCR_DYNH_EVENT = 14, /* ScrEvent — new Event() and AbortSignal's abort events */
|
||||
SCR_DYNH_EVENT_TARGET = 39, /* ScrEventTarget — new EventTarget() */
|
||||
SCR_DYNH_FETCH_HEADERS = 13, /* native static-fetch response Headers */
|
||||
SCR_DYNH_FETCH_REQUEST = 34,
|
||||
SCR_DYNH_FETCH_RESPONSE = 12, /* native static-fetch Response */
|
||||
SCR_DYNH_FILE = 42, /* ScrBlob with a name — new File() */
|
||||
SCR_DYNH_FILE_HANDLE = 17, /* fs/promises.FileHandle */
|
||||
SCR_DYNH_GLOBAL = 22, /* native global object identity */
|
||||
SCR_DYNH_H2_SESSION = 4, /* ScrH2Session — Http2Session (client & server) */
|
||||
SCR_DYNH_H2_STREAM = 5, /* ScrH2Stream — Http2Stream (client & server) */
|
||||
SCR_DYNH_HASH = 52, /* node:crypto Hash (scr_lib.c) */
|
||||
SCR_DYNH_HMAC = 53, /* node:crypto Hmac (scr_lib.c) */
|
||||
SCR_DYNH_HTTP_AGENT = 7, /* ScrHttpAgent — http.Agent / https.Agent */
|
||||
SCR_DYNH_HTTP_CLIENT = 6, /* ScrHttpClientReq — http.ClientRequest */
|
||||
SCR_DYNH_HTTP_REQ = 0, /* ScrHttpReq — IncomingMessage (server req & client res) */
|
||||
SCR_DYNH_HTTP_RES = 1, /* ScrHttpRes — ServerResponse */
|
||||
SCR_DYNH_ITERATOR = 31, /* live native array/string/byte iterator */
|
||||
SCR_DYNH_ITERATOR_HELPER = 54, /* an iterator helper object over a checked iterator (scr_json.c) */
|
||||
SCR_DYNH_KEY_OBJECT = 43, /* node:crypto secret KeyObject (scr_lib.c) */
|
||||
SCR_DYNH_MAP = 29, /* native Map<unknown, unknown>, shared backing */
|
||||
SCR_DYNH_MESSAGE_EVENT = 50, /* ScrEvent with data — BroadcastChannel's 'message' */
|
||||
SCR_DYNH_MESSAGE_PORT = 36,
|
||||
SCR_DYNH_NET_BLOCK_LIST = 48, /* net.BlockList (scr_net.c) */
|
||||
SCR_DYNH_NET_SERVER = 3, /* ScrNetServer — net.Server (http.Server rides the same handle) */
|
||||
SCR_DYNH_NET_SOCKET = 2, /* ScrNetSocket — net.Socket */
|
||||
SCR_DYNH_NET_SOCKET_ADDRESS = 47, /* net.SocketAddress (scr_net.c) */
|
||||
SCR_DYNH_REGEXP = 26, /* native RegExp, shared compiled pattern */
|
||||
SCR_DYNH_SEARCH_PARAMS = 33, /* live WHATWG URLSearchParams */
|
||||
SCR_DYNH_SEGMENTER = 19, /* default Unicode grapheme segmentation */
|
||||
SCR_DYNH_SEGMENTS = 20, /* iterable immutable segmentation result */
|
||||
SCR_DYNH_SET = 25, /* native Set<unknown>, shared backing map */
|
||||
SCR_DYNH_SHARED_ARRAY_BUFFER = 37,
|
||||
SCR_DYNH_STDIO = 27, /* stable process stdin/stdout/stderr values */
|
||||
SCR_DYNH_URL = 28, /* native WHATWG URL */
|
||||
SCR_DYNH_WEAK_MAP = 23, /* native weak-key metadata */
|
||||
SCR_DYNH_WEAK_SET = 24, /* native weak membership */
|
||||
SCR_DYNH_WEB_CONTROLLER = 11, /* native ReadableStreamDefaultController */
|
||||
SCR_DYNH_WEB_READER = 10, /* native ReadableStreamDefaultReader */
|
||||
SCR_DYNH_WEB_STREAM = 9, /* native WHATWG ReadableStream */
|
||||
SCR_DYNH_WORKER = 35,
|
||||
} ScrDynHandleTag;
|
||||
|
||||
/* Every per-tag table (scr_dynh_ops in scr_json.c, the message clone hooks
|
||||
* in scr_message.c) is sized by this fixed capacity, never by
|
||||
* SCR_DYNH_COUNT. Appending a tag for a link-gated unit therefore changes
|
||||
* no always-linked storage or code: a program that never boxes the new
|
||||
* handle links byte-identical objects. Raising the capacity is a
|
||||
* program-wide layout change; do it in a change of its own, measured as
|
||||
* neutral, and only when the enum outgrows it. */
|
||||
* in scr_message.c) is sized by this fixed capacity, never by the number of
|
||||
* tags. Adding a tag for a link-gated unit therefore changes no
|
||||
* always-linked storage or code: a program that never boxes the new handle
|
||||
* links byte-identical objects. Raising the capacity is a program-wide
|
||||
* layout change; do it in a change of its own, measured as neutral, and
|
||||
* only when the tags outgrow it. */
|
||||
#define SCR_DYNH_CAPACITY 128
|
||||
_Static_assert(SCR_DYNH_COUNT <= SCR_DYNH_CAPACITY,
|
||||
"ScrDynHandleTag outgrew SCR_DYNH_CAPACITY: raise it in its own change");
|
||||
|
||||
typedef struct ScrDyn ScrDyn;
|
||||
typedef struct ScrBytes ScrBytes; /* full definition below (C11 repeat) */
|
||||
@@ -998,6 +1003,10 @@ ScrError *scr_error_from_dyn(const ScrDyn *d); /* scr_async_dyn.c (gated) */
|
||||
* the gated extraction reads/writes through these). */
|
||||
ScrError *scr_errdyn_err_of(const ScrDyn *d); /* +1 or NULL */
|
||||
void scr_errdyn_put(ScrError *e, ScrDyn *d); /* retains both sides */
|
||||
/* Sets an own property on an error: its checked-dynamic node, which casts
|
||||
* and util.inspect read. Moves value. */
|
||||
void scr_error_set_prop(ScrError *e, const char *key, size_t key_len, ScrDyn *value);
|
||||
ScrDyn *scr_errdyn_view_of(const ScrError *e); /* borrowed or NULL */
|
||||
/* ToBoolean over the dyn kind (JS-exact); borrowed, never throws. */
|
||||
bool scr_dyn_truthy(const ScrDyn *d);
|
||||
/* JS String() over a dyn value (arrays join, [object Object], the error
|
||||
|
||||
@@ -312,6 +312,31 @@ static ScrDyn *sf_type_reason(const char *message, ScrError **error_out) {
|
||||
return d;
|
||||
}
|
||||
|
||||
/* Request's URL parse failure: with the URL parser's TypeError pending,
|
||||
* replace it by undici's TypeError "Failed to parse URL from <input>"
|
||||
* whose (non-enumerable) cause is the parser error. */
|
||||
static void sf_throw_url_parse_failure(const ScrStr *input) {
|
||||
ScrCaught *caught = scr_exc_take();
|
||||
ScrDyn *cause = scr_caught_to_dyn(caught);
|
||||
scr_caught_release(caught);
|
||||
/* Sized without the NUL so it stays out of the unit's shared mergeable
|
||||
* string section (which every program linking this unit carries). */
|
||||
static const char prefix[25] = "Failed to parse URL from ";
|
||||
size_t message_len = sizeof prefix + input->len;
|
||||
char *message = malloc(message_len);
|
||||
if (!message) sf_oom();
|
||||
memcpy(message, prefix, sizeof prefix);
|
||||
memcpy(message + sizeof prefix, input->data, input->len);
|
||||
ScrStr *text = scr_str_new(message, message_len);
|
||||
free(message);
|
||||
ScrError *e = scr_error_new(SCR_ERR_TYPE, text);
|
||||
scr_str_release(text);
|
||||
scr_error_set_cause(e, cause);
|
||||
e->cause_enumerable = false;
|
||||
scr_dyn_release(cause);
|
||||
scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg());
|
||||
}
|
||||
|
||||
/* ── AbortSignal ─────────────────────────────────────────────────── */
|
||||
|
||||
static void sf_signal_track_reason(SfSignal *s) {
|
||||
@@ -2386,6 +2411,50 @@ static ScrStr **sf_headers_sorted_names(SfHeaders *h, size_t *count_out) {
|
||||
return names;
|
||||
}
|
||||
|
||||
static bool sf_headers_is_set_cookie(const ScrStr *name) {
|
||||
return name->len == 10 && memcmp(name->data, "set-cookie", 10) == 0;
|
||||
}
|
||||
|
||||
/* The `index`th entry of Fetch's "sort and combine" list: names in
|
||||
* byte order, each with its combined value, except that every set-cookie
|
||||
* value is its own entry (in insertion order), as Node's Headers
|
||||
* iterator yields them. False past the end. Outputs are +1. */
|
||||
static bool sf_headers_sorted_entry(SfHeaders *h, size_t index,
|
||||
ScrStr **name_out,
|
||||
ScrStr **value_out) {
|
||||
size_t count = 0;
|
||||
ScrStr **names = sf_headers_sorted_names(h, &count);
|
||||
bool found = false;
|
||||
size_t at = 0;
|
||||
for (size_t i = 0; i < count && !found; i++) {
|
||||
if (!sf_headers_is_set_cookie(names[i])) {
|
||||
if (at == index) {
|
||||
*name_out = scr_str_retain(names[i]);
|
||||
*value_out = sf_headers_get_value(h, names[i]);
|
||||
found = true;
|
||||
}
|
||||
at++;
|
||||
continue;
|
||||
}
|
||||
size_t n = (size_t)scr_arr_len(h->pairs);
|
||||
for (size_t j = 0; j + 1 < n && !found; j += 2) {
|
||||
ScrStr *key = scr_arr_get_ref(h->pairs, (double)j);
|
||||
bool match = sf_headers_is_set_cookie(key);
|
||||
scr_str_release(key);
|
||||
if (!match) continue;
|
||||
if (at == index) {
|
||||
*name_out = scr_str_retain(names[i]);
|
||||
*value_out = scr_arr_get_ref(h->pairs, (double)(j + 1));
|
||||
found = true;
|
||||
}
|
||||
at++;
|
||||
}
|
||||
}
|
||||
for (size_t j = 0; j < count; j++) scr_str_release(names[j]);
|
||||
free(names);
|
||||
return found;
|
||||
}
|
||||
|
||||
static ScrDyn *sf_headers_invoke(void *ptr, ScrDyn *self,
|
||||
const char *method,
|
||||
ScrDyn *const *args, size_t argc,
|
||||
@@ -2438,20 +2507,15 @@ static ScrDyn *sf_headers_invoke(void *ptr, ScrDyn *self,
|
||||
* a future name skips it, inserting a later name visits it, and
|
||||
* inserting before the current index can make the current name appear
|
||||
* again. The old one-time snapshot was only safe while every native
|
||||
* Headers handle was immutable. */
|
||||
* Headers handle was immutable. Set-Cookie values are visited one by
|
||||
* one (sf_headers_sorted_entry). */
|
||||
for (size_t i = 0;; i++) {
|
||||
size_t count = 0;
|
||||
ScrStr **names = sf_headers_sorted_names(h, &count);
|
||||
if (i >= count) {
|
||||
for (size_t j = 0; j < count; j++) scr_str_release(names[j]);
|
||||
free(names);
|
||||
break;
|
||||
}
|
||||
ScrStr *value = sf_headers_get_value(h, names[i]);
|
||||
ScrStr *name = NULL;
|
||||
ScrStr *value = NULL;
|
||||
if (!sf_headers_sorted_entry(h, i, &name, &value)) break;
|
||||
ScrDyn *boxed_value = scr_dyn_new_str(value);
|
||||
ScrDyn *boxed_name = scr_dyn_new_str(names[i]);
|
||||
for (size_t j = 0; j < count; j++) scr_str_release(names[j]);
|
||||
free(names);
|
||||
ScrDyn *boxed_name = scr_dyn_new_str(name);
|
||||
scr_str_release(name);
|
||||
ScrDyn *call_args[3] = {boxed_value, boxed_name, self};
|
||||
scr_dyn_this_push_dyn(this_arg);
|
||||
ScrDyn *result =
|
||||
@@ -4415,14 +4479,39 @@ static bool sf_start_hop(SfTransfer *t) {
|
||||
}
|
||||
if (connection_close) sf_push_header_text(headers, "connection", "close", 5);
|
||||
else sf_push_header_text(headers, "connection", "keep-alive", 10);
|
||||
/* Headers keeps each Set-Cookie value separately, but undici sends its
|
||||
* header list's combined value: one line, the values joined by ", ",
|
||||
* at the position of the first one. */
|
||||
bool set_cookie_sent = false;
|
||||
for (size_t i = 0; i + 1 < user_len; i += 2) {
|
||||
ScrStr *name = scr_arr_get_ref(t->headers, (double)i);
|
||||
ScrStr *value = scr_arr_get_ref(t->headers, (double)(i + 1));
|
||||
if (sf_eq_ci(name, "host") || sf_eq_ci(name, "connection")) {
|
||||
if (sf_eq_ci(name, "host") || sf_eq_ci(name, "connection") ||
|
||||
(set_cookie_sent && sf_eq_ci(name, "set-cookie"))) {
|
||||
scr_str_release(name);
|
||||
scr_str_release(value);
|
||||
continue;
|
||||
}
|
||||
if (sf_eq_ci(name, "set-cookie")) {
|
||||
set_cookie_sent = true;
|
||||
for (size_t j = i + 2; j + 1 < user_len; j += 2) {
|
||||
ScrStr *later = scr_arr_get_ref(t->headers, (double)j);
|
||||
bool match = sf_eq_ci(later, "set-cookie");
|
||||
scr_str_release(later);
|
||||
if (!match) continue;
|
||||
ScrStr *next = scr_arr_get_ref(t->headers, (double)(j + 1));
|
||||
size_t joined_len = value->len + 2 + next->len;
|
||||
char *joined = malloc(joined_len ? joined_len : 1);
|
||||
if (!joined) sf_oom();
|
||||
memcpy(joined, value->data, value->len);
|
||||
memcpy(joined + value->len, ", ", 2);
|
||||
memcpy(joined + value->len + 2, next->data, next->len);
|
||||
scr_str_release(next);
|
||||
scr_str_release(value);
|
||||
value = scr_str_new(joined, joined_len);
|
||||
free(joined);
|
||||
}
|
||||
}
|
||||
scr_arr_push_ref(headers, name);
|
||||
scr_arr_push_ref(headers, value);
|
||||
}
|
||||
@@ -4578,14 +4667,33 @@ static ScrDyn *sf_response_init_get(
|
||||
return NULL;
|
||||
}
|
||||
|
||||
ScrDyn *scr_fetch_response_new(ScrDyn *body, ScrDyn *init) {
|
||||
/* The Response constructor, and Response.json(data, init) when json_data
|
||||
* is non-NULL: undici converts ResponseInit, serializes the data (TypeError
|
||||
* "Value is not JSON serializable" when JSON.stringify answers undefined),
|
||||
* then initializes the response exactly as the constructor does, with
|
||||
* "application/json" as the default content type. */
|
||||
typedef ScrDyn *(*SfJsonSerialize)(const ScrDyn *value);
|
||||
|
||||
/* The JSON serializer reaches the constructor only through Response.json's
|
||||
* entry point, so programs that never call it link no JSON walker. */
|
||||
static ScrDyn *sf_response_json_serialize(const ScrDyn *value) {
|
||||
ScrStr *gap = scr_str_new("", 0);
|
||||
ScrDyn *serialized = scr_json_stringify_replacer(value, scr_dyn_undefined(), gap);
|
||||
scr_str_release(gap);
|
||||
return serialized;
|
||||
}
|
||||
|
||||
static ScrDyn *sf_response_construct(ScrDyn *body, ScrDyn *init,
|
||||
const ScrDyn *json_data,
|
||||
SfJsonSerialize serialize) {
|
||||
ScrArr *header_pairs = NULL;
|
||||
ScrArr *converted_headers = NULL;
|
||||
ScrBytes *body_bytes = NULL;
|
||||
ScrStr *status_text = NULL;
|
||||
SfStream *stream = NULL;
|
||||
bool null_body =
|
||||
!body || body->kind == SCR_DYN_UNDEF || body->kind == SCR_DYN_NULL;
|
||||
bool json_body = json_data != NULL;
|
||||
bool null_body = !json_body &&
|
||||
(!body || body->kind == SCR_DYN_UNDEF || body->kind == SCR_DYN_NULL);
|
||||
bool text_body = false;
|
||||
int status = 200;
|
||||
bool status_range_error = false;
|
||||
@@ -4594,7 +4702,7 @@ ScrDyn *scr_fetch_response_new(ScrDyn *body, ScrDyn *init) {
|
||||
* string arm therefore runs before ResponseInit's dictionary conversion,
|
||||
* while a ReadableStream is only brand-converted here — its locked/
|
||||
* disturbed state belongs to the later body-extraction phase. */
|
||||
if (!null_body &&
|
||||
if (!null_body && !json_body &&
|
||||
!(body->kind == SCR_DYN_HANDLE &&
|
||||
body->v.handle.tag == SCR_DYNH_WEB_STREAM)) {
|
||||
if (body->kind == SCR_DYN_BYTES) {
|
||||
@@ -4668,7 +4776,25 @@ ScrDyn *scr_fetch_response_new(ScrDyn *body, ScrDyn *init) {
|
||||
/* Extract BodyInit before validating the response metadata. This makes a
|
||||
* locked/disturbed stream win over the later status range, statusText HTTP
|
||||
* reason-phrase, and HeadersInit validation exactly as in pinned Node. */
|
||||
if (null_body) {
|
||||
if (json_body) {
|
||||
ScrDyn *serialized = serialize(json_data);
|
||||
if (!serialized) goto fail;
|
||||
if (serialized->kind != SCR_DYN_STR) {
|
||||
static const char unserializable[30] = "Value is not JSON serializable";
|
||||
scr_dyn_release(serialized);
|
||||
scr_throw_error_msg(SCR_ERR_TYPE, unserializable, sizeof unserializable);
|
||||
goto fail;
|
||||
}
|
||||
ScrStr *encoding = scr_str_new("utf8", 4);
|
||||
body_bytes = scr_bytes_from_str(serialized->v.str, encoding);
|
||||
scr_str_release(encoding);
|
||||
scr_dyn_release(serialized);
|
||||
stream = sf_stream_new_native();
|
||||
sf_stream_enqueue_bytes(stream, body_bytes);
|
||||
sf_stream_close(stream);
|
||||
scr_bytes_release(body_bytes);
|
||||
body_bytes = NULL;
|
||||
} else if (null_body) {
|
||||
stream = sf_stream_new_native();
|
||||
sf_stream_close(stream);
|
||||
} else if (body->kind == SCR_DYN_HANDLE &&
|
||||
@@ -4719,6 +4845,11 @@ ScrDyn *scr_fetch_response_new(ScrDyn *body, ScrDyn *init) {
|
||||
sf_push_header_text(header_pairs, "content-type",
|
||||
"text/plain;charset=UTF-8", 24);
|
||||
}
|
||||
if (json_body && !sf_pairs_have(header_pairs, "content-type")) {
|
||||
static const char json_type[16] = "application/json";
|
||||
scr_arr_push_ref(header_pairs, scr_str_new("content-type", 12));
|
||||
scr_arr_push_ref(header_pairs, scr_str_new(json_type, sizeof json_type));
|
||||
}
|
||||
|
||||
SfResponse *response = calloc(1, sizeof *response);
|
||||
if (!response) sf_oom();
|
||||
@@ -4744,6 +4875,15 @@ fail:
|
||||
return NULL;
|
||||
}
|
||||
|
||||
ScrDyn *scr_fetch_response_new(ScrDyn *body, ScrDyn *init) {
|
||||
return sf_response_construct(body, init, NULL, NULL);
|
||||
}
|
||||
|
||||
ScrDyn *scr_fetch_response_json_static(ScrDyn *data, ScrDyn *init) {
|
||||
return sf_response_construct(NULL, init, data ? data : scr_dyn_undefined(),
|
||||
&sf_response_json_serialize);
|
||||
}
|
||||
|
||||
/* Request uses the same body stream and Headers ownership as Response. */
|
||||
typedef struct SfRequest {
|
||||
size_t rc;
|
||||
@@ -4809,6 +4949,7 @@ ScrDyn *scr_fetch_request_new(ScrDyn *input, ScrDyn *init) {
|
||||
ScrStr *url_text = source ? scr_str_retain(source->url) : scr_dyn_string_coerce_js(input);
|
||||
if (!url_text) return NULL;
|
||||
ScrUrl *url = scr_url_new(url_text);
|
||||
if (!url) sf_throw_url_parse_failure(url_text);
|
||||
scr_str_release(url_text);
|
||||
if (!url) return NULL;
|
||||
if (url->userinfo->len) { scr_url_release(url); sf_type_error("Request cannot be constructed from a URL that includes credentials"); return NULL; }
|
||||
@@ -5118,6 +5259,7 @@ ScrPromise *scr_fetch_static(ScrStr *url, ScrDyn *init) {
|
||||
|
||||
ScrUrl *u = scr_url_new(url);
|
||||
if (!u) {
|
||||
sf_throw_url_parse_failure(url);
|
||||
scr_dyn_release(coerced_body);
|
||||
scr_arr_release(headers);
|
||||
scr_str_release(method);
|
||||
|
||||
@@ -166,6 +166,7 @@ ScrDyn *scr_fetch_request_new(ScrDyn *input, ScrDyn *init);
|
||||
ScrDyn *scr_fetch_headers_new(ScrDyn *init);
|
||||
ScrPromise *scr_fetch_static(ScrStr *url, ScrDyn *init); /* +1 promise<Response handle> */
|
||||
ScrDyn *scr_fetch_response_new(ScrDyn *body, ScrDyn *init); /* borrowed args; +1 Response handle or NULL pending */
|
||||
ScrDyn *scr_fetch_response_json_static(ScrDyn *data, ScrDyn *init); /* Response.json; borrowed args; +1 Response handle or NULL pending */
|
||||
ScrPromise *scr_fetch_response_json(ScrDyn *response); /* +1 promise<dyn> */
|
||||
ScrPromise *scr_fetch_response_text(ScrDyn *response); /* +1 promise<dyn> */
|
||||
ScrPromise *scr_fetch_response_array_buffer(ScrDyn *response); /* +1 promise<dyn> */
|
||||
|
||||
@@ -801,14 +801,32 @@ static ScrStr *scr_insp_array_buffer(const ScrBytes *h, const char *cls, double
|
||||
return out;
|
||||
}
|
||||
|
||||
static ScrStr *scr_insp_dyn_error(ScrDyn *d, double recurse, double depth);
|
||||
|
||||
/* A runtime error through its node: the cached one, or one built from its
|
||||
* fields. */
|
||||
ScrStr *scr_insp_error_node(ScrError *e, double recurse, double depth) {
|
||||
ScrDyn *view = scr_errdyn_view_of(e);
|
||||
if (view != NULL && view->kind == SCR_DYN_OBJ) return scr_insp_dyn_error(view, recurse, depth);
|
||||
ScrDyn *fields = scr_error_dyn_fields(e);
|
||||
ScrStr *out = scr_insp_dyn_error(fields, recurse, depth);
|
||||
scr_dyn_release(fields);
|
||||
return out;
|
||||
}
|
||||
|
||||
/* Errors: the STACKLESS rendering — compiled binaries carry no stack, so
|
||||
* the base is formatError's bracket form `[Name: message]` / `[Name]`
|
||||
* (Node prints these exact forms for an error whose stack is empty;
|
||||
* stack-carrying Node output is a documented divergence). A stamped
|
||||
* `code` slot renders as the one extra own property Node would show:
|
||||
* `[Error: m] { code: 'X' }`, `[Error]` beyond depth. Message newlines
|
||||
* indent like Node's formatError tail. */
|
||||
* stack-carrying Node output is a documented divergence). A runtime error
|
||||
* renders through its checked-dynamic node, so the own properties Node
|
||||
* shows follow in key order: the system-error fields, code, and the extra
|
||||
* properties a throw site attached (`[Error: m] { errno: -2, code:
|
||||
* 'ENOENT', ... }`). Other errors render a stamped `code` slot as their
|
||||
* one extra property: `[Error: m] { code: 'X' }`, `[Error]` beyond depth.
|
||||
* Message newlines indent like Node's formatError tail. */
|
||||
ScrStr *scr_insp_error(ScrError *e, double recurse, double depth) {
|
||||
if (e->vt >= &scr_error_vts[0] && e->vt < &scr_error_vts[SCR_ERR_COUNT])
|
||||
return scr_insp_error_node(e, recurse, depth);
|
||||
bool has_code = e->code != NULL; /* the stamped slot, read directly (borrowed) */
|
||||
if (has_code && recurse > depth) {
|
||||
/* `[${getCtxStyle(...)}]` with the trailing space sliced: [Name] */
|
||||
@@ -897,13 +915,53 @@ static const ScrDyn *insp_error_slot(const ScrDyn *d, const char *key, size_t le
|
||||
return v != NULL && v->kind == SCR_DYN_STR ? v : NULL;
|
||||
}
|
||||
|
||||
/* AssertionError's custom inspect (lib/internal/assert/assertion_error.js)
|
||||
* renders the error at depth 0 and shortens string actual/expected values
|
||||
* with addEllipsis: past 10 lines, the first 10 and "\n..."; otherwise past
|
||||
* 512 characters, the text from offset 512 on and "...". +1. */
|
||||
static ScrDyn *insp_assertion_ellipsis(ScrDyn *value) {
|
||||
if (value->kind != SCR_DYN_STR) return scr_dyn_retain(value);
|
||||
ScrStr *text = value->v.str;
|
||||
size_t lines = 1, cut = 0;
|
||||
for (size_t i = 0; i < text->len; i++) {
|
||||
if (text->data[i] != '\n') continue;
|
||||
if (++lines == 11) {
|
||||
cut = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
InspBuf b = {0};
|
||||
if (lines > 10) {
|
||||
ib_bytes(&b, text->data, cut);
|
||||
ib_cstr(&b, "\n...");
|
||||
} else if (scr_str_utf16_len(text) > 512) {
|
||||
ScrStr *tail = scr_str_slice(text, 512, scr_str_utf16_len(text));
|
||||
ib_str(&b, tail);
|
||||
ib_cstr(&b, "...");
|
||||
scr_str_release(tail);
|
||||
} else {
|
||||
return scr_dyn_retain(value);
|
||||
}
|
||||
ScrStr *shortened = ib_take(&b);
|
||||
ScrDyn *out = scr_dyn_new_str(shortened);
|
||||
scr_str_release(shortened);
|
||||
return out;
|
||||
}
|
||||
|
||||
/* The checked-dynamic Error encoding (scr_error_dyn_fields) rendered like
|
||||
* scr_insp_error: the stackless bracket form `[Name: message]`, then the
|
||||
* remaining enumerable own properties (code, errno, syscall, ...) in
|
||||
* Node's `{ ... }` tail. */
|
||||
static ScrStr *scr_insp_dyn_error(ScrDyn *d, double recurse, double depth) {
|
||||
/* An error already on the traversal stack (a spawnSync error's own
|
||||
* `error`): Node's [Circular *N], and <ref *N> on the outer rendering. */
|
||||
double circular = scr_insp_circ_check(d);
|
||||
if (circular != 0) return scr_insp_circular(circular);
|
||||
const ScrDyn *name = insp_error_slot(d, "name", 4);
|
||||
const ScrDyn *message = insp_error_slot(d, "message", 7);
|
||||
bool assertion = name != NULL && name->v.str->len == 14 &&
|
||||
memcmp(name->v.str->data, "AssertionError", 14) == 0;
|
||||
double value_depth = assertion ? recurse : depth;
|
||||
size_t extra = 0;
|
||||
for (size_t i = 0; i < d->v.obj.len; i++) {
|
||||
ScrDynEntry *ent = &d->v.obj.entries[i];
|
||||
@@ -934,6 +992,7 @@ static ScrStr *scr_insp_dyn_error(ScrDyn *d, double recurse, double depth) {
|
||||
ScrStr *base_str = ib_take(&base);
|
||||
if (extra == 0) return base_str;
|
||||
scr_insp_begin(recurse + 1);
|
||||
scr_insp_seen_push(d);
|
||||
for (size_t i = 0; i < d->v.obj.len; i++) {
|
||||
ScrDynEntry *ent = &d->v.obj.entries[i];
|
||||
if (!ent->enumerable) continue;
|
||||
@@ -941,7 +1000,11 @@ static ScrStr *scr_insp_dyn_error(ScrDyn *d, double recurse, double depth) {
|
||||
(ent->key_len == 7 && memcmp(ent->key, "message", 7) == 0) ||
|
||||
(ent->key_len == 5 && memcmp(ent->key, "stack", 5) == 0))
|
||||
continue;
|
||||
ScrStr *val = scr_insp_dyn(ent->value, recurse + 1, depth);
|
||||
bool shortened = assertion && ((ent->key_len == 6 && memcmp(ent->key, "actual", 6) == 0) ||
|
||||
(ent->key_len == 8 && memcmp(ent->key, "expected", 8) == 0));
|
||||
ScrDyn *shown = shortened ? insp_assertion_ellipsis(ent->value) : scr_dyn_retain(ent->value);
|
||||
ScrStr *val = scr_insp_dyn(shown, recurse + 1, value_depth);
|
||||
scr_dyn_release(shown);
|
||||
InspBuf eb = {0};
|
||||
insp_key_into(&eb, ent->key, ent->key_len);
|
||||
ib_cstr(&eb, ": ");
|
||||
@@ -953,10 +1016,12 @@ static ScrStr *scr_insp_dyn_error(ScrDyn *d, double recurse, double depth) {
|
||||
}
|
||||
ScrStr *b0 = scr_str_new("{", 1);
|
||||
ScrStr *b1 = scr_str_new("}", 1);
|
||||
ScrStr *out = scr_insp_end(base_str, b0, b1, recurse + 1, false, false);
|
||||
ScrStr *body = scr_insp_end(base_str, b0, b1, recurse + 1, false, false);
|
||||
scr_str_release(b0);
|
||||
scr_str_release(b1);
|
||||
scr_str_release(base_str);
|
||||
ScrStr *out = scr_insp_ref_wrap(d, body);
|
||||
scr_str_release(body);
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,10 +26,15 @@ ScrStr *scr_insp_regex(ScrRegex *re); /* /source/flags */
|
||||
/* A Date (its time value): toISOString(), or "Invalid Date" for NaN (+1). */
|
||||
ScrStr *scr_insp_date(double ms);
|
||||
ScrStr *scr_insp_buffer(ScrBytes *b); /* <Buffer aa bb ...>, 50-byte cap */
|
||||
/* The stackless error form: [Name: message] / [Name], plus the stamped
|
||||
* code slot as its one extra property ({ code: 'X' }); [Name] beyond
|
||||
* depth when a code makes it composite. */
|
||||
/* The stackless error form: [Name: message] / [Name], then a runtime
|
||||
* error's own properties through its checked-dynamic node, or another
|
||||
* error's stamped code slot as its one extra property ({ code: 'X' });
|
||||
* [Name] beyond depth when a code makes it composite. */
|
||||
ScrStr *scr_insp_error(ScrError *e, double recurse, double depth);
|
||||
/* A runtime error rendered through its checked-dynamic node (the cached
|
||||
* one, or one built from its fields), so Node's own properties follow the
|
||||
* bracket form. */
|
||||
ScrStr *scr_insp_error_node(ScrError *e, double recurse, double depth);
|
||||
/* The checked-dynamic tree: shape lives in the value, so the whole
|
||||
* traversal is here. dyn-boxed bytes render in the checked-dynamic tree's documented
|
||||
* Uint8Array identity. */
|
||||
|
||||
+127
-10
@@ -2568,10 +2568,26 @@ static bool scr_record_source_declared(const ScrRecordSource *source, const char
|
||||
return scr_record_source_index(source, key, key_len) < source->count;
|
||||
}
|
||||
|
||||
/* Frees the table at thread exit (a worker's table dies with its thread):
|
||||
* the storage is detached before any remaining view is released. */
|
||||
static void scr_record_source_teardown(void) {
|
||||
ScrRecordSource *table = scr_record_sources;
|
||||
size_t capacity = scr_record_source_capacity;
|
||||
scr_record_sources = NULL;
|
||||
scr_record_source_capacity = scr_record_source_count = scr_record_source_deleted = 0;
|
||||
free(scr_record_source_filter);
|
||||
scr_record_source_filter = NULL;
|
||||
for (size_t i = 0; i < capacity; i++) {
|
||||
if (table[i].rec && table[i].rec != SCR_RECORD_SOURCE_DELETED) scr_dyn_release(table[i].view);
|
||||
}
|
||||
free(table);
|
||||
}
|
||||
|
||||
/* Inserts the entry of a record the caller just built; the table takes
|
||||
* the entry's view. A fresh record has no entry: the previous owner of its
|
||||
* address left the table when it was freed (the dispose hook). */
|
||||
static void scr_record_source_put(const ScrRecordSource *entry) {
|
||||
if (!scr_record_sources) scr_atexit(scr_record_source_teardown);
|
||||
scr_record_source_reserve();
|
||||
if (!scr_record_source_filter) {
|
||||
scr_record_source_filter = calloc(((size_t)1 << SCR_RECORD_SOURCE_FILTER_BITS) / 64,
|
||||
@@ -6392,10 +6408,13 @@ ScrStr *scr_dyn_format_j(const ScrDyn *d) {
|
||||
* IDENTITY-CACHED: one error instance boxes to ONE dyn node, however many
|
||||
* times it crosses (Node passes the error OBJECT through, so `found.error
|
||||
* === thrown` and re-crossings compare reference-equal — the tracing
|
||||
* suite's shape). The cache retains both sides for the process (like the
|
||||
* dc registry; released atexit before the RC audit) and the node
|
||||
* SNAPSHOTS name/message/code at first crossing — a later mutation of the
|
||||
* error is invisible through it (SEMANTICS.md). Two pointer-keyed hash
|
||||
* suite's shape). The cache retains both sides; an entry lives while
|
||||
* either side is referenced elsewhere, and a sweep releases the entries
|
||||
* whose error and node only the cache still references (the rest are
|
||||
* released atexit before the RC audit). The node also holds the error's
|
||||
* extra own properties (scr_error_set_prop) and SNAPSHOTS name/message/
|
||||
* code at first crossing — a later mutation of the error is invisible
|
||||
* through it (SEMANTICS.md). Two pointer-keyed hash
|
||||
* indexes find an entry by either side: Promise.allSettled and
|
||||
* Promise.any cross every rejection reason. Both sides are unique per
|
||||
* entry (an error boxes once; an alien view enters once). */
|
||||
@@ -6410,6 +6429,9 @@ static SCR_TL bool scr_errdyn_teardown_registered = false;
|
||||
/* Open-addressed indexes holding entry index + 1 (0 = empty slot). */
|
||||
static SCR_TL size_t *scr_errdyn_by_err = NULL, *scr_errdyn_by_dyn = NULL;
|
||||
static SCR_TL size_t scr_errdyn_slots = 0; /* a power of two */
|
||||
/* The entry count that triggers the next sweep: twice the entries the
|
||||
* previous sweep kept, so sweeping costs amortized O(1) per entry. */
|
||||
static SCR_TL size_t scr_errdyn_sweep_at = 64;
|
||||
|
||||
static size_t scr_errdyn_hash(const void *p) {
|
||||
size_t x = (size_t)(uintptr_t)p >> 4;
|
||||
@@ -6445,8 +6467,70 @@ static size_t scr_errdyn_find(const void *key, bool by_err) {
|
||||
|
||||
static void scr_errdyn_teardown(void);
|
||||
|
||||
/* The entries of a node that hold the node itself (a spawnSync error's
|
||||
* `error`): references the node makes to itself, not reachability. */
|
||||
static size_t scr_errdyn_self_refs(const ScrDyn *d) {
|
||||
if (d->kind != SCR_DYN_OBJ) return 0;
|
||||
size_t n = 0;
|
||||
for (size_t i = 0; i < d->v.obj.len; i++) n += d->v.obj.entries[i].value == d;
|
||||
return n;
|
||||
}
|
||||
|
||||
/* Releases a node the cache owned, dropping its references to itself
|
||||
* first so the release frees it. */
|
||||
static void scr_errdyn_release_node(ScrDyn *d) {
|
||||
for (size_t i = 0; d->kind == SCR_DYN_OBJ && i < d->v.obj.len; i++) {
|
||||
ScrDynEntry *entry = &d->v.obj.entries[i];
|
||||
if (entry->value != d) continue;
|
||||
entry->value = scr_dyn_retain(scr_dyn_undefined());
|
||||
scr_dyn_release(d);
|
||||
}
|
||||
scr_dyn_release(d);
|
||||
}
|
||||
|
||||
/* Releases the entries nothing but the cache references. A field node
|
||||
* holds no reference to its error and a registered subclass box holds
|
||||
* one, so an entry whose node has only the cache's reference and whose
|
||||
* error has no reference beyond the cache's and its box's is
|
||||
* unreachable. The indexes are rebuilt over the
|
||||
* kept entries before anything is released (a teardown never sees a
|
||||
* stale entry); an entry a released node kept alive goes in a later
|
||||
* sweep. */
|
||||
static void scr_errdyn_sweep(void) {
|
||||
ScrErrDynEnt *dead = NULL;
|
||||
size_t n_dead = 0, kept = 0;
|
||||
for (size_t i = 0; i < scr_errdyn_n; i++) {
|
||||
ScrErrDynEnt ent = scr_errdyn_cache[i];
|
||||
size_t boxed = ent.dyn->kind == SCR_DYN_TYPED_REF && ent.dyn->v.typed_ref.ptr == (void *)ent.err;
|
||||
if (ent.dyn->rc == 1 + scr_errdyn_self_refs(ent.dyn) && ent.err->rc == 1 + boxed) {
|
||||
if (dead == NULL) {
|
||||
dead = malloc((scr_errdyn_n - i) * sizeof *dead);
|
||||
if (!dead) scr_trap("scriptc: out of memory\n");
|
||||
}
|
||||
dead[n_dead++] = ent;
|
||||
} else {
|
||||
scr_errdyn_cache[kept++] = ent;
|
||||
}
|
||||
}
|
||||
scr_errdyn_n = kept;
|
||||
scr_errdyn_sweep_at = kept * 2 > 64 ? kept * 2 : 64;
|
||||
if (n_dead == 0) return;
|
||||
memset(scr_errdyn_by_err, 0, scr_errdyn_slots * sizeof *scr_errdyn_by_err);
|
||||
memset(scr_errdyn_by_dyn, 0, scr_errdyn_slots * sizeof *scr_errdyn_by_dyn);
|
||||
for (size_t i = 0; i < scr_errdyn_n; i++) {
|
||||
scr_errdyn_index_put(scr_errdyn_by_err, scr_errdyn_cache[i].err, i, true);
|
||||
scr_errdyn_index_put(scr_errdyn_by_dyn, scr_errdyn_cache[i].dyn, i, false);
|
||||
}
|
||||
for (size_t i = 0; i < n_dead; i++) {
|
||||
scr_error_release(dead[i].err);
|
||||
scr_errdyn_release_node(dead[i].dyn);
|
||||
}
|
||||
free(dead);
|
||||
}
|
||||
|
||||
/* Appends an entry (both sides retained) and indexes it. */
|
||||
static void scr_errdyn_append(ScrError *e, ScrDyn *d) {
|
||||
if (scr_errdyn_n >= scr_errdyn_sweep_at) scr_errdyn_sweep();
|
||||
if (scr_errdyn_n == scr_errdyn_cap) {
|
||||
scr_errdyn_cap = scr_errdyn_cap ? scr_errdyn_cap * 2 : 8;
|
||||
scr_errdyn_cache = realloc(scr_errdyn_cache, scr_errdyn_cap * sizeof *scr_errdyn_cache);
|
||||
@@ -6502,17 +6586,21 @@ void scr_error_register_dyn(const ScrVt *vt, ScrDyn *(*box)(void *)) {
|
||||
}
|
||||
|
||||
static void scr_errdyn_teardown(void) {
|
||||
for (size_t i = 0; i < scr_errdyn_n; i++) {
|
||||
scr_error_release(scr_errdyn_cache[i].err);
|
||||
scr_dyn_release(scr_errdyn_cache[i].dyn);
|
||||
}
|
||||
free(scr_errdyn_cache);
|
||||
/* Detach the storage first: no release below sees a half-torn cache. */
|
||||
ScrErrDynEnt *cache = scr_errdyn_cache;
|
||||
size_t n = scr_errdyn_n;
|
||||
scr_errdyn_cache = NULL;
|
||||
scr_errdyn_n = scr_errdyn_cap = 0;
|
||||
free(scr_errdyn_by_err);
|
||||
free(scr_errdyn_by_dyn);
|
||||
scr_errdyn_by_err = scr_errdyn_by_dyn = NULL;
|
||||
scr_errdyn_slots = 0;
|
||||
scr_errdyn_sweep_at = 64;
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
scr_error_release(cache[i].err);
|
||||
scr_errdyn_release_node(cache[i].dyn);
|
||||
}
|
||||
free(cache);
|
||||
}
|
||||
|
||||
ScrDyn *scr_error_dyn_fields(const ScrError *e) {
|
||||
@@ -6540,7 +6628,11 @@ ScrDyn *scr_error_dyn_fields(const ScrError *e) {
|
||||
scr_dyn_obj_set(d, "message", 7, scr_dyn_new_str(e->message));
|
||||
d->v.obj.entries[d->v.obj.len - 1].enumerable = e->message_enumerable;
|
||||
}
|
||||
if (e->stack) scr_dyn_obj_set(d, "stack", 5, scr_dyn_new_str(e->stack));
|
||||
if (e->stack) {
|
||||
/* Node's own `stack` is non-enumerable. */
|
||||
scr_dyn_obj_set(d, "stack", 5, scr_dyn_new_str(e->stack));
|
||||
d->v.obj.entries[d->v.obj.len - 1].enumerable = false;
|
||||
}
|
||||
if (e->system_message) {
|
||||
/* SystemError: code, the read-only `info` context ({ message, path,
|
||||
* syscall, errno, code }), then errno, syscall, path and dest. */
|
||||
@@ -6617,6 +6709,23 @@ ScrDyn *scr_dyn_from_error(const ScrError *e) {
|
||||
return d;
|
||||
}
|
||||
|
||||
/* An own property on an error, as a throw site attaches Node's extra
|
||||
* fields (a dns error's hostname, an exec error's status): set on the
|
||||
* error's checked-dynamic node, which casts, the checked-dynamic surface
|
||||
* and util.inspect read. Moves value. */
|
||||
void scr_error_set_prop(ScrError *e, const char *key, size_t key_len, ScrDyn *value) {
|
||||
ScrDyn *view = scr_dyn_from_error(e);
|
||||
scr_dyn_obj_set(view, key, key_len, value);
|
||||
scr_dyn_release(view);
|
||||
}
|
||||
|
||||
/* The error's checked-dynamic node when it has one (borrowed), else NULL:
|
||||
* util.inspect reads its extra own properties. */
|
||||
ScrDyn *scr_errdyn_view_of(const ScrError *e) {
|
||||
size_t hit = scr_errdyn_find(e, true);
|
||||
return hit == SIZE_MAX ? NULL : scr_errdyn_cache[hit].dyn;
|
||||
}
|
||||
|
||||
/* The %Error EXTRACTION (dynCheck of `u as Error` / an instanceof-Error
|
||||
* narrow, and the dyn-boxed thunk's Error-typed parameters): the REVERSE
|
||||
* of scr_dyn_from_error, riding the same identity cache — a dyn error
|
||||
@@ -7741,7 +7850,15 @@ static void scr_dyn_object_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value, con
|
||||
scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&buffer));
|
||||
return;
|
||||
}
|
||||
bool created = scr_dyn_entry(recv, key) == NULL;
|
||||
scr_dyn_obj_set(recv, key->data, key->len, scr_dyn_retain(value));
|
||||
if (created && key->len == 5 && memcmp(key->data, "stack", 5) == 0 &&
|
||||
scr_errdyn_find(recv, false) != SIZE_MAX) {
|
||||
/* An error's own `stack` exists from construction as a non-enumerable
|
||||
* data property, so assigning it adds no enumerable key. */
|
||||
ScrDynEntry *entry = scr_dyn_entry(recv, key);
|
||||
if (entry) entry->enumerable = false;
|
||||
}
|
||||
scr_error_sync_cause(recv, key);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -8738,12 +8738,10 @@ static void scr_crypto_not_xof(void) {
|
||||
ScrStr *text = scr_str_new(message, sizeof message - 1);
|
||||
ScrError *e = scr_error_new(SCR_ERR_ERROR, text);
|
||||
scr_str_release(text);
|
||||
ScrDyn *view = scr_dyn_from_error(e);
|
||||
scr_dyn_obj_set(view, "library", 7, scr_crypto_dyn_str(scr_str_new("digital envelope routines", 25)));
|
||||
scr_dyn_obj_set(view, "reason", 6, scr_crypto_dyn_str(scr_str_new("not XOF or invalid length", 25)));
|
||||
scr_error_set_prop(e, "library", 7, scr_crypto_dyn_str(scr_str_new("digital envelope routines", 25)));
|
||||
scr_error_set_prop(e, "reason", 6, scr_crypto_dyn_str(scr_str_new("not XOF or invalid length", 25)));
|
||||
scr_error_set_code(e, code);
|
||||
scr_dyn_obj_set(view, "code", 4, scr_crypto_dyn_str(scr_str_new(code, sizeof code - 1)));
|
||||
scr_dyn_release(view);
|
||||
scr_error_set_prop(e, "code", 4, scr_crypto_dyn_str(scr_str_new(code, sizeof code - 1)));
|
||||
scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg());
|
||||
}
|
||||
|
||||
@@ -10489,6 +10487,72 @@ ScrStr *scr_date_to_iso(double ms) {
|
||||
return scr_str_new(buf, (size_t)len);
|
||||
}
|
||||
|
||||
/* Node's Date.prototype.toUTCString over a stored (TimeClip'd) time
|
||||
* value: "Www, DD Mmm YYYY HH:mm:ss GMT", a "-" and at least four digits
|
||||
* for negative years, and "Invalid Date" for NaN. Never throws. +1.
|
||||
* The text is assembled from function-local arrays rather than string
|
||||
* literals: literals would land in this unit's shared mergeable string
|
||||
* section and grow every executable, while these arrays are removed with
|
||||
* the function by --gc-sections when a program never calls it. */
|
||||
ScrStr *scr_date_to_utc_string(double ms) {
|
||||
/* Sized without the terminating NUL, so none is a C string the
|
||||
* compiler could move into a mergeable string section. */
|
||||
static const char invalid[12] = "Invalid Date";
|
||||
static const char weekdays[21] = "SunMonTueWedThuFriSat";
|
||||
static const char months[36] = "JanFebMarAprMayJunJulAugSepOctNovDec";
|
||||
if (!(fabs(ms) <= 8640000000000000.0)) return scr_str_new(invalid, sizeof invalid);
|
||||
double t = trunc(ms);
|
||||
double dayd = floor(t / 86400000.0);
|
||||
long long msday = (long long)(t - dayd * 86400000.0);
|
||||
long long days = (long long)dayd;
|
||||
int weekday = (int)(((days + 4) % 7 + 7) % 7);
|
||||
long long z = days + 719468;
|
||||
long long era = (z >= 0 ? z : z - 146096) / 146097;
|
||||
unsigned long long doe = (unsigned long long)(z - era * 146097);
|
||||
unsigned long long yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
|
||||
long long y = (long long)yoe + era * 400;
|
||||
unsigned long long doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
||||
unsigned long long mp = (5 * doy + 2) / 153;
|
||||
unsigned long long d = doy - (153 * mp + 2) / 5 + 1;
|
||||
unsigned long long m = mp < 10 ? mp + 3 : mp - 9;
|
||||
if (m <= 2) y += 1;
|
||||
int hh = (int)(msday / 3600000), mi = (int)(msday / 60000 % 60),
|
||||
ss = (int)(msday / 1000 % 60);
|
||||
char buf[40];
|
||||
size_t n = 0;
|
||||
memcpy(buf, weekdays + 3 * weekday, 3);
|
||||
n = 3;
|
||||
buf[n++] = ',';
|
||||
buf[n++] = ' ';
|
||||
buf[n++] = (char)('0' + d / 10);
|
||||
buf[n++] = (char)('0' + d % 10);
|
||||
buf[n++] = ' ';
|
||||
memcpy(buf + n, months + 3 * (m - 1), 3);
|
||||
n += 3;
|
||||
buf[n++] = ' ';
|
||||
if (y < 0) buf[n++] = '-';
|
||||
unsigned long long year = (unsigned long long)(y < 0 ? -y : y);
|
||||
char digits[20];
|
||||
size_t count = 0;
|
||||
do {
|
||||
digits[count++] = (char)('0' + year % 10);
|
||||
year /= 10;
|
||||
} while (year > 0);
|
||||
while (count < 4) digits[count++] = '0';
|
||||
while (count > 0) buf[n++] = digits[--count];
|
||||
int parts[3] = {hh, mi, ss};
|
||||
for (int i = 0; i < 3; i++) {
|
||||
buf[n++] = i == 0 ? ' ' : ':';
|
||||
buf[n++] = (char)('0' + parts[i] / 10);
|
||||
buf[n++] = (char)('0' + parts[i] % 10);
|
||||
}
|
||||
buf[n++] = ' ';
|
||||
buf[n++] = 'G';
|
||||
buf[n++] = 'M';
|
||||
buf[n++] = 'T';
|
||||
return scr_str_new(buf, n);
|
||||
}
|
||||
|
||||
/* ── new Date(dateString).getTime() ──────────────────────────────────────
|
||||
*
|
||||
* The BOUNDED date-string parse (documented divergence — V8's parser
|
||||
|
||||
@@ -175,7 +175,7 @@ static void check_documents(void) {
|
||||
/* A link-gated unit's handle crosses threads through the transfer hooks in
|
||||
* its ops: share on the sender, adopt on the receiver, drop with the
|
||||
* message. The test borrows the last capacity slot, which no unit uses, so
|
||||
* it also proves that slots past SCR_DYNH_COUNT work. */
|
||||
* it also proves that slots past the tags in use work. */
|
||||
#define TRANSFER_TAG ((ScrDynHandleTag)(SCR_DYNH_CAPACITY - 1))
|
||||
typedef struct {
|
||||
_Atomic size_t rc;
|
||||
|
||||
@@ -113,6 +113,9 @@ double scr_date_get_time(double ms);
|
||||
* 0–9999). THROWS Node's "Invalid time value" RangeError on NaN or
|
||||
* |ms| > 8.64e15 and returns NULL; +1 otherwise. */
|
||||
ScrStr *scr_date_to_iso(double ms);
|
||||
/* Node's Date.prototype.toUTCString ("Thu, 01 Jan 1970 00:00:00 GMT";
|
||||
* "Invalid Date" for NaN). Never throws; +1. */
|
||||
ScrStr *scr_date_to_utc_string(double ms);
|
||||
/* new Date(dateString).getTime(): the BOUNDED parse (the X509 validity
|
||||
* shape + ECMA's format with an explicit offset); NaN elsewhere. */
|
||||
double scr_date_parse_get_time(ScrStr *s);
|
||||
|
||||
@@ -395,9 +395,9 @@ static ScrDyn *uc_callbackify_rejected(ScrClosure *closure, ScrDyn *const *args,
|
||||
ScrError *e = scr_error_new(SCR_ERR_ERROR, text);
|
||||
scr_str_release(text);
|
||||
scr_error_set_code(e, "ERR_FALSY_VALUE_REJECTION");
|
||||
scr_error_set_prop(e, "reason", 6, scr_dyn_retain(reason));
|
||||
err = scr_dyn_from_error(e);
|
||||
scr_error_release(e);
|
||||
scr_dyn_obj_set(err, "reason", 6, scr_dyn_retain(reason));
|
||||
}
|
||||
uc_schedule_callback(cb, err, scr_dyn_retain(scr_dyn_undefined()), 1);
|
||||
scr_dyn_release(cb);
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
// util.inspect of errors that carry Node's extra own properties: the
|
||||
// AssertionError fields (rendered at depth 0, with long string values
|
||||
// shortened), the falsy-rejection reason util.callbackify attaches, an
|
||||
// OpenSSL-style crypto error's library and reason, and a system error's
|
||||
// errno, syscall and path. Each error's stack is cleared first, so Node
|
||||
// prints the same stackless form.
|
||||
import * as assert from "node:assert";
|
||||
import * as crypto from "node:crypto";
|
||||
import * as fs from "node:fs";
|
||||
import { callbackify, inspect } from "node:util";
|
||||
|
||||
function show(e: unknown): void {
|
||||
if (e instanceof Error) {
|
||||
e.stack = "";
|
||||
console.log(inspect(e));
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
assert.strictEqual(1, 2);
|
||||
} catch (e) {
|
||||
show(e);
|
||||
}
|
||||
type Doc = { a: number; nested?: { b: number[] } };
|
||||
const left: Doc = { a: 1, nested: { b: [1, 2] } };
|
||||
const right: Doc = { a: 2 };
|
||||
try {
|
||||
assert.deepStrictEqual(left, right, "custom message");
|
||||
} catch (e) {
|
||||
show(e);
|
||||
}
|
||||
try {
|
||||
assert.notStrictEqual("x".repeat(600), "x".repeat(600));
|
||||
} catch (e) {
|
||||
show(e);
|
||||
}
|
||||
try {
|
||||
assert.ok(false);
|
||||
} catch (e) {
|
||||
show(e);
|
||||
}
|
||||
try {
|
||||
throw new RangeError("no extra properties");
|
||||
} catch (e) {
|
||||
show(e);
|
||||
}
|
||||
try {
|
||||
crypto.createHash("sha256", { outputLength: 5 });
|
||||
} catch (e) {
|
||||
show(e);
|
||||
}
|
||||
try {
|
||||
fs.readFileSync("/no/such/dir/file.txt");
|
||||
} catch (e) {
|
||||
show(e);
|
||||
console.log(inspect({ error: e }, { depth: 0 }));
|
||||
}
|
||||
|
||||
async function rejectsNull(): Promise<void> {
|
||||
return Promise.reject(null);
|
||||
}
|
||||
callbackify(rejectsNull)((err: NodeJS.ErrnoException | null) => {
|
||||
show(err);
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
// Errors that cross into checked-dynamic code (a cast of a caught error)
|
||||
// in a retry loop, on the main thread and in a worker at the same time:
|
||||
// every crossing reads the properties of its own error.
|
||||
import * as assert from "node:assert";
|
||||
import { Worker, isMainThread, parentPort } from "node:worker_threads";
|
||||
|
||||
function round(n: number, offset: number): number {
|
||||
let total = 0;
|
||||
for (let i = 0; i < n; i++) {
|
||||
try {
|
||||
assert.strictEqual(i + offset, -1);
|
||||
} catch (e) {
|
||||
const err = e as { code?: string; operator?: string; actual?: number };
|
||||
total += (err.code ?? "").length + (err.operator ?? "").length + (err.actual === i + offset ? 1 : 0);
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
if (isMainThread) {
|
||||
const worker = new Worker(new URL(import.meta.url));
|
||||
const fromWorker = new Promise<number>((resolve) => worker.once("message", resolve));
|
||||
let sum = 0;
|
||||
for (let r = 0; r < 5; r++) sum += round(5000, r);
|
||||
console.log("main", sum);
|
||||
console.log("worker", await fromWorker);
|
||||
} else {
|
||||
let sum = 0;
|
||||
for (let r = 0; r < 5; r++) sum += round(5000, 100 + r);
|
||||
parentPort?.postMessage(sum);
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// child_process errors carry Node's own properties: the sync forms'
|
||||
// status, signal, output, pid, stdout and stderr (Buffers, or strings with
|
||||
// an encoding option), the async forms' code, killed, signal and cmd (with
|
||||
// stdout and stderr when promisified), and a spawn failure's errno,
|
||||
// syscall, path and spawnargs. Pids vary, so only their type prints.
|
||||
import { exec, execFile, execFileSync, execSync } from "node:child_process";
|
||||
import { inspect, promisify } from "node:util";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
function keys(e: unknown): string {
|
||||
return Object.keys(e as object).join(",");
|
||||
}
|
||||
|
||||
function show(label: string, e: unknown): void {
|
||||
if (!(e instanceof Error)) return;
|
||||
const value: unknown = e;
|
||||
const pid = typeof (value as { pid?: number }).pid;
|
||||
e.stack = "";
|
||||
const text = inspect(e).replace(/pid: \d+/, "pid: <pid>");
|
||||
console.log(label, pid, keys(e));
|
||||
console.log(text);
|
||||
}
|
||||
|
||||
try {
|
||||
execFileSync("/bin/sh", ["-c", "echo out; echo err 1>&2; exit 3"], { stdio: "pipe" });
|
||||
} catch (e) {
|
||||
const err = e as { status?: number | null; signal?: string | null; stdout?: Buffer; stderr?: Buffer };
|
||||
console.log("status", err.status, err.signal, err.stdout?.toString(), err.stderr?.toString());
|
||||
show("execFileSync", e);
|
||||
}
|
||||
try {
|
||||
execSync("echo text; exit 4", { encoding: "utf8", stdio: "pipe" });
|
||||
} catch (e) {
|
||||
const err = e as { status?: number | null; stdout?: string; output?: (string | null)[] };
|
||||
console.log("encoding", err.status, JSON.stringify(err.stdout), inspect(err.output));
|
||||
}
|
||||
try {
|
||||
execSync("exit 5", { stdio: "ignore" });
|
||||
} catch (e) {
|
||||
const err = e as { output?: unknown[]; stdout?: unknown };
|
||||
console.log("ignored", inspect(err.output), err.stdout);
|
||||
}
|
||||
try {
|
||||
execFileSync("/bin/sh", ["-c", "kill -TERM $$"], { stdio: "pipe" });
|
||||
} catch (e) {
|
||||
const err = e as { status?: number | null; signal?: string | null };
|
||||
console.log("signal", err.status, err.signal);
|
||||
}
|
||||
try {
|
||||
execFileSync("/no/such/binary", ["a", "b"]);
|
||||
} catch (e) {
|
||||
show("spawn failure", e);
|
||||
const err = e as { errno?: number; syscall?: string; path?: string; spawnargs?: string[] };
|
||||
console.log(err.errno, err.syscall, err.path, inspect(err.spawnargs));
|
||||
}
|
||||
try {
|
||||
execFileSync("/bin/sh", ["-c", "echo partial; exec sleep 5"], { timeout: 100, stdio: "pipe" });
|
||||
} catch (e) {
|
||||
const err = e as { code?: string; status?: number | null; signal?: string | null; stdout?: Buffer };
|
||||
console.log("timeout", err.code, err.status, err.signal, err.stdout?.toString());
|
||||
}
|
||||
|
||||
execFile("/bin/sh", ["-c", "echo o; exit 6"], (err, stdout) => {
|
||||
const fields: unknown = err;
|
||||
const e = fields as { code?: number | null; killed?: boolean; signal?: string | null; cmd?: string };
|
||||
console.log("callback", e.code, e.killed, e.signal, e.cmd, JSON.stringify(stdout));
|
||||
show("callback error", err);
|
||||
execFile("/no/such/binary", ["x"], (missing) => {
|
||||
show("callback spawn failure", missing);
|
||||
exec("echo shell; exit 4", (shellError) => {
|
||||
show("exec callback", shellError);
|
||||
void promised();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function promised(): Promise<void> {
|
||||
try {
|
||||
await execFileAsync("/bin/sh", ["-c", "echo po; echo pe 1>&2; exit 7"]);
|
||||
} catch (e) {
|
||||
show("promisified", e);
|
||||
}
|
||||
try {
|
||||
await execFileAsync("/bin/sh", ["-c", "exec sleep 5"], { timeout: 100 });
|
||||
} catch (e) {
|
||||
const err = e as { code?: number | null; killed?: boolean; signal?: string | null };
|
||||
console.log("promisified timeout", err.code, err.killed, err.signal);
|
||||
}
|
||||
try {
|
||||
await execFileAsync("/no/such/binary");
|
||||
} catch (e) {
|
||||
show("promisified spawn failure", e);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// Iterator objects from strings and typed arrays, Iterator.from over the
|
||||
// built-in iterables, return() on helper objects, and flatMap over Set,
|
||||
// Map and string results.
|
||||
const log: string[] = [];
|
||||
function* count(label: string, n: number): Generator<number> {
|
||||
try {
|
||||
for (let i = 1; i <= n; i++) {
|
||||
log.push(`${label}${i}`);
|
||||
yield i;
|
||||
}
|
||||
} finally {
|
||||
log.push(`${label}:closed`);
|
||||
}
|
||||
}
|
||||
function flush(what: unknown): void {
|
||||
console.log(what, "|", log.join(" "));
|
||||
log.length = 0;
|
||||
}
|
||||
|
||||
// String iterators step by code point; a lone surrogate is one step.
|
||||
const text = "a\u{1F600}b\uD800";
|
||||
const chars = text[Symbol.iterator]();
|
||||
console.log(chars.next(), chars.next().value?.length, [...chars].length);
|
||||
console.log(text[Symbol.iterator]().map((c) => c.codePointAt(0)!.toString(16)).toArray());
|
||||
console.log("naïve"[Symbol.iterator]().filter((c) => c !== "ï").toArray().join(""));
|
||||
for (const c of "xyz"[Symbol.iterator]().drop(1)) console.log("char", c);
|
||||
console.log("hello"[Symbol.iterator]().some((c) => c === "l"), "hello"[Symbol.iterator]().find((c) => c > "h"));
|
||||
|
||||
// Typed array iterators.
|
||||
const bytes = new Uint8Array([9, 8, 7]);
|
||||
console.log(bytes.values().map((x) => x * 2).toArray(), bytes.keys().filter((i) => i > 0).toArray());
|
||||
console.log(bytes.entries().map(([i, v]) => i * v).toArray());
|
||||
const floats = new Float64Array([0.5, 1.5, 2.5]).values().map((x) => x + 1);
|
||||
console.log(floats.next(), [...floats]);
|
||||
for (const [i, v] of new Int16Array([-1, 2, -3, 4]).entries().filter(([, v]) => v > 0)) console.log(i, v);
|
||||
|
||||
// Iterator.from returns each built-in iterable's own iterator.
|
||||
const list = [1, 2];
|
||||
const fromList = Iterator.from(list);
|
||||
list.push(3);
|
||||
console.log([...fromList], Iterator.from("a\u{1F600}").toArray());
|
||||
console.log(Iterator.from(new Set(["p", "q"])).toArray(), Iterator.from(new Map([["k", 1]])).toArray());
|
||||
console.log(Iterator.from(new Uint8Array([4, 5])).reduce((a, b) => a + b, 0));
|
||||
const fromGen = Iterator.from(count("a", 3));
|
||||
flush([fromGen.next(), fromGen.take(1).toArray()]);
|
||||
flush(Iterator.from(count("b", 9).map((x) => x * 3)).take(2).toArray());
|
||||
|
||||
// return() on helper objects.
|
||||
const started = count("c", 3);
|
||||
started.next();
|
||||
const doubled = started.map((x) => x * 2);
|
||||
flush([doubled.return!(), started.next()]);
|
||||
const fresh = count("d", 3);
|
||||
const kept = fresh.filter(() => true);
|
||||
flush([kept.return?.(), fresh.next(), kept.next()]);
|
||||
const stepping = count("e", 5).drop(1);
|
||||
flush([stepping.next(), stepping.return!(), stepping.next()]);
|
||||
flush(count("f", 4).map((x) => x).take(2).return!());
|
||||
|
||||
// flatMap over Set and Map results, and a string result's TypeError.
|
||||
console.log([1, 2].values().flatMap((x) => new Set([x, x * 10])).toArray());
|
||||
console.log([1, 2].values().flatMap((x) => new Map([[`k${x}`, x]])).toArray());
|
||||
const pairs = new Map<string, number>([["m", 1], ["n", 2]]);
|
||||
for (const [k, v] of [0].values().flatMap(() => pairs)) console.log(k, v);
|
||||
try {
|
||||
count("g", 3).flatMap((x) => `s${x}`).toArray();
|
||||
} catch (e) {
|
||||
log.push(`${(e as Error).name}: ${(e as Error).message}`);
|
||||
}
|
||||
flush("string result");
|
||||
@@ -0,0 +1,70 @@
|
||||
// The JavaScript form: iterator objects from strings and typed arrays,
|
||||
// Iterator.from over the built-in iterables, return() on helper objects,
|
||||
// and flatMap over Set, Map and string results.
|
||||
const log = [];
|
||||
function* count(label , n ) {
|
||||
try {
|
||||
for (let i = 1; i <= n; i++) {
|
||||
log.push(`${label}${i}`);
|
||||
yield i;
|
||||
}
|
||||
} finally {
|
||||
log.push(`${label}:closed`);
|
||||
}
|
||||
}
|
||||
function flush(what ) {
|
||||
console.log(what, "|", log.join(" "));
|
||||
log.length = 0;
|
||||
}
|
||||
|
||||
// String iterators step by code point; a lone surrogate is one step.
|
||||
const text = "a\u{1F600}b\uD800";
|
||||
const chars = text[Symbol.iterator]();
|
||||
console.log(chars.next(), chars.next().value.length, [...chars].length);
|
||||
console.log(text[Symbol.iterator]().map((c) => c.length).toArray());
|
||||
console.log("naïve"[Symbol.iterator]().filter((c) => c !== "ï").toArray().join(""));
|
||||
for (const c of "xyz"[Symbol.iterator]().drop(1)) console.log("char", c);
|
||||
console.log("hello"[Symbol.iterator]().some((c) => c === "l"), "hello"[Symbol.iterator]().find((c) => c > "h"));
|
||||
|
||||
// Typed array iterators.
|
||||
const bytes = new Uint8Array([9, 8, 7]);
|
||||
console.log(bytes.values().map((x) => x * 2).toArray(), bytes.keys().filter((i) => i > 0).toArray());
|
||||
console.log(bytes.entries().map(([i, v]) => i * v).toArray());
|
||||
const floats = new Float64Array([0.5, 1.5, 2.5]).values().map((x) => x + 1);
|
||||
console.log(floats.next(), [...floats]);
|
||||
for (const [i, v] of new Int16Array([-1, 2, -3, 4]).entries().filter(([, v]) => v > 0)) console.log(i, v);
|
||||
|
||||
// Iterator.from returns each built-in iterable's own iterator.
|
||||
const list = [1, 2];
|
||||
const fromList = Iterator.from(list);
|
||||
list.push(3);
|
||||
console.log([...fromList], Iterator.from("a\u{1F600}").toArray());
|
||||
console.log(Iterator.from(new Set(["p", "q"])).toArray(), Iterator.from(new Map([["k", 1]])).toArray());
|
||||
console.log(Iterator.from(new Uint8Array([4, 5])).reduce((a, b) => a + b, 0));
|
||||
const fromGen = Iterator.from(count("a", 3));
|
||||
flush([fromGen.next(), fromGen.take(1).toArray()]);
|
||||
flush(Iterator.from(count("b", 9).map((x) => x * 3)).take(2).toArray());
|
||||
|
||||
// return() on helper objects.
|
||||
const started = count("c", 3);
|
||||
started.next();
|
||||
const doubled = started.map((x) => x * 2);
|
||||
flush([doubled.return(), started.next()]);
|
||||
const fresh = count("d", 3);
|
||||
const kept = fresh.filter(() => true);
|
||||
flush([kept.return?.(), fresh.next(), kept.next()]);
|
||||
const stepping = count("e", 5).drop(1);
|
||||
flush([stepping.next(), stepping.return(), stepping.next()]);
|
||||
flush(count("f", 4).map((x) => x).take(2).return());
|
||||
|
||||
// flatMap over Set and Map results, and a string result's TypeError.
|
||||
console.log([1, 2].values().flatMap((x) => new Set([x, x * 10])).toArray());
|
||||
console.log([1, 2].values().flatMap((x) => new Map([[`k${x}`, x]])).toArray());
|
||||
const pairs = new Map ([["m", 1], ["n", 2]]);
|
||||
for (const [k, v] of [0].values().flatMap(() => pairs)) console.log(k, v);
|
||||
try {
|
||||
count("g", 3).flatMap((x) => `s${x}`).toArray();
|
||||
} catch (e) {
|
||||
log.push(`${e.name}: ${e.message}`);
|
||||
}
|
||||
flush("string result");
|
||||
@@ -0,0 +1,27 @@
|
||||
// A base method calling `this.set()` where only a subclass is ever
|
||||
// constructed devirtualizes to the subclass override; a polymorphic `this`
|
||||
// return resolves per declaring class, so the direct call's subclass
|
||||
// result upcasts to the base method's type (was an internal compiler error).
|
||||
class Cookies {
|
||||
values: string[] = [];
|
||||
set(value: string): this {
|
||||
this.values.push(value);
|
||||
return this;
|
||||
}
|
||||
delete(name: string): this {
|
||||
return this.set(`${name}=; Max-Age=0`);
|
||||
}
|
||||
}
|
||||
class MirroredCookies extends Cookies {
|
||||
mirror: string[] = [];
|
||||
override set(value: string): this {
|
||||
super.set(value);
|
||||
this.mirror.push(value.toUpperCase());
|
||||
return this;
|
||||
}
|
||||
}
|
||||
const cookies = new MirroredCookies();
|
||||
cookies.set("a=1").delete("b").set("c=3");
|
||||
console.log(cookies.values.join(" | "));
|
||||
console.log(cookies.mirror.join(" | "));
|
||||
console.log(cookies.delete("d") === cookies, cookies.values.length);
|
||||
@@ -0,0 +1,36 @@
|
||||
// Class fields typed as native fetch handles (Headers, Request, Response),
|
||||
// directly, optional, nullable, in arrays, and inside record fields, hold
|
||||
// the live handle: mutation through one reference is seen through another.
|
||||
class Holder {
|
||||
headers: Headers;
|
||||
list: Headers[] = [];
|
||||
maybe?: Response;
|
||||
req: Request | null = null;
|
||||
bag: { headers: Headers; status: number };
|
||||
constructor(headers: Headers) {
|
||||
this.headers = headers;
|
||||
this.bag = { headers, status: 1 };
|
||||
}
|
||||
}
|
||||
const holder = new Holder(new Headers({ a: "1" }));
|
||||
console.log(holder.headers.get("a"), holder.list.length, holder.maybe === undefined, holder.req);
|
||||
holder.list.push(new Headers({ b: "2" }));
|
||||
holder.maybe = new Response("x", { status: 201 });
|
||||
holder.req = new Request("http://localhost/p", { method: "POST" });
|
||||
console.log(holder.list[0]!.get("b"), holder.maybe.status, holder.req.method, holder.req.url);
|
||||
holder.headers.set("c", "3");
|
||||
console.log(holder.bag.headers.get("c"), holder.bag.headers === holder.headers);
|
||||
|
||||
// Symbol-keyed fields hold the handles the same way.
|
||||
const NATIVE = Symbol("native");
|
||||
class Wrapper {
|
||||
[NATIVE]: Response;
|
||||
constructor(body: string) {
|
||||
this[NATIVE] = new Response(body, { status: 202 });
|
||||
}
|
||||
get status(): number {
|
||||
return this[NATIVE].status;
|
||||
}
|
||||
}
|
||||
const wrapped = new Wrapper("ok");
|
||||
console.log(wrapped.status, wrapped[NATIVE].headers.get("content-type"));
|
||||
@@ -0,0 +1,36 @@
|
||||
// A getter that never returns a value compiles: annotated `never` or
|
||||
// unannotated (the removed-API getter idiom, which throws), and a getter
|
||||
// without a return statement, whose reads answer undefined.
|
||||
class Req {
|
||||
ok = 1;
|
||||
get page(): never {
|
||||
throw new Error("page has been removed");
|
||||
}
|
||||
get ua(): never {
|
||||
throw new TypeError("ua has been removed");
|
||||
}
|
||||
public get legacy() {
|
||||
throw new RangeError("legacy has been removed");
|
||||
}
|
||||
get nothing() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const r = new Req();
|
||||
try {
|
||||
console.log(r.page);
|
||||
} catch (e) {
|
||||
console.log(e instanceof Error ? `${e.name}: ${e.message}` : e);
|
||||
}
|
||||
try {
|
||||
const ua = r.ua;
|
||||
console.log("unreachable", ua);
|
||||
} catch (e) {
|
||||
console.log(e instanceof Error ? `${e.name}: ${e.message}` : e);
|
||||
}
|
||||
try {
|
||||
console.log(r.legacy);
|
||||
} catch (e) {
|
||||
console.log(e instanceof Error ? `${e.name}: ${e.message}` : e);
|
||||
}
|
||||
console.log(r.ok, r.nothing);
|
||||
@@ -0,0 +1,43 @@
|
||||
// Symbol-keyed (and literal-keyed) TypeScript fields without an
|
||||
// initializer take their declared type when the constructor assigns them
|
||||
// unconditionally; an optional one starts undefined. A field assigned
|
||||
// later (here from a method the constructor calls) reads through the
|
||||
// checked deferred-initialization slot.
|
||||
const Internal = Symbol("internal");
|
||||
const Registered = Symbol.for("pkg.registered");
|
||||
const Pending = Symbol("pending");
|
||||
|
||||
class Box {
|
||||
[Internal]: { n: number };
|
||||
[Registered]: string[];
|
||||
["literal"]: number;
|
||||
[Pending]?: string[];
|
||||
constructor(n: number) {
|
||||
this[Internal] = { n };
|
||||
this[Registered] = ["a"];
|
||||
this["literal"] = n * 2;
|
||||
}
|
||||
get n(): number {
|
||||
return this[Internal].n;
|
||||
}
|
||||
}
|
||||
|
||||
const b = new Box(5);
|
||||
console.log(b.n, b[Registered].length, b.literal, b[Pending] === undefined);
|
||||
b[Internal].n = 7;
|
||||
b[Registered].push("b");
|
||||
b[Pending] = ["p"];
|
||||
console.log(b.n, b[Registered].join(","), b[Pending]?.join(","));
|
||||
console.log(JSON.stringify(Object.entries({ literal: b.literal })));
|
||||
|
||||
class Late {
|
||||
[Internal]!: { n: number };
|
||||
constructor(flag: boolean) {
|
||||
if (flag) this[Internal] = { n: 1 };
|
||||
else this.init();
|
||||
}
|
||||
init(): void {
|
||||
this[Internal] = { n: 2 };
|
||||
}
|
||||
}
|
||||
console.log(new Late(true)[Internal].n, new Late(false)[Internal].n);
|
||||
@@ -0,0 +1,26 @@
|
||||
// Date.prototype.toUTCString: "Www, DD Mmm YYYY HH:mm:ss GMT" in UTC, a
|
||||
// leading "-" and at least four digits for negative years, "Invalid Date"
|
||||
// for an invalid time value.
|
||||
const times = [
|
||||
0,
|
||||
-1,
|
||||
1445412480000,
|
||||
951782400000, // 2000-02-29
|
||||
Date.UTC(1969, 11, 31, 23, 59, 59, 999),
|
||||
Date.UTC(0, 0, 1) - 1900 * 365.2425 * 86400000,
|
||||
Date.UTC(-1, 5, 15, 12, 30, 45),
|
||||
Date.UTC(-271821, 3, 20),
|
||||
8.64e15,
|
||||
-8.64e15,
|
||||
1234.9,
|
||||
-1234.9,
|
||||
NaN,
|
||||
8.64e15 + 1,
|
||||
];
|
||||
for (const t of times) {
|
||||
const d = new Date(t);
|
||||
console.log(d.toUTCString());
|
||||
}
|
||||
for (let day = 0; day < 7; day++) console.log(new Date(Date.UTC(2024, 0, day + 1)).toUTCString());
|
||||
for (let month = 0; month < 12; month++) console.log(new Date(Date.UTC(2023, month, 9, 8, 7, 6)).toUTCString());
|
||||
console.log(new Date("2015-10-21T07:28:00Z").toUTCString(), new Date("garbage").toUTCString());
|
||||
@@ -0,0 +1,18 @@
|
||||
// Array destructuring of a Map or Set walks its iterator: Map entries as
|
||||
// [key, value] pairs, Set values, holes and rest included.
|
||||
const m = new Map([["a", "1"], ["b", "2"], ["c", "3"]]);
|
||||
const [[name, value], ...rest] = m;
|
||||
console.log(name, value, rest.length, JSON.stringify(rest));
|
||||
const [first, , third] = m;
|
||||
console.log(JSON.stringify(first), JSON.stringify(third));
|
||||
const s = new Set([3, 1, 2]);
|
||||
const [x, y, ...others] = s;
|
||||
console.log(x, y, others);
|
||||
let [p] = new Map<string, number>([["only", 1]]);
|
||||
p = ["changed", 2];
|
||||
console.log(p);
|
||||
function firstKey(map: Map<string, number>): string {
|
||||
const [[k]] = map;
|
||||
return k;
|
||||
}
|
||||
console.log(firstKey(new Map([["z", 26]])));
|
||||
@@ -0,0 +1,63 @@
|
||||
// Headers iteration follows Fetch's "sort and combine": names in byte
|
||||
// order with their combined values, except that every Set-Cookie value is
|
||||
// visited on its own. get() still joins them; getSetCookie() lists them.
|
||||
const h = new Headers();
|
||||
h.append("set-cookie", "a=1; Path=/");
|
||||
h.append("x-b", "1");
|
||||
h.append("Set-Cookie", "b=2, c=3");
|
||||
h.append("x-a", "1");
|
||||
h.append("x-a", "2");
|
||||
h.append("set-cookie", "d=4");
|
||||
h.forEach((value, name) => console.log(`${name}: ${value}`));
|
||||
console.log(JSON.stringify(h.get("set-cookie")));
|
||||
console.log(JSON.stringify(h.getSetCookie()));
|
||||
|
||||
// A thisArg and the Headers object reach the callback.
|
||||
const self = { tag: "this" };
|
||||
h.forEach(function (this: typeof self, _value, name, headers) {
|
||||
if (name === "x-b") console.log(this.tag, headers === h);
|
||||
}, self);
|
||||
|
||||
// Iteration is live and indexes the expanded list: deleting the cookies
|
||||
// while visiting the first one shifts the next name into its slot.
|
||||
const live = new Headers();
|
||||
live.append("set-cookie", "a=1");
|
||||
live.append("set-cookie", "b=2");
|
||||
live.append("x-z", "z");
|
||||
const seen: string[] = [];
|
||||
live.forEach((value, name) => {
|
||||
seen.push(`${name}=${value}`);
|
||||
if (value === "a=1") live.delete("set-cookie");
|
||||
});
|
||||
console.log(seen.join(" | "));
|
||||
|
||||
// Appending a cookie during the walk is visited when it sorts later.
|
||||
const grow = new Headers();
|
||||
grow.append("set-cookie", "a=1");
|
||||
grow.append("x-y", "y");
|
||||
const grown: string[] = [];
|
||||
grow.forEach((value, name) => {
|
||||
grown.push(`${name}=${value}`);
|
||||
if (value === "a=1") grow.append("set-cookie", "b=2");
|
||||
});
|
||||
console.log(grown.join(" | "));
|
||||
|
||||
// Only set-cookie stays separate; cookie and other repeats combine.
|
||||
const plain = new Headers([
|
||||
["cookie", "a=1"],
|
||||
["cookie", "b=2"],
|
||||
["accept", "x"],
|
||||
["accept", "y"],
|
||||
]);
|
||||
plain.forEach((value, name) => console.log(`${name}: ${value}`));
|
||||
console.log(JSON.stringify(plain.getSetCookie()));
|
||||
|
||||
// A response's headers walk the same way.
|
||||
const response = new Response("ok", {
|
||||
headers: [
|
||||
["set-cookie", "s=1"],
|
||||
["set-cookie", "t=2"],
|
||||
["content-type", "text/plain"],
|
||||
],
|
||||
});
|
||||
response.headers.forEach((value, name) => console.log(`${name}: ${value}`));
|
||||
@@ -0,0 +1,41 @@
|
||||
// An unparsable fetch() or Request URL is undici's TypeError
|
||||
// "Failed to parse URL from <input>", whose non-enumerable cause is the
|
||||
// URL parser's TypeError.
|
||||
function report(label: string, e: unknown): void {
|
||||
const serialized = JSON.stringify(e);
|
||||
if (!(e instanceof Error)) {
|
||||
console.log(label, "not an Error");
|
||||
return;
|
||||
}
|
||||
console.log(label, e.name, e.message, e instanceof TypeError);
|
||||
console.log(" has cause:", "cause" in e, "own enumerable:", serialized);
|
||||
console.log(
|
||||
" cause:",
|
||||
e.cause instanceof Error ? `${e.cause.name} ${e.cause.message} ${e.cause instanceof TypeError}` : String(e.cause),
|
||||
);
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
for (const input of ["http://127.0.0.1:1http://x/", "/relative/path", "", "http://[::1"]) {
|
||||
try {
|
||||
await fetch(input);
|
||||
console.log("resolved?");
|
||||
} catch (e) {
|
||||
report(`fetch(${JSON.stringify(input)})`, e);
|
||||
}
|
||||
}
|
||||
try {
|
||||
new Request("not a url");
|
||||
console.log("constructed?");
|
||||
} catch (e) {
|
||||
report("new Request", e);
|
||||
}
|
||||
// A valid URL with credentials keeps its own message.
|
||||
try {
|
||||
await fetch("http://user:pass@127.0.0.1:1/");
|
||||
} catch (e) {
|
||||
report("credentials", e);
|
||||
}
|
||||
}
|
||||
|
||||
void main();
|
||||
@@ -0,0 +1,44 @@
|
||||
// fetch() sends its header list's combined values: repeated Set-Cookie
|
||||
// request headers go out as one line, the values joined by ", ", at the
|
||||
// position of the first one, whatever form the headers were given in.
|
||||
import { createServer } from "node:http";
|
||||
|
||||
function shown(raw: string[]): string {
|
||||
const lines: string[] = [];
|
||||
for (let i = 0; i < raw.length; i += 2) {
|
||||
const name = raw[i]!;
|
||||
const lower = name.toLowerCase();
|
||||
if (lower === "host" || lower === "connection" || lower.startsWith("accept") || lower.startsWith("sec-") || lower === "user-agent") continue;
|
||||
lines.push(`${name}: ${raw[i + 1]}`);
|
||||
}
|
||||
return lines.join(" | ");
|
||||
}
|
||||
|
||||
const server = createServer((req, res) => {
|
||||
res.end(shown(req.rawHeaders));
|
||||
});
|
||||
|
||||
server.listen(0, "127.0.0.1", async () => {
|
||||
const addr = server.address();
|
||||
const port = typeof addr === "object" && addr ? addr.port : 0;
|
||||
const url = `http://127.0.0.1:${port}/`;
|
||||
|
||||
const headers = new Headers();
|
||||
headers.append("set-cookie", "a=1");
|
||||
headers.append("x-first", "1");
|
||||
headers.append("set-cookie", "b=2");
|
||||
console.log(await (await fetch(url, { headers })).text());
|
||||
|
||||
const pairs: [string, string][] = [
|
||||
["x-before", "0"],
|
||||
["set-cookie", "c=3"],
|
||||
["set-cookie", "d=4; Path=/"],
|
||||
];
|
||||
console.log(await (await fetch(url, { headers: pairs })).text());
|
||||
|
||||
console.log(await (await fetch(url, { headers: { "set-cookie": "e=5", "x-record": "r" } })).text());
|
||||
|
||||
const single = new Headers({ "set-cookie": "only=1" });
|
||||
console.log(await (await fetch(url, { headers: single })).text());
|
||||
server.close();
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
// A generic call with a literal `undefined` argument binds T = undefined;
|
||||
// the parameter holds undefined like a declared `v: undefined` parameter.
|
||||
function kind<T>(v: T): string {
|
||||
return typeof v;
|
||||
}
|
||||
console.log(kind(undefined), kind(null), kind(1), kind("s"));
|
||||
function same<T>(a: T, b: T): boolean {
|
||||
return a === b;
|
||||
}
|
||||
console.log(same(undefined, undefined));
|
||||
function wrap<T>(v: T): { v: T } {
|
||||
return { v };
|
||||
}
|
||||
const w = wrap(undefined);
|
||||
console.log(JSON.stringify(w), w.v === undefined, "v" in w);
|
||||
function describe<T>(label: string, v: T): string {
|
||||
return `${label}=${String(v)}`;
|
||||
}
|
||||
console.log(describe("u", undefined));
|
||||
@@ -0,0 +1,44 @@
|
||||
// JSON.stringify of a class instance whose class (or a base) declares
|
||||
// toJSON serializes what toJSON(key) returns: the root key is "", nested
|
||||
// hooks run through the same walker, and a toJSON answering undefined
|
||||
// makes the whole result undefined.
|
||||
class Url {
|
||||
href: string;
|
||||
constructor(href: string) {
|
||||
this.href = href;
|
||||
}
|
||||
toJSON(): string {
|
||||
return this.href;
|
||||
}
|
||||
}
|
||||
class NextUrl extends Url {
|
||||
base = "ignored";
|
||||
}
|
||||
class Keyed {
|
||||
toJSON(key: string): { key: string; at: Url[] } {
|
||||
return { key, at: [new Url("n")] };
|
||||
}
|
||||
}
|
||||
class Missing {
|
||||
toJSON(): undefined {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
console.log(JSON.stringify(new Url("http://x/")));
|
||||
console.log(JSON.stringify(new NextUrl("http://y/")));
|
||||
console.log(JSON.stringify(new Keyed()));
|
||||
console.log(JSON.stringify(new Keyed(), null, 2));
|
||||
function serialize(value: Missing): string {
|
||||
const text = JSON.stringify(value);
|
||||
return text === undefined ? "undefined result" : `text ${text}`;
|
||||
}
|
||||
console.log(serialize(new Missing()), JSON.stringify(new Missing()) === undefined);
|
||||
let calls = 0;
|
||||
class Counted {
|
||||
toJSON(): number {
|
||||
calls++;
|
||||
return calls;
|
||||
}
|
||||
}
|
||||
const c = new Counted();
|
||||
console.log(JSON.stringify(c), JSON.stringify(c), calls);
|
||||
@@ -0,0 +1,38 @@
|
||||
// JSON.stringify of an unknown (or generic) root whose value is undefined,
|
||||
// a function, or a symbol returns undefined, not text. Wherever the program
|
||||
// can tell the two apart (undefined/null comparisons, typeof, ??, ?., truth
|
||||
// tests, returns declaring undefined, and bindings used that way) the
|
||||
// native result is the actual undefined. Nested values follow Node's
|
||||
// member rules: dropped from objects, null in arrays.
|
||||
const u: unknown = undefined;
|
||||
const f: unknown = () => 1;
|
||||
const s: unknown = Symbol("x");
|
||||
const ok: unknown = { a: [1, undefined] };
|
||||
for (const v of [u, f, s, ok]) {
|
||||
const t = JSON.stringify(v);
|
||||
console.log(t === undefined, typeof t, t ?? "fb", t?.length, t == null, t !== undefined);
|
||||
}
|
||||
for (const v of [u, ok]) {
|
||||
const t = JSON.stringify(v);
|
||||
if (t) console.log("truthy", t.length);
|
||||
else console.log("falsy");
|
||||
console.log(!t, t ? "y" : "n", t || "or", t && "and");
|
||||
}
|
||||
console.log(JSON.stringify(u) === undefined, typeof JSON.stringify(f), JSON.stringify(s) ?? "fb2", JSON.stringify(u)?.length);
|
||||
console.log(JSON.stringify(u) || "or2", JSON.stringify(ok) || "or3", !JSON.stringify(u));
|
||||
function raw<T>(v: T): string | undefined {
|
||||
return JSON.stringify(v);
|
||||
}
|
||||
console.log(typeof raw(u), raw(ok));
|
||||
function ser<T>(v: T): string {
|
||||
const text = JSON.stringify(v);
|
||||
if (text === undefined) return "undefined result";
|
||||
return `string ${text} ${text.length}`;
|
||||
}
|
||||
console.log(ser(u), ser({ a: 1 }), ser(1), ser("s"), ser(ok));
|
||||
// Unobserved uses keep the plain string path.
|
||||
const plain = JSON.stringify(ok);
|
||||
console.log(plain.length, plain, `${JSON.stringify(u)}`, "x" + JSON.stringify(u));
|
||||
// A module-scope binding keeps the string slot; direct uses still observe.
|
||||
const top = JSON.stringify(ok);
|
||||
console.log(top.length, JSON.stringify(u) === undefined);
|
||||
@@ -0,0 +1,37 @@
|
||||
// `...(cond && { k: v })` spreads nothing when cond is a falsy primitive
|
||||
// (false, 0, ""), so it is the optional-field idiom `...(cond ? { k: v } : {})`:
|
||||
// cond evaluates once, v only when taken. (Fields print one by one: the
|
||||
// declared-shape property order of spread-built records is a documented
|
||||
// limitation.)
|
||||
type C = { name: string; name2: string; httpOnly?: boolean; maxAge?: number; path?: string; sameSite?: string };
|
||||
function parse(attrs: Record<string, string | undefined>, flag: boolean): C {
|
||||
const httponly = attrs.httponly;
|
||||
const maxage = attrs["max-age"];
|
||||
const samesite = attrs.samesite;
|
||||
let calls = 0;
|
||||
const lower = (s: string): string => {
|
||||
calls++;
|
||||
return s.toLowerCase();
|
||||
};
|
||||
const c: C = {
|
||||
name: "a",
|
||||
...(flag && { httpOnly: true }),
|
||||
...(httponly && { path: httponly }),
|
||||
...(typeof maxage === "string" && { maxAge: Number(maxage) }),
|
||||
name2: "/",
|
||||
...(samesite && { sameSite: lower(samesite) }),
|
||||
};
|
||||
console.log("calls", calls);
|
||||
return c;
|
||||
}
|
||||
for (const [attrs, flag] of [
|
||||
[{}, false],
|
||||
[{ httponly: "yes", "max-age": "10", samesite: "Lax" }, true],
|
||||
[{ httponly: "", "max-age": "", samesite: "" }, true],
|
||||
] as [Record<string, string | undefined>, boolean][]) {
|
||||
const c = parse(attrs, flag);
|
||||
console.log(c.name, c.name2, c.httpOnly, c.path, c.maxAge, c.sameSite, "httpOnly" in c, "path" in c);
|
||||
}
|
||||
const n: number = Number(process.argv.length > 99 ? "1" : "0");
|
||||
const d: { v?: number; name: string } = { name: "n", ...(n && { v: n }) };
|
||||
console.log(d.name, d.v, "v" in d);
|
||||
@@ -0,0 +1,28 @@
|
||||
// An optional call of a standard-library method that always exists on a
|
||||
// never-nullish receiver is the plain call (`getSetCookie?.()` is always
|
||||
// present on Node 24).
|
||||
const h = new Headers();
|
||||
h.append("set-cookie", "a=1");
|
||||
h.append("set-cookie", "b=2");
|
||||
const cookies = h.getSetCookie?.() ?? [];
|
||||
console.log(JSON.stringify(cookies));
|
||||
console.log(h.get?.("set-cookie"));
|
||||
console.log(h.has?.("x-missing"));
|
||||
h.set?.("x-set", "1");
|
||||
h.forEach?.((value, name) => console.log(`${name}=${value}`));
|
||||
|
||||
// The receiver evaluates once; arguments evaluate after it.
|
||||
let reads = 0;
|
||||
function headers(): Headers {
|
||||
reads++;
|
||||
return h;
|
||||
}
|
||||
function arg(): string {
|
||||
console.log("argument after receiver", reads);
|
||||
return "x-set";
|
||||
}
|
||||
console.log(headers().get?.(arg()), reads);
|
||||
|
||||
// Strings and arrays: the same rule.
|
||||
const s = "Hello";
|
||||
console.log(s.toUpperCase?.(), [3, 1, 2].sort?.().join(","));
|
||||
@@ -0,0 +1,27 @@
|
||||
// .toString() on a flow-narrowed union whose storage union is wider: the
|
||||
// read retags to the proven arms, then formats like String().
|
||||
import type { OutgoingHttpHeaders } from "node:http";
|
||||
|
||||
function host(headers?: OutgoingHttpHeaders): string {
|
||||
if (headers?.host && !Array.isArray(headers.host)) return headers.host.toString();
|
||||
return "";
|
||||
}
|
||||
console.log(JSON.stringify([host({ host: "a:1" }), host({ host: 3 }), host({}), host()]));
|
||||
|
||||
function label(v: string | number | string[] | undefined): string {
|
||||
if (v === undefined) return "none";
|
||||
if (Array.isArray(v)) return `list(${v.length})`;
|
||||
return v.toString();
|
||||
}
|
||||
console.log(label("x"), label(1.5), label(-0), label(["a", "b"]), label(undefined));
|
||||
|
||||
function flag(v: boolean | number | null): string {
|
||||
return v === null ? "null" : v.toString();
|
||||
}
|
||||
console.log(flag(true), flag(false), flag(1e21), flag(null));
|
||||
|
||||
type Field = string | number | { nested: string };
|
||||
function field(f: Field): string {
|
||||
return typeof f === "object" ? f.nested : f.toString();
|
||||
}
|
||||
console.log(field("s"), field(42), field({ nested: "n" }));
|
||||
@@ -1,9 +1,13 @@
|
||||
// Iterator helpers lower over Map/Set iterators, generators and array
|
||||
// iterators. These helper forms are refused explicitly.
|
||||
// Iterator helpers lower over Map/Set iterators, generators and array,
|
||||
// string and typed array iterators. These helper forms are refused
|
||||
// explicitly.
|
||||
const set = new Set<number>([1, 2]);
|
||||
// flatMap callbacks return arrays or generators.
|
||||
set.values().flatMap((value) => new Set([value, value + 1]));
|
||||
// return() on a helper object.
|
||||
const mapped = set.values().map((value) => value * 2);
|
||||
mapped.return!();
|
||||
// return() on an iterator object that is not provably a helper object
|
||||
// (array, string, Map and Set iterators have no return method).
|
||||
function close(it: IteratorObject<number>): void {
|
||||
it.return!();
|
||||
}
|
||||
close(set.values().map((value) => value * 2));
|
||||
// throw() on a helper object (helpers have no throw method).
|
||||
set.values().map((value) => value + 1).throw!(new Error("stop"));
|
||||
// Each helper call must produce a diagnostic.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// The es2025-lib surface WITHOUT a lowering fences by name: resizable
|
||||
// ArrayBuffers and transfer, SharedArrayBuffer.grow, Atomics.waitAsync,
|
||||
// first-class iterator objects, and the unlowered forms of the
|
||||
// Iterator.from over objects with their own next method, and the unlowered forms of the
|
||||
// implemented statics (Promise.try with fewer arguments than parameters,
|
||||
// groupBy's literal-union keys, a set-like non-Set argument).
|
||||
|
||||
@@ -21,8 +21,8 @@ sab.grow(16);
|
||||
const ia = new Int32Array(4);
|
||||
const waited = Atomics.waitAsync(ia, 0, 0, 1);
|
||||
|
||||
// First-class iterator objects.
|
||||
const it = Iterator.from([1, 2, 3].values());
|
||||
// Iterator.from over an object with its own next method.
|
||||
const it = Iterator.from({ next: (): IteratorResult<number> => ({ value: 1, done: false }) });
|
||||
|
||||
// Promise.try with fewer arguments than the callback's parameters.
|
||||
const tried = Promise.try((a: number, b?: number) => a + (b ?? 0), 1);
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
// The string iterator has no iterator-helper lowering; the call is
|
||||
// refused instead of failing at run time.
|
||||
const upper = "abc"[Symbol.iterator]().map((c) => c.toUpperCase());
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
// 100,000 caught AssertionErrors read through a cast, on the main thread
|
||||
// or (with the "worker" argument) in a worker; prints the resident-set
|
||||
// growth in MiB after a warm-up round.
|
||||
import * as assert from "node:assert";
|
||||
import { Worker, isMainThread, parentPort } from "node:worker_threads";
|
||||
|
||||
function round(n: number): number {
|
||||
let total = 0;
|
||||
for (let i = 0; i < n; i++) {
|
||||
try {
|
||||
assert.strictEqual(i, -1);
|
||||
} catch (e) {
|
||||
const err = e as { code?: string; operator?: string };
|
||||
total += (err.code ?? "").length + (err.operator ?? "").length;
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
function measure(): number {
|
||||
round(20000);
|
||||
const before = process.memoryUsage().rss;
|
||||
for (let r = 0; r < 5; r++) round(20000);
|
||||
return Math.round((process.memoryUsage().rss - before) / 1048576);
|
||||
}
|
||||
|
||||
if (!isMainThread) {
|
||||
parentPort?.postMessage(measure());
|
||||
} else if (process.argv[2] === "worker") {
|
||||
const worker = new Worker(new URL(import.meta.url));
|
||||
worker.once("message", (growth: number) => console.log(growth));
|
||||
} else {
|
||||
console.log(measure());
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// Response.json(data, init): ResponseInit converts, the data serializes with
|
||||
// JSON.stringify (TypeError when it answers undefined), then the response
|
||||
// initializes like the constructor, defaulting content-type to
|
||||
// application/json.
|
||||
function show(label: string, make: () => Response): void {
|
||||
try {
|
||||
const r = make();
|
||||
console.log(label, r.status, JSON.stringify(r.statusText), r.headers.get("content-type"), r.headers.get("x-a"));
|
||||
} catch (e) {
|
||||
console.log(label, e instanceof Error ? `${e.name}: ${e.message}` : String(e));
|
||||
}
|
||||
}
|
||||
show("plain", () => Response.json({ ok: true }));
|
||||
show("init", () => Response.json([1, 2], { status: 201, statusText: "Made", headers: { "x-a": "1" } }));
|
||||
show("content-type", () => Response.json("s", { headers: { "content-type": "text/x" } }));
|
||||
show("headers object", () => Response.json(null, { headers: new Headers([["x-a", "h"]]) }));
|
||||
show("undefined", () => Response.json(undefined));
|
||||
const fn: unknown = () => 1;
|
||||
show("function", () => Response.json(fn));
|
||||
show("range", () => Response.json({}, { status: 99 }));
|
||||
show("undefined before range", () => Response.json(undefined, { status: 99 }));
|
||||
show("null body status", () => Response.json({}, { status: 204 }));
|
||||
show("bad header", () => Response.json({}, { headers: { "bad name": "x" } }));
|
||||
show("undefined before bad header", () => Response.json(undefined, { headers: { "bad name": "x" } }));
|
||||
show("bad statusText", () => Response.json({}, { statusText: "a\nb" }));
|
||||
const data: unknown = JSON.parse('{"a":[1,"x",null],"n":{"deep":true}}');
|
||||
const r = Response.json({ data, d: new Date(0), skip: undefined });
|
||||
console.log(r.bodyUsed, await r.text(), r.bodyUsed);
|
||||
const parsed: unknown = await Response.json({ round: [1, 2] }).json();
|
||||
console.log(JSON.stringify(parsed));
|
||||
@@ -1,17 +1,17 @@
|
||||
collection-iterator-helpers.ts:5:22 - error SC2020: '.flatMap over a callback returning 'Set<number>'' is part of the standard library types but has no scriptc lowering yet
|
||||
collection-iterator-helpers.ts:8:3 - error SC2020: 'IteratorObject<number, unknown, unknown>.return' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
4 | // flatMap callbacks return arrays or generators.
|
||||
5 | set.values().flatMap((value) => new Set([value, value + 1]));
|
||||
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
6 | // return() on a helper object.
|
||||
7 | function close(it: IteratorObject<number>): void {
|
||||
8 | it.return!();
|
||||
| ^~~~~~~~~
|
||||
9 | }
|
||||
|
||||
hint: callbacks returning an array or a generator are the supported forms
|
||||
hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations)
|
||||
|
||||
collection-iterator-helpers.ts:8:1 - error SC2020: 'IteratorObject<number, undefined, unknown>.return' is part of the standard library types but has no scriptc lowering yet
|
||||
collection-iterator-helpers.ts:12:1 - error SC2020: 'IteratorObject<number, undefined, unknown>.throw' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
7 | const mapped = set.values().map((value) => value * 2);
|
||||
8 | mapped.return!();
|
||||
| ^~~~~~~~~~~~~
|
||||
9 | // Each helper call must produce a diagnostic.
|
||||
11 | // throw() on a helper object (helpers have no throw method).
|
||||
12 | set.values().map((value) => value + 1).throw!(new Error("stop"));
|
||||
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
13 | // Each helper call must produce a diagnostic.
|
||||
|
||||
hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations)
|
||||
@@ -5,7 +5,7 @@ date-formatters.ts:4:1 - error SC2020: 'Date.prototype.toString' is part of the
|
||||
| ^~~~~~~~~~~~~~~
|
||||
5 | date.toLocaleString();
|
||||
|
||||
hint: getTime(), valueOf(), toISOString(), toJSON(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering
|
||||
hint: getTime(), valueOf(), toISOString(), toUTCString(), toJSON(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering
|
||||
|
||||
date-formatters.ts:5:1 - error SC2020: 'Date.prototype.toLocaleString' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
@@ -14,4 +14,4 @@ date-formatters.ts:5:1 - error SC2020: 'Date.prototype.toLocaleString' is part o
|
||||
| ^~~~~~~~~~~~~~~~~~~~~
|
||||
6 |
|
||||
|
||||
hint: getTime(), valueOf(), toISOString(), toJSON(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering
|
||||
hint: getTime(), valueOf(), toISOString(), toUTCString(), toJSON(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering
|
||||
@@ -45,12 +45,12 @@ es2025-fences.ts:22:16 - error SC2020: 'Atomics.waitAsync' is part of the standa
|
||||
|
||||
es2025-fences.ts:25:12 - error SC2020: 'Iterator.from' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
24 | // First-class iterator objects.
|
||||
25 | const it = Iterator.from([1, 2, 3].values());
|
||||
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
24 | // Iterator.from over an object with its own next method.
|
||||
25 | const it = Iterator.from({ next: (): IteratorResult<number> => ({ value: 1, done: false }) });
|
||||
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
26 |
|
||||
|
||||
hint: iterator helpers apply directly to generators and to array, Map and Set iterators: arr.values().map(f).take(n).toArray()
|
||||
hint: Iterator.from accepts arrays, strings, typed arrays, Sets, Maps and iterator objects such as generators
|
||||
|
||||
es2025-fences.ts:28:27 - error SC2020: 'Promise.try over a callback of type '(a: number, b?: number | undefined) => number' with 1 argument' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ stdlib-fence.ts:25:1 - error SC2020: 'Date.prototype.setUTCFullYear with argumen
|
||||
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
26 | const sameDate = mutableDate === mutableDate; // scalar storage must not fake object identity
|
||||
|
||||
hint: getTime(), valueOf(), toISOString(), toJSON(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering
|
||||
hint: getTime(), valueOf(), toISOString(), toUTCString(), toJSON(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering
|
||||
|
||||
stdlib-fence.ts:26:18 - error SC1043: comparing non-number, non-string values are not supported yet
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user