Merge remote-tracking branch 'origin/ni/i6-subclasses' into ni/i7-routing

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Malte Ubl
2026-10-28 11:57:10 -07:00
co-authored by Claude Opus 5.5
50 changed files with 958 additions and 53 deletions
+4
View File
@@ -1258,6 +1258,9 @@ interface Response {
* direct use and points at bytes(). */
arrayBuffer(): Promise<ArrayBuffer>;
bytes(): Promise<Uint8Array>;
/* A copy whose body is one branch of the body's tee; the original keeps
* the other. */
clone(): Response;
}
interface ResponseInit {
headers?: Headers | Record<string, string> | readonly (readonly [string, string])[];
@@ -1335,6 +1338,7 @@ interface Request {
text(): Promise<string>;
arrayBuffer(): Promise<ArrayBuffer>;
bytes(): Promise<Uint8Array>;
clone(): Request;
}
interface RequestInit {
method?: string;
@@ -22,6 +22,7 @@ export function executableLinkFeatures(mod: IrModule, largePages = false): Nativ
...(features.stdioExtended ? { stdioExtended: true } : {}),
...(features.nodeIsland ? { nodeIsland: true } : {}),
...(features.moduleNamespace ? { moduleNamespace: true } : {}),
...(features.moduleNamespaceDyn ? { moduleNamespaceDyn: true } : {}),
zlib: features.zlib,
...(features.brotli ? { brotli: true } : {}),
assert: features.assert,
@@ -306,6 +306,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"process.builtinModule": "scr_process_builtin_module",
"process.builtinUnsupported": "scr_process_builtin_unsupported",
"module.namespaceValue": "scr_module_namespace_value",
"module.namespaceDyn": "scr_module_namespace_dyn_value",
"fs.callbackValue": "scr_fs_callback_value",
"fs.callbackCall": "scr_fs_callback_call",
"process.hrtimeValue": "scr_process_hrtime_value",
@@ -40,6 +40,9 @@ export interface NativeLinkFeatures {
/** Module namespace objects as checked-dynamic values link
* scr_module_namespace.c; absent (false) otherwise. */
moduleNamespace?: boolean;
/** A module namespace boxed for typed code links
* scr_module_namespace_dyn.c; absent (false) otherwise. */
moduleNamespaceDyn?: boolean;
zlib: boolean;
/** node:zlib's Brotli forms: scr_brotli.c and the vendored brotli archive. */
brotli?: boolean;
@@ -164,6 +164,9 @@ export interface CcOptions {
/** Module namespace objects as checked-dynamic values: compiles
* scr_module_namespace.c. */
moduleNamespace?: boolean;
/** A module namespace boxed for typed code: compiles
* scr_module_namespace_dyn.c. */
moduleNamespaceDyn?: boolean;
/** The program uses the process-events surface (signal/exit listeners,
* stdin events, for-await over stdin — moduleUsesProcessEvents on the
* IR): compiles scr_events.c into the binary. Event-free binaries keep
@@ -205,6 +205,8 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
unit((u) => u.opts.wasm, "scr_wasm.c"),
// Module namespace objects as checked-dynamic values.
unit((u) => u.opts.moduleNamespace === true, "scr_module_namespace.c"),
// A module namespace as the frozen null-prototype object typed code sees.
unit((u) => u.opts.moduleNamespaceDyn === true, "scr_module_namespace_dyn.c"),
unit(
(u) =>
u.opts.dynAsync ||
@@ -103,6 +103,7 @@ export interface RuntimeFeatureSet extends NativeLinkFeatures {
nodeIsland: boolean;
brotli: boolean;
moduleNamespace: boolean;
moduleNamespaceDyn: boolean;
nativeFetch: boolean;
abortSignalOnly: boolean;
netEffective: boolean;
@@ -312,6 +313,7 @@ export function effectiveRuntimeFeatures(
nodeIsland: features.nodeIsland === true,
brotli: features.brotli === true,
moduleNamespace: features.moduleNamespace === true,
moduleNamespaceDyn: features.moduleNamespaceDyn === true,
nativeFetch,
// The fetch unit has AbortController and AbortSignal; scr_abort.c is
// the same implementation without the fetch transport.
+18 -3
View File
@@ -231,7 +231,7 @@ function buildFetchCompatProfile() {
"body",
"bodyUsed",
],
responseCalls: ["json", "text", "bytes"],
responseCalls: ["json", "text", "bytes", "clone"],
readableStreamReads: ["locked"],
readableStreamCalls: ["cancel", "getReader"],
},
@@ -287,6 +287,13 @@ function buildFetchCompatProfile() {
facets: ["body-consumption", "promise-settlement", "state-machine", "error-shape"],
evidence: [fixture("static-request")],
})),
{
id: "stdlib.request.clone",
name: "Request.clone",
kind: "method",
facets: ["body-consumption", "identity", "state-machine", "error-shape"],
evidence: [fixture("static-clone")],
},
{
id: "stdlib.response.constructor",
name: "Response constructor",
@@ -538,6 +545,13 @@ function buildFetchCompatProfile() {
? [fixture("static")]
: [fixture("static"), fixture("static-stream")],
})),
{
id: "stdlib.response.clone",
name: "Response.clone",
kind: "method",
facets: ["body-consumption", "identity", "state-machine", "error-shape"],
evidence: [fixture("static-clone")],
},
],
inventory: {
interfaces: [
@@ -672,7 +686,7 @@ function buildFetchCompatProfile() {
"bytes",
"attribute",
].map((member) =>
["clone", "blob", "formData", "attribute"].includes(member)
["blob", "formData", "attribute"].includes(member)
? unsupportedEntry(
`stdlib.request.${member}`,
"Request",
@@ -706,7 +720,8 @@ function buildFetchCompatProfile() {
(member) => staticEntry(`stdlib.response.${member}`, "Response", member, "prototype"),
),
staticEntry("stdlib.response.arrayBuffer", "Response", "arrayBuffer", "prototype"),
...["clone", "blob", "formData"].map((member) =>
staticEntry("stdlib.response.clone", "Response", "clone", "prototype"),
...["blob", "formData"].map((member) =>
unsupportedEntry(
`stdlib.response.${member}`,
"Response",
@@ -67,6 +67,7 @@ export interface EarlyExecutableNativeFeatures {
stdioExtended?: boolean;
nodeIsland?: boolean;
moduleNamespace?: boolean;
moduleNamespaceDyn?: boolean;
zlib: boolean;
brotli?: boolean;
assert: boolean;
@@ -700,8 +700,7 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
// String(NaN) is "NaN" in the number formatter; `NaN !== NaN` holds
// because f64 compares are IEEE compares).
if (expr.text === "Infinity" || expr.text === "NaN") {
const sym = lowerer.checker.getSymbolAtLocation(expr);
if (lowerer.isStdlibSymbol(sym)) {
if (lowerer.isStdlibGlobal(expr, expr.text)) {
return { kind: "numLit", value: expr.text === "NaN" ? NaN : Infinity, type: F64, loc };
}
}
@@ -729,7 +728,7 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
return { kind: "libCall", fn: "dyn.arrayConstructor", args: [], type: DYN, loc };
if (
Object.values(BYTES_ELEMENT_NAME).includes(expr.text) &&
lowerer.isStdlibSymbol(lowerer.checker.getSymbolAtLocation(expr))
lowerer.isStdlibGlobal(expr, expr.text)
) {
return {
kind: "libCall",
@@ -16,6 +16,7 @@ import * as ts from "../ts7/adapter.js";
import {
BYTES_ELEMENT_NAME,
DYN,
F64,
STRING,
UNDEFINED_T,
type IrExpr,
@@ -133,6 +134,8 @@ export function jsGlobalValueByName(lowerer: Lowerer, name: string, loc: SrcLoc)
/** jsGlobalValueByName for the globals other than the interned builtin
* functions and constructors (builtinGlobalValue). */
export function jsSpecialGlobalValue(lowerer: Lowerer, name: string, loc: SrcLoc): IrExpr | null {
if (name === "NaN" || name === "Infinity")
return { kind: "numLit", value: name === "NaN" ? NaN : Infinity, type: F64, loc };
if (name === "String" || name === "Number" || name === "Boolean" || name === "BigInt")
return primitiveCtorClosure(lowerer, name, loc, true);
if (name === "ArrayBuffer")
@@ -41,7 +41,11 @@ import {
type UnitEvents,
type WaveHost,
} from "./unit-journal.js";
import { moduleNamespaceObject, moduleNamespaceValue } from "./module-namespace-elements.js";
import {
moduleNamespaceDynValue,
moduleNamespaceObject,
moduleNamespaceValue,
} from "./module-namespace-elements.js";
import { keyedHelperName, moduleTagNumber, portableRelativePath } from "./stable-names.js";
import { positionTag, StructuralPositions } from "./structural-positions.js";
import {
@@ -201,6 +205,7 @@ import {
captureContextBinding,
declareContextLocal,
declareContextThis,
newFnCtx,
type FnCtx,
} from "./function-context.js";
import type { FrontendServices } from "../services.js";
@@ -5256,6 +5261,79 @@ export class Lowerer {
return this.moduleOrder.find((sf) => tsgoPath(resolve(sf.fileName)) === path) ?? null;
}
/** A module namespace taken as a checked-dynamic value (`const u: unknown
* = ns`, `take(ns)`): the runtime's namespace object — one per module,
* Node's sorted export keys, live reads through a compiled getter, and
* Node's read-only write errors — built by `moduleNamespaceDynValue`.
* `node:` builtins have no compiled source and no complete export table,
* so they keep their fence. */
moduleNamespaceDyn(ns: IrExpr & { type: IrType & { kind: "moduleNs" } }): IrExpr | null {
if (!ns.type.moduleId.startsWith("file:")) return null;
return moduleNamespaceDynValue(this, ns);
}
/** A `Promise<module namespace>` taken as `Promise<unknown>` (or as
* `unknown`): one lifted async mapper per module awaits the promise and
* answers the same namespace object a bare namespace value boxes to, so
* the settlement is Node's namespace while the value stays a promise.
* The import() promise reaches these slots alone, and the runtime's
* per-module namespace cache keeps `await mapped === await original`; a
* rejection propagates unchanged. */
moduleNamespacePromiseDynValue(
value: IrExpr & { type: IrType & { kind: "promise" } },
): IrExpr | null {
const inner = value.type.inner;
if (inner.kind !== "moduleNs" || !inner.moduleId.startsWith("file:")) return null;
const loc = value.loc;
const name = `%module.promise:${inner.moduleId}`;
const promiseT: IrType & { kind: "promise" } = { kind: "promise", inner };
if (!this.liftedFns.some((fn) => fn.name === name)) {
const context = newFnCtx(false, null, null, DYN);
context.isAsync = true;
const previousClass = this.currentClass;
this.currentClass = null;
this.fnStack.push(context);
let namespace: IrExpr | null = null;
try {
const awaited: IrExpr & { type: IrType & { kind: "moduleNs" } } = {
kind: "awaitExpr",
value: varRef("promise", promiseT, loc),
type: inner,
loc,
};
namespace = moduleNamespaceDynValue(this, awaited);
if (namespace) {
const fn: IrFunction = {
name,
params: [{ localId: "promise", name: "promise", type: promiseT }],
returnType: DYN,
locals: [
{ id: "promise", name: "promise", type: promiseT, mutable: false },
...context.locals,
],
body: [{ kind: "return", value: namespace, loc }],
async: true,
loc,
};
this.liftedFns.push(fn);
}
} finally {
this.fnStack.pop();
this.currentClass = previousClass;
}
// A namespace with no compiled source has no dynamic value: keep the
// promise unconverted so the destination's own fence reports it.
if (!namespace) return null;
}
return {
kind: "call",
callee: name,
args: [value],
type: { kind: "promise", inner: DYN },
loc,
};
}
builtinErrorInfoOf(symbol: ts.Symbol | null | undefined): ClassInfo | null {
return builtinErrorInfoOf(this, symbol);
}
@@ -12010,6 +12088,21 @@ export class Lowerer {
const result = this.coerceToExpected(expr.result, expected);
return { ...expr, result, type: result.type };
}
// A `Promise<module namespace>` widening into `Promise<unknown>`, or
// into an `unknown` slot: map the payload through the namespace's
// dynamic value (moduleNamespacePromiseDynValue). A destination that
// wants the namespace type itself keeps the promise unchanged; the
// union path reuses this through its single-promise-arm conversion.
if (
expr.type.kind === "promise" &&
expr.type.inner.kind === "moduleNs" &&
(expected.kind === "dyn" || (expected.kind === "promise" && expected.inner.kind === "dyn"))
) {
const mapped = this.moduleNamespacePromiseDynValue(
expr as IrExpr & { type: IrType & { kind: "promise" } },
);
if (mapped) expr = mapped;
}
// An instance of a class declared `extends Array` is its array (the
// same object); an array slot of another element type converts it as
// an array.
@@ -12157,6 +12250,16 @@ export class Lowerer {
const object = moduleNamespaceObject(this, ns) ?? moduleNamespaceValue(this, ns);
if (object) return object;
}
// A TypeScript source's module namespace as an untyped value: the
// runtime's namespace object with live reads, Node's sorted keys and
// read-only writes. `node:` builtins have no compiled source or
// complete export table here and keep their fence below.
if (expr.type.kind === "moduleNs") {
const boxed = this.moduleNamespaceDyn(
expr as IrExpr & { type: IrType & { kind: "moduleNs" } },
);
if (boxed) return boxed;
}
if (
expr.type.kind === "moduleNs" &&
expr.type.moduleId === "builtin:events" &&
@@ -4,6 +4,7 @@ import * as ts from "../ts7/adapter.js";
import {
DYN,
STRING,
SYMBOL_T,
type IrExpr,
type IrFunction,
type IrStmt,
@@ -173,28 +174,28 @@ export function moduleNamespaceObject(
};
}
/** A module namespace taken as an untyped value whose exports a copy
* cannot represent: a binding that can change after initialization (`let`,
* `var`), or an export the copy cannot name. The value is the runtime's
* namespace object (scr_module_namespace_value): one per module, whose
* property reads call a compiled getter for the export's current value and
* whose writes throw Node's TypeError. An export whose value has no
* checked-dynamic form throws its fence when it is read, and listing the
* object's keys or inspecting it throws the "not supported yet" error. */
export function moduleNamespaceValue(
/** The compiled getter behind a namespace object: one lifted function per
* module that answers an export's current value by name, plus the module's
* sorted export names. An export whose value has no checked-dynamic form
* throws its fence when it is read. */
function moduleNamespaceGetter(
lowerer: Lowerer,
ns: IrExpr & { type: IrType & { kind: "moduleNs" } },
): IrExpr | null {
): { name: string; keys: string[] } | null {
const source = lowerer.sourceFileOfModuleNamespace(ns.type);
if (!source) return null;
const symbol = lowerer.checker.getSymbolAtLocation(source);
if (!symbol) return null;
const exports: { key: string; declaration: ts.Node | undefined }[] = [];
const exports: { key: string; declaration: ts.Node | undefined; symbol: ts.Symbol }[] = [];
for (const exported of lowerer.checker.getExportsOfModule(symbol)) {
const target =
exported.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(exported) : exported;
if (!(target.flags & ts.SymbolFlags.Value)) continue;
exports.push({ key: ts.symbolName(exported), declaration: target.valueDeclaration });
exports.push({
key: ts.symbolName(exported),
declaration: target.valueDeclaration,
symbol: target,
});
}
exports.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
const name = `%module.namespace:${ns.type.moduleId}`;
@@ -206,24 +207,37 @@ export function moduleNamespaceValue(
lowerer.fnStack.push(context);
try {
const body: IrStmt[] = [];
for (const { key, declaration } of exports) {
for (const { key, declaration, symbol: exportSymbol } of exports) {
const node = declaration ?? source;
const diagsBefore = lowerer.diags.length;
let read: IrStmt[];
try {
const identifier = declaration?.name as ts.Node | undefined;
if (!identifier || !ts.isIdentifier(identifier)) {
lowerer.unsupported(
"SC1090",
node,
`reading the unnamed export '${key}' through a module namespace object`,
);
let value: IrExpr;
if (identifier && ts.isIdentifier(identifier)) {
value = lowerer.lowerExprExpecting(identifier, DYN);
} else {
// `export default <expression>` and the anonymous default
// class/function have no named binding to read. The module's
// pre-registered default global holds the value the export
// statement snapshotted — the same storage a default import
// reads — so the namespace property answers it instead of
// fencing. A default whose type has no checked-dynamic form
// still fails the coercion below and keeps its fence.
const storage = lowerer.globalsBySymbol.get(exportSymbol);
if (!storage) {
lowerer.unsupported(
"SC1090",
node,
`reading the unnamed export '${key}' through a module namespace object`,
);
}
value = lowerer.coerceToExpected(varRef(storage.id, storage.type, loc), DYN);
}
const value = lowerer.lowerExprExpecting(identifier, DYN);
if (value.type.kind !== "dyn") {
lowerer.unsupported(
"SC1101",
identifier,
identifier ?? node,
`reading the export '${key}' ('${lowerer.fmt(value.type)}') through a module namespace object`,
);
}
@@ -263,6 +277,24 @@ export function moduleNamespaceValue(
lowerer.currentClass = previousClass;
}
}
return { name, keys: exports.map(({ key }) => key) };
}
/** A module namespace taken as an untyped value whose exports a copy
* cannot represent: a binding that can change after initialization (`let`,
* `var`), or an export the copy cannot name. The value is the runtime's
* namespace object (scr_module_namespace_value): one per module, whose
* property reads call a compiled getter for the export's current value and
* whose writes throw Node's TypeError. An export whose value has no
* checked-dynamic form throws its fence when it is read, and listing the
* object's keys or inspecting it throws the "not supported yet" error. */
export function moduleNamespaceValue(
lowerer: Lowerer,
ns: IrExpr & { type: IrType & { kind: "moduleNs" } },
): IrExpr | null {
const getter = moduleNamespaceGetter(lowerer, ns);
if (!getter) return null;
const loc = ns.loc;
const value: IrExpr = {
kind: "libCall",
fn: "module.namespaceValue",
@@ -270,13 +302,82 @@ export function moduleNamespaceValue(
strLit(ns.type.moduleId, loc),
{
kind: "arrayLit",
elems: exports.map(({ key }) => strLit(key, loc)),
elems: getter.keys.map((key) => strLit(key, loc)),
type: arrayOf(STRING),
loc,
},
{
kind: "dynFrom",
value: { kind: "closure", fnName: name, captures: [], type: funcOf([STRING], DYN), loc },
value: {
kind: "closure",
fnName: getter.name,
captures: [],
type: funcOf([STRING], DYN),
loc,
},
type: DYN,
loc,
},
],
type: DYN,
loc,
};
if (ns.kind === "moduleNsRef") return value;
return {
kind: "seqExpr",
stmts: [{ kind: "exprStmt", expr: ns, loc }],
result: value,
type: DYN,
loc,
};
}
/** A module namespace boxed for typed code through a dynamic slot
* (`const u: unknown = ns`, `take(ns)`): Node's namespace object as
* scr_module_namespace_dyn.c builds it — one non-extensible null-prototype
* object per module whose own sorted export names are enumerable read-only
* accessors, whose `Symbol.toStringTag` is "Module", whose reads call the same
* compiled getter, and whose writes and coerces throw Node's TypeErrors. A
* `node:` builtin has no compiled export table, so it keeps its fence. */
export function moduleNamespaceDynValue(
lowerer: Lowerer,
ns: IrExpr & { type: IrType & { kind: "moduleNs" } },
): IrExpr | null {
const getter = moduleNamespaceGetter(lowerer, ns);
if (!getter) return null;
const loc = ns.loc;
const value: IrExpr = {
kind: "libCall",
fn: "module.namespaceDyn",
args: [
strLit(ns.type.moduleId, loc),
{
kind: "arrayLit",
elems: getter.keys.map((key) => strLit(key, loc)),
type: arrayOf(STRING),
loc,
},
{
kind: "dynFrom",
value: {
kind: "closure",
fnName: getter.name,
captures: [],
type: funcOf([STRING], DYN),
loc,
},
type: DYN,
loc,
},
{
kind: "dynFrom",
value: {
kind: "libCall",
fn: "sym.wellKnown",
args: [strLit("toStringTag", loc)],
type: SYMBOL_T,
loc,
},
type: DYN,
loc,
},
@@ -7,8 +7,10 @@
* (`%dyn.class.call.N`) that calls such a method already holds a capsule
* of the same instance at its exact class: its receiver after
* normalization. The helper now calls a copy of the method that returns
* nothing and answers its own receiver, so the call boxes nothing. Other
* callers (vtables, typed code, method values) keep the method itself. */
* nothing and answers its own receiver, so the call boxes nothing. A
* direct call whose result nothing reads calls the copy too. Other callers
* (vtables, method values, calls that use the result) keep the method
* itself. */
import type { IrExpr, IrFunction, IrStmt } from "../../ir/ir.js";
import { VOID } from "../../ir/ir.js";
import { varRef } from "../../ir/build.js";
@@ -57,6 +59,20 @@ export function returnDispatchReceivers(lowerer: Lowerer, functions: IrFunction[
];
}
}
// A direct call whose result nothing reads (`builder.set(1);` in typed
// code) needs no capsule at all.
for (const fn of [...functions]) {
everyStmtList(fn.body, {
stmt: (s) => {
if (s.kind === "exprStmt" && s.expr.kind === "call") {
const copy = copyOf(s.expr.callee);
if (copy) s.expr = { ...s.expr, callee: copy.name, type: VOID };
}
return true;
},
expr: () => true,
});
}
}
/** The helper's receiver is assigned only by its normalization, so it is
+1
View File
@@ -702,6 +702,7 @@ async function compileExecutableNative(
...(features.stdioExtended ? { stdioExtended: true } : {}),
...(features.nodeIsland ? { nodeIsland: true } : {}),
...(features.moduleNamespace ? { moduleNamespace: true } : {}),
...(features.moduleNamespaceDyn ? { moduleNamespaceDyn: true } : {}),
zlib: features.zlib,
...(features.brotli ? { brotli: true } : {}),
assert: features.assert,
@@ -3629,6 +3629,11 @@ function buildLibFnSignatures() {
* (scr_module_namespace_value): the module id, its sorted export names
* and the compiled getter. One object per module. Never throws. */
"module.namespaceValue": { argTypes: [STRING, arrayOf(STRING), DYN], result: DYN },
/** A module namespace as typed code sees it through a dynamic slot
* (scr_module_namespace_dyn_value): the module id, its sorted export
* names, the compiled getter, and the Symbol.toStringTag key. Draws
* Node's frozen null-prototype object, one per module. Never throws. */
"module.namespaceDyn": { argTypes: [STRING, arrayOf(STRING), DYN, DYN], result: DYN },
"fs.callbackValue": { argTypes: [STRING], result: DYN },
"fs.callbackCall": { argTypes: [STRING, DYN], result: DYN },
"process.hrtimeValue": { argTypes: [], result: DYN },
@@ -1197,6 +1197,8 @@ const RUNTIME_FEATURE_RULES = {
/** Module namespace objects as checked-dynamic values
* (scr_module_namespace.c). */
moduleNamespace: rule({ fns: ["module.namespaceValue"] }),
/** A module namespace boxed for typed code (scr_module_namespace_dyn.c). */
moduleNamespaceDyn: rule({ fns: ["module.namespaceDyn"] }),
/** The WebAssembly API (scr_wasm.c). */
wasm: rule({ prefixes: ["wasm."] }),
/** crypto.subtle importKey/sign/verify/digest and CryptoKey:
@@ -1296,6 +1298,7 @@ function noRuntimeFeatures(): RuntimeFeatures {
urlIdna: false,
stdioExtended: false,
moduleNamespace: false,
moduleNamespaceDyn: false,
stream: false,
streamDyn: false,
handleProps: false,
@@ -34,6 +34,7 @@ const features = {
nodeIsland: false,
brotli: false,
moduleNamespace: false,
moduleNamespaceDyn: false,
regex: true,
copying: false,
textDecoderLegacy: false,
+4 -6
View File
@@ -5130,9 +5130,8 @@
"id": "stdlib.request.clone",
"kind": "stdlib",
"name": "Request.clone",
"status": "unsupported",
"code": "SC2020",
"note": "Node 24.22.0 / Undici 7.29.1; typed source has no compiler bridge for this interface"
"status": "static",
"note": "Node 24.22.0 / Undici 7.29.1; facets: body-consumption, identity, state-machine, error-shape; differential evidence: fixture:static-clone"
},
{
"id": "stdlib.request.constructor",
@@ -5329,9 +5328,8 @@
"id": "stdlib.response.clone",
"kind": "stdlib",
"name": "Response.clone",
"status": "unsupported",
"code": "SC2020",
"note": "Node 24.22.0 / Undici 7.29.1; the native fetch runtime does not implement this Response operation"
"status": "static",
"note": "Node 24.22.0 / Undici 7.29.1; facets: body-consumption, identity, state-machine, error-shape; differential evidence: fixture:static-clone"
},
{
"id": "stdlib.response.constructor",
@@ -0,0 +1,8 @@
{
"order": [
"<repo>/tests/corpus/8412-module-namespace-untyped/mod.ts",
"<repo>/tests/corpus/8412-module-namespace-untyped/mod-default.ts",
"<repo>/tests/corpus/8412-module-namespace-untyped/main.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/dynamic-method-receiver-statements.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/fetch-clone-untyped.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/fetch-clone.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/shorthand-builtin-values.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/shorthand-builtin-values.ts"
],
"diags": []
}
@@ -95,6 +95,8 @@ export const OPTIONAL_RUNTIME_SOURCES = [
["scr_perf_loop.c", "perfLoop"],
// Module namespace objects as checked-dynamic values.
["scr_module_namespace.c", "moduleNamespace"],
// The same namespace as the frozen null-prototype object typed code sees.
["scr_module_namespace_dyn.c", "moduleNamespaceDyn"],
// Blob and File box their reads as dyn promises through scr_async_dyn.c.
["scr_blob.c", "dynAsync"],
["scr_zlib.c", "zlib"],
+7
View File
@@ -34,6 +34,13 @@ ScrDyn *scr_process_builtin_unsupported(ScrStr *id, ScrStr *member);
* one handle per module `id`, whose reads call `getter` with the export
* name when `keys` lists it. Borrows all three; +1 handle. */
ScrDyn *scr_module_namespace_value(ScrStr *id, ScrArr *keys, ScrDyn *getter);
/* A module namespace as checked-dynamic code sees it
* (scr_module_namespace_dyn.c): one non-extensible null-prototype object per
* module `id`, Node's sorted export names as enumerable read-only accessors
* whose getters call `getter` with the name, `Symbol.toStringTag` "Module"
* (the well-known symbol is `tag_key`), and Node's strict-mode write
* TypeErrors. Borrows all four; +1 object. */
ScrDyn *scr_module_namespace_dyn_value(ScrStr *id, ScrArr *keys, ScrDyn *getter, ScrDyn *tag_key);
ScrDyn *scr_global_native(ScrArr *known);
ScrDyn *scr_global_native_init(ScrArr *known, ScrDyn *(*console_get)(void));
ScrDyn *scr_console_native(void);
@@ -0,0 +1,143 @@
/* Module namespace objects handed to typed code as checked-dynamic values.
*
* In Node a module namespace is a non-extensible null-prototype object whose
* own properties are the module's exports in sorted order, which are
* enumerable and read-only, whose Symbol.toStringTag is "Module", and whose
* ToPrimitive finds no method. Every generic dynamic operation —
* Object.keys/values/entries/getOwnPropertyNames, for-in, `in`,
* JSON.stringify, identity, member reads and calls — already handles that
* shape through scr_json.c's object
* dispatch, so this unit only builds the object and links only into programs
* that box a namespace.
*
* The exports stay live: each property is an accessor whose getter calls the
* compiled namespace getter with the export name (the same getter the
* JavaScript-source representation uses), and whose setter throws Node's
* strict-mode read-only TypeError. An unknown name is not a member, so the
* object's own not-extensible state answers Node's "Cannot add property".
* One object per module keeps its identity.
*
* Node models the exports as writable data properties on a sealed object, so
* Object.isFrozen answers false and Object.isSealed true. Live reads need
* accessors here, and the accessor shape cannot be sealed without also
* freezing: Object.isFrozen and Object.isExtensible answer as Node's do,
* Object.isSealed does not. */
#include "scr_runtime.h"
#include <stdlib.h>
#include <string.h>
typedef struct ScrModuleNamespaceDyn {
ScrStr *id;
ScrDyn *object; /* retained for the program's lifetime */
struct ScrModuleNamespaceDyn *next;
} ScrModuleNamespaceDyn;
static SCR_TL ScrModuleNamespaceDyn *scr_module_namespace_dyns;
static SCR_TL bool scr_module_namespace_dyn_cleanup_registered;
static void scr_module_namespace_dyn_cleanup(void) {
while (scr_module_namespace_dyns) {
ScrModuleNamespaceDyn *ns = scr_module_namespace_dyns;
scr_module_namespace_dyns = ns->next;
scr_str_release(ns->id);
scr_dyn_release(ns->object);
free(ns);
}
}
/* An export's current value: the compiled getter takes the export name as a
* dyn string and answers it +1. */
static ScrDyn *scr_module_namespace_dyn_read(ScrClosure *clo, ScrDyn *const *args, size_t argc) {
(void)args;
(void)argc;
ScrDyn *getter = scr_box_get_ref(clo->caps[0]);
ScrDyn *name = scr_box_get_ref(clo->caps[1]);
ScrDyn *argument = scr_dyn_retain(name);
ScrDyn *value = scr_dyn_call(getter, &argument, 1, "module namespace export");
scr_dyn_release(argument);
scr_dyn_release(name);
scr_dyn_release(getter);
return value;
}
/* A namespace export is read-only: Node's strict-mode TypeError names the
* export and the namespace's [object Module] tag. */
static ScrDyn *scr_module_namespace_dyn_write(ScrClosure *clo, ScrDyn *const *args, size_t argc) {
(void)args;
(void)argc;
ScrDyn *name = scr_box_get_ref(clo->caps[0]);
ScrJsonBuf text;
scr_jb_init(&text);
scr_jb_puts(&text, "Cannot assign to read only property '");
scr_jb_write(&text, name->v.str->data, name->v.str->len);
scr_jb_puts(&text, "' of object '[object Module]'");
ScrStr *message = scr_jb_finish(&text);
scr_throw_error_msg(SCR_ERR_TYPE, message->data, message->len);
scr_str_release(message);
scr_dyn_release(name);
return NULL;
}
/* `thunk` as a dynamic function over one or two captured dynamic values. */
static ScrDyn *scr_module_namespace_dyn_fn(ScrDynThunk thunk, ScrDyn *first, ScrDyn *second) {
ScrClosure *closure = scr_closure_new(NULL, second ? 2 : 1);
closure->caps[0] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v);
scr_box_set_ref(closure->caps[0], scr_dyn_retain(first));
if (second) {
closure->caps[1] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v);
scr_box_set_ref(closure->caps[1], scr_dyn_retain(second));
}
return scr_dyn_new_func(closure, thunk, 0, "native:module namespace", "module namespace export");
}
/* The namespace object: null prototype, one enumerable read-only accessor per
* export in `keys` order, a non-writable Symbol.toStringTag of "Module", and
* not extensible. */
static ScrDyn *scr_module_namespace_dyn_build(ScrArr *keys, ScrDyn *getter, ScrDyn *tag_key) {
ScrDyn *object = scr_dyn_new_obj_null_proto();
for (size_t i = 0; i < keys->len; i++) {
ScrStr *key = scr_arr_get_ref(keys, (double)i); /* +1 */
ScrDyn *name = scr_dyn_new_str(key);
ScrDyn *read = scr_module_namespace_dyn_fn(scr_module_namespace_dyn_read, getter, name);
ScrDyn *write = scr_module_namespace_dyn_fn(scr_module_namespace_dyn_write, name, NULL);
scr_dyn_obj_set_accessor(object, key->data, key->len, read, write);
scr_dyn_release(name);
scr_str_release(key);
}
ScrStr *tag = scr_str_new("Module", 6);
ScrDyn *descriptor = scr_dyn_new_obj();
scr_dyn_obj_set(descriptor, "value", 5, scr_dyn_new_str(tag));
scr_dyn_obj_set(descriptor, "writable", 8, scr_dyn_new_bool(false));
scr_dyn_obj_set(descriptor, "enumerable", 10, scr_dyn_new_bool(false));
scr_dyn_obj_set(descriptor, "configurable", 12, scr_dyn_new_bool(false));
scr_dyn_release(scr_dyn_define_property(object, tag_key, descriptor));
scr_str_release(tag);
scr_dyn_release(descriptor);
scr_dyn_release(scr_dyn_prevent_extensions(object));
if (scr_exc_pending()) {
/* The tag key is a well-known symbol, so this cannot fail in practice; a
* pending exception still must not be cached. */
scr_dyn_release(object);
return NULL;
}
return object;
}
ScrDyn *scr_module_namespace_dyn_value(ScrStr *id, ScrArr *keys, ScrDyn *getter, ScrDyn *tag_key) {
for (ScrModuleNamespaceDyn *ns = scr_module_namespace_dyns; ns; ns = ns->next) {
if (ns->id->len == id->len && memcmp(ns->id->data, id->data, id->len) == 0)
return scr_dyn_retain(ns->object);
}
if (!scr_module_namespace_dyn_cleanup_registered) {
scr_atexit(scr_module_namespace_dyn_cleanup);
scr_module_namespace_dyn_cleanup_registered = true;
}
ScrDyn *object = scr_module_namespace_dyn_build(keys, getter, tag_key);
if (!object) return NULL;
ScrModuleNamespaceDyn *ns = malloc(sizeof(*ns));
if (!ns) scr_trap("scriptc: out of memory\n");
*ns = (ScrModuleNamespaceDyn){ scr_str_retain(id), scr_dyn_retain(object), scr_module_namespace_dyns };
scr_module_namespace_dyns = ns;
return object;
}
@@ -1,4 +1,4 @@
exports.timers = () => {
setTimeout(() => console.log("island timer 1"), 15);
setTimeout(() => console.log("island timer 2"), 40);
setTimeout(() => console.log("island timer 1"), 150);
setTimeout(() => console.log("island timer 2"), 250);
};
@@ -7,12 +7,14 @@ import { callSync } from "@scriptc/node";
process.on("uncaughtException", (error) => {
console.log("caught", error.message);
});
// The island's timers (150 and 250 ms) are scheduled before these (10 and
// 50 ms), with gaps wide enough that a slow run keeps the delay order.
callSync("./island.cjs", "timers");
setTimeout(() => {
console.log("native timer throws");
throw new Error("native boom");
}, 5);
setTimeout(() => console.log("native timer after"), 10);
}, 10);
setTimeout(() => console.log("native timer after"), 50);
setImmediate(() => {
queueMicrotask(() => {
throw new Error("from a microtask");
@@ -0,0 +1,77 @@
// A module namespace taken as a checked-dynamic value: Node's export keys,
// live reads, function typeof, member calls, `in`, for-in, JSON, the
// [object Module] tag, the read-only write TypeErrors and the "Cannot
// convert object to primitive value" coercion, plus `Promise<namespace>`
// widening into `Promise<unknown>` and `unknown`.
import * as declared from "./mod.ts";
import * as expressionDefault from "./mod-default.ts";
function keys(value: unknown): string {
return Object.keys(value as any).join(",");
}
function describe(value: unknown): void {
const dyn = value as any;
console.log("keys", keys(value));
console.log("kinds", valueKinds(value));
console.log("names", Object.getOwnPropertyNames(dyn).join(","));
console.log("alpha", dyn.alpha, typeof dyn.beta, typeof dyn.default);
console.log("call", dyn.beta());
console.log("tag", Object.prototype.toString.call(dyn));
console.log("in", "alpha" in dyn, "toString" in dyn);
const forIn: string[] = [];
for (const key in dyn) forIn.push(key);
console.log("for-in", forIn.join(","));
console.log("json", JSON.stringify(dyn));
try {
console.log("string", String(dyn));
} catch (error) {
console.log("string threw", (error as Error).message);
}
try {
dyn.alpha = 2;
console.log("write", dyn.alpha);
} catch (error) {
console.log("write threw", (error as Error).message);
}
try {
dyn.gamma = 2;
console.log("add", dyn.gamma);
} catch (error) {
console.log("add threw", (error as Error).message);
}
console.log("live", dyn.alpha);
}
function valueKinds(value: unknown): string {
const dyn = value as any;
const kinds: string[] = [];
for (const key of Object.keys(dyn)) kinds.push(typeof dyn[key]);
return kinds.join(",");
}
function bump(value: unknown): void {
const dyn = value as any;
dyn.bump();
console.log("after bump", dyn.count);
}
async function main(): Promise<void> {
// An expression default is snapshotted once by the export statement;
// the boxed namespace's `default` read answers that value, like Node.
const boxedDefault: unknown = expressionDefault;
const dynDefault = boxedDefault as any;
console.log("expression default", dynDefault.default, typeof dynDefault.default);
console.log("expression keys", keys(boxedDefault), expressionDefault.default);
describe(declared);
bump(declared);
const loaded = await import("./mod.ts");
console.log("same", loaded === declared);
describe(loaded);
const widened: Promise<unknown> | undefined = import("./mod.ts");
if (widened) console.log("widened", keys(await widened));
const boxed: unknown = import("./mod.ts");
console.log("unknown", keys(await boxed));
}
main();
@@ -0,0 +1,4 @@
// An EXPRESSION default (`export default <expr>`): Node evaluates the
// expression once, at the export statement, and the namespace object's
// `default` property answers that snapshotted value.
export default 42;
@@ -0,0 +1,12 @@
export const alpha = 1;
export let count = 0;
export function beta(): string {
return "b" + String(count);
}
export function bump(): void {
count += 1;
}
export const obj = { k: alpha };
export default class Gamma {
readonly tag = "g";
}
@@ -0,0 +1,46 @@
// Untyped methods that return their receiver, called as statements on
// receivers whose class is known: side effects, throws, overrides, and
// results that are used.
class Builder {
constructor() {
this.parts = [];
this.count = 0;
}
add(part) {
if (part === "") throw new TypeError("empty part");
this.parts.push(part);
this.count++;
return this;
}
reset() {
this.parts.length = 0;
return this;
}
}
class LoudBuilder extends Builder {
add(part) {
super.add(part.toUpperCase());
return this;
}
}
const b = new Builder();
for (let i = 0; i < 1000; i++) {
b.add("p" + (i % 3));
if (i % 100 === 99) b.reset();
}
console.log(b.count, b.parts.join(","));
b.add("x").add("y");
const same = b.add("z");
console.log(same === b, b.parts.join(","));
try {
b.add("");
} catch (e) {
console.log(e instanceof TypeError, e.message, b.count);
}
const loud = new LoudBuilder();
loud.add("a");
loud.add("b").add("c");
console.log(loud.parts.join(","), loud.count, loud instanceof Builder);
const builders = [new Builder(), new LoudBuilder()];
for (const each of builders) each.add("q");
console.log(builders.map((each) => each.parts.join("")).join("|"));
+28
View File
@@ -0,0 +1,28 @@
// Response.clone() and Request.clone() in JavaScript on values the checker
// types as Response and Request: the clone's metadata, headers and body
// branch, and Node's TypeErrors for a used body.
const describe = (e) => `${e.name}: ${e.message}`;
const original = new Response("from js", { status: 202, headers: { "x-c": "3" } });
const copy = original.clone();
console.log("meta", copy.status, copy.headers.get("x-c"), copy instanceof Response);
console.log("bodies", await original.text(), await copy.text());
try {
copy.clone();
} catch (e) {
console.log("used", describe(e));
}
const request = new Request("http://example.com/js", { method: "POST", body: "js body" });
const requestCopy = request.clone();
console.log("request", requestCopy.method, requestCopy.url, await requestCopy.text(), await request.text());
try {
request.clone();
} catch (e) {
console.log("request used", describe(e));
}
const viewed = new Response("viewed");
const [left] = viewed.clone().body.tee();
const reader = left.getReader();
console.log("tee of clone", new TextDecoder().decode((await reader.read()).value), await viewed.text());
+80
View File
@@ -0,0 +1,80 @@
// Response.clone() and Request.clone() on typed values: the clone keeps the
// status, status text, URL and a copy of the headers, its body is one
// branch of the original's tee, a used or locked body refuses with Node's
// TypeError, and a Request clone keeps the method, URL, headers and body.
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
import type { AddressInfo } from "node:net";
const describe = (e: unknown): string => `${(e as Error).name}: ${(e as Error).message}`;
async function responses(): Promise<void> {
const original = new Response("hello", { status: 201, statusText: "Made", headers: { "x-a": "1" } });
const copy = original.clone();
console.log("meta", copy.status, copy.statusText, copy.ok, copy.headers.get("x-a"), copy.bodyUsed, copy.body === original.body);
copy.headers.set("x-a", "changed");
console.log("headers", original.headers.get("x-a"), copy.headers.get("x-a"));
console.log("bodies", await copy.text(), await original.text(), original.bodyUsed, copy.bodyUsed);
try {
original.clone();
} catch (e) {
console.log("used", describe(e));
}
const chained = new Response("chain").clone().clone();
console.log("chained", await chained.text());
const twice = new Response("twice");
const first = twice.clone();
const second = twice.clone();
console.log("twice", await second.text(), await first.text(), await twice.text());
const locked = new Response("locked");
locked.body!.getReader();
try {
locked.clone();
} catch (e) {
console.log("locked", describe(e));
}
const empty = new Response(null, { status: 204 });
const emptyCopy = empty.clone();
console.log("null body", emptyCopy.status, emptyCopy.body === null, await emptyCopy.text() === "");
const json = Response.json({ n: 1 });
const jsonCopy = json.clone();
console.log("json", JSON.stringify(await jsonCopy.json()), jsonCopy.headers.get("content-type"));
}
async function requests(): Promise<void> {
const request = new Request("http://example.com/path?q=1", { method: "PUT", body: "payload", headers: { "x-b": "2" } });
const copy = request.clone();
console.log("request", copy.method, copy.url, copy.headers.get("x-b"), copy.bodyUsed);
console.log("request bodies", await copy.text(), await request.text());
try {
request.clone();
} catch (e) {
console.log("request used", describe(e));
}
const get = new Request("http://example.com/");
const getCopy = get.clone();
console.log("get", getCopy.method, getCopy.body === null, getCopy.redirect);
}
async function fetched(): Promise<void> {
const server = createServer((_req: IncomingMessage, res: ServerResponse) => {
res.setHeader("x-served", "yes");
res.write("part one;");
setTimeout(() => res.end("part two"), 5);
});
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", () => resolve()));
const { port } = server.address() as AddressInfo;
const response = await fetch(`http://127.0.0.1:${port}/`);
const copy = response.clone();
console.log("fetched", copy.status, copy.headers.get("x-served"), copy.url === response.url);
console.log("fetched bodies", await copy.text(), await response.text());
server.close();
}
await responses();
await requests();
await fetched();
+97
View File
@@ -0,0 +1,97 @@
// A builtin global in a shorthand property (`{ Uint8Array }`) holds the
// value the long form (`{ Uint8Array: Uint8Array }`) and a plain read hold.
const short = {
Uint8Array,
Int8Array,
Uint8ClampedArray,
Uint16Array,
Int16Array,
Uint32Array,
Int32Array,
Float32Array,
Float64Array,
NaN,
Infinity,
String,
Number,
Boolean,
BigInt,
ArrayBuffer,
Array,
Map,
JSON,
Math,
encodeURIComponent,
decodeURI,
crypto,
Buffer,
fetch,
console,
setTimeout,
};
const long = {
Uint8Array: Uint8Array,
Int8Array: Int8Array,
Uint8ClampedArray: Uint8ClampedArray,
Uint16Array: Uint16Array,
Int16Array: Int16Array,
Uint32Array: Uint32Array,
Int32Array: Int32Array,
Float32Array: Float32Array,
Float64Array: Float64Array,
NaN: NaN,
Infinity: Infinity,
String: String,
Number: Number,
Boolean: Boolean,
BigInt: BigInt,
ArrayBuffer: ArrayBuffer,
Array: Array,
Map: Map,
JSON: JSON,
Math: Math,
encodeURIComponent: encodeURIComponent,
decodeURI: decodeURI,
crypto: crypto,
Buffer: Buffer,
fetch: fetch,
console: console,
setTimeout: setTimeout,
};
for (const key of Object.keys(long)) {
const same = Number.isNaN(long[key]) ? Number.isNaN(short[key]) : short[key] === long[key];
console.log(key, same, typeof short[key]);
}
console.log(short.Uint8Array === Uint8Array, short.Float64Array === Float64Array);
console.log(Object.keys(short).length, Object.keys(short)[0], Object.keys(short)[9]);
// The typed-array constructors construct and keep their names.
const bytes = new short.Uint8Array([1, 2, 300]);
console.log(bytes, bytes instanceof Uint8Array, short.Uint8Array.name, short.Int32Array.BYTES_PER_ELEMENT);
console.log(new short.Float64Array(2).length, short.Int16Array.from([1, -2]));
console.log(short.NaN !== short.NaN, short.Infinity > 1e308, -short.Infinity);
// Shorthand destructuring from the global object reads the same values.
function fromGlobal() {
const { Uint8Array, Float32Array, NaN, Infinity, Buffer } = globalThis;
return [Uint8Array === long.Uint8Array, Float32Array === long.Float32Array, Number.isNaN(NaN), Infinity, Buffer === long.Buffer];
}
console.log(fromGlobal());
// A local binding with a builtin's name is the local.
function shadowed() {
const Uint8Array = "local";
const NaN = 5;
return { Uint8Array, NaN };
}
console.log(shadowed());
// The global object's NaN and Infinity are numbers, read or destructured.
function numbersFromGlobal() {
const { NaN: notANumber, Infinity: infinite } = globalThis;
return [typeof notANumber, notANumber !== notANumber, infinite, infinite === Infinity];
}
console.log(typeof globalThis.NaN, globalThis.NaN !== globalThis.NaN, globalThis.Infinity, -globalThis.Infinity);
console.log(numbersFromGlobal());
+51
View File
@@ -0,0 +1,51 @@
// A builtin global in a shorthand property (`{ Uint8Array }`) holds the
// value the long form (`{ Uint8Array: Uint8Array }`) holds.
const short: Record<string, unknown> = {
Uint8Array,
Int8Array,
Uint8ClampedArray,
Uint16Array,
Int16Array,
Uint32Array,
Int32Array,
Float32Array,
Float64Array,
NaN,
Infinity,
String,
ArrayBuffer,
encodeURIComponent,
};
const long: Record<string, unknown> = {
Uint8Array: Uint8Array,
Int8Array: Int8Array,
Uint8ClampedArray: Uint8ClampedArray,
Uint16Array: Uint16Array,
Int16Array: Int16Array,
Uint32Array: Uint32Array,
Int32Array: Int32Array,
Float32Array: Float32Array,
Float64Array: Float64Array,
NaN: NaN,
Infinity: Infinity,
String: String,
ArrayBuffer: ArrayBuffer,
encodeURIComponent: encodeURIComponent,
};
for (const key of Object.keys(long)) {
const value = long[key];
const same =
typeof value === "number" && Number.isNaN(value)
? Number.isNaN(short[key])
: short[key] === value;
console.log(key, same, typeof short[key]);
}
const numbers = { NaN, Infinity };
console.log(numbers.NaN !== numbers.NaN, numbers.Infinity > 1e308, -numbers.Infinity);
const ctors = { Uint8Array, Float64Array };
const bytes = new ctors.Uint8Array([1, 2, 300]);
console.log(bytes, ctors.Uint8Array.name, new ctors.Float64Array(3).length);
+44
View File
@@ -0,0 +1,44 @@
// Response.clone() and Request.clone() in a static build: fetched
// responses (a whole body, and one that arrives in chunks across a
// multibyte character), constructed responses and requests, the clone's
// metadata and header copy, a clone of a clone, and Node's TypeErrors for a
// used or locked body.
const base = process.argv[2]!;
const describe = (e: unknown): string => (e instanceof Error ? `${e.name}: ${e.message}` : String(e));
const fetched = await fetch(`${base}/text`);
const fetchedCopy = fetched.clone();
console.log(fetchedCopy.status, fetchedCopy.headers.get("x-kind"), fetchedCopy.url === fetched.url, fetchedCopy.redirected);
console.log(await fetchedCopy.text(), await fetched.text(), fetched.bodyUsed, fetchedCopy.bodyUsed);
try {
fetched.clone();
} catch (e) {
console.log("used", describe(e));
}
const chunked = await fetch(`${base}/chunked`);
const chunkedCopy = chunked.clone().clone();
console.log(await chunked.text(), await chunkedCopy.text());
const made = new Response("made", { status: 203, statusText: "Copied", headers: { "x-a": "1" } });
const madeCopy = made.clone();
madeCopy.headers.set("x-a", "2");
console.log(madeCopy.status, madeCopy.statusText, made.headers.get("x-a"), madeCopy.headers.get("x-a"));
console.log(await made.text(), await madeCopy.text());
const locked = new Response("locked");
locked.body!.getReader();
try {
locked.clone();
} catch (e) {
console.log("locked", describe(e));
}
const request = new Request(`${base}/post-echo`, { method: "POST", body: "request body", headers: { "x-b": "3" } });
const requestCopy = request.clone();
console.log(requestCopy.method, requestCopy.headers.get("x-b"), await requestCopy.text(), await request.text());
try {
request.clone();
} catch (e) {
console.log("request used", describe(e));
}
+6 -3
View File
@@ -271,21 +271,24 @@ describe("imports", () => {
it("named and namespace imports keep the loop's timers", async (t) => {
t.mock.timers.enable({ apis: ["setTimeout"] });
const log: string[] = [];
// The three timers are real and scheduled one after another, so their
// delays are 100 ms apart: a slow run (a sanitized build on a busy
// machine) still sees them fire in delay order.
await Promise.all([
sleep(5).then(() => {
sleep(300).then(() => {
log.push("named promise");
}),
new Promise<void>((resolve) =>
namedSetTimeout(() => {
log.push("named");
resolve();
}, 2),
}, 100),
),
new Promise<void>((resolve) =>
timersNamespace.setTimeout(() => {
log.push("namespace");
resolve();
}, 3),
}, 200),
),
]);
t.mock.timers.tick(10);
+1 -1
View File
@@ -25,7 +25,7 @@ fetch("https://example.invalid/", fetchInit);
async function inspectResponse(url: string): Promise<void> {
const response = await fetch(url);
console.log(response.type);
response.clone();
response.formData();
}
void inspectResponse;
ReadableStream.from(new Set([1, 2]));
@@ -21,15 +21,15 @@ fenced.ts:27:15 - error SC2020: 'Response.type' is typed by @types/node but has
26 | const response = await fetch(url);
27 | console.log(response.type);
| ^~~~~~~~~~~~~
28 | response.clone();
28 | response.formData();
hint: the member is outside the native static handle projection
fenced.ts:28:3 - error SC2020: 'Response.clone' is typed by @types/node but has no scriptc lowering yet
fenced.ts:28:3 - error SC2020: 'Response.formData' is typed by @types/node but has no scriptc lowering yet
27 | console.log(response.type);
28 | response.clone();
| ^~~~~~~~~~~~~~
28 | response.formData();
| ^~~~~~~~~~~~~~~~~
29 | }
hint: the native fetch runtime does not implement this Response operation
@@ -0,0 +1 @@
{ "plain": 4451, "san": 57918 }
@@ -0,0 +1 @@
{ "plain": 2600, "san": 48000 }
@@ -0,0 +1 @@
{ "plain": 2600, "san": 48000 }
@@ -0,0 +1 @@
{ "plain": 3100, "san": 4000 }
@@ -0,0 +1 @@
{ "plain": 1800, "san": 2400 }
+1
View File
@@ -164,6 +164,7 @@ describe(`static fetch differential${sanitize ? " (sanitized)" : ""}`, () => {
"static-abort-throw",
"static-dispatch-throw",
"static-response-json",
"static-clone",
] as const;
test.for(
staticCases.flatMap((name) => (["llvm"] as const).map((backend) => [name, backend] as const)),
+3
View File
@@ -29,6 +29,9 @@ const cases = [
].flatMap((fixture) => ["native", "wasm32-wasi"].map((target) => ({ fixture, target })));
test.for(cases)(
"published $fixture runs statically on $target",
// Bundling three.js and building it with the sanitizer can pass the
// default 300 s on a busy runner, as in three.test.ts.
{ timeout: 1_200_000 },
async ({ fixture, target }, context) => {
if (target === "wasm32-wasi" && !hasZig) context.skip();
const dir = await mkdtemp("/tmp/scriptc-spatial-");
+3 -3
View File
@@ -388,7 +388,7 @@ test("fetch interface object bindings retain the inventory fence code", () => {
source:
'/// <reference types="node" />\n' +
"function headers(value: Headers): void { const { entries } = value; }\n" +
"function response(value: Response): void { const { clone } = value; }\n" +
"function response(value: Response): void { const { blob } = value; }\n" +
"function stream(value: ReadableStream<Uint8Array>): void { const { tee } = value; }\n" +
"function assign(value: Headers, entries: unknown): void { ({ entries } = value); }\n" +
"void headers; void response; void stream; void assign;\n",
@@ -447,7 +447,7 @@ test("unimplemented Response and ReadableStream calls stay fenced", () => {
source:
'/// <reference types="node" />\n' +
"function response(value: Response): void {\n" +
" void value.clone(); void value.blob(); void value.formData();\n" +
" void value.blob(); void value.formData();\n" +
"}\n" +
"function stream(value: ReadableStream<Uint8Array>): void {\n" +
" void value.tee(); void value.pipeTo(null as never);\n" +
@@ -458,7 +458,7 @@ test("unimplemented Response and ReadableStream calls stay fenced", () => {
const coverage = analyze(file, {}).coverage;
expect(coverage.preflightFailed).toBe(false);
expect([...new Set(coverage.diagnostics.map((d) => d.code))]).toEqual(["SC2020"]);
expect(coverage.diagnostics.filter((d) => d.code === "SC2020")).toHaveLength(5);
expect(coverage.diagnostics.filter((d) => d.code === "SC2020")).toHaveLength(4);
}
});