diff --git a/packages/compiler/src/backend/llvm/shapes.ts b/packages/compiler/src/backend/llvm/shapes.ts index 9cceaa4434..e100c89633 100644 --- a/packages/compiler/src/backend/llvm/shapes.ts +++ b/packages/compiler/src/backend/llvm/shapes.ts @@ -1054,6 +1054,7 @@ export function arrNewCall( elem.kind === "promise" || // promise entries (Promise.all inputs): full REF story elem.kind === "child" || // spawned child handles: scr_child_* adapters, no trace elem.kind === "netServer" || + elem.kind === "stats" || // fs.Stats snapshots: scr_stats_* adapters, no trace elem.kind === "dyn" || // native collection seeds/drains: traced REF elem.kind === "regex" || // RegExp values: scr_regex_* adapters, no trace (no refs inside) (elem.kind === "array" && traceAdapter(host, elem) !== null); diff --git a/packages/compiler/src/frontend/lowering/expressions/array-literals.ts b/packages/compiler/src/frontend/lowering/expressions/array-literals.ts index ef2aa9778a..5aabf42e10 100644 --- a/packages/compiler/src/frontend/lowering/expressions/array-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/array-literals.ts @@ -458,7 +458,14 @@ export function lowerArrayLiteral( // A same-family array whose ELEMENT lifts (string[] into a // (string | symbol)[] literal — per-element wrap/width copy): // the interned width helper reshapes before the spread copies. - if (src.type.kind === "array" && !typeEquals(src.type, type)) { + // A fixed TUPLE (`[...FILES]` with FILES typed `[string, string, + // string]`) spreads its positions in order: the width helper copies + // the tuple's fields into an array of the literal's element type. + if ( + (src.type.kind === "array" || + (src.type.kind === "record" && lowerer.shapes.get(src.type.shapeId)?.tuple === true)) && + !typeEquals(src.type, type) + ) { const w = lowerer.widthCoerce(src, type); if (w) src = w; } diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index ae864c2cbf..dbb7fd0a9e 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -2140,6 +2140,57 @@ function lowerStringMethodCallWithOptionalArgs( return changed ? { ...lowered, args } : lowered; } +/** `.then(String)` / `.then(Number)` / `.then(Boolean)` over a promise of + * a number, string or boolean: the ambient constructor converts its first + * argument (the settled value), so the handler is an interned closure over + * the settled type applying that conversion. The constructors' own value + * form takes a string, which a number or boolean settled value is not. + * Null for every other handler. */ +function primitiveConversionHandler( + lowerer: Lowerer, + node: ts.Expression, + inner: IrType, +): IrExpr | null { + if (!ts.isIdentifier(node)) return null; + const name = node.text; + if (name !== "String" && name !== "Number" && name !== "Boolean") return null; + // A string settled value already fits the constructors' value form. + if (inner.kind !== "f64" && inner.kind !== "bool") return null; + if (!lowerer.isStdlibSymbol(lowerer.resolveValueSymbol(node) ?? undefined)) return null; + const loc = locOf(node); + const ret: IrType = name === "String" ? STRING : name === "Number" ? F64 : BOOL; + const fnT = funcOf([inner], ret); + const key = `then.${name}:${typeKey(inner)}`; + let fnName = lowerer.arrHofHelpers.get(key); + if (fnName === undefined) { + fnName = `%then.${name}.${lowerer.arrHofHelpers.size}`; + lowerer.arrHofHelpers.set(key, fnName); + const value = varRef("v.0", inner, loc); + let body: IrExpr; + if (name === "Boolean") body = { kind: "toBool", operand: value, type: BOOL, loc }; + else if (name === "String") body = { kind: "toString", operand: value, type: STRING, loc }; + else if (inner.kind === "f64") body = value; + else + body = { + kind: "ternary", + cond: value, + then: { kind: "numLit", value: 1, type: F64, loc }, + else_: { kind: "numLit", value: 0, type: F64, loc }, + type: F64, + loc, + }; + lowerer.liftedFns.push({ + name: fnName, + params: [{ localId: "v.0", name: "v", type: inner }], + returnType: ret, + locals: [{ id: "v.0", name: "v", type: inner, mutable: false }], + body: [{ kind: "return", value: body, loc }], + loc, + }); + } + return { kind: "closure", fnName, captures: [], type: fnT, loc }; +} + function lowerNumberConstructorValue( lowerer: Lowerer, argNode: ts.Expression, @@ -5697,7 +5748,9 @@ export function lowerPromiseMethodCall( if (inner.kind === "moduleNs") markModuleNsHandlerParams(lowerer, call.arguments.nodes[0]!, inner); - let cb = lowerer.lowerExpr(call.arguments.nodes[0]!); + let cb = + primitiveConversionHandler(lowerer, call.arguments.nodes[0]!, inner) ?? + lowerer.lowerExpr(call.arguments.nodes[0]!); // A TYPED handler on a DYN-settling promise (the tracePromise // result's `.then((value) => ...)` — the checker's generic // instantiation typed the parameter, but the settled value is a diff --git a/packages/compiler/src/frontend/lowering/lower-inspect.ts b/packages/compiler/src/frontend/lowering/lower-inspect.ts index 20c48a21dd..4eda3207ba 100644 --- a/packages/compiler/src/frontend/lowering/lower-inspect.ts +++ b/packages/compiler/src/frontend/lowering/lower-inspect.ts @@ -1,4 +1,12 @@ -import { dynUndefinedExpr, boolLit, helperLoc, numLit, strLit, varRef } from "../../ir/build.js"; +import { + dynUndefinedExpr, + boolLit, + helperLoc, + nodeThrowExpr, + numLit, + strLit, + varRef, +} from "../../ir/build.js"; import { InternalCompilerError } from "../../errors.js"; import * as ts from "../ts7/adapter.js"; @@ -126,6 +134,334 @@ function errorRenderable(info: ClassInfo): boolean { return info.subclasses.every((s) => errorRenderable(s)); } +/** Error keys Node's error inspector folds into the base or hides. */ +const ERROR_SPECIAL_KEYS = new Set(["name", "message", "stack", "cause", "errors"]); + +/** The declared own (enumerable) fields of a user error class, in layout + * order (the base chain first). */ +function ownErrorFields(info: ClassInfo): { name: string; type: IrType }[] { + // The builtin bases' layout fields (name, message) are runtime slots the + // error base already renders, not own enumerable properties. + const builtin = new Set(); + for (let c = info.base; c; c = c.base) { + if (c.builtinError || c.def.runtime) for (const f of c.def.fields) builtin.add(f.name); + } + return info.def.fields.filter( + (f) => isClassOwnEnumerableFieldName(f.name) && !builtin.has(f.name), + ); +} + +/** `info` and its descendants that declare fields, most-derived first (a + * runtime instance tests against its own class before an ancestor's). */ +function fieldedErrorClasses(info: ClassInfo): ClassInfo[] { + const out: ClassInfo[] = []; + const visit = (c: ClassInfo): void => { + for (const s of c.subclasses) visit(s); + if (!c.builtinError && !c.def.runtime && ownErrorFields(c).length > 0) out.push(c); + }; + visit(info); + return out; +} + +/** Why a fielded error class's own properties have no static rendering, + * or null when every field renders. `visiting` and `out` are the caller's + * inspectSupport walk: a field typed as the class itself (`next?: Chain`) + * is a recursive shape, not a new walk. */ +function fieldedErrorReason( + lowerer: Lowerer, + info: ClassInfo, + visiting: Set = new Set(), + out: { recursive: boolean } = { recursive: false }, +): string | null { + const key = `error:${info.def.name}`; + if (visiting.has(key)) { + out.recursive = true; + return null; + } + visiting.add(key); + try { + for (const f of ownErrorFields(info)) { + if (ERROR_SPECIAL_KEYS.has(f.name)) + return `its field '${f.name}' is one Node's error inspector treats specially`; + const why = inspectSupport(lowerer, f.type, visiting, out); + if (why !== null) return `its field '${f.name}': ${why}`; + } + return null; + } finally { + visiting.delete(key); + } +} + +/** True when an instance of `c` can contain itself: its fields reach `c` + * or one of its bases, whose slots can hold a `c` (`next: Chain` in a + * subclass of Chain, a field typed as Error). */ +function errorInstanceReachesItself(lowerer: Lowerer, c: ClassInfo): boolean { + const holders = new Set(); + for (let a: ClassInfo | null = c; a; a = a.base) holders.add(a.def.name); + const seen = new Set(); + const walk = (u: IrType): boolean => { + for (const x of typeConstituents(lowerer, u)) { + if (x.kind === "object" && holders.has(x.className)) return true; + const k = typeKey(x); + if (!seen.has(k)) { + seen.add(k); + if (walk(x)) return true; + } + } + return false; + }; + return walk({ kind: "object", className: c.def.name }); +} + +/** An error value whose runtime class may declare fields + * (`class Bailable extends Error { bail = false }`): Node prints the error + * base (`[Bailable [Error]: m]`, the stackless form compiled errors use) + * followed by the own properties, `[Bailable [Error]: m] { bail: true }`, + * with the usual single-line/multi-line choice counting the base. Each + * fielded class (most-derived first) is an instanceof arm; other values + * keep the runtime's error rendering. Beyond the depth budget a fielded + * instance prints `[Name]`, as Node does for any object with keys. */ +function fieldedErrorInspectHelper( + lowerer: Lowerer, + t: IrType & { kind: "object" }, + info: ClassInfo, + loc: SrcLoc, +): string { + const key = `insp.fieldedError:${typeKey(t)}`; + const existing = lowerer.inspectHelpers.get(key); + if (existing) return existing; + const name = `%util.insp.${lowerer.inspectHelpers.size}`; + lowerer.inspectHelpers.set(key, name); + const v = (): IrExpr => varRef("v.0", t, loc); + const r = (): IrExpr => varRef("r.0", F64, loc); + const d = (): IrExpr => varRef("d.0", F64, loc); + const rPlus1 = (): IrExpr => ({ + kind: "bin", + op: "+", + left: r(), + right: numLit(1, loc), + type: F64, + loc, + }); + const base = (): IrExpr => ({ + kind: "libCall", + fn: "insp.error", + args: [v(), r(), d()], + type: STRING, + loc, + }); + const body: IrStmt[] = []; + for (const c of userErrorClasses(info)) { + const cls: IrType & { kind: "object" } = { kind: "object", className: c.def.name }; + const display = c.def.jsName ?? c.decl?.name?.text ?? c.def.name.replace(/^%/, ""); + if (ownErrorFields(c).length === 0) { + // A field-less program class: the header rule alone. + const header: IrStmt = { + kind: "return", + value: improvedErrorBase(lowerer, v(), base(), display, loc), + loc, + }; + if (c.def.name === t.className) { + body.push(header); + break; + } + body.push({ + kind: "if", + cond: { kind: "instanceOf", value: v(), className: c.def.name, type: BOOL, loc }, + then: [header], + else_: null, + loc, + }); + continue; + } + const why = fieldedErrorReason(lowerer, c); + if (why !== null) { + // An instance of a subclass whose own properties have no rendering + // throws a named refusal instead of printing a partial object. + body.push({ + kind: "if", + cond: { kind: "instanceOf", value: v(), className: c.def.name, type: BOOL, loc }, + then: [ + { + kind: "return", + value: nodeThrowExpr( + 0, + "SC2020", + `inspecting a '${display}' error (${why}) is not supported yet`, + STRING, + loc, + ), + loc, + }, + ], + else_: null, + loc, + }); + continue; + } + const instance: IrExpr = + c.def.name === t.className ? v() : { kind: "downcast", value: v(), type: cls, loc }; + // A class whose fields can hold the instance itself (`next: Chain`) + // runs Node's circular machinery, as other cycle-capable objects do: + // an instance already on the traversal stack renders [Circular *N], + // before the depth answer, and the frame's text gets the + // prefix when the walk found it circular. + const onCycle = errorInstanceReachesItself(lowerer, c); + const circular = (): IrExpr => varRef("cc.0", F64, loc); + const arm: IrStmt[] = [ + ...(onCycle + ? ([ + { + kind: "varDecl", + localId: "cc.0", + init: { kind: "libCall", fn: "insp.circCheck", args: [v()], type: F64, loc }, + loc, + }, + { + kind: "if", + cond: { + kind: "bin", + op: ">", + left: circular(), + right: numLit(0, loc), + type: BOOL, + loc, + }, + then: [ + { + kind: "return", + value: { + kind: "libCall", + fn: "insp.circular", + args: [circular()], + type: STRING, + loc, + }, + loc, + }, + ], + else_: null, + loc, + }, + ] satisfies IrStmt[]) + : []), + { + kind: "if", + cond: { kind: "bin", op: ">", left: r(), right: d(), type: BOOL, loc }, + then: [{ kind: "return", value: strLit(`[${display}]`, loc), loc }], + else_: null, + loc, + }, + { + kind: "varDecl", + localId: "base.0", + init: improvedErrorBase(lowerer, v(), base(), display, loc), + loc, + }, + { + kind: "exprStmt", + expr: { kind: "libCall", fn: "insp.begin", args: [rPlus1()], type: VOID, loc }, + loc, + }, + ...(onCycle + ? ([ + { + kind: "exprStmt", + expr: { kind: "libCall", fn: "insp.seenPush", args: [v()], type: VOID, loc }, + loc, + }, + ] satisfies IrStmt[]) + : []), + ]; + for (const f of ownErrorFields(c)) { + const read: IrExpr = { + kind: "fieldGet", + obj: instance, + className: c.def.name, + field: f.name, + type: f.type, + loc, + }; + arm.push({ + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "insp.entry", + args: [ + concatAll( + [ + strLit(`${inspectKey(f.name)}: `, loc), + inspectExpr(lowerer, f.type, read, rPlus1(), d(), loc), + ], + loc, + ), + boolLit(false, loc), + ], + type: VOID, + loc, + }, + loc, + }); + } + const ended: IrExpr = { + kind: "libCall", + fn: "insp.end", + args: [ + varRef("base.0", STRING, loc), + strLit("{", loc), + strLit("}", loc), + rPlus1(), + boolLit(false, loc), + boolLit(false, loc), + ], + type: STRING, + loc, + }; + arm.push({ + kind: "return", + value: onCycle + ? { kind: "libCall", fn: "insp.refWrap", args: [v(), ended], type: STRING, loc } + : ended, + loc, + }); + body.push( + c.def.name === t.className + ? { kind: "block", body: arm, loc } + : { + kind: "if", + cond: { kind: "instanceOf", value: v(), className: c.def.name, type: BOOL, loc }, + then: arm, + else_: null, + loc, + }, + ); + if (c.def.name === t.className) break; + } + if (body.at(-1)?.kind !== "return" && body.at(-1)?.kind !== "block") + body.push({ kind: "return", value: base(), loc }); + lowerer.liftedFns.push({ + name, + params: [ + { localId: "v.0", name: "v", type: t }, + { localId: "r.0", name: "r", type: F64 }, + { localId: "d.0", name: "d", type: F64 }, + ], + returnType: STRING, + locals: [ + { id: "v.0", name: "v", type: t, mutable: false }, + { id: "r.0", name: "r", type: F64, mutable: false }, + { id: "d.0", name: "d", type: F64, mutable: false }, + { id: "base.0", name: "base", type: STRING, mutable: false }, + { id: "cc.0", name: "cc", type: F64, mutable: false }, + { id: "name.0", name: "name", type: STRING, mutable: true }, + { id: "raw.0", name: "raw", type: STRING, mutable: true }, + ], + body, + loc, + }); + return name; +} + /** The program-declared classes among `info` and its descendants, most * derived first (an instance tests against its own class before an * ancestor's). Runtime-provided error classes are not among them. */ @@ -208,7 +544,7 @@ function userErrorInspectHelper( /** Node's improveStack over the stackless base (`[Error: m]`): when the * instance's constructor name differs from its `name` and the name ends in - * "Error", the header names both — `[Plain [Error]: m]`, or + * "Error", the header names both — `[Bailable [Error]: m]`, or * `[FetchError: m]` when the constructor name already contains it. */ function improvedErrorBase( lowerer: Lowerer, @@ -222,7 +558,14 @@ function improvedErrorBase( receiver: IrExpr, args: IrExpr[], type: IrType, - ): IrExpr => ({ kind: "strIntrinsic", method, receiver, args, type, loc }); + ): IrExpr => ({ + kind: "strIntrinsic", + method, + receiver, + args, + type, + loc, + }); const and = (left: IrExpr, right: IrExpr): IrExpr => ({ kind: "logical", op: "&&", @@ -401,9 +744,16 @@ function inspectSupport( // for those classes (the runtime's capsule view). if (eventTargetRootOf(info) !== null) return null; if (isErrorClass(lowerer, t.className)) { - return errorRenderable(info) - ? null - : "error subclasses with declared fields have no inspect lowering yet (the extra own properties are runtime-dynamic)"; + if (errorRenderable(info)) return null; + // Subclasses with declared fields render the error base followed + // by their own properties. The static class's own fields must + // render; a subclass whose fields cannot is a runtime refusal on + // its own instances only (see fieldedErrorInspectHelper). + const own = fieldedErrorClasses(info).find((c) => c === info); + const ownWhy = own ? fieldedErrorReason(lowerer, own, visiting, out) : null; + if (ownWhy !== null) + return `'${t.className.replace(/^%(m\d+\.)?/, "")}' error values (${ownWhy})`; + return null; } // A class with subclasses, or a JavaScript class, renders through its // checked-dynamic view, which names the instance's own class and holds @@ -553,6 +903,15 @@ export function inspectExpr( } if (isErrorClass(lowerer, t.className)) { const info = lowerer.classes.get(t.className); + if (info && !errorRenderable(info)) { + return { + kind: "call", + callee: fieldedErrorInspectHelper(lowerer, t, info, loc), + args: [value, recurse, depth], + type: STRING, + loc, + }; + } if (info && userErrorClasses(info).length > 0) { return { kind: "call", diff --git a/packages/compiler/src/frontend/ts7/semantic-model.ts b/packages/compiler/src/frontend/ts7/semantic-model.ts index 4920c96ae5..b0fa3da51f 100644 --- a/packages/compiler/src/frontend/ts7/semantic-model.ts +++ b/packages/compiler/src/frontend/ts7/semantic-model.ts @@ -536,6 +536,14 @@ export class SemanticType { // this snapshot's type with the same panic instead of a new // request. if (e instanceof Error && e.message.startsWith("panic:")) { + // The widened union (the cloned empty tuple widens to a plain + // array type) is readable and describes the same values; answer + // with its members when typescript-go can serialize them. + const widened = this.widenedConstituents(); + if (widened !== undefined) { + this.constituents = widened; + return widened; + } (e as { unreadableConstituents?: true }).unreadableConstituents = true; this.constituentsPanic = e; } @@ -544,6 +552,20 @@ export class SemanticType { } return this.constituents; } + /** The members of this union's widened type, or undefined when widening + * changes nothing or its members are unreadable too. */ + private widenedConstituents(): SemanticType[] | undefined { + try { + const data = this.project.request("getWidenedType", { type: this.id }); + if (data === null || data.id === this.id) return undefined; + const widened = this.project.type(data); + if ((widened.flags & TypeFlags.Union) === 0) return [widened]; + return this.project.fetchTypes(widened.id, "getTypesOfType"); + } catch (e) { + if (e instanceof Error && e.message.startsWith("panic:")) return undefined; + throw e; + } + } dispose(): void { this.constituents = undefined; this.constituentsPanic = undefined; diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 431d9fe3e4..1986ef5a00 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -4184,7 +4184,17 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const emptyTuple = (part: ts.Type): boolean => checker.isTupleType(part) && checker.getTypeArguments(part as ts.TypeReference).length === 0; - const hasArrayPart = parts.some((part) => checker.isArrayType(part)); + // The match-result fallback `s.match(re) || []` (and `?? []`) widens + // to `RegExpMatchArray | []`: the empty tuple is one of the match + // array's own values too. + const matchArray = (part: ts.Type): boolean => { + const name = part.getSymbol()?.name; + return ( + (name === "RegExpMatchArray" || name === "RegExpExecArray") && + (part.getSymbol()?.declarations.some((d) => ctx.isStdlibFile(d.getSourceFile())) ?? false) + ); + }; + const hasArrayPart = parts.some((part) => checker.isArrayType(part) || matchArray(part)); for (const part of parts) { if (hasArrayPart && emptyTuple(part)) continue; // TypeScript's client can retain impossible intersections in a diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index d6ff0fe72f..6589f530e2 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -647,6 +647,9 @@ export function isSupportedArrayElem(t: IrType): boolean { case "regex": case "child": case "netServer": + // fs.Stats snapshots (`await Promise.all(paths.map(stat))`): immutable + // refcounted handles with their own retain/release adapters. + case "stats": case "symbol": case "classval": case "moduleNs": diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6520-match-or-empty-widened.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6520-match-or-empty-widened.ts.json new file mode 100644 index 0000000000..562b721741 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6520-match-or-empty-widened.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6520-match-or-empty-widened.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6521-tuple-spread-into-array.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6521-tuple-spread-into-array.ts.json new file mode 100644 index 0000000000..c464e19219 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6521-tuple-spread-into-array.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6521-tuple-spread-into-array.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6522-stats-arrays.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6522-stats-arrays.ts.json new file mode 100644 index 0000000000..6b1bc14e5a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6522-stats-arrays.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6522-stats-arrays.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6523-then-primitive-constructors.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6523-then-primitive-constructors.ts.json new file mode 100644 index 0000000000..35ec854b9f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6523-then-primitive-constructors.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6523-then-primitive-constructors.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6524-match-or-empty-tuple.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6524-match-or-empty-tuple.ts.json new file mode 100644 index 0000000000..8a6728d332 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6524-match-or-empty-tuple.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6524-match-or-empty-tuple.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6525-error-subclass-fields.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6525-error-subclass-fields.ts.json new file mode 100644 index 0000000000..8ff1df1280 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6525-error-subclass-fields.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6525-error-subclass-fields.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6526-error-subclass-cycles.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6526-error-subclass-cycles.ts.json new file mode 100644 index 0000000000..9353afa09f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6526-error-subclass-cycles.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6526-error-subclass-cycles.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/checker-panic-tuple.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/checker-panic-tuple.ts.json deleted file mode 100644 index 4a57502d93..0000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/checker-panic-tuple.ts.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/diagnostics/checker-panic-tuple.ts" - ], - "diags": [] -} diff --git a/tests/corpus/6187-js-match-or-empty-array.js b/tests/corpus/6187-js-match-or-empty-array.js index 2cce0e17d6..5343f68271 100644 --- a/tests/corpus/6187-js-match-or-empty-array.js +++ b/tests/corpus/6187-js-match-or-empty-array.js @@ -1,8 +1,7 @@ -// @deferred-fences: 1 // `s.match(re) || []` types the empty array as an empty tuple that the // typescript-go API panics to describe (moment.js's configFromStringAndFormat). -// The panic stays with the statements whose lowering asks about that -// type, so the rest of the program compiles and runs. +// The union reads through its widened members, so the whole program +// compiles, including the statements whose lowering asks about that type. /** @param {string} s */ function firstDigit(s) { @@ -18,3 +17,4 @@ function digitCount(s) { const parsers = { firstDigit, digitCount }; console.log(typeof parsers.firstDigit, parsers.digitCount('a1b22')); +console.log(firstDigit('a1b22'), firstDigit('none')); diff --git a/tests/corpus/6520-match-or-empty-widened.ts b/tests/corpus/6520-match-or-empty-widened.ts new file mode 100644 index 0000000000..ffb6d49abc --- /dev/null +++ b/tests/corpus/6520-match-or-empty-widened.ts @@ -0,0 +1,15 @@ +// `s.match(re) ?? []` and `s.match(re) || []`: typescript-go types the +// empty literal by the tuple-like match result and cannot serialize the +// union's members (an upstream API panic); the widened union describes +// the same values, so the binding is an ordinary string array and the +// statement compiles instead of reporting the checker crash. +function scripts(text: string): string[] { + const matches = text.match(/(['"]?)([\S^'"]+\.js)\1/g) ?? []; + return matches.map((match) => match.replace(/^['"]|['"]$/g, "")).slice(0, 5); +} +console.log(scripts(`RUN node a.js 'b.js' "c.js"`), scripts("no scripts here")); + +const words = "alpha beta".match(/\w+/g) || []; +console.log(words.length, words.join("+")); +const none = "".match(/x/) || []; +console.log(none.length); diff --git a/tests/corpus/6521-tuple-spread-into-array.ts b/tests/corpus/6521-tuple-spread-into-array.ts new file mode 100644 index 0000000000..64112f7ba2 --- /dev/null +++ b/tests/corpus/6521-tuple-spread-into-array.ts @@ -0,0 +1,12 @@ +// Spreading a fixed tuple into an array literal copies its positions in +// order into the literal's element type. +const SCRIPT_FILES: [string, string, string, string, string] = ["a.ts", "a.mts", "a.cts", "a.js", "a.mjs"]; +const DATA_FILES = ["a.json"]; +const files = [...SCRIPT_FILES, ...DATA_FILES]; +console.log(files.length, files.join(",")); +const PAIR = ["x", "y"] as const; +const more: string[] = [...PAIR, "z", ...SCRIPT_FILES.slice(0, 1)]; +console.log(more); +const numbers: [number, number, number] = [1, 2, 3]; +const widened: (number | string)[] = [...numbers, "end"]; +console.log(widened); diff --git a/tests/corpus/6522-stats-arrays.ts b/tests/corpus/6522-stats-arrays.ts new file mode 100644 index 0000000000..61818df3dc --- /dev/null +++ b/tests/corpus/6522-stats-arrays.ts @@ -0,0 +1,31 @@ +// fs.Stats values in arrays: Promise.all over stat() promises answers a +// Stats[] whose elements reduce, map and index like any array. +import { mkdirSync, rmSync, writeFileSync } from "node:fs"; +import { readdir, stat } from "node:fs/promises"; +import * as path from "node:path"; +import { tmpdir } from "node:os"; + +async function packBytes(dir: string): Promise { + try { + const entries = await readdir(dir, { withFileTypes: true }); + const packs = entries.filter((entry) => entry.isFile() && entry.name.endsWith(".pack")); + const stats = await Promise.all(packs.map((entry) => stat(path.join(dir, entry.name)))); + return stats.reduce((total, s) => total + s.size, 0); + } catch { + return undefined; + } +} + +async function main(): Promise { + const dir = path.join(tmpdir(), `scriptc-6522-${process.pid}`); + mkdirSync(dir, { recursive: true }); + writeFileSync(path.join(dir, "a.pack"), "12345"); + writeFileSync(path.join(dir, "b.pack"), "123"); + writeFileSync(path.join(dir, "c.idx"), "1"); + console.log(await packBytes(dir), await packBytes(path.join(dir, "missing"))); + const all = await Promise.all(["a.pack", "c.idx"].map((name) => stat(path.join(dir, name)))); + console.log(all.length, all.map((s) => s.isFile()).join(","), all[1]!.size); + rmSync(dir, { recursive: true, force: true }); +} + +main(); diff --git a/tests/corpus/6523-then-primitive-constructors.ts b/tests/corpus/6523-then-primitive-constructors.ts new file mode 100644 index 0000000000..da0df6f190 --- /dev/null +++ b/tests/corpus/6523-then-primitive-constructors.ts @@ -0,0 +1,19 @@ +// The ambient String, Number and Boolean as `.then` handlers over a +// number or boolean settled value convert it, as calling them would. +async function flag(value: boolean): Promise { + return value; +} + +async function main(): Promise { + const a = await flag(true).then(String); + const b = await flag(false) + .then(String) + .catch(() => "unknown"); + const n = await flag(true).then(Number); + const s = await Promise.resolve(1.5).then(String); + const t = await Promise.resolve(0).then(Boolean); + const u = await Promise.resolve(-0).then(String); + console.log(a, b, n + 1, s, t, u); +} + +main(); diff --git a/tests/diagnostics/checker-panic-tuple.ts b/tests/corpus/6524-match-or-empty-tuple.ts similarity index 57% rename from tests/diagnostics/checker-panic-tuple.ts rename to tests/corpus/6524-match-or-empty-tuple.ts index cf59bc1f00..6ab2906aa4 100644 --- a/tests/diagnostics/checker-panic-tuple.ts +++ b/tests/corpus/6524-match-or-empty-tuple.ts @@ -1,6 +1,6 @@ // `''.match(/x/) || []` drives tsgo into the TupleType interface-conversion // panic (upstream signature 01/13) whenever the union's members are -// serialized: the union stays unmappable, so the statement reports its own -// refusals — a failed compile, never a crashed CLI. +// serialized; the widened union's members are readable, so the binding is +// the match array (the empty tuple is one of its values) and compiles. const a = ''.match(/x/) || []; console.log(a.length); diff --git a/tests/corpus/6525-error-subclass-fields.ts b/tests/corpus/6525-error-subclass-fields.ts new file mode 100644 index 0000000000..7a288bce97 --- /dev/null +++ b/tests/corpus/6525-error-subclass-fields.ts @@ -0,0 +1,38 @@ +// Inspecting an Error-typed value whose runtime class declares fields +// prints the stackless error base, with Node's constructor-name header +// (`[Bailable [Error]: m]`), followed by the own properties — on one line +// or broken across lines by the same width rule as any object, and as +// `[Name]` beyond the depth budget. (The stack is cleared: compiled errors +// carry none.) +import * as util from "node:util"; +class Bailable extends Error { + bail = false; +} +class Detailed extends Bailable { + attempts: number[] = []; + label = "x"; +} +function clean(e: E): E { + e.stack = ""; + return e; +} +function show(error: Error): void { + console.log(util.format(error)); + console.log(util.format("%s", error)); + console.log(util.inspect({ wrapped: { deeper: { error } } })); +} +const b = clean(new Bailable("download failed")); +b.bail = true; +show(b); +show(clean(new Error("plain"))); +const d = clean(new Detailed("y".repeat(70))); +d.attempts.push(1, 2); +show(d); +class FetchFailedError extends Error { + status = 0; +} +const f = clean(new FetchFailedError("not found")); +f.status = 404; +show(f); +class Plain extends Error {} +show(clean(new Plain("field-less beside fielded subclasses"))); diff --git a/tests/corpus/6526-error-subclass-cycles.ts b/tests/corpus/6526-error-subclass-cycles.ts new file mode 100644 index 0000000000..df6a7dc36e --- /dev/null +++ b/tests/corpus/6526-error-subclass-cycles.ts @@ -0,0 +1,37 @@ +// Inspecting Error subclasses whose fields hold the same class or one of +// its bases: a chain renders each link, and a cycle renders as Node's +// circular reference (`` and `[Circular *1]`), also when the cycle +// runs through a base-typed field of a subclass. (The stack is cleared: +// compiled errors carry none.) +import * as util from "node:util"; +class Chain extends Error { + next: Chain | undefined = undefined; +} +class Tagged extends Chain { + tag = "t"; +} +function clean(e: E): E { + e.stack = ""; + return e; +} +function show(e: Error): void { + console.log(util.inspect(e)); +} + +const tail = clean(new Chain("tail")); +const head = clean(new Chain("head")); +head.next = tail; +show(head); +console.log(util.inspect({ wrapped: { head } })); + +const loop = clean(new Chain("loop")); +loop.next = loop; +show(loop); + +const a = clean(new Tagged("a")); +const b = clean(new Chain("b")); +a.next = b; +b.next = a; +show(a); +show(b); +show(clean(new Error("plain"))); diff --git a/tests/harness/__snapshots__/checker-panic-tuple.ts.txt b/tests/harness/__snapshots__/checker-panic-tuple.ts.txt deleted file mode 100644 index 3927e55189..0000000000 --- a/tests/harness/__snapshots__/checker-panic-tuple.ts.txt +++ /dev/null @@ -1,13 +0,0 @@ -checker-panic-tuple.ts:5:11 - error SC1090: logical operators on union-typed values outside conditions (narrow first: check a discriminant field, or compare with '!== undefined'/'!== null' for unit arms) are not supported yet - - 4 | // refusals — a failed compile, never a crashed CLI. - 5 | const a = ''.match(/x/) || []; - | ^~~~~~~~~~~~~~~~~~~ - 6 | console.log(a.length); - -checker-panic-tuple.ts:6:13 - error SC1090: reading 'length' on a union-typed value (every arm must be an object/record with a same-typed field 'length'; narrow first: check a discriminant field, or compare with '!== undefined'/'!== null' for unit arms) is not supported yet - - 5 | const a = ''.match(/x/) || []; - 6 | console.log(a.length); - | ^~~~~~~~ - 7 | \ No newline at end of file diff --git a/tests/harness/corpus-costs/6526-error-subclass-cycles.ts.json b/tests/harness/corpus-costs/6526-error-subclass-cycles.ts.json new file mode 100644 index 0000000000..f1c2cc30be --- /dev/null +++ b/tests/harness/corpus-costs/6526-error-subclass-cycles.ts.json @@ -0,0 +1 @@ +{ "plain": 2600, "san": 3400 }