diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 87ffea5df..ef83bf07e 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -316,6 +316,7 @@ export const LIB_FN_SYMS: Record = { "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", @@ -820,6 +821,7 @@ export const LIB_FN_SYMS: Record = { // 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", diff --git a/packages/compiler/src/compat/fetch-profile.ts b/packages/compiler/src/compat/fetch-profile.ts index c1b43a810..c4d1e3f50 100644 --- a/packages/compiler/src/compat/fetch-profile.ts +++ b/packages/compiler/src/compat/fetch-profile.ts @@ -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", diff --git a/packages/compiler/src/frontend/lowering/builtins/dates.ts b/packages/compiler/src/frontend/lowering/builtins/dates.ts index fa880bdc9..0101a3ec3 100644 --- a/packages/compiler/src/frontend/lowering/builtins/dates.ts +++ b/packages/compiler/src/frontend/lowering/builtins/dates.ts @@ -34,7 +34,7 @@ const DATE_GETTER_FNS: Readonly> = { }; 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 }; diff --git a/packages/compiler/src/frontend/lowering/builtins/json.ts b/packages/compiler/src/frontend/lowering/builtins/json.ts index 013b16e7c..a08b108ad 100644 --- a/packages/compiler/src/frontend/lowering/builtins/json.ts +++ b/packages/compiler/src/frontend/lowering/builtins/json.ts @@ -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. */ diff --git a/packages/compiler/src/frontend/lowering/call-signatures.ts b/packages/compiler/src/frontend/lowering/call-signatures.ts index d98f80feb..9375576f7 100644 --- a/packages/compiler/src/frontend/lowering/call-signatures.ts +++ b/packages/compiler/src/frontend/lowering/call-signatures.ts @@ -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) { diff --git a/packages/compiler/src/frontend/lowering/devirtualize.ts b/packages/compiler/src/frontend/lowering/devirtualize.ts index 48a5712e3..b71080a3f 100644 --- a/packages/compiler/src/frontend/lowering/devirtualize.ts +++ b/packages/compiler/src/frontend/lowering/devirtualize.ts @@ -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(); + /** Lowered bodies' return types: an override may return a subclass + * (`set(): this` resolves per declaring class). */ + private readonly returnTypes = new Map(); /** Classes some retained IR allocates or exposes as a class value. */ private readonly allocated = new Set(); private readonly exportedNames: ReadonlySet; @@ -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 diff --git a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts index c95731fc7..78d783a11 100644 --- a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts @@ -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(); 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(); 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) & { diff --git a/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts b/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts index ceedc3e94..55feafc62 100644 --- a/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts +++ b/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts @@ -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)) { diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index f431d4e54..23b8b69d2 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -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 @@ -1464,6 +1465,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) ?? @@ -4320,8 +4323,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) }; } diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 0fef178ff..a4b771ded 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -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) { diff --git a/packages/compiler/src/frontend/lowering/lower-stdlib.ts b/packages/compiler/src/frontend/lowering/lower-stdlib.ts index 20caf184b..9fbe87c3d 100644 --- a/packages/compiler/src/frontend/lowering/lower-stdlib.ts +++ b/packages/compiler/src/frontend/lowering/lower-stdlib.ts @@ -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, }; diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index e4b2591d5..43ad3819b 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -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"; @@ -2515,6 +2516,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", diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 5729a6c80..6493bc5f3 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -242,6 +242,7 @@ import { isOverloadedCallableMemberType, isUnitOnlyTsType, isWorkerHandleType, + isFetchHandleType, jsOpenObjectType, mapType, ShapeRegistry, @@ -3113,12 +3114,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) ); } diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index 8e02032ff..e3b508d86 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -2224,6 +2224,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", diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 463fd384b..d61464b07 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -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, +): 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 diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index b72c837ce..fc4c383ff 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -119,6 +119,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "fetch.requestNew", "fetch.headersNew", "fetch.responseNew", + "fetch.responseJsonStatic", "fetch.abortTimeout", "fetch.abortAny", "abort.timeout", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index d2c503214..00475fd1e 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -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): @@ -4197,6 +4198,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 }, diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index e29c32ee3..0aa510b8d 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -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; 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. diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 239c089fb..44b6814db 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -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; 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; diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json index a167254c2..350a81a2d 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -4216,6 +4216,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", @@ -5293,9 +5300,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", diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-devirtualized-this-return.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-devirtualized-this-return.ts.json new file mode 100644 index 000000000..f75299bf2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-devirtualized-this-return.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/class-devirtualized-this-return.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-fetch-handle-fields.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-fetch-handle-fields.ts.json new file mode 100644 index 000000000..69dfd8454 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-fetch-handle-fields.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/class-fetch-handle-fields.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-never-getter.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-never-getter.ts.json new file mode 100644 index 000000000..4b5ee103f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-never-getter.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/class-never-getter.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-symbol-field-constructor-assigned.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-symbol-field-constructor-assigned.ts.json new file mode 100644 index 000000000..4078df7c7 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-symbol-field-constructor-assigned.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/class-symbol-field-constructor-assigned.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/date-to-utc-string.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/date-to-utc-string.ts.json new file mode 100644 index 000000000..6c5e37d54 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/date-to-utc-string.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/date-to-utc-string.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/destructure-map-set-iterables.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/destructure-map-set-iterables.ts.json new file mode 100644 index 000000000..fee4d0ebc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/destructure-map-set-iterables.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/destructure-map-set-iterables.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fetch-headers-set-cookie-iteration.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fetch-headers-set-cookie-iteration.ts.json new file mode 100644 index 000000000..f0d413603 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fetch-headers-set-cookie-iteration.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/fetch-headers-set-cookie-iteration.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fetch-invalid-url-cause.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fetch-invalid-url-cause.ts.json new file mode 100644 index 000000000..70f7a83fd --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fetch-invalid-url-cause.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/fetch-invalid-url-cause.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fetch-request-set-cookie-wire.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fetch-request-set-cookie-wire.ts.json new file mode 100644 index 000000000..9357bc1d8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fetch-request-set-cookie-wire.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/fetch-request-set-cookie-wire.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/generic-undefined-argument.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/generic-undefined-argument.ts.json new file mode 100644 index 000000000..bbc59d2ca --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/generic-undefined-argument.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/generic-undefined-argument.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-stringify-class-tojson.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-stringify-class-tojson.ts.json new file mode 100644 index 000000000..9b1aa55ac --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-stringify-class-tojson.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/json-stringify-class-tojson.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-stringify-unknown-undefined-root.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-stringify-unknown-undefined-root.ts.json new file mode 100644 index 000000000..8c424c6a5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-stringify-unknown-undefined-root.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/json-stringify-unknown-undefined-root.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/object-spread-and-conditional.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/object-spread-and-conditional.ts.json new file mode 100644 index 000000000..c58247184 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/object-spread-and-conditional.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/object-spread-and-conditional.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/optional-call-native-method.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/optional-call-native-method.ts.json new file mode 100644 index 000000000..a8678d806 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/optional-call-native-method.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/optional-call-native-method.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/union-narrowed-tostring.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/union-narrowed-tostring.ts.json new file mode 100644 index 000000000..454df784e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/union-narrowed-tostring.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/union-narrowed-tostring.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_fetch.c b/packages/runtime/src/scr_fetch.c index eff2ae6bc..17beb9122 100644 --- a/packages/runtime/src/scr_fetch.c +++ b/packages/runtime/src/scr_fetch.c @@ -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 " + * 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); diff --git a/packages/runtime/src/scr_generator.h b/packages/runtime/src/scr_generator.h index f0ecb9d85..a92cd3222 100644 --- a/packages/runtime/src/scr_generator.h +++ b/packages/runtime/src/scr_generator.h @@ -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 */ 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 */ ScrPromise *scr_fetch_response_text(ScrDyn *response); /* +1 promise */ ScrPromise *scr_fetch_response_array_buffer(ScrDyn *response); /* +1 promise */ diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index af57ab057..5aad7be8b 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -10489,6 +10489,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 diff --git a/packages/runtime/src/scr_number.h b/packages/runtime/src/scr_number.h index f980daeb5..892625553 100644 --- a/packages/runtime/src/scr_number.h +++ b/packages/runtime/src/scr_number.h @@ -102,6 +102,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); diff --git a/tests/corpus/class-devirtualized-this-return.ts b/tests/corpus/class-devirtualized-this-return.ts new file mode 100644 index 000000000..e7f2fc9fa --- /dev/null +++ b/tests/corpus/class-devirtualized-this-return.ts @@ -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); diff --git a/tests/corpus/class-fetch-handle-fields.ts b/tests/corpus/class-fetch-handle-fields.ts new file mode 100644 index 000000000..b06df1313 --- /dev/null +++ b/tests/corpus/class-fetch-handle-fields.ts @@ -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")); diff --git a/tests/corpus/class-never-getter.ts b/tests/corpus/class-never-getter.ts new file mode 100644 index 000000000..497fa3d25 --- /dev/null +++ b/tests/corpus/class-never-getter.ts @@ -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); diff --git a/tests/corpus/class-symbol-field-constructor-assigned.ts b/tests/corpus/class-symbol-field-constructor-assigned.ts new file mode 100644 index 000000000..55e6e4e4a --- /dev/null +++ b/tests/corpus/class-symbol-field-constructor-assigned.ts @@ -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); diff --git a/tests/corpus/date-to-utc-string.ts b/tests/corpus/date-to-utc-string.ts new file mode 100644 index 000000000..633de5439 --- /dev/null +++ b/tests/corpus/date-to-utc-string.ts @@ -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()); diff --git a/tests/corpus/destructure-map-set-iterables.ts b/tests/corpus/destructure-map-set-iterables.ts new file mode 100644 index 000000000..295a224d8 --- /dev/null +++ b/tests/corpus/destructure-map-set-iterables.ts @@ -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([["only", 1]]); +p = ["changed", 2]; +console.log(p); +function firstKey(map: Map): string { + const [[k]] = map; + return k; +} +console.log(firstKey(new Map([["z", 26]]))); diff --git a/tests/corpus/fetch-headers-set-cookie-iteration.ts b/tests/corpus/fetch-headers-set-cookie-iteration.ts new file mode 100644 index 000000000..4d39bab70 --- /dev/null +++ b/tests/corpus/fetch-headers-set-cookie-iteration.ts @@ -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}`)); diff --git a/tests/corpus/fetch-invalid-url-cause.ts b/tests/corpus/fetch-invalid-url-cause.ts new file mode 100644 index 000000000..5517c4ae2 --- /dev/null +++ b/tests/corpus/fetch-invalid-url-cause.ts @@ -0,0 +1,41 @@ +// An unparsable fetch() or Request URL is undici's TypeError +// "Failed to parse URL from ", 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 { + 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(); diff --git a/tests/corpus/fetch-request-set-cookie-wire.ts b/tests/corpus/fetch-request-set-cookie-wire.ts new file mode 100644 index 000000000..54789a3fb --- /dev/null +++ b/tests/corpus/fetch-request-set-cookie-wire.ts @@ -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(); +}); diff --git a/tests/corpus/generic-undefined-argument.ts b/tests/corpus/generic-undefined-argument.ts new file mode 100644 index 000000000..97d438549 --- /dev/null +++ b/tests/corpus/generic-undefined-argument.ts @@ -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(v: T): string { + return typeof v; +} +console.log(kind(undefined), kind(null), kind(1), kind("s")); +function same(a: T, b: T): boolean { + return a === b; +} +console.log(same(undefined, undefined)); +function wrap(v: T): { v: T } { + return { v }; +} +const w = wrap(undefined); +console.log(JSON.stringify(w), w.v === undefined, "v" in w); +function describe(label: string, v: T): string { + return `${label}=${String(v)}`; +} +console.log(describe("u", undefined)); diff --git a/tests/corpus/json-stringify-class-tojson.ts b/tests/corpus/json-stringify-class-tojson.ts new file mode 100644 index 000000000..617e5ac25 --- /dev/null +++ b/tests/corpus/json-stringify-class-tojson.ts @@ -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); diff --git a/tests/corpus/json-stringify-unknown-undefined-root.ts b/tests/corpus/json-stringify-unknown-undefined-root.ts new file mode 100644 index 000000000..52528afbf --- /dev/null +++ b/tests/corpus/json-stringify-unknown-undefined-root.ts @@ -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(v: T): string | undefined { + return JSON.stringify(v); +} +console.log(typeof raw(u), raw(ok)); +function ser(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); diff --git a/tests/corpus/object-spread-and-conditional.ts b/tests/corpus/object-spread-and-conditional.ts new file mode 100644 index 000000000..dd42063d0 --- /dev/null +++ b/tests/corpus/object-spread-and-conditional.ts @@ -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, 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, 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); diff --git a/tests/corpus/optional-call-native-method.ts b/tests/corpus/optional-call-native-method.ts new file mode 100644 index 000000000..e3b9aa4a7 --- /dev/null +++ b/tests/corpus/optional-call-native-method.ts @@ -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(",")); diff --git a/tests/corpus/union-narrowed-tostring.ts b/tests/corpus/union-narrowed-tostring.ts new file mode 100644 index 000000000..93660b332 --- /dev/null +++ b/tests/corpus/union-narrowed-tostring.ts @@ -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" })); diff --git a/tests/fixtures/fetch/static-response-json/main.mts b/tests/fixtures/fetch/static-response-json/main.mts new file mode 100644 index 000000000..3bbde4977 --- /dev/null +++ b/tests/fixtures/fetch/static-response-json/main.mts @@ -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)); diff --git a/tests/harness/__snapshots__/date-formatters.ts.txt b/tests/harness/__snapshots__/date-formatters.ts.txt index 66d98d861..90928dae7 100644 --- a/tests/harness/__snapshots__/date-formatters.ts.txt +++ b/tests/harness/__snapshots__/date-formatters.ts.txt @@ -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 \ No newline at end of file + hint: getTime(), valueOf(), toISOString(), toUTCString(), toJSON(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering \ No newline at end of file diff --git a/tests/harness/__snapshots__/stdlib-fence.ts.txt b/tests/harness/__snapshots__/stdlib-fence.ts.txt index 337354eec..95fbe1fff 100644 --- a/tests/harness/__snapshots__/stdlib-fence.ts.txt +++ b/tests/harness/__snapshots__/stdlib-fence.ts.txt @@ -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 diff --git a/tests/harness/fetch.test.ts b/tests/harness/fetch.test.ts index 6b04acf59..337eafec6 100644 --- a/tests/harness/fetch.test.ts +++ b/tests/harness/fetch.test.ts @@ -163,6 +163,7 @@ describe(`static fetch differential${sanitize ? " (sanitized)" : ""}`, () => { "static-listener-noncallable", "static-abort-throw", "static-dispatch-throw", + "static-response-json", ] as const; test.for( staticCases.flatMap((name) => (["llvm"] as const).map((backend) => [name, backend] as const)), diff --git a/tests/harness/library-profile.test.ts b/tests/harness/library-profile.test.ts index fc243ccb5..caa3b4e72 100644 --- a/tests/harness/library-profile.test.ts +++ b/tests/harness/library-profile.test.ts @@ -552,6 +552,7 @@ describe("library profile fences", () => { "stdlib.date.now", "stdlib.date.parse", "stdlib.date.toISOString", + "stdlib.date.toUTCString", "stdlib.date.valueOf", ]); expect(r.profile.fences[0]!.surfaces.every((s) => s.detector !== undefined)).toBe(true);