From 302f0f9fa99fc2e72b41aa2920f2a51fb7fe275f Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 19 Oct 2026 17:02:31 -0500 Subject: [PATCH] Compile the remaining corpus programs with the native compiler The native compiler failed on corpus programs that the Node-hosted compiler compiles. Four compiler causes: - A record that width-lifts into several members of a discriminated union had no conversion plan. As a field of a larger record, that removed the outer member it belonged to, and the outer record could then lift into the only member left: a `recordSet` statement built with a nested `recordGet` literal became a `break` statement, and a switch on `kind` read it with the `recordSet` layout. The native compiler stopped at the narrowing guard on 5585-record-results-inlined.ts and similar programs. Such a record now converts through the discriminant: each member it width-lifts into is a route, and the value's discriminant selects the member at run time, as same-layout members already do. - Three compiler paths asserted a value that can be null, and the native compiler threw "null is not representable in the target union": stableCreateRequireBindingReason7 (`cache.get(decl)!` from a cache that stores null for stable createRequire bindings; 2631-create-require, 2932-create-require-program), the Int32 contract check (`catchLocalId!` for a bindingless `catch {}`; 4970-int32-call-abi) and StringIndexBounds.binding (a memo that stores null). The caches are read without assertions and the catch test checks for null. - scriptc adopts alwaysStrict, esModuleInterop and allowSyntheticDefaultImports from a project's tsconfig, but the TS 7 SDK schema no longer declares them. A native compiler keeps only declared fields when it builds or parses a typed record, so these options were dropped, and the default-import check refused `import fs from "node:fs"` (SC1012, 2391-default-builtin-imports and the 3055 to 3057 module-setting variants). The options a project reports are parsed with these fields declared, and Ts7CompilerOptions uses the same type. - An object-literal method kept its inferred return type where an arrow function or function expression takes the return type of its contextual signature. For a method that returns an array of statement literals, a `varDecl` literal with `init: null` was converted to the member whose `init` holds only expressions, and the conversion threw (Array.fromAsync's array loop: 5854-array-from-async, 5855-array-from-async-js). A synchronous object-literal method now takes its slot from the property of the same name on the literal's contextual type and adopts its return the same way. Adds corpus 6022 and 6024. 2391, 2631, 3055, 4970, 5585 and 5854 run in the native-frontend self-host check, which now also links the dyn-async unit when the program uses it, as the production build does. --- packages/compiler/src/contracts/check.ts | 5 +- .../src/frontend/lowering/call-signatures.ts | 37 +++++- .../lowering/coercions/structural-plans.ts | 16 +++ .../frontend/lowering/coercions/structural.ts | 7 +- .../src/frontend/lowering/coercions/unions.ts | 10 +- .../compiler/src/frontend/lowering/lowerer.ts | 3 +- .../frontend/lowering/string-index-bounds.ts | 3 +- .../src/frontend/lowering/union-retag.ts | 36 +++--- .../src/frontend/lowering/width-lift.ts | 2 +- packages/compiler/src/frontend/program.ts | 3 +- .../compiler/src/frontend/ts7/program-host.ts | 15 ++- packages/compiler/src/frontend/ts7/session.ts | 30 ++++- .../6022-width-lift-discriminant.ts.json | 6 + ...-object-literal-method-slot-return.ts.json | 6 + tests/corpus/6022-width-lift-discriminant.ts | 54 +++++++++ .../6024-object-literal-method-slot-return.ts | 114 ++++++++++++++++++ .../6022-width-lift-discriminant.ts.json | 1 + ...-object-literal-method-slot-return.ts.json | 1 + tests/harness/native-frontend.ts | 8 ++ 19 files changed, 312 insertions(+), 45 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6022-width-lift-discriminant.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6024-object-literal-method-slot-return.ts.json create mode 100644 tests/corpus/6022-width-lift-discriminant.ts create mode 100644 tests/corpus/6024-object-literal-method-slot-return.ts create mode 100644 tests/harness/corpus-costs/6022-width-lift-discriminant.ts.json create mode 100644 tests/harness/corpus-costs/6024-object-literal-method-slot-return.ts.json diff --git a/packages/compiler/src/contracts/check.ts b/packages/compiler/src/contracts/check.ts index 3c25c9b1b..5934624bf 100644 --- a/packages/compiler/src/contracts/check.ts +++ b/packages/compiler/src/contracts/check.ts @@ -889,10 +889,7 @@ class Int32Contracts { const value = s.kind === "varDecl" ? s.init : s.value; if (note && value && !int32(value)) fail(note, value, value.loc, "the value stored into"); - } else if ( - s.kind === "forOf" || - (s.kind === "tryCatch" && s.catchLocalId !== undefined) - ) { + } else if (s.kind === "forOf" || (s.kind === "tryCatch" && s.catchLocalId !== null)) { const id = s.kind === "forOf" ? s.localId : s.catchLocalId!; const note = localNote(id); if (note) diff --git a/packages/compiler/src/frontend/lowering/call-signatures.ts b/packages/compiler/src/frontend/lowering/call-signatures.ts index 9d9c9668c..55f46a7b0 100644 --- a/packages/compiler/src/frontend/lowering/call-signatures.ts +++ b/packages/compiler/src/frontend/lowering/call-signatures.ts @@ -864,9 +864,18 @@ export function lambdaSignature( // Union-inferred returns adopt too (a mixed-return body inferring a // SUB-union of the slot's union — adopting is a no-op when the two + // A synchronous object-literal method adopts its slot's return the same + // way: its slot is the property of that name on the literal's + // contextual type. + const literalMethod = + ts.isMethodDeclaration(node) && + ts.isObjectLiteralExpression(node.parent) && + !node.asteriskToken && + node.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) !== true; if ( - (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) && - (!isAsyncLike || ret.kind === "promise") + ((ts.isArrowFunction(node) || ts.isFunctionExpression(node)) && + (!isAsyncLike || ret.kind === "promise")) || + literalMethod ) { // The slot's signature: the contextual type stripped of its nullish // parts (an OPTIONAL callback field's context is the whole @@ -874,7 +883,10 @@ export function lambdaSignature( // declared in USER code: stdlib callback slots (flatMap's // `U | readonly U[]`, sort comparators) have intrinsic lowerings // that inspect the INFERRED type, so they must not widen. - const ctxType = lowerer.checker.getContextualType(node); + const ctxType = + literalMethod && ts.isMethodDeclaration(node) && node.parent + ? objectLiteralMethodSlot(lowerer, node, node.parent) + : lowerer.checker.getContextualType(node); const ctxSigs = ctxType ? lowerer.checker.getCallSignatures(lowerer.checker.getNonNullableType(ctxType)) : []; @@ -943,6 +955,7 @@ export function lambdaSignature( const inferredLiteral = literalSlot !== undefined && (literalType === null || literalType.kind === "dyn"); if ( + !literalMethod && !isAsyncLike && !node.type && !inferredLiteral && @@ -958,3 +971,21 @@ export function lambdaSignature( const funcType = funcTypeFromParamShapes(shapes, ret); return { shapes, funcType }; } + +/** The type of the property an object-literal method fills on the + * literal's contextual type; undefined when the literal has no contextual + * type or the method has a computed name. */ +function objectLiteralMethodSlot( + lowerer: Lowerer, + node: ts.MethodDeclaration, + literal: ts.Node, +): ts.Type | undefined { + if (!ts.isIdentifier(node.name) && !ts.isStringLiteral(node.name)) return undefined; + const literalType = lowerer.checker.getContextualType(literal); + if (!literalType) return undefined; + const property = lowerer.checker.getPropertyOfType( + lowerer.checker.getNonNullableType(literalType), + node.name.text, + ); + return property ? lowerer.checker.getTypeOfSymbol(property) : undefined; +} diff --git a/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts b/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts index 78c7cb73d..83798a89c 100644 --- a/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts +++ b/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts @@ -78,6 +78,22 @@ export function widthLiftPlan(lowerer: Lowerer, src: IrType, dst: IrType): Width }); if (absentOnly.length === 1) selected = absentOnly; } + // A record that width-lifts into several members of a discriminated + // union is converted by its discriminant's value at run time, when + // every candidate member is one of the routes. + if (selected.length > 1 && src.kind === "record" && def.discriminant) { + const shape = lowerer.shapes.get(src.shapeId); + const plan = + shape && + planRecordUnionWrap( + shape, + def, + (id) => lowerer.shapes.get(id), + (from, to) => lowerer.widthLiftPlan(from, to), + ); + if (plan && selected.every(({ tag }) => plan.routes.some((route) => route.tag === tag))) + return { how: "discriminantWrap", width: true }; + } if (selected.length !== 1) return null; return { how: "liftWrap", tag: selected[0]!.tag, arm: selected[0]!.arm }; } diff --git a/packages/compiler/src/frontend/lowering/coercions/structural.ts b/packages/compiler/src/frontend/lowering/coercions/structural.ts index 925f59d9b..5931b33a7 100644 --- a/packages/compiler/src/frontend/lowering/coercions/structural.ts +++ b/packages/compiler/src/frontend/lowering/coercions/structural.ts @@ -99,7 +99,12 @@ export function applyWidthLift( case "discriminantWrap": { if (dst.kind !== "union" || value.type.kind !== "record") throw new InternalCompilerError("lowerer bug: record discriminator lift shape"); - const helper = lowerer.recordUnionWrapHelper(value.type, dst.unionId, loc); + const helper = lowerer.recordUnionWrapHelper( + value.type, + dst.unionId, + loc, + lift.width === true, + ); if (!helper) throw new InternalCompilerError( "lowerer bug: planned record discriminator lift failed to intern", diff --git a/packages/compiler/src/frontend/lowering/coercions/unions.ts b/packages/compiler/src/frontend/lowering/coercions/unions.ts index 0356b714e..84c625c3f 100644 --- a/packages/compiler/src/frontend/lowering/coercions/unions.ts +++ b/packages/compiler/src/frontend/lowering/coercions/unions.ts @@ -265,13 +265,19 @@ export function recordUnionWrapHelper( source: IrType & { kind: "record" }, toId: string, loc: SrcLoc, + width = false, ): string | null { const shape = lowerer.shapes.get(source.shapeId); const to = lowerer.unions.get(toId); if (!shape || !to) return null; - const plan = planRecordUnionWrap(shape, to, (id) => lowerer.shapes.get(id)); + const plan = planRecordUnionWrap( + shape, + to, + (id) => lowerer.shapes.get(id), + width ? (from, dst) => lowerer.widthLiftPlan(from, dst) : undefined, + ); if (!plan) return null; - const key = `recordWrap:${source.shapeId}:${toId}`; + const key = `recordWrap${width ? "Width" : ""}:${source.shapeId}:${toId}`; const existing = lowerer.coercions.retags.get(key); if (existing) return existing; const name = `%record.wrap.${lowerer.coercions.retags.size}`; diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 9e7359a34..3c9a18713 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -9143,8 +9143,9 @@ export class Lowerer { source: IrType & { kind: "record" }, toId: string, loc: SrcLoc, + width = false, ): string | null { - return recordUnionWrapHelper(this, source, toId, loc); + return recordUnionWrapHelper(this, source, toId, loc, width); } unionRetagHelper( diff --git a/packages/compiler/src/frontend/lowering/string-index-bounds.ts b/packages/compiler/src/frontend/lowering/string-index-bounds.ts index acb41514f..32b96e63a 100644 --- a/packages/compiler/src/frontend/lowering/string-index-bounds.ts +++ b/packages/compiler/src/frontend/lowering/string-index-bounds.ts @@ -243,7 +243,8 @@ export class StringIndexBounds { /** Every write of a block-scoped local or parameter, or null for any * other binding (module imports, var, class members, ...). */ private binding(symbol: ts.Symbol): Binding | null { - if (this.bindings.has(symbol)) return this.bindings.get(symbol)!; + const cached = this.bindings.get(symbol); + if (cached !== undefined) return cached; const result = this.scanBinding(symbol); this.bindings.set(symbol, result); return result; diff --git a/packages/compiler/src/frontend/lowering/union-retag.ts b/packages/compiler/src/frontend/lowering/union-retag.ts index 8d1c61d09..82ec8226c 100644 --- a/packages/compiler/src/frontend/lowering/union-retag.ts +++ b/packages/compiler/src/frontend/lowering/union-retag.ts @@ -41,11 +41,15 @@ export interface RecordUnionWrapPlan { /** Recursive variants can retain distinct ids for the same field layout. * An independently inferred record may intern to either id, so that id - * alone cannot select its semantic destination when wrapping into a union. */ + * alone cannot select its semantic destination when wrapping into a union. + * With `widthLift`, members the record only width-lifts into are routes + * too: a record's own fields do not say which of them its discriminant + * names, so the value's discriminant selects the member at run time. */ export function planRecordUnionWrap( source: IrRecordShape, to: IrUnionDef, shapeOf: (id: string) => IrRecordShape | undefined, + widthLift?: (src: IrType, dst: IrType) => WidthLift | null, ): RecordUnionWrapPlan | null { const discriminant = to.discriminant; if (!discriminant || source.tuple || source.indexValue || shapeHasAccessorSlots(source)) @@ -57,30 +61,22 @@ export function planRecordUnionWrap( const arm = to.arms[tag]!; if (arm.kind !== "record") continue; const target = shapeOf(arm.shapeId); - if ( - !target || - target.tuple || - target.indexValue || - target.fields.length !== source.fields.length - ) - continue; - let same = true; - for (let i = 0; i < source.fields.length; i++) { + if (!target || target.tuple || target.indexValue) continue; + let same = target.fields.length === source.fields.length; + for (let i = 0; same && i < source.fields.length; i++) { const a = source.fields[i]!, b = target.fields[i]!; - if (a.name !== b.name || !typeEquals(a.type, b.type)) { - same = false; - break; - } + if (a.name !== b.name || !typeEquals(a.type, b.type)) same = false; } - if (!same) continue; + let lift: WidthLift | null = same + ? { how: arm.shapeId === source.id ? "copy" : "width" } + : null; + if (lift === null && widthLift !== undefined) + lift = widthLift({ kind: "record", shapeId: source.id }, arm); + if (lift === null) continue; const entry = discriminant.cases.find((candidate) => candidate.tag === tag); if (!entry) return null; - routes.push({ - tag, - lift: { how: arm.shapeId === source.id ? "copy" : "width" }, - values: entry.values, - }); + routes.push({ tag, lift, values: entry.values }); } if (routes.length < 2 || discriminantOwners(to, shapeOf) === null) return null; return { field: discriminant.field, fieldType, routes }; diff --git a/packages/compiler/src/frontend/lowering/width-lift.ts b/packages/compiler/src/frontend/lowering/width-lift.ts index 40c533d72..6b1ad8c84 100644 --- a/packages/compiler/src/frontend/lowering/width-lift.ts +++ b/packages/compiler/src/frontend/lowering/width-lift.ts @@ -6,7 +6,7 @@ import type { IrType } from "../../ir/ir.js"; export type WidthLift = | { how: "copy" } | { how: "wrap"; tag: number } - | { how: "discriminantWrap" } + | { how: "discriminantWrap"; width?: true } | { how: "retag" } | { how: "liftWrap"; tag: number; arm: IrType } | { how: "width" } diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index bde168c38..5270f7bb7 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -2042,7 +2042,8 @@ function stableCreateRequireBindingReason7( decl: ts.VariableDeclaration, ): string | null { const cache = program.analysis.createRequireReasons; - if (cache.has(decl)) return cache.get(decl)!; + const cached = cache.get(decl); + if (cached !== undefined) return cached; const check = (): string | null => { const stmt = decl.parent?.parent; if ( diff --git a/packages/compiler/src/frontend/ts7/program-host.ts b/packages/compiler/src/frontend/ts7/program-host.ts index 2f2fdf76b..fa1eeab8f 100644 --- a/packages/compiler/src/frontend/ts7/program-host.ts +++ b/packages/compiler/src/frontend/ts7/program-host.ts @@ -21,9 +21,13 @@ import { dirname, join, resolve } from "node:path"; import { Ts7Api } from "./api.js"; import type { Ts7TimingInfo } from "./session-timing.js"; import type { Ts7FileSystem } from "./rpc-filesystem.js"; -import type { CompilerOptions, Diagnostic } from "./semantic-types.js"; +import type { Diagnostic } from "./semantic-types.js"; import type { SourceFile } from "./ast-types.js"; -import type { Ts7SessionProject as Project, Ts7SessionSnapshot as Snapshot } from "./session.js"; +import type { + Ts7ProjectCompilerOptions, + Ts7SessionProject as Project, + Ts7SessionSnapshot as Snapshot, +} from "./session.js"; import { CheckerFacade } from "./checker.js"; import { moduleDetectionKindName, @@ -44,8 +48,9 @@ import { /** The compiler options our createProgram accepts: TS7's CompilerOptions * shape (numeric enums for target/module/moduleResolution — the enums module * generates them from the pinned SDK) with the 5.9.3 "lib.es2025.d.ts" lib - * spelling also accepted. */ -export type Ts7CompilerOptions = CompilerOptions; + * spelling also accepted, plus the project options scriptc adopts that the + * TS7 schema no longer declares (Ts7ProjectCompilerOptions). */ +export type Ts7CompilerOptions = Ts7ProjectCompilerOptions; /* tsconfig JSON wants enum NAMES; the options object carries TS7's numeric * enum values. Generated name lookups preserve the SDK's reverse mappings @@ -282,7 +287,7 @@ export class Ts7Program { private readonly configPath: string, ) {} - getCompilerOptions(): CompilerOptions { + getCompilerOptions(): Ts7CompilerOptions { return this.project.program.getCompilerOptions(); } diff --git a/packages/compiler/src/frontend/ts7/session.ts b/packages/compiler/src/frontend/ts7/session.ts index 4933da9d6..bc4c09496 100644 --- a/packages/compiler/src/frontend/ts7/session.ts +++ b/packages/compiler/src/frontend/ts7/session.ts @@ -17,6 +17,24 @@ import type { Ts7SourceMetadata, } from "./session-schema.generated.js"; +/** Project options tsgo still reports for a tsconfig but the TS 7 SDK + * schema no longer declares. scriptc adopts them (ADOPTED_OPTIONS), so the + * options a project reports are parsed with them declared: a native + * compiler keeps only declared fields when it parses the response. */ +export interface Ts7ProjectCompilerOptions extends Ts7CompilerOptionsData { + alwaysStrict?: boolean; + esModuleInterop?: boolean; + allowSyntheticDefaultImports?: boolean; +} + +interface Ts7ReportedProjectData extends Ts7ProjectData { + compilerOptions: Ts7ProjectCompilerOptions; +} + +interface Ts7ReportedSnapshotData extends Ts7SnapshotData { + projects: Ts7ReportedProjectData[]; +} + export interface Ts7FileChanges { changed?: SemanticDocument[]; created?: SemanticDocument[]; @@ -103,7 +121,7 @@ export class Ts7Session { updateSnapshot(params: Ts7Update = {}): Ts7SessionSnapshot { const paths = this.initialize(); - const data = parseSemanticJson( + const data = parseSemanticJson( this.text("updateSnapshot", JSON.stringify(params)), ); const snapshot = new Ts7SessionSnapshot(data, this, paths); @@ -159,7 +177,7 @@ export class Ts7SessionSnapshot { private disposed = false; constructor( - data: Ts7SnapshotData, + data: Ts7ReportedSnapshotData, private readonly session: Ts7Session, readonly paths: Ts7Paths, ) { @@ -188,7 +206,7 @@ export class Ts7SessionSnapshot { } getDefaultProjectForFile(file: SemanticDocument): Ts7SessionProject | undefined { this.ensureActive(); - const data = this.session.request("getDefaultProjectForFile", { + const data = this.session.request("getDefaultProjectForFile", { snapshot: this.id, file, }); @@ -214,12 +232,12 @@ export class Ts7SessionSnapshot { export class Ts7SessionProject { readonly id: string; readonly configFileName: string; - readonly compilerOptions: Ts7CompilerOptionsData; + readonly compilerOptions: Ts7ProjectCompilerOptions; readonly rootFiles: string[]; readonly program: Ts7SessionProgram; readonly checker: SemanticChecker; - constructor(data: Ts7ProjectData, snapshot: Ts7SessionSnapshot, session: Ts7Session) { + constructor(data: Ts7ReportedProjectData, snapshot: Ts7SessionSnapshot, session: Ts7Session) { this.id = data.id; this.configFileName = data.configFileName; this.compilerOptions = data.compilerOptions; @@ -260,7 +278,7 @@ export class Ts7SessionProgram { ...(file === undefined ? {} : { file }), }; } - getCompilerOptions(): Ts7CompilerOptionsData { + getCompilerOptions(): Ts7ProjectCompilerOptions { this.ensureActive(); return this.project.compilerOptions; } diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6022-width-lift-discriminant.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6022-width-lift-discriminant.ts.json new file mode 100644 index 000000000..38c0cb787 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6022-width-lift-discriminant.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6022-width-lift-discriminant.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6024-object-literal-method-slot-return.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6024-object-literal-method-slot-return.ts.json new file mode 100644 index 000000000..c04cbf304 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6024-object-literal-method-slot-return.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6024-object-literal-method-slot-return.ts" + ], + "diags": [] +} diff --git a/tests/corpus/6022-width-lift-discriminant.ts b/tests/corpus/6022-width-lift-discriminant.ts new file mode 100644 index 000000000..73bb5059b --- /dev/null +++ b/tests/corpus/6022-width-lift-discriminant.ts @@ -0,0 +1,54 @@ +// A record built in a map callback whose fields width-lift into several +// members of a discriminated union (a nested literal with a narrowed field, +// and a member that only needs `kind` and `loc`): its discriminant selects +// the member. +interface Loc { + start: number; +} +type Expr = + | { kind: "get"; obj: Expr; field: string; loc: Loc } + | { kind: "ref"; name: string; loc: Loc } + | { kind: "undef"; loc: Loc }; +type Stmt = + | { kind: "set"; obj: Expr; field: string; value: Expr; loc: Loc } + | { kind: "break"; label?: string; loc: Loc } + | { kind: "return"; value: Expr | null; loc: Loc }; + +function showExpr(expr: Expr): string { + switch (expr.kind) { + case "get": + return `${showExpr(expr.obj)}.${expr.field}`; + case "ref": + return expr.name; + case "undef": + return "undefined"; + } +} + +function show(stmt: Stmt): string { + switch (stmt.kind) { + case "set": + return `${showExpr(stmt.obj)}.${stmt.field} = ${showExpr(stmt.value)}`; + case "break": + return `break${stmt.label === undefined ? "" : " " + stmt.label}`; + case "return": + return `return ${stmt.value === null ? "-" : showExpr(stmt.value)}`; + } +} + +function copies(fields: string[], source: Expr & { kind: "ref" }, dst: Expr, loc: Loc): Stmt[] { + return fields.map((field) => ({ + kind: "set", + obj: dst, + field, + value: { kind: "get", obj: source, field, loc }, + loc, + })); +} + +const loc: Loc = { start: 0 }; +const out: Expr = { kind: "ref", name: "out", loc }; +const span: Expr = { kind: "ref", name: "span", loc }; +if (span.kind === "ref") console.log(copies(["lo", "hi"], span, out, loc).map(show).join("; ")); +const stops: Stmt[] = ["a", "b"].map((label) => ({ kind: "break", label, loc })); +console.log(stops.map(show).join("; ")); diff --git a/tests/corpus/6024-object-literal-method-slot-return.ts b/tests/corpus/6024-object-literal-method-slot-return.ts new file mode 100644 index 000000000..b5fa7ddd5 --- /dev/null +++ b/tests/corpus/6024-object-literal-method-slot-return.ts @@ -0,0 +1,114 @@ +// An object-literal method returns the type its slot declares. The literal +// statements below have their own inferred type, a union of records whose +// `init` fields are `Expr` or `null` and whose tryCatch member has fields +// the declared `Stmt` does not; the method still returns `Stmt[]`. +type Expr = + | { kind: "num"; value: number } + | { kind: "str"; text: string } + | { kind: "ref"; name: string }; +type Stmt = + | { kind: "varDecl"; name: string; init: Expr | null } + | { kind: "exprStmt"; expr: Expr } + | { kind: "if"; cond: Expr; then: Stmt[]; else_: Stmt[] | null } + | { kind: "loop"; init: Stmt | null; body: Stmt[] } + | { + kind: "tryCatch"; + tryBody: Stmt[]; + catchBody: Stmt[] | null; + catchName: string | null; + finallyBody: Stmt[] | null; + }; + +interface Step { + label: string; + build(source: Expr, append: (value: Expr) => Stmt[]): Stmt[]; + first(source: Expr): Stmt | Expr; +} + +const ref = (name: string): Expr => ({ kind: "ref", name }); + +function arrayStep(label: string): Step { + return { + label, + build(source, append) { + const loop: Stmt = { + kind: "loop", + init: { kind: "varDecl", name: "cursor", init: { kind: "num", value: 0 } }, + body: [...append(ref("value"))], + }; + return [ + { kind: "varDecl", name: "array", init: source }, + { kind: "varDecl", name: "close", init: ref("flag") }, + { kind: "varDecl", name: "value", init: null }, + { + kind: "tryCatch", + tryBody: [loop], + catchBody: null, + catchName: null, + finallyBody: [{ kind: "if", cond: ref("close"), then: [], else_: null }], + }, + ]; + }, + first(source) { + if (source.kind === "num") return { kind: "varDecl", name: "n", init: null }; + return source; + }, + }; +} + +function show(expr: Expr | null): string { + if (expr === null) return "null"; + switch (expr.kind) { + case "num": + return String(expr.value); + case "str": + return JSON.stringify(expr.text); + case "ref": + return expr.name; + } +} + +function print(stmts: Stmt[], depth: number): void { + const pad = " ".repeat(depth); + for (const s of stmts) { + switch (s.kind) { + case "varDecl": + console.log(`${pad}let ${s.name} = ${show(s.init)}`); + break; + case "exprStmt": + console.log(`${pad}${show(s.expr)}`); + break; + case "if": + console.log(`${pad}if ${show(s.cond)} (else: ${s.else_ === null ? "none" : s.else_.length})`); + print(s.then, depth + 1); + break; + case "loop": + console.log(`${pad}loop`); + if (s.init) print([s.init], depth + 1); + print(s.body, depth + 1); + break; + case "tryCatch": + console.log(`${pad}try (catch: ${s.catchName ?? "none"})`); + print(s.tryBody, depth + 1); + if (s.catchBody) print(s.catchBody, depth + 1); + if (s.finallyBody) { + console.log(`${pad}finally`); + print(s.finallyBody, depth + 1); + } + break; + } + } +} + +const step = arrayStep("array"); +console.log(step.label); +const stmts = step.build({ kind: "str", text: "items" }, (value) => [ + { kind: "exprStmt", expr: value }, + { kind: "varDecl", name: "done", init: null }, +]); +print(stmts, 0); +console.log(stmts.length, JSON.stringify(stmts[2])); +for (const source of [{ kind: "num", value: 1 }, ref("x")] satisfies Expr[]) { + const head = step.first(source); + console.log(head.kind, "name" in head ? head.name : "", JSON.stringify(head)); +} diff --git a/tests/harness/corpus-costs/6022-width-lift-discriminant.ts.json b/tests/harness/corpus-costs/6022-width-lift-discriminant.ts.json new file mode 100644 index 000000000..c0f25a6f7 --- /dev/null +++ b/tests/harness/corpus-costs/6022-width-lift-discriminant.ts.json @@ -0,0 +1 @@ +{ "plain": 3800, "san": 7400 } diff --git a/tests/harness/corpus-costs/6024-object-literal-method-slot-return.ts.json b/tests/harness/corpus-costs/6024-object-literal-method-slot-return.ts.json new file mode 100644 index 000000000..80c95a135 --- /dev/null +++ b/tests/harness/corpus-costs/6024-object-literal-method-slot-return.ts.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 4400 } diff --git a/tests/harness/native-frontend.ts b/tests/harness/native-frontend.ts index 475db54aa..048a08251 100644 --- a/tests/harness/native-frontend.ts +++ b/tests/harness/native-frontend.ts @@ -20,6 +20,7 @@ import { moduleUsesBigInt, moduleUsesCopying, moduleUsesDynInvoke, + moduleUsesDynAsync, moduleUsesInspect, moduleUsesRegex, moduleUsesSymbol, @@ -65,7 +66,13 @@ export const programs = [ "nullish-long-chain.ts", "class-array-optional-return.ts", "contextual-array-union-write.ts", + "2391-default-builtin-imports/main.ts", + "3055-default-builtin-nodenext/main.ts", + "5854-array-from-async.ts", "6020-class-extends-function-constructor.js", + "5585-record-results-inlined.ts", + "2631-create-require/main.ts", + "4970-int32-call-abi.ts", ]; interface LowerRequest { @@ -224,6 +231,7 @@ export async function checkProgram(frontend: NativeFrontend, file: string): Prom copying: moduleUsesCopying(module), inspect: moduleUsesInspect(module), dynInvoke: moduleUsesDynInvoke(module), + dynAsync: moduleUsesDynAsync(module), symbol: moduleUsesSymbol(module), bigint: moduleUsesBigInt(module), zlib: moduleUsesZlib(module),