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 6e4d3aba1..faa550fdb 100644 --- a/packages/compiler/src/frontend/lowering/call-signatures.ts +++ b/packages/compiler/src/frontend/lowering/call-signatures.ts @@ -1058,9 +1058,21 @@ 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. A literal under `satisfies` keeps its own type (the + // constraint is only checked), so its methods keep their inferred + // returns: callers of the value see those, not the constraint's. + const literalMethod = + ts.isMethodDeclaration(node) && + ts.isObjectLiteralExpression(node.parent) && + !node.asteriskToken && + node.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) !== true && + !isSatisfiesOperand(node.parent); 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 @@ -1068,7 +1080,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)) : []; @@ -1136,6 +1151,7 @@ export function lambdaSignature( const inferredLiteral = literalSlot !== undefined && (literalType === null || literalType.kind === "dyn"); if ( + !literalMethod && !isAsyncLike && !node.type && !inferredLiteral && @@ -1151,3 +1167,35 @@ export function lambdaSignature( const funcType = funcTypeFromParamShapes(shapes, ret); return { shapes, funcType }; } + +/** Whether the object literal `literal` is the operand of a `satisfies` + * expression, directly or as a property value of literals that are. */ +function isSatisfiesOperand(literal: ts.Node): boolean { + let n: ts.Node | undefined = literal; + while (n !== undefined) { + const parent: ts.Node | undefined = n.parent; + if (parent === undefined) return false; + if (ts.isParenthesizedExpression(parent)) n = parent; + else if (ts.isPropertyAssignment(parent) && parent.initializer === n) n = parent.parent; + else return ts.isSatisfiesExpression(parent) && parent.expression === n; + } + return false; +} + +/** 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/string-index-bounds.ts b/packages/compiler/src/frontend/lowering/string-index-bounds.ts index 6e422b231..af1b00ec6 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/program.ts b/packages/compiler/src/frontend/program.ts index 218a31116..db43b7749 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -2121,7 +2121,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 928b2bb58..cc867d208 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, SourceFileMetadata } from "./semantic-types.js"; +import type { Diagnostic, SourceFileMetadata } 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, @@ -45,8 +49,9 @@ import { packageShimDeclarationText } from "../package-shims.js"; /** 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 @@ -288,7 +293,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 87b97d25e..c58588f6d 100644 --- a/packages/compiler/src/frontend/ts7/session.ts +++ b/packages/compiler/src/frontend/ts7/session.ts @@ -18,6 +18,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[]; @@ -104,7 +122,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); @@ -160,7 +178,7 @@ export class Ts7SessionSnapshot { private disposed = false; constructor( - data: Ts7SnapshotData, + data: Ts7ReportedSnapshotData, private readonly session: Ts7Session, readonly paths: Ts7Paths, ) { @@ -189,7 +207,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, }); @@ -215,12 +233,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; @@ -261,7 +279,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..f1756ea7c --- /dev/null +++ b/tests/corpus/6024-object-literal-method-slot-return.ts @@ -0,0 +1,130 @@ +// 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)); +} + +// A literal under `satisfies` keeps its own type, so its methods keep their +// inferred returns: callers read the fields the constraint does not name. +interface Point { + x: number; +} +const located = { + point() { + return { x: 1, y: 2 }; + }, + rows() { + return [{ x: 3, label: "three" }]; + }, +} satisfies { point(): Point; rows(): Point[] }; +const point = located.point(); +console.log(point.x, point.y, located.rows()[0].label); 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 bcd1d6ae0..30b3e336d 100644 --- a/tests/harness/native-frontend.ts +++ b/tests/harness/native-frontend.ts @@ -56,7 +56,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", // Re-chosen key orders rebuild the inspect helpers after lowering. "key-order-late-optional.ts", "5888-inspect-sorted.ts",