feat(compiler): statically compile the TypeScript semantic client

- Replace SDK semantic objects with native type, symbol, and signature registries.
- Route production checker queries through the owned client while preserving identity and snapshot lifetimes.
- Support scalar BigInt conversion and class union checks needed by native semantic decoding.
This commit is contained in:
Chris Tate
2026-09-28 01:39:10 -05:00
committed by GitHub
parent cac0a658f6
commit 4682d4cf5b
18 changed files with 1934 additions and 145 deletions
@@ -3804,24 +3804,11 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
lowerer.noLowering(`BigInt with ${expr.arguments.length} arguments`, expr);
}
const argNode = expr.arguments[0]!;
const arg = lowerer.lowerExpr(argNode);
if (arg.type.kind === "bigint") return arg;
if (arg.type.kind === "string") {
return { kind: "libCall", fn: "bigint.parse", args: [arg], type: BIGINT_T, loc };
}
if (arg.type.kind === "f64") {
return { kind: "libCall", fn: "bigint.fromF64", args: [arg], type: BIGINT_T, loc };
}
if (arg.type.kind === "bool") {
return {
kind: "ternary",
cond: arg,
then: { kind: "libCall", fn: "bigint.parse", args: [{ kind: "strLit", value: "1", type: STRING, loc }], type: BIGINT_T, loc },
else_: { kind: "libCall", fn: "bigint.parse", args: [{ kind: "strLit", value: "0", type: STRING, loc }], type: BIGINT_T, loc },
type: BIGINT_T,
loc,
};
}
// Captured optional storage can change after TypeScript's guard.
// BigInt consumes the stored value, including its nullish tag.
const arg = optionalCallValue(lowerer, argNode) ?? lowerer.lowerExpr(argNode);
const converted = lowerBigIntConstructorValue(lowerer, arg, loc);
if (converted) return converted;
lowerer.noLowering(`BigInt of ${lowerer.fmt(arg.type)} values`, argNode, "string, number, boolean, and bigint arguments are supported");
}
@@ -5098,6 +5085,56 @@ function lowerNumberConstructorValue(lowerer: Lowerer, argNode: ts.Expression, l
);
}
/** A runtime-optional local retains its full tagged storage even after a
* checker guard. Dispatch BigInt by the actual value, including nullish
* TypeErrors, rather than interpreting that storage as the narrowed arm. */
function lowerBigIntConstructorValue(lowerer: Lowerer, arg: IrExpr, loc: SrcLoc): IrExpr | null {
if (arg.type.kind === "bigint") return arg;
if (arg.type.kind === "string") return { kind: "libCall", fn: "bigint.parse", args: [arg], type: BIGINT_T, loc };
if (arg.type.kind === "f64") return { kind: "libCall", fn: "bigint.fromF64", args: [arg], type: BIGINT_T, loc };
if (arg.type.kind === "bool") {
return {
kind: "ternary", cond: arg,
then: { kind: "libCall", fn: "bigint.parse", args: [{ kind: "strLit", value: "1", type: STRING, loc }], type: BIGINT_T, loc },
else_: { kind: "libCall", fn: "bigint.parse", args: [{ kind: "strLit", value: "0", type: STRING, loc }], type: BIGINT_T, loc },
type: BIGINT_T, loc,
};
}
if (isUnitType(arg.type)) {
const name = arg.type.kind === "nullT" ? "null" : "undefined";
return defaultAfterUndefined(arg, nodeThrowExpr(1, "", `Cannot convert ${name} to a BigInt`, BIGINT_T, loc));
}
if (arg.type.kind !== "union") return null;
const unionId = arg.type.unionId;
const arms = lowerer.unions.get(unionId)?.arms;
if (!arms || !arms.every((arm) => arm.kind === "bigint" || arm.kind === "string" || arm.kind === "f64" || arm.kind === "bool" || isUnitType(arm))) return null;
const key = `bigint.scalar:${unionId}`;
let helper = lowerer.widthHelpers.get(key);
if (!helper) {
helper = `%bigint.scalar.${lowerer.widthHelpers.size}`;
lowerer.widthHelpers.set(key, helper);
const value = varRef("value.0", arg.type, loc);
const body: IrStmt[] = [];
arms.forEach((arm, tag) => {
const narrowed: IrExpr = isUnitType(arm)
? { kind: "unitLit", unit: arm.kind === "nullT" ? "null" : "undefined", type: arm, loc }
: { kind: "unionNarrow", unionId, tag, value, type: arm, loc };
const converted = lowerBigIntConstructorValue(lowerer, narrowed, loc);
if (!converted) throw new InternalCompilerError("BigInt scalar union has an unsupported arm");
const ret: IrStmt = { kind: "return", value: converted, loc };
body.push(tag === arms.length - 1 ? ret : {
kind: "if", cond: { kind: "unionIsTag", unionId, tag, negated: false, value, type: BOOL, loc },
then: [ret], else_: null, loc,
});
});
lowerer.liftedFns.push({
name: helper, params: [{ localId: "value.0", name: "value", type: arg.type }],
returnType: BIGINT_T, locals: [{ id: "value.0", name: "value", type: arg.type, mutable: false }], body, loc,
});
}
return { kind: "call", callee: helper, args: [arg], type: BIGINT_T, loc };
}
function immediatePrimitiveWrapperToString(lowerer: Lowerer, node: ts.Expression): IrExpr | null {
const stringValue = stringWrapperToString(lowerer, node);
if (stringValue) return stringValue;
@@ -2502,15 +2502,17 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
}
const tag = lowerer.armTag(expr.type.unionId, narrowed);
if (tag < 0) {
// A proven-present optional base class can narrow further to a
// subclass. The payload is still stored under the base class tag;
// unwrap it before applying the ordinary class downcast.
// A base-class arm can narrow to a subclass after instanceof. The
// payload retains its base-class tag even when the union also has
// records or other classes. Require one unambiguous containing arm.
const arms = lowerer.unions.get(expr.type.unionId)?.arms ?? [];
const valueTag = arms.findIndex((arm) => !isUnitType(arm));
const valueTag = narrowed.kind === "object"
? arms.findIndex((arm) => arm.kind === "object" && lowerer.isSubclassOf(narrowed.className, arm.className))
: -1;
const valueType = arms[valueTag];
if (
narrowed.kind === "object" && valueType?.kind === "object" &&
arms.every((arm, i) => i === valueTag || isUnitType(arm)) &&
arms.every((arm, i) => i === valueTag || arm.kind !== "object" || !lowerer.isSubclassOf(narrowed.className, arm.className)) &&
lowerer.isSubclassOf(narrowed.className, valueType.className)
) {
return {
@@ -7987,6 +7989,42 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
`'instanceof ${target.def.name.replace(/^%/, "")}' on 'unknown' values (only the Error classes answer — test 'instanceof Error' and read '.name')`,
);
}
if (left.type.kind === "union") {
const unionId = left.type.unionId;
const arms = lowerer.unions.get(unionId)?.arms;
// Ordinary records and scalar values do not carry a class prototype.
// Dynamic/opaque runtime objects keep their existing instanceof fences.
if (arms?.every((arm) => arm.kind === "object" || arm.kind === "record" ||
arm.kind === "f64" || arm.kind === "string" || arm.kind === "bool" ||
arm.kind === "bigint" || arm.kind === "symbol" || isUnitType(arm))) {
const key = `instanceof.union:${unionId}:${target.def.name}`;
let helper = lowerer.widthHelpers.get(key);
if (!helper) {
helper = `%instanceof.union.${lowerer.widthHelpers.size}`;
lowerer.widthHelpers.set(key, helper);
const value: IrExpr = { kind: "varRef", localId: "value.0", type: left.type, loc };
const body: IrStmt[] = [];
arms.forEach((arm, tag) => {
if (arm.kind !== "object") return;
const lhsInfo = lowerer.classes.get(arm.className);
if (!lhsInfo) throw new InternalCompilerError(`lowerer bug: unknown class ${arm.className}`);
const result: IrExpr = lowerer.inHierarchy(lhsInfo) && lowerer.inHierarchy(target)
? { kind: "instanceOf", value: { kind: "unionNarrow", unionId, tag, value, type: arm, loc }, className: target.def.name, type: BOOL, loc }
: { kind: "boolLit", value: arm.className === target.def.name || lowerer.isSubclassOf(arm.className, target.def.name), type: BOOL, loc };
body.push({
kind: "if", cond: { kind: "unionIsTag", unionId, tag, negated: false, value, type: BOOL, loc },
then: [{ kind: "return", value: result, loc }], else_: null, loc,
});
});
body.push({ kind: "return", value: { kind: "boolLit", value: false, type: BOOL, loc }, loc });
lowerer.liftedFns.push({
name: helper, params: [{ localId: "value.0", name: "value", type: left.type }], returnType: BOOL,
locals: [{ id: "value.0", name: "value", type: left.type, mutable: false }], body, loc,
});
}
return { kind: "call", callee: helper, args: [left], type: BOOL, loc };
}
}
if (left.type.kind !== "object") {
lowerer.unsupported(
"SC1090",
+28 -106
View File
@@ -1,25 +1,19 @@
/** Transitional SDK boundary. The AST itself is the same concrete model
* used by the native client. The SDK still owns Type/Symbol/Signature
* registries, diagnostics, and snapshots; node-bearing queries below send
* our handles directly, without manufacturing SDK RemoteNode objects. */
import { Checker, NodeHandle, Program } from "typescript/unstable/sync";
import type { DocumentIdentifier, DocumentPosition, Project, ReferencedSymbolEntry, Signature, SignatureUsage, Symbol as TsSymbol, Type } from "typescript/unstable/sync";
* used by the native client. The SDK still owns program metadata and
* snapshots. ASTs, checker queries, and semantic identities are owned here;
* the casts retain the frontend's existing type-only interface surface. */
import { Program } from "typescript/unstable/sync";
import type { Checker, DocumentIdentifier, Project, Snapshot } from "typescript/unstable/sync";
import { resolveFileName } from "typescript/unstable/proto";
import type { SignatureResponse, SymbolResponse, TypeResponse } from "typescript/unstable/proto";
import type { Node, Path, SourceFile, TypeNode } from "typescript/unstable/ast";
import type { NodeBuilderFlags, SymbolFlags, SyntaxKind } from "./enums.js";
import type { Path, SourceFile } from "typescript/unstable/ast";
import { AstFile, AstNode } from "./ast-node.js";
import { SemanticChecker } from "./semantic-checker.js";
import { SemanticSnapshot, type SemanticTransport } from "./semantic-model.js";
type SdkClient = ConstructorParameters<typeof Checker>[2];
type Registry = ConstructorParameters<typeof Checker>[3];
type Cache = ConstructorParameters<typeof Program>[3];
type CanonicalPath = (fileName: string) => Path;
export function astNodeId(node: Node): string {
if (!(node instanceof AstNode)) throw new Error("TypeScript query requires a scriptc AST node");
return node.id;
}
function decode(bytes: Uint8Array, timing?: ReturnType<SdkClient["getTimingCollector"]>): AstFile {
return new AstFile(bytes, (nodes, pos, end) => {
// The SDK's NodeArray interface adds properties to an ordinary array.
@@ -52,98 +46,26 @@ class NativeAstProgram extends Program {
}
}
class NativeAstChecker extends Checker {
constructor(
private readonly astSnapshot: number,
private readonly astProject: Project,
private readonly astClient: SdkClient,
private readonly astRegistry: Registry,
) { super(astSnapshot, astProject, astClient, astRegistry); }
private request<T>(method: string, params: object): T {
return this.astClient.apiRequest<T>(method, { snapshot: this.astSnapshot, project: this.astProject.id, ...params });
/** Install every project before publishing a snapshot: shared symbols may
* name another project's canonical context on their very first response. */
export function installNativeAst(snapshot: Snapshot, client: SdkClient, cache: Cache, canonical: CanonicalPath, transport: SemanticTransport): () => void {
const semantic = new SemanticSnapshot(snapshot.id, transport);
const projects = snapshot.getProjects();
for (const project of projects) {
const target = project as unknown as { program: Program; checker: Checker };
target.program = new NativeAstProgram(snapshot.id, project, client, cache, canonical);
semantic.addProject(project.id, (path) => {
const file = project.program.getSourceFile(path);
if (file === undefined) return undefined;
if (!(file instanceof AstNode)) throw new Error("TypeScript semantic query requires a scriptc AST");
return file;
});
}
private nodeType(method: string, node: Node): Type | undefined {
const data = this.request<TypeResponse | null>(method, { location: astNodeId(node) });
return data === null ? undefined : this.astRegistry.getOrCreateType(data);
}
private nodeSymbol(method: string, node: Node): TsSymbol | undefined {
const data = this.request<SymbolResponse | null>(method, { location: astNodeId(node) });
return data === null ? undefined : this.astRegistry.getOrCreateSymbol(data);
}
private nodeSignature(method: string, node: Node): Signature | undefined {
const data = this.request<SignatureResponse | null>(method, { location: astNodeId(node) });
return data === null ? undefined : this.astRegistry.getOrCreateSignature(data);
}
override getSymbolAtLocation(node: Node): TsSymbol | undefined;
override getSymbolAtLocation(nodes: readonly Node[]): (TsSymbol | undefined)[];
override getSymbolAtLocation(input: Node | readonly Node[]): TsSymbol | undefined | (TsSymbol | undefined)[] {
if (!Array.isArray(input)) return this.nodeSymbol("getSymbolAtLocation", input as Node);
const data = this.request<(SymbolResponse | null)[]>("getSymbolsAtLocations", { locations: input.map(astNodeId) });
return data.map((value) => value === null ? undefined : this.astRegistry.getOrCreateSymbol(value));
}
override getTypeAtLocation(node: Node): Type | undefined;
override getTypeAtLocation(nodes: readonly Node[]): (Type | undefined)[];
override getTypeAtLocation(input: Node | readonly Node[]): Type | undefined | (Type | undefined)[] {
if (!Array.isArray(input)) return this.nodeType("getTypeAtLocation", input as Node);
const data = this.request<(TypeResponse | null)[]>("getTypeAtLocations", { locations: input.map(astNodeId) });
return data.map((value) => value === null ? undefined : this.astRegistry.getOrCreateType(value));
}
override getResolvedSignature(node: Node): Signature | undefined { return this.nodeSignature("getResolvedSignature", node); }
override getSignatureFromDeclaration(node: Node): Signature | undefined { return this.nodeSignature("getSignatureFromDeclaration", node); }
override getContextualType(node: Node): Type | undefined { return this.nodeType("getContextualType", node); }
override getTypeFromTypeNode(node: Node): Type | undefined { return this.nodeType("getTypeFromTypeNode", node); }
override getShorthandAssignmentValueSymbol(node: Node): TsSymbol | undefined { return this.nodeSymbol("getShorthandAssignmentValueSymbol", node); }
override getExportSpecifierLocalTargetSymbol(node: Node): TsSymbol | undefined { return this.nodeSymbol("getExportSpecifierLocalTargetSymbol", node); }
override getConstantValue(node: Node): string | number | undefined {
return this.request<string | number | null>("getConstantValue", { location: astNodeId(node) }) ?? undefined;
}
override isContextSensitive(node: Node): boolean {
return this.request<boolean>("isContextSensitive", { location: astNodeId(node) });
}
override getTypeOfSymbolAtLocation(symbol: TsSymbol, location: Node): Type {
const data = this.request<TypeResponse | null>("getTypeOfSymbolAtLocation", { symbol: symbol.id, location: astNodeId(location) });
if (data === null) throw new Error(`getTypeOfSymbolAtLocation returned no type for symbol ${symbol.id}`);
return this.astRegistry.getOrCreateType(data);
}
override typeToString(type: Type, enclosingDeclaration?: Node, flags?: number): string {
return this.request<string>("typeToString", { type: type.id, location: enclosingDeclaration === undefined ? undefined : astNodeId(enclosingDeclaration), flags });
}
override resolveName(name: string, meaning: SymbolFlags, location?: Node | DocumentPosition, excludeGlobals?: boolean): TsSymbol | undefined {
const node = location !== undefined && "kind" in location ? location : undefined;
const position = location !== undefined && "document" in location ? location : undefined;
const data = this.request<SymbolResponse | null>("resolveName", { name, meaning, location: node === undefined ? undefined : astNodeId(node), file: position?.document, position: position?.position, excludeGlobals });
return data === null ? undefined : this.astRegistry.getOrCreateSymbol(data);
}
override getReferencedSymbolsForNode(node: Node, position: number): ReferencedSymbolEntry[] {
const data = this.request<{ definition: string; symbol?: SymbolResponse; references?: string[] }[] | null>("getReferencedSymbolsForNode", { node: astNodeId(node), position });
return (data ?? []).map((entry) => ({
definition: new NodeHandle(entry.definition, this.astProject),
symbol: entry.symbol === undefined ? undefined : this.astRegistry.getOrCreateSymbol(entry.symbol),
references: (entry.references ?? []).map((handle) => new NodeHandle(handle, this.astProject)),
}));
}
override getSignatureUsage(signatureDecl: Node): SignatureUsage[] {
const data = this.request<{ name: string; call?: string }[] | null>("getSignatureUsages", { signatureDecl: astNodeId(signatureDecl) });
return (data ?? []).map((entry) => ({ name: new NodeHandle(entry.name, this.astProject), call: entry.call === undefined ? undefined : new NodeHandle(entry.call, this.astProject) }));
}
override typeToTypeNode(type: Type, enclosingDeclaration?: Node, flags?: number): TypeNode | undefined {
const bytes = this.astClient.apiRequestBinary("typeToTypeNode", { snapshot: this.astSnapshot, project: this.astProject.id, type: type.id, location: enclosingDeclaration === undefined ? undefined : astNodeId(enclosingDeclaration), flags });
return bytes === undefined ? undefined : decode(bytes).root as unknown as TypeNode;
}
override signatureToSignatureDeclaration(signature: Signature, kind: SyntaxKind, enclosingDeclaration?: Node, flags?: NodeBuilderFlags): Node | undefined {
const bytes = this.astClient.apiRequestBinary("signatureToSignatureDeclaration", { snapshot: this.astSnapshot, project: this.astProject.id, signature: signature.id, kind, location: enclosingDeclaration === undefined ? undefined : astNodeId(enclosingDeclaration), flags });
return bytes === undefined ? undefined : decode(bytes).root as unknown as Node;
for (const project of projects) {
// The frontend's structural Type/Node interfaces are a type-only bridge.
// No native object is copied, adapted, or inserted in an SDK registry.
const target = project as unknown as { checker: Checker };
target.checker = new SemanticChecker(semantic.getProject(project.id)) as unknown as Checker;
}
}
/** Wire the AST boundary before publishing a new snapshot. The registry's
* canonical Project object remains the same, so declaration handles resolve
* through this program and return the exact nodes used by frontend walks. */
export function installNativeAst(snapshot: number, project: Project, client: SdkClient, cache: Cache, canonical: CanonicalPath): void {
const registry = (project.checker as unknown as { objectRegistry: Registry }).objectRegistry;
const target = project as unknown as { program: Program; checker: Checker };
target.program = new NativeAstProgram(snapshot, project, client, cache, canonical);
target.checker = new NativeAstChecker(snapshot, project, client, registry);
return () => { semantic.dispose(); };
}
+18 -6
View File
@@ -7,7 +7,7 @@ import { registerTs7FileSystem, TS7_FILE_SYSTEM_CALLBACKS, type Ts7FileSystem }
import { spawnTs7Wire } from "./rpc-process.js";
import { installNativeAst } from "./ast-sdk.js";
// The pinned SDK still owns snapshots and checker object identity.
// The pinned SDK still owns snapshots and program metadata.
// These hidden helpers have no public package exports. Derive their types
// from Snapshot's constructor rather than duplicating the SDK's contracts.
// This bridge can go away when those object models also compile statically;
@@ -51,11 +51,16 @@ class SnapshotClient {
}
apiRequestBinary(method: string, params: unknown): Uint8Array | undefined {
const payload = Buffer.from(JSON.stringify(params), "utf8");
const result = this.requestEncoded(method, JSON.stringify(params));
return result.length === 0 ? undefined : result;
}
requestEncoded(method: string, json: string): Uint8Array {
const payload = Buffer.from(json, "utf8");
const start = performance.now();
const result = this.rpc.requestBytes(method, payload);
this.timing?.record({ method, roundTripMs: performance.now() - start, bytesSent: payload.length, bytesReceived: result.length });
return result.length === 0 ? undefined : result;
return result;
}
getTimingCollector(): TimingCollector | undefined {
@@ -71,8 +76,8 @@ class SnapshotClient {
}
/** The API operations scriptc uses, backed by its own synchronous client.
* Keep upstream Snapshot/Project objects until their checker registries
* have a native replacement. ASTs use the owned model. No upstream API
* Keep upstream Snapshot/Project objects for lifecycle and metadata. ASTs
* and checker registries use concrete native models. No upstream API
* or Client instance is constructed, and all requests use Ts7RpcClient. */
export class Ts7Api {
private readonly client: SnapshotClient;
@@ -116,10 +121,17 @@ export class Ts7Api {
// The upstream class's private fields make its Client nominal. Only
// these three public operations are used by Snapshot and its children.
const snapshot = new Snapshot(data, this.client as unknown as SdkClient, this.cache, canonical, () => {
releaseSemantic();
this.snapshots.delete(snapshot);
if (snapshot !== this.latest) this.cache.releaseSnapshot(snapshot.id);
});
for (const project of snapshot.getProjects()) installNativeAst(snapshot.id, project, this.client as unknown as SdkClient, this.cache, canonical);
const releaseSemantic = installNativeAst(snapshot, this.client as unknown as SdkClient, this.cache, canonical, {
text: (method, payload) => {
const bytes = this.client.requestEncoded(method, payload);
return bytes.length === 0 ? "null" : Buffer.from(bytes).toString("utf8");
},
binary: (method, payload) => this.client.requestEncoded(method, payload),
});
this.latest = snapshot;
this.snapshots.add(snapshot);
return snapshot;
@@ -0,0 +1,288 @@
import type { SemanticIndexInfoData as IndexInfoResponse, SemanticSignatureData as SignatureResponse, SemanticSymbolData as SymbolResponse, SemanticTypePredicateData as TypePredicateResponse, SemanticTypeData as TypeResponse } from "./semantic-schema.generated.js";
import { AstFile, AstNode } from "./ast-node.js";
import { SemanticNodeHandle, SemanticProject, SemanticSignature, SemanticSymbol, SemanticType, type SemanticDocument, type SemanticQuery } from "./semantic-model.js";
import { SemanticSymbolFlags } from "./semantic-schema.generated.js";
export interface SemanticIndexInfo {
keyType: SemanticType;
valueType: SemanticType;
isReadonly: boolean;
declaration: SemanticNodeHandle | undefined;
}
export interface SemanticTypePredicate {
kind: number;
parameterIndex: number;
parameterName: string | undefined;
type: SemanticType | undefined;
}
export interface SemanticReferencedSymbol {
definition: SemanticNodeHandle;
symbol: SemanticSymbol | undefined;
references: SemanticNodeHandle[];
}
export interface SemanticSignatureUsage {
name: SemanticNodeHandle;
call: SemanticNodeHandle | undefined;
}
export interface SemanticJsDocTag {
name: string;
text?: string;
}
interface WellKnownSymbols { unknown: number; undefined: number; arguments: number; }
export interface SemanticCompletionOptions {
triggerCharacter?: string;
includeSymbol?: boolean;
}
interface CompletionResponse {
isIncomplete: boolean;
entries: {
name: string;
kind?: number;
sortText?: string;
insertText?: string;
filterText?: string;
detail?: string;
labelDetails?: { detail?: string; description?: string };
symbol?: SymbolResponse;
}[];
}
export interface SemanticCompletion {
name: string;
kind: number | undefined;
sortText: string | undefined;
insertText: string | undefined;
filterText: string | undefined;
detail: string | undefined;
labelDetails: { detail?: string; description?: string } | undefined;
symbol: SemanticSymbol | undefined;
}
/** The pinned checker's request surface over native objects. Every query
* converges on the same snapshot/project registries, including batch and
* lazy-object queries. No SDK object is constructed or consulted here. */
export class SemanticChecker {
private wellKnown: WellKnownSymbols | undefined;
constructor(readonly project: SemanticProject) {}
private type(method: string, query: SemanticQuery): SemanticType | undefined {
const data = this.project.request<TypeResponse | null>(method, query);
return data === null ? undefined : this.project.type(data);
}
private symbol(method: string, query: SemanticQuery): SemanticSymbol | undefined {
const data = this.project.request<SymbolResponse | null>(method, query);
return data === null ? undefined : this.project.symbol(data);
}
private signature(method: string, query: SemanticQuery): SemanticSignature | undefined {
const data = this.project.request<SignatureResponse | null>(method, query);
return data === null ? undefined : this.project.signature(data);
}
private types(method: string, query: SemanticQuery): SemanticType[] {
const data = this.project.request<TypeResponse[] | null>(method, query);
return (data ?? []).map((item) => this.project.type(item));
}
private symbols(method: string, query: SemanticQuery): SemanticSymbol[] {
const data = this.project.request<SymbolResponse[] | null>(method, query);
return (data ?? []).map((item) => this.project.symbol(item));
}
private optionalTypes(method: string, query: SemanticQuery): (SemanticType | undefined)[] {
const data = this.project.request<(TypeResponse | null)[]>(method, query);
return data.map((item) => item === null ? undefined : this.project.type(item));
}
private optionalSymbols(method: string, query: SemanticQuery): (SemanticSymbol | undefined)[] {
const data = this.project.request<(SymbolResponse | null)[]>(method, query);
return data.map((item) => item === null ? undefined : this.project.symbol(item));
}
getTypeAtLocation(node: AstNode): SemanticType | undefined;
getTypeAtLocation(nodes: readonly AstNode[]): (SemanticType | undefined)[];
getTypeAtLocation(input: AstNode | readonly AstNode[]): SemanticType | undefined | (SemanticType | undefined)[] {
if (Array.isArray(input)) return this.optionalTypes("getTypeAtLocations", { locations: (input as readonly AstNode[]).map((node) => node.id) });
return this.type("getTypeAtLocation", { location: (input as AstNode).id });
}
getSymbolAtLocation(node: AstNode): SemanticSymbol | undefined;
getSymbolAtLocation(nodes: readonly AstNode[]): (SemanticSymbol | undefined)[];
getSymbolAtLocation(input: AstNode | readonly AstNode[]): SemanticSymbol | undefined | (SemanticSymbol | undefined)[] {
if (Array.isArray(input)) return this.optionalSymbols("getSymbolsAtLocations", { locations: (input as readonly AstNode[]).map((node) => node.id) });
return this.symbol("getSymbolAtLocation", { location: (input as AstNode).id });
}
getTypeAtPosition(file: SemanticDocument, position: number): SemanticType | undefined;
getTypeAtPosition(file: SemanticDocument, positions: readonly number[]): (SemanticType | undefined)[];
getTypeAtPosition(file: SemanticDocument, positions: number | readonly number[]): SemanticType | undefined | (SemanticType | undefined)[] {
return typeof positions === "number"
? this.type("getTypeAtPosition", { file, position: positions })
: this.optionalTypes("getTypesAtPositions", { file, positions });
}
getSymbolAtPosition(file: SemanticDocument, position: number): SemanticSymbol | undefined;
getSymbolAtPosition(file: SemanticDocument, positions: readonly number[]): (SemanticSymbol | undefined)[];
getSymbolAtPosition(file: SemanticDocument, positions: number | readonly number[]): SemanticSymbol | undefined | (SemanticSymbol | undefined)[] {
return typeof positions === "number"
? this.symbol("getSymbolAtPosition", { file, position: positions })
: this.optionalSymbols("getSymbolsAtPositions", { file, positions });
}
getTypeOfSymbol(symbol: SemanticSymbol): SemanticType | undefined;
getTypeOfSymbol(symbols: readonly SemanticSymbol[]): (SemanticType | undefined)[];
getTypeOfSymbol(input: SemanticSymbol | readonly SemanticSymbol[]): SemanticType | undefined | (SemanticType | undefined)[] {
if (Array.isArray(input)) return this.optionalTypes("getTypesOfSymbols", { symbols: (input as readonly SemanticSymbol[]).map((symbol) => symbol.id) });
return this.type("getTypeOfSymbol", { symbol: (input as SemanticSymbol).id });
}
getDeclaredTypeOfSymbol(symbol: SemanticSymbol): SemanticType {
const type = this.type("getDeclaredTypeOfSymbol", { symbol: symbol.id });
if (type === undefined) throw new Error(`getDeclaredTypeOfSymbol returned no type for symbol ${symbol.id}`);
return type;
}
getTypeOfSymbolAtLocation(symbol: SemanticSymbol, node: AstNode): SemanticType {
const type = this.type("getTypeOfSymbolAtLocation", { symbol: symbol.id, location: node.id });
if (type === undefined) throw new Error(`getTypeOfSymbolAtLocation returned no type for symbol ${symbol.id}`);
return type;
}
getContextualType(node: AstNode): SemanticType | undefined { return this.type("getContextualType", { location: node.id }); }
getTypeFromTypeNode(node: AstNode): SemanticType | undefined { return this.type("getTypeFromTypeNode", { location: node.id }); }
getShorthandAssignmentValueSymbol(node: AstNode): SemanticSymbol | undefined { return this.symbol("getShorthandAssignmentValueSymbol", { location: node.id }); }
getExportSpecifierLocalTargetSymbol(node: AstNode): SemanticSymbol | undefined { return this.symbol("getExportSpecifierLocalTargetSymbol", { location: node.id }); }
getResolvedSignature(node: AstNode): SemanticSignature | undefined { return this.signature("getResolvedSignature", { location: node.id }); }
getSignatureFromDeclaration(node: AstNode): SemanticSignature | undefined { return this.signature("getSignatureFromDeclaration", { location: node.id }); }
getReturnTypeOfSignature(signature: SemanticSignature): SemanticType | undefined { return this.type("getReturnTypeOfSignature", { signature: signature.id }); }
getRestTypeOfSignature(signature: SemanticSignature): SemanticType | undefined { return this.type("getRestTypeOfSignature", { signature: signature.id }); }
getParameterType(signature: SemanticSignature, index: number): SemanticType | undefined { return this.type("getParameterType", { signature: signature.id, index }); }
getBaseTypeOfLiteralType(type: SemanticType): SemanticType | undefined { return this.type("getBaseTypeOfLiteralType", { type: type.id }); }
getNonNullableType(type: SemanticType): SemanticType | undefined { return this.type("getNonNullableType", { type: type.id }); }
getWidenedType(type: SemanticType): SemanticType | undefined { return this.type("getWidenedType", { type: type.id }); }
getApparentType(type: SemanticType): SemanticType | undefined { return this.type("getApparentType", { type: type.id }); }
getConstraintOfTypeParameter(type: SemanticType): SemanticType | undefined { return this.type("getConstraintOfTypeParameter", { type: type.id }); }
getBaseConstraintOfType(type: SemanticType): SemanticType | undefined { return this.type("getBaseConstraintOfType", { type: type.id }); }
getBaseTypes(type: SemanticType): SemanticType[] { return this.types("getBaseTypes", { type: type.id }); }
getTypeArguments(type: SemanticType): SemanticType[] { return this.types("getTypeArguments", { type: type.id }); }
getPropertiesOfType(type: SemanticType): SemanticSymbol[] { return this.symbols("getPropertiesOfType", { type: type.id }); }
getPropertyOfType(type: SemanticType, name: string): SemanticSymbol | undefined { return this.symbol("getPropertyOfType", { type: type.id, name }); }
getExportsOfModule(symbol: SemanticSymbol): SemanticSymbol[] { return this.symbols("getExportsOfModule", { symbol: symbol.id }); }
getMemberInModuleExports(symbol: SemanticSymbol, name: string): SemanticSymbol | undefined { return this.symbol("getMemberInModuleExports", { symbol: symbol.id, name }); }
getImmediateAliasedSymbol(symbol: SemanticSymbol): SemanticSymbol | undefined { return this.symbol("getImmediateAliasedSymbol", { symbol: symbol.id }); }
getAliasedSymbol(symbol: SemanticSymbol): SemanticSymbol {
const result = this.symbol("getAliasedSymbol", { symbol: symbol.id });
if (result === undefined) throw new Error(`getAliasedSymbol returned no symbol for symbol ${symbol.id}`);
return result;
}
resolveName(name: string, meaning: number, location?: AstNode | { document: SemanticDocument; position: number }, excludeGlobals?: boolean): SemanticSymbol | undefined {
const node = location instanceof AstNode ? location : undefined;
const position = location !== undefined && !(location instanceof AstNode) ? location : undefined;
return this.symbol("resolveName", { name, meaning, location: node?.id, file: position?.document, position: position?.position, excludeGlobals });
}
getResolvedSymbol(node: AstNode): SemanticSymbol | undefined {
const text = node.text;
return text ? this.resolveName(text, SemanticSymbolFlags.Value | SemanticSymbolFlags.ExportValue, node) : undefined;
}
private intrinsic(method: string): SemanticType {
const result = this.type(method, {});
if (result === undefined) throw new Error(`${method} returned no intrinsic type`);
return result;
}
getAnyType(): SemanticType { return this.intrinsic("getAnyType"); }
getStringType(): SemanticType { return this.intrinsic("getStringType"); }
getNumberType(): SemanticType { return this.intrinsic("getNumberType"); }
getBooleanType(): SemanticType { return this.intrinsic("getBooleanType"); }
getVoidType(): SemanticType { return this.intrinsic("getVoidType"); }
getUndefinedType(): SemanticType { return this.intrinsic("getUndefinedType"); }
getNullType(): SemanticType { return this.intrinsic("getNullType"); }
getNeverType(): SemanticType { return this.intrinsic("getNeverType"); }
getUnknownType(): SemanticType { return this.intrinsic("getUnknownType"); }
getBigIntType(): SemanticType { return this.intrinsic("getBigIntType"); }
getESSymbolType(): SemanticType { return this.intrinsic("getESSymbolType"); }
typeToString(type: SemanticType, enclosingDeclaration?: AstNode, flags?: number): string {
return this.project.request<string>("typeToString", { type: type.id, location: enclosingDeclaration?.id, flags });
}
isContextSensitive(node: AstNode): boolean { return this.project.request<boolean>("isContextSensitive", { location: node.id }); }
isArrayType(type: SemanticType): boolean { return this.project.request<boolean>("isArrayType", { type: type.id }); }
isArrayLikeType(type: SemanticType): boolean { return this.project.request<boolean>("isArrayLikeType", { type: type.id }); }
isTupleType(type: SemanticType): boolean { return this.project.request<boolean>("isTupleType", { type: type.id }); }
isTypeAssignableTo(source: SemanticType, target: SemanticType): boolean {
return this.project.request<boolean>("isTypeAssignableTo", { source: source.id, target: target.id });
}
getConstantValue(node: AstNode): string | number | undefined {
return this.project.request<string | number | null>("getConstantValue", { location: node.id }) ?? undefined;
}
getSignaturesOfType(type: SemanticType, kind: number): SemanticSignature[] {
const data = this.project.request<SignatureResponse[]>("getSignaturesOfType", { type: type.id, kind });
return data.map((item) => this.project.signature(item));
}
getTypePredicateOfSignature(signature: SemanticSignature): SemanticTypePredicate | undefined {
const data = this.project.request<TypePredicateResponse | null>("getTypePredicateOfSignature", { signature: signature.id });
return data === null ? undefined : {
kind: data.kind, parameterIndex: data.parameterIndex, parameterName: data.parameterName,
type: data.type === undefined ? undefined : this.project.type(data.type),
};
}
getIndexInfosOfType(type: SemanticType): SemanticIndexInfo[] {
const data = this.project.request<IndexInfoResponse[] | null>("getIndexInfosOfType", { type: type.id });
return (data ?? []).map((item) => ({
keyType: this.project.type(item.keyType), valueType: this.project.type(item.valueType),
isReadonly: item.isReadonly ?? false,
declaration: item.declaration === undefined ? undefined : new SemanticNodeHandle(item.declaration, this.project),
}));
}
private getWellKnownSymbols(): WellKnownSymbols {
this.project.ensureActive();
if (this.wellKnown === undefined) this.wellKnown = this.project.request<WellKnownSymbols>("getWellKnownSymbols");
return this.wellKnown;
}
isUnknownSymbol(symbol: SemanticSymbol): boolean { return symbol.id === this.getWellKnownSymbols().unknown; }
isUndefinedSymbol(symbol: SemanticSymbol): boolean { return symbol.id === this.getWellKnownSymbols().undefined; }
isArgumentsSymbol(symbol: SemanticSymbol): boolean { return symbol.id === this.getWellKnownSymbols().arguments; }
getReferencesToSymbolInFile(file: SemanticDocument, symbol: SemanticSymbol): SemanticNodeHandle[] {
const data = this.project.request<string[] | null>("getReferencesToSymbolInFile", { file, symbol: symbol.id });
return (data ?? []).map((handle) => new SemanticNodeHandle(handle, this.project));
}
getReferencedSymbolsForNode(node: AstNode, position: number): SemanticReferencedSymbol[] {
const data = this.project.request<{ definition: string; symbol?: SymbolResponse; references?: string[] }[] | null>("getReferencedSymbolsForNode", { node: node.id, position });
return (data ?? []).map((item) => ({
definition: new SemanticNodeHandle(item.definition, this.project),
symbol: item.symbol === undefined ? undefined : this.project.symbol(item.symbol),
references: (item.references ?? []).map((handle) => new SemanticNodeHandle(handle, this.project)),
}));
}
getSignatureUsage(node: AstNode): SemanticSignatureUsage[] {
const data = this.project.request<{ name: string; call?: string }[] | null>("getSignatureUsages", { signatureDecl: node.id });
return (data ?? []).map((item) => ({
name: new SemanticNodeHandle(item.name, this.project),
call: item.call === undefined ? undefined : new SemanticNodeHandle(item.call, this.project),
}));
}
getJsDocTagsOfSymbol(symbol: SemanticSymbol): SemanticJsDocTag[] {
return this.project.request<SemanticJsDocTag[] | null>("getJsDocTags", { symbol: symbol.id }) ?? [];
}
getDocumentationCommentOfSymbol(symbol: SemanticSymbol): string {
return this.project.request<string>("getDocumentationComment", { symbol: symbol.id });
}
getCompletionsAtPosition(file: SemanticDocument, position: number, options?: SemanticCompletionOptions): { isIncomplete: boolean; entries: SemanticCompletion[] } | undefined {
const data = this.project.request<CompletionResponse | null>("getCompletionsAtPosition", { file, position, triggerCharacter: options?.triggerCharacter, includeSymbol: options?.includeSymbol });
if (data === null) return undefined;
return {
isIncomplete: data.isIncomplete,
entries: data.entries.map((item) => ({
name: item.name, kind: item.kind, sortText: item.sortText, insertText: item.insertText,
filterText: item.filterText, detail: item.detail, labelDetails: item.labelDetails,
symbol: item.symbol === undefined ? undefined : this.project.symbol(item.symbol),
})),
};
}
typeToTypeNode(type: SemanticType, enclosingDeclaration?: AstNode, flags?: number): AstNode | undefined {
const bytes = this.project.requestBinary("typeToTypeNode", { type: type.id, location: enclosingDeclaration?.id, flags });
return bytes.length === 0 ? undefined : new AstFile(bytes).root;
}
signatureToSignatureDeclaration(signature: SemanticSignature, kind: number, enclosingDeclaration?: AstNode, flags?: number): AstNode | undefined {
const bytes = this.project.requestBinary("signatureToSignatureDeclaration", { signature: signature.id, kind, location: enclosingDeclaration?.id, flags });
return bytes.length === 0 ? undefined : new AstFile(bytes).root;
}
dispose(): void { this.project.dispose(); }
}
@@ -0,0 +1,43 @@
import { expect, test } from "vitest";
import { checkSemanticJsonStrings, parseSemanticJson } from "./semantic-json.js";
test("semantic JSON preserves ordinary strings, escaped slashes, quotes, pairs and nulls", () => {
const values = ["", "\uFEFFhéllo 🌍", "\\ud800", 'quoted "\\ud800"', "\ud800\udc00", "\udbff\udfff", "\\", null];
for (const value of values) {
const json = JSON.stringify({ value });
expect(parseSemanticJson(json)).toEqual({ value });
expect(() => checkSemanticJsonStrings(json, false)).not.toThrow();
}
for (const json of ['"\\ud800\\udc00"', '"\\uDBFF\\uDFFF"', '{"\\uD800\\uDC00":"ok"}']) {
expect(() => checkSemanticJsonStrings(json, false)).not.toThrow();
expect(parseSemanticJson(json)).toEqual(JSON.parse(json));
}
});
test("all lone code units remain exact on Node and are refused at the native boundary", () => {
for (let unit = 0xd800; unit <= 0xdfff; unit++) {
const text = String.fromCharCode(unit);
for (const value of [text, { [text]: "key", value: text }, [text, "after"]]) {
const json = JSON.stringify(value);
expect(parseSemanticJson(json)).toEqual(value);
expect(() => checkSemanticJsonStrings(json, false)).toThrow("cannot preserve lone UTF-16 surrogates");
}
}
});
test("backslash parity and separated surrogate escapes cannot bypass the boundary", () => {
for (let count = 1; count <= 8; count++) {
const json = '"' + "\\".repeat(count) + 'ud800"';
if (count % 2 === 0) expect(() => checkSemanticJsonStrings(json, false)).not.toThrow();
else expect(() => checkSemanticJsonStrings(json, false)).toThrow("cannot preserve");
}
for (const json of ['"\\ud800x\\udc00"', '["\\ud800","\\udc00"]', '"\\ud800\\ud800"', '"\\udc00\\ud800"']) {
expect(() => checkSemanticJsonStrings(json, false)).toThrow("cannot preserve");
}
});
test("malformed JSON remains a parse error rather than an accepted response", () => {
for (const json of ['"\\uZZZZ"', '"\\uD8"', '"unclosed', '{"value":}', 'undefined']) {
expect(() => parseSemanticJson(json)).toThrow(SyntaxError);
}
});
@@ -0,0 +1,42 @@
/** Check the only JSON string form the native runtime cannot yet retain.
* JSON.parse itself validates grammar. A valid surrogate pair is preserved;
* a lone escape must not silently change a type literal or symbol name. */
export function checkSemanticJsonStrings(json: string, preservesSurrogates: boolean): void {
if (preservesSurrogates) return;
let inString = false;
for (let index = 0; index < json.length; index++) {
const code = json.charCodeAt(index);
if (code === 34) { inString = !inString; continue; }
if (!inString || code !== 92) continue;
if (json.charCodeAt(++index) !== 117) continue;
const unit = jsonHexUnit(json, index + 1);
index += 4;
if (unit < 0xd800 || unit > 0xdfff) continue;
if (unit <= 0xdbff && json.charCodeAt(index + 1) === 92 && json.charCodeAt(index + 2) === 117) {
const next = jsonHexUnit(json, index + 3);
if (next >= 0xdc00 && next <= 0xdfff) { index += 6; continue; }
}
throw new Error("TypeScript semantic response: runtime cannot preserve lone UTF-16 surrogates");
}
}
function jsonHexUnit(json: string, offset: number): number {
if (offset + 4 > json.length) return -1;
let value = 0;
for (let index = offset; index < offset + 4; index++) {
const code = json.charCodeAt(index);
const digit = code >= 48 && code <= 57 ? code - 48 : code >= 65 && code <= 70 ? code - 55 : code >= 97 && code <= 102 ? code - 87 : -1;
if (digit < 0) return -1;
value = value * 16 + digit;
}
return value;
}
// Capability-based so the boundary automatically disappears once native
// strings and JSON parsing support lossless UTF-16. Probe once per process.
const preservesJsonSurrogates = (JSON.parse('"\\ud800"') as string).charCodeAt(0) === 0xd800;
export function parseSemanticJson<T>(json: string): T {
checkSemanticJsonStrings(json, preservesJsonSurrogates);
return JSON.parse(json) as T;
}
@@ -0,0 +1,251 @@
import { describe, expect, test } from "vitest";
import type { SymbolResponse, TypeResponse } from "typescript/unstable/proto";
import { isBigIntLiteralType, isBooleanLiteralType, isClassOrInterfaceType, isConditionalType, isErrorType, isIndexType, isIndexedAccessType, isIntersectionType, isIntrinsicType, isLiteralType, isNumberLiteralType, isObjectType, isStringLiteralType, isStringMappingType, isSubstitutionType, isTemplateLiteralType, isTupleType, isTypeParameter, isTypeReference, isUnionType, type Type } from "typescript/unstable/sync";
import { SemanticNodeHandle, SemanticSnapshot, SemanticType } from "./semantic-model.js";
import { SemanticObjectFlags as ObjectFlags, SemanticSignatureFlags as SignatureFlags, SemanticTypeFlags as TypeFlags } from "./semantic-schema.generated.js";
function harness() {
const requests: { method: string; query: Record<string, unknown> }[] = [];
const answers = new Map<string, unknown>();
const snapshot = new SemanticSnapshot(17, {
text(method, payload) {
requests.push({ method, query: JSON.parse(payload) });
if (!answers.has(method)) throw new Error(`Unexpected request ${method}`);
return JSON.stringify(answers.get(method));
},
binary() { throw new Error("Unexpected binary request"); },
});
const first = snapshot.addProject("/first.json", () => undefined);
const second = snapshot.addProject("/second.json", () => undefined);
const symbol = (id: number, extra: Partial<SymbolResponse> = {}): SymbolResponse => ({
id, flags: 2, checkFlags: 0, name: `name${id}`, project: first.id, ...extra,
} as SymbolResponse);
return { snapshot, first, second, requests, answers, symbol };
}
test("types/signatures are project-local and symbols are snapshot-wide", () => {
const { snapshot, first, second, symbol } = harness();
const data = { id: 1, flags: TypeFlags.Number };
expect(first.type(data)).toBe(first.type({ ...data }));
expect(second.type(data)).not.toBe(first.type(data));
expect(first.signature({ id: 1, flags: 0 })).toBe(first.signature({ id: 1, flags: 0 }));
expect(second.signature({ id: 1, flags: 0 })).not.toBe(first.signature({ id: 1, flags: 0 }));
expect(second.symbol(symbol(1))).toBe(first.symbol(symbol(1)));
expect(second.symbol(symbol(1)).canonicalProject).toBe(first);
const separate = new SemanticSnapshot(snapshot.id, snapshot.transport);
const third = separate.addProject(first.id, () => undefined);
expect(third.symbol(symbol(1))).not.toBe(first.symbol(symbol(1)));
expect(third.type(data)).not.toBe(first.type(data));
});
test("canonical projects must exist before a symbol is registered", () => {
const { first, symbol, snapshot } = harness();
const missing = symbol(2, { project: "/later.json" as SymbolResponse["project"] });
expect(() => first.symbol(missing)).toThrow("Unknown TypeScript canonical project");
const later = snapshot.addProject("/later.json", () => undefined);
expect(first.symbol(missing).canonicalProject).toBe(later);
expect(() => snapshot.addProject(later.id, () => undefined)).toThrow("Duplicate TypeScript project");
});
test("absent handles and empty known lists do not issue requests", () => {
const { first, requests, symbol } = harness();
const type = first.type({ id: 1, flags: TypeFlags.Number });
expect(type.getSymbol()).toBeUndefined();
expect(type.getAliasSymbol()).toBeUndefined();
expect(type.getTarget()).toBeUndefined();
expect(type.getFreshType()).toBeUndefined();
expect(type.getRegularType()).toBeUndefined();
expect(type.getObjectType()).toBeUndefined();
expect(type.getIndexType()).toBeUndefined();
expect(type.getCheckType()).toBeUndefined();
expect(type.getExtendsType()).toBeUndefined();
expect(type.getBaseType()).toBeUndefined();
expect(type.getConstraint()).toBeUndefined();
expect(type.getTypes()).toBeUndefined();
expect(type.getBaseTypes()).toBeUndefined();
expect(type.getTypeParameters()).toEqual([]);
expect(type.getOuterTypeParameters()).toEqual([]);
expect(type.getLocalTypeParameters()).toEqual([]);
expect(type.getAliasTypeArguments()).toEqual([]);
const signature = first.signature({ id: 1, flags: 0 });
expect(signature.getTypeParameters()).toEqual([]);
expect(signature.getParameters()).toEqual([]);
expect(signature.getThisParameter()).toBeUndefined();
expect(signature.getTarget()).toBeUndefined();
const sym = first.symbol(symbol(1));
expect(sym.getParent()).toBeUndefined();
expect(sym.getExportSymbol()).toBe(sym);
expect(requests).toEqual([]);
});
describe("lazy type handles", () => {
const cases = [
["symbol", "getSymbolOfType", "getSymbol"],
["aliasSymbol", "getAliasSymbolOfType", "getAliasSymbol"],
["target", "getTargetOfType", "getTarget"],
["freshType", "getFreshTypeOfType", "getFreshType"],
["regularType", "getRegularTypeOfType", "getRegularType"],
["objectType", "getObjectTypeOfType", "getObjectType"],
["indexType", "getIndexTypeOfType", "getIndexType"],
["checkType", "getCheckTypeOfType", "getCheckType"],
["extendsType", "getExtendsTypeOfType", "getExtendsType"],
["baseType", "getBaseTypeOfType", "getBaseType"],
["substConstraint", "getConstraintOfType", "getConstraint"],
] as const;
for (const [field, method, accessor] of cases) {
test(`${accessor} resolves once and reuses the correct identity registry`, () => {
const { first, second, answers, requests, symbol } = harness();
const isSymbol = field === "symbol" || field === "aliasSymbol";
const data = isSymbol ? symbol(2) : { id: 2, flags: TypeFlags.String };
answers.set(method, data);
const type = first.type({ id: 1, flags: 0, [field]: 2 });
const result = type[accessor]();
expect(type[accessor]()).toBe(result);
expect(requests).toEqual([{ method, query: { snapshot: 17, project: first.id, objectId: 1 } }]);
expect(result).toBe(isSymbol ? second.symbol(data as SymbolResponse) : first.type(data as TypeResponse));
if (!isSymbol) expect(second.type(data as TypeResponse)).not.toBe(result);
});
}
});
test("a partially cached type list fetches once, preserves order and duplicates", () => {
const { first, answers, requests } = harness();
const one = first.type({ id: 1, flags: TypeFlags.String });
const two = { id: 2, flags: TypeFlags.Number };
const data = { id: 3, flags: TypeFlags.Object, typeParameters: [2, 1, 2] };
answers.set("getTypeParametersOfType", [two, { id: one.id, flags: one.flags }, two]);
const type = first.type(data);
const parameters = type.getTypeParameters();
expect(parameters).toEqual([first.type(two), one, first.type(two)]);
expect(type.getTypeParameters()).toEqual(parameters);
expect(requests).toHaveLength(1);
});
test("unknown constituent lists fetch while non-constituent types stay local", () => {
const { first, answers, requests } = harness();
answers.set("getTypesOfType", [{ id: 2, flags: TypeFlags.String }]);
for (const flags of [TypeFlags.Union, TypeFlags.Intersection, TypeFlags.TemplateLiteral]) {
const type = first.type({ id: flags, flags });
expect(type.getTypes()?.[0]).toBe(first.type({ id: 2, flags: TypeFlags.String }));
}
expect(requests).toHaveLength(3);
});
test("conditional branches have independent lazy state, including a retry after refusal", () => {
const { first, answers, requests } = harness();
const type = first.type({ id: 3, flags: TypeFlags.Conditional });
answers.set("getTrueTypeOfConditionalType", null);
expect(() => type.getTrueType()).toThrow("returned null type");
answers.set("getTrueTypeOfConditionalType", { id: 1, flags: TypeFlags.String });
answers.set("getFalseTypeOfConditionalType", { id: 2, flags: TypeFlags.Number });
const yes = type.getTrueType();
const no = type.getFalseType();
expect(yes).not.toBe(no);
expect(type.getTrueType()).toBe(yes);
expect(type.getFalseType()).toBe(no);
expect(requests.map((request) => request.method)).toEqual(["getTrueTypeOfConditionalType", "getTrueTypeOfConditionalType", "getFalseTypeOfConditionalType"]);
});
test("base types use the checker type field, not an objectId request", () => {
const { first, answers, requests } = harness();
const type = first.type({ id: 1, flags: TypeFlags.Object, objectFlags: ObjectFlags.Interface });
answers.set("getBaseTypes", [{ id: 2, flags: TypeFlags.Object, objectFlags: ObjectFlags.Class }]);
expect(type.getBaseTypes()?.[0]?.isClassOrInterface()).toBe(true);
expect(requests).toEqual([{ method: "getBaseTypes", query: { snapshot: 17, project: first.id, type: 1 } }]);
});
test("symbol tables keep escaped keys, display names, canonical context and cache identity", () => {
const { first, second, answers, requests, symbol } = harness();
const parent = second.symbol(symbol(1, { parent: 2, exportSymbol: 3 }));
answers.set("getMembersOfSymbol", [symbol(4, { name: "___x" as SymbolResponse["name"] })]);
answers.set("getExportsOfSymbol", [symbol(5, { name: "__x" as SymbolResponse["name"] })]);
answers.set("getParentOfSymbol", symbol(2));
answers.set("getExportSymbolOfSymbol", symbol(3));
const members = parent.getMembers();
const exports = parent.getExports();
expect(members.get("___x")?.name).toBe("__x");
expect(exports.get("__x")?.name).toBe("__x");
expect(parent.getMembers()).toBe(members);
expect(parent.getExports()).toBe(exports);
expect(parent.getParent()).toBe(first.symbol(symbol(2)));
expect(parent.getExportSymbol()).toBe(first.symbol(symbol(3)));
expect(requests.every((request) => request.query["project"] === first.id)).toBe(true);
expect(requests).toHaveLength(4);
});
test("signature handles use type, symbol and signature registries independently", () => {
const { first, answers, requests, symbol } = harness();
const signature = first.signature({ id: 1, flags: SignatureFlags.HasRestParameter | SignatureFlags.Construct | SignatureFlags.Abstract, typeParameters: [2], parameters: [2, 3], thisParameter: 3, target: 2 });
answers.set("getTypeParametersOfSignature", [{ id: 2, flags: TypeFlags.TypeParameter }]);
answers.set("getParametersOfSignature", [symbol(2), symbol(3)]);
answers.set("getTargetOfSignature", { id: 2, flags: 0 });
expect(signature.getTypeParameters()[0]?.isTypeParameter()).toBe(true);
const parameters = signature.getParameters();
expect(signature.getThisParameter()).toBe(parameters[1]);
expect(signature.getParameters()).toEqual(parameters);
expect(signature.getTypeParameters()[0]).toBe(first.type({ id: 2, flags: TypeFlags.TypeParameter }));
expect(signature.getTarget()).toBe(first.signature({ id: 2, flags: 0 }));
expect(signature.getTarget()).not.toBe(signature);
expect(signature.hasRestParameter && signature.isConstruct && signature.isAbstract).toBe(true);
expect(requests).toHaveLength(3);
});
test("literal metadata preserves zero, false, empty strings and arbitrary precision integers", () => {
const { first } = harness();
const values = [
{ flags: TypeFlags.StringLiteral, input: "", expected: "" },
{ flags: TypeFlags.NumberLiteral, input: 0, expected: 0 },
{ flags: TypeFlags.BooleanLiteral, input: false, expected: false },
{ flags: TypeFlags.BigIntLiteral, input: "-123456789012345678901234567890", expected: -123456789012345678901234567890n },
];
for (const [index, { flags, input, expected }] of values.entries()) {
expect(first.type({ id: index + 1, flags, value: input }).value).toBe(expected);
}
expect(first.type({ id: 6, flags: TypeFlags.String, value: null }).value).toBeUndefined();
});
test("native semantic predicates match all pinned TypeScript flag classifications", () => {
const { first } = harness();
const predicates = [
["isUnionType", isUnionType], ["isIntersectionType", isIntersectionType], ["isObjectType", isObjectType],
["isClassOrInterface", isClassOrInterfaceType], ["isIntrinsicType", isIntrinsicType], ["isErrorType", isErrorType],
["isLiteralType", isLiteralType], ["isStringLiteralType", isStringLiteralType], ["isNumberLiteralType", isNumberLiteralType],
["isBigIntLiteralType", isBigIntLiteralType], ["isBooleanLiteralType", isBooleanLiteralType],
["isTypeReference", isTypeReference], ["isTupleType", isTupleType], ["isIndexType", isIndexType],
["isIndexedAccessType", isIndexedAccessType], ["isConditionalType", isConditionalType], ["isSubstitutionType", isSubstitutionType],
["isTemplateLiteralType", isTemplateLiteralType], ["isStringMappingType", isStringMappingType], ["isTypeParameter", isTypeParameter],
] as const;
let id = 0;
for (const flags of Object.values(TypeFlags)) for (const objectFlags of [0, ObjectFlags.Class, ObjectFlags.Interface, ObjectFlags.Reference, ObjectFlags.Tuple]) {
const type = first.type({ id: ++id, flags, objectFlags, intrinsicName: "error" });
for (const [name, oracle] of predicates) expect(type[name](), `${name} flags=${flags} objectFlags=${objectFlags}`).toBe(oracle(type as unknown as Type));
}
});
test("project disposal cannot poison another project, and snapshot disposal seals every cache", () => {
const { snapshot, first, second, symbol, requests } = harness();
const old = first.type({ id: 1, flags: TypeFlags.Number });
const shared = first.symbol(symbol(1));
first.dispose();
first.dispose();
expect(() => old.getTarget()).toThrow("project is disposed");
expect(() => first.type({ id: 1, flags: TypeFlags.Number })).toThrow("project is disposed");
expect(second.type({ id: 1, flags: TypeFlags.String })).toBeInstanceOf(SemanticType);
expect(second.symbol(symbol(1))).toBe(shared);
snapshot.dispose();
snapshot.dispose();
expect(() => second.type({ id: 1, flags: TypeFlags.Number })).toThrow("snapshot is disposed");
expect(() => shared.getMembers()).toThrow("snapshot is disposed");
expect(() => snapshot.addProject("later", () => undefined)).toThrow("snapshot is disposed");
expect(requests).toEqual([]);
});
test("node handles validate their serialized identity and retain canonical context", () => {
const { first } = harness();
const handle = new SemanticNodeHandle("7.79./file.with.dots.ts", first);
expect([handle.index, handle.kind, handle.path]).toEqual([7, 79, "/file.with.dots.ts"]);
expect(handle.resolve()).toBeUndefined();
expect(() => new SemanticNodeHandle("bad", first)).toThrow();
first.dispose();
expect(() => handle.resolve()).toThrow("disposed");
});
@@ -0,0 +1,413 @@
import type { SemanticSignatureData as SignatureResponse, SemanticSymbolData as SymbolResponse, SemanticTypeData as TypeResponse } from "./semantic-schema.generated.js";
import { AstNode } from "./ast-node.js";
import { parseAstNodeHandle } from "./ast-wire.js";
import type { SemanticChecker, SemanticJsDocTag } from "./semantic-checker.js";
import { parseSemanticJson } from "./semantic-json.js";
import { SemanticObjectFlags as ObjectFlags, SemanticSignatureFlags as SignatureFlags, SemanticTypeFlags as TypeFlags } from "./semantic-schema.generated.js";
/** JSON and binary operations stay at the transport boundary. The concrete
* semantic objects below need neither the SDK nor a JavaScript engine. */
export interface SemanticTransport {
text: (method: string, payload: string) => string;
binary: (method: string, payload: string) => Uint8Array;
}
export type SemanticDocument = string | { uri: string };
/** The pinned checker protocol's request fields. A single concrete record
* keeps generic response decoding independent of structural object spread. */
export interface SemanticQuery {
snapshot?: number | undefined;
project?: string | undefined;
objectId?: number | undefined;
location?: string | undefined;
locations?: string[] | undefined;
file?: SemanticDocument | undefined;
position?: number | undefined;
positions?: readonly number[] | undefined;
type?: number | undefined;
symbol?: number | undefined;
symbols?: number[] | undefined;
signature?: number | undefined;
source?: number | undefined;
target?: number | undefined;
kind?: number | undefined;
name?: string | undefined;
flags?: number | undefined;
meaning?: number | undefined;
excludeGlobals?: boolean | undefined;
index?: number | undefined;
node?: string | undefined;
signatureDecl?: string | undefined;
triggerCharacter?: string | undefined;
includeSymbol?: boolean | undefined;
}
export class SemanticProgram {
constructor(private readonly sourceFile: (path: string) => AstNode | undefined) {}
getSourceFile(path: string): AstNode | undefined { return this.sourceFile(path); }
}
/** Symbols are snapshot-wide; TypeScript type and signature handles are
* project-local. Preserve both scopes even when numeric handles coincide. */
export class SemanticSnapshot {
private readonly projects = new Map<string, SemanticProject>();
private readonly symbols = new Map<number, SemanticSymbol>();
private disposed = false;
constructor(readonly id: number, readonly transport: SemanticTransport) {}
addProject(id: string, sourceFile: (path: string) => AstNode | undefined): SemanticProject {
this.ensureActive();
if (this.projects.has(id)) throw new Error(`Duplicate TypeScript project ${id}`);
const project = new SemanticProject(id, this, new SemanticProgram(sourceFile));
this.projects.set(id, project);
return project;
}
getProject(id: string): SemanticProject {
this.ensureActive();
const project = this.projects.get(id);
if (project === undefined) throw new Error(`Unknown TypeScript canonical project ${id}`);
return project;
}
symbol(data: SymbolResponse): SemanticSymbol {
this.ensureActive();
let symbol = this.symbols.get(data.id);
if (symbol === undefined) {
symbol = new SemanticSymbol(data, this.getProject(data.project));
this.symbols.set(data.id, symbol);
}
return symbol;
}
cachedSymbol(id: number): SemanticSymbol | undefined {
this.ensureActive();
return this.symbols.get(id);
}
ensureActive(): void {
if (this.disposed) throw new Error("TypeScript semantic snapshot is disposed");
}
dispose(): void {
if (this.disposed) return;
this.disposed = true;
for (const project of this.projects.values()) project.dispose();
this.projects.clear();
this.symbols.clear();
}
}
export class SemanticProject {
private readonly types = new Map<number, SemanticType>();
private readonly signatures = new Map<number, SemanticSignature>();
private disposed = false;
constructor(readonly id: string, readonly snapshot: SemanticSnapshot, readonly program: SemanticProgram) {}
ensureActive(): void {
this.snapshot.ensureActive();
if (this.disposed) throw new Error("TypeScript semantic project is disposed");
}
request<T>(method: string, query: SemanticQuery = {}): T {
this.ensureActive();
query.snapshot = this.snapshot.id;
query.project = this.id;
return parseSemanticJson<T>(this.snapshot.transport.text(method, JSON.stringify(query)));
}
requestBinary(method: string, query: SemanticQuery): Uint8Array {
this.ensureActive();
query.snapshot = this.snapshot.id;
query.project = this.id;
return this.snapshot.transport.binary(method, JSON.stringify(query));
}
type(data: TypeResponse): SemanticType {
this.ensureActive();
let type = this.types.get(data.id);
if (type === undefined) {
type = new SemanticType(data, this);
this.types.set(data.id, type);
}
return type;
}
signature(data: SignatureResponse): SemanticSignature {
this.ensureActive();
let signature = this.signatures.get(data.id);
if (signature === undefined) {
signature = new SemanticSignature(data, this);
this.signatures.set(data.id, signature);
}
return signature;
}
symbol(data: SymbolResponse): SemanticSymbol {
this.ensureActive();
return this.snapshot.symbol(data);
}
fetchType(source: number, method: string, handle: number | undefined | false): SemanticType | undefined {
this.ensureActive();
if (handle !== false) {
if (handle === undefined || handle === 0) return undefined;
const cached = this.types.get(handle);
if (cached !== undefined) return cached;
}
const data = this.request<TypeResponse | null>(method, { objectId: source });
if (data === null) throw new Error(`${method} returned null type for ${source}`);
return this.type(data);
}
fetchSymbol(source: number, method: string, handle: number | undefined): SemanticSymbol | undefined {
this.ensureActive();
if (handle === undefined || handle === 0) return undefined;
const cached = this.snapshot.cachedSymbol(handle);
if (cached !== undefined) return cached;
const data = this.request<SymbolResponse | null>(method, { objectId: source });
if (data === null) throw new Error(`${method} returned null symbol for ${source}`);
return this.symbol(data);
}
fetchSignature(source: number, method: string, handle: number | undefined): SemanticSignature | undefined {
this.ensureActive();
if (handle === undefined || handle === 0) return undefined;
const cached = this.signatures.get(handle);
if (cached !== undefined) return cached;
const data = this.request<SignatureResponse | null>(method, { objectId: source });
if (data === null) throw new Error(`${method} returned null signature for ${source}`);
return this.signature(data);
}
fetchTypes(source: number, method: string, handles?: readonly number[]): SemanticType[] {
this.ensureActive();
if (handles !== undefined) {
const cached: SemanticType[] = [];
for (const id of handles) {
const type = this.types.get(id);
if (type === undefined) break;
cached.push(type);
}
if (cached.length === handles.length) return cached;
}
const data = this.request<TypeResponse[] | null>(method, { objectId: source });
return (data ?? []).map((item) => this.type(item));
}
fetchSymbols(source: number, method: string, handles?: readonly number[]): SemanticSymbol[] {
this.ensureActive();
if (handles !== undefined) {
const cached: SemanticSymbol[] = [];
for (const id of handles) {
const symbol = this.snapshot.cachedSymbol(id);
if (symbol === undefined) break;
cached.push(symbol);
}
if (cached.length === handles.length) return cached;
}
const data = this.request<SymbolResponse[] | null>(method, { objectId: source });
return (data ?? []).map((item) => this.symbol(item));
}
dispose(): void {
if (this.disposed) return;
this.disposed = true;
this.types.clear();
this.signatures.clear();
}
}
/** A handle records its producing project, so declaration resolution is
* unambiguous even when a symbol is reused by a second project's checker. */
export class SemanticNodeHandle {
readonly index: number;
readonly kind: number;
readonly path: string;
constructor(readonly handle: string, readonly canonicalProject: SemanticProject) {
const parsed = parseAstNodeHandle(handle);
this.index = parsed.index;
this.kind = parsed.kind;
this.path = parsed.path;
}
resolve(project: SemanticProject = this.canonicalProject): AstNode | undefined {
this.canonicalProject.ensureActive();
const file = project.program.getSourceFile(this.path);
return file?.file.resolve(this.handle);
}
}
export class SemanticSymbol {
readonly id: number;
readonly escapedName: string;
readonly name: string;
readonly flags: number;
readonly checkFlags: number;
readonly declarations: SemanticNodeHandle[];
readonly valueDeclaration: SemanticNodeHandle | undefined;
private readonly parent: number | undefined;
private readonly exportSymbol: number | undefined;
private membersCache: Map<string, SemanticSymbol> | undefined;
private exportsCache: Map<string, SemanticSymbol> | undefined;
constructor(data: SymbolResponse, readonly canonicalProject: SemanticProject) {
this.id = data.id;
this.escapedName = data.name;
this.name = data.name.startsWith("___") ? data.name.slice(1) : data.name;
this.flags = data.flags;
this.checkFlags = data.checkFlags;
this.declarations = (data.declarations ?? []).map((handle) => new SemanticNodeHandle(handle, canonicalProject));
this.valueDeclaration = data.valueDeclaration ? new SemanticNodeHandle(data.valueDeclaration, canonicalProject) : undefined;
this.parent = data.parent;
this.exportSymbol = data.exportSymbol;
}
getParent(): SemanticSymbol | undefined {
return this.canonicalProject.fetchSymbol(this.id, "getParentOfSymbol", this.parent);
}
getExportSymbol(): SemanticSymbol {
this.canonicalProject.ensureActive();
if (this.exportSymbol === undefined || this.exportSymbol === 0) return this;
const result = this.canonicalProject.fetchSymbol(this.id, "getExportSymbolOfSymbol", this.exportSymbol);
if (result === undefined) throw new Error(`Missing export symbol for ${this.id}`);
return result;
}
private table(method: string): Map<string, SemanticSymbol> {
const table = new Map<string, SemanticSymbol>();
for (const symbol of this.canonicalProject.fetchSymbols(this.id, method)) table.set(symbol.escapedName, symbol);
return table;
}
getMembers(): Map<string, SemanticSymbol> {
this.canonicalProject.ensureActive();
if (this.membersCache === undefined) this.membersCache = this.table("getMembersOfSymbol");
return this.membersCache;
}
getExports(): Map<string, SemanticSymbol> {
this.canonicalProject.ensureActive();
if (this.exportsCache === undefined) this.exportsCache = this.table("getExportsOfSymbol");
return this.exportsCache;
}
getJsDocTags(checker: SemanticChecker): SemanticJsDocTag[] { return checker.getJsDocTagsOfSymbol(this); }
getDocumentationComment(checker: SemanticChecker): string { return checker.getDocumentationCommentOfSymbol(this); }
}
/** Kind-specific accessors share one nominal layout. The legacy frontend
* still narrows its type-only SDK interfaces at the boundary; native code
* uses this concrete class and tests the same flag predicates. */
export class SemanticType {
readonly id: number;
readonly flags: number;
readonly objectFlags: number | undefined;
readonly value: string | number | boolean | bigint | undefined;
readonly intrinsicName: string | undefined;
readonly isThisType: boolean | undefined;
readonly elementFlags: number[] | undefined;
readonly fixedLength: number | undefined;
readonly readonly: boolean | undefined;
readonly texts: string[] | undefined;
private trueType: number | false = false;
private falseType: number | false = false;
constructor(private readonly data: TypeResponse, readonly project: SemanticProject) {
this.id = data.id;
this.flags = data.flags;
this.objectFlags = data.objectFlags;
const value = data.value;
if ((data.flags & TypeFlags.BigIntLiteral) !== 0 && value != null) {
if (typeof value !== "string") throw new Error("TypeScript bigint literal is not a decimal string");
this.value = BigInt(value);
} else {
this.value = value ?? undefined;
}
this.intrinsicName = data.intrinsicName;
this.isThisType = data.isThisType;
this.elementFlags = data.elementFlags;
this.fixedLength = data.fixedLength;
this.readonly = data.readonly;
this.texts = data.texts;
}
getSymbol(): SemanticSymbol | undefined { return this.project.fetchSymbol(this.id, "getSymbolOfType", this.data.symbol); }
getAliasSymbol(): SemanticSymbol | undefined { return this.project.fetchSymbol(this.id, "getAliasSymbolOfType", this.data.aliasSymbol); }
getTarget(): SemanticType | undefined { return this.project.fetchType(this.id, "getTargetOfType", this.data.target); }
getFreshType(): SemanticType | undefined { return this.project.fetchType(this.id, "getFreshTypeOfType", this.data.freshType); }
getRegularType(): SemanticType | undefined { return this.project.fetchType(this.id, "getRegularTypeOfType", this.data.regularType); }
getObjectType(): SemanticType | undefined { return this.project.fetchType(this.id, "getObjectTypeOfType", this.data.objectType); }
getIndexType(): SemanticType | undefined { return this.project.fetchType(this.id, "getIndexTypeOfType", this.data.indexType); }
getCheckType(): SemanticType | undefined { return this.project.fetchType(this.id, "getCheckTypeOfType", this.data.checkType); }
getExtendsType(): SemanticType | undefined { return this.project.fetchType(this.id, "getExtendsTypeOfType", this.data.extendsType); }
getBaseType(): SemanticType | undefined { return this.project.fetchType(this.id, "getBaseTypeOfType", this.data.baseType); }
getConstraint(): SemanticType | undefined { return this.project.fetchType(this.id, "getConstraintOfType", this.data.substConstraint); }
getTypeParameters(): SemanticType[] { return this.project.fetchTypes(this.id, "getTypeParametersOfType", this.data.typeParameters ?? []); }
getOuterTypeParameters(): SemanticType[] { return this.project.fetchTypes(this.id, "getOuterTypeParametersOfType", this.data.outerTypeParameters ?? []); }
getLocalTypeParameters(): SemanticType[] { return this.project.fetchTypes(this.id, "getLocalTypeParametersOfType", this.data.localTypeParameters ?? []); }
getAliasTypeArguments(): SemanticType[] { return this.project.fetchTypes(this.id, "getAliasTypeArgumentsOfType", this.data.aliasTypeArguments ?? []); }
getTypes(): SemanticType[] | undefined {
this.project.ensureActive();
if ((this.flags & (TypeFlags.UnionOrIntersection | TypeFlags.TemplateLiteral)) === 0) return undefined;
return this.project.fetchTypes(this.id, "getTypesOfType");
}
getBaseTypes(): SemanticType[] | undefined {
this.project.ensureActive();
if (!this.isClassOrInterface()) return undefined;
const data = this.project.request<TypeResponse[] | null>("getBaseTypes", { type: this.id });
return (data ?? []).map((item) => this.project.type(item));
}
getTrueType(): SemanticType {
const result = this.project.fetchType(this.id, "getTrueTypeOfConditionalType", this.trueType);
if (result === undefined) throw new Error(`Missing conditional true type for ${this.id}`);
this.trueType = result.id;
return result;
}
getFalseType(): SemanticType {
const result = this.project.fetchType(this.id, "getFalseTypeOfConditionalType", this.falseType);
if (result === undefined) throw new Error(`Missing conditional false type for ${this.id}`);
this.falseType = result.id;
return result;
}
isClassOrInterface(): boolean { return this.isObjectType() && ((this.objectFlags ?? 0) & ObjectFlags.ClassOrInterface) !== 0; }
isUnionType(): boolean { return (this.flags & TypeFlags.Union) !== 0; }
isIntersectionType(): boolean { return (this.flags & TypeFlags.Intersection) !== 0; }
isObjectType(): boolean { return (this.flags & TypeFlags.Object) !== 0; }
isIntrinsicType(): boolean { return (this.flags & TypeFlags.Intrinsic) !== 0; }
isErrorType(): boolean { return this.isIntrinsicType() && this.intrinsicName === "error"; }
isLiteralType(): boolean { return (this.flags & TypeFlags.Literal) !== 0; }
isStringLiteralType(): boolean { return (this.flags & TypeFlags.StringLiteral) !== 0; }
isNumberLiteralType(): boolean { return (this.flags & TypeFlags.NumberLiteral) !== 0; }
isBigIntLiteralType(): boolean { return (this.flags & TypeFlags.BigIntLiteral) !== 0; }
isBooleanLiteralType(): boolean { return (this.flags & TypeFlags.BooleanLiteral) !== 0; }
isTypeReference(): boolean { return this.isObjectType() && ((this.objectFlags ?? 0) & ObjectFlags.Reference) !== 0; }
isTupleType(): boolean { return this.isObjectType() && ((this.objectFlags ?? 0) & ObjectFlags.Tuple) !== 0; }
isIndexType(): boolean { return (this.flags & TypeFlags.Index) !== 0; }
isIndexedAccessType(): boolean { return (this.flags & TypeFlags.IndexedAccess) !== 0; }
isConditionalType(): boolean { return (this.flags & TypeFlags.Conditional) !== 0; }
isSubstitutionType(): boolean { return (this.flags & TypeFlags.Substitution) !== 0; }
isTemplateLiteralType(): boolean { return (this.flags & TypeFlags.TemplateLiteral) !== 0; }
isStringMappingType(): boolean { return (this.flags & TypeFlags.StringMapping) !== 0; }
isTypeParameter(): boolean { return (this.flags & TypeFlags.TypeParameter) !== 0; }
}
export class SemanticSignature {
readonly id: number;
readonly flags: number;
readonly declaration: SemanticNodeHandle | undefined;
constructor(private readonly data: SignatureResponse, readonly project: SemanticProject) {
this.id = data.id;
this.flags = data.flags;
this.declaration = data.declaration ? new SemanticNodeHandle(data.declaration, project) : undefined;
}
getTypeParameters(): SemanticType[] { return this.project.fetchTypes(this.id, "getTypeParametersOfSignature", this.data.typeParameters ?? []); }
getParameters(): SemanticSymbol[] { return this.project.fetchSymbols(this.id, "getParametersOfSignature", this.data.parameters ?? []); }
getThisParameter(): SemanticSymbol | undefined { return this.project.fetchSymbol(this.id, "getThisParameterOfSignature", this.data.thisParameter); }
getTarget(): SemanticSignature | undefined { return this.project.fetchSignature(this.id, "getTargetOfSignature", this.data.target); }
get hasRestParameter(): boolean { return (this.flags & SignatureFlags.HasRestParameter) !== 0; }
get isConstruct(): boolean { return (this.flags & SignatureFlags.Construct) !== 0; }
get isAbstract(): boolean { return (this.flags & SignatureFlags.Abstract) !== 0; }
}
@@ -0,0 +1,297 @@
// Generated from typescript@7.0.2 by scripts/generate-ts7-ast-schema.mjs.
// TypeScript is Copyright Microsoft Corporation, licensed under Apache-2.0.
// Regenerate when changing the TypeScript pin; do not edit by hand.
export const SemanticTypeFlags = {
None: 0,
Any: 1,
Unknown: 2,
Undefined: 4,
Null: 8,
Void: 16,
String: 32,
Number: 64,
BigInt: 128,
Boolean: 256,
ESSymbol: 512,
StringLiteral: 1024,
NumberLiteral: 2048,
BigIntLiteral: 4096,
BooleanLiteral: 8192,
UniqueESSymbol: 16384,
EnumLiteral: 32768,
Enum: 65536,
NonPrimitive: 131072,
Never: 262144,
TypeParameter: 524288,
Object: 1048576,
Index: 2097152,
TemplateLiteral: 4194304,
StringMapping: 8388608,
Substitution: 16777216,
IndexedAccess: 33554432,
Conditional: 67108864,
Union: 134217728,
Intersection: 268435456,
Reserved1: 536870912,
Reserved2: 1073741824,
Reserved3: -2147483648,
AnyOrUnknown: 3,
Nullable: 12,
Literal: 15360,
Unit: 97292,
Freshable: 80896,
StringOrNumberLiteral: 3072,
StringOrNumberLiteralOrUnique: 19456,
DefinitelyFalsy: 15388,
PossiblyFalsy: 15868,
Intrinsic: 393983,
StringLike: 12583968,
NumberLike: 67648,
BigIntLike: 4224,
BooleanLike: 8448,
EnumLike: 98304,
ESSymbolLike: 16896,
VoidLike: 20,
Primitive: 12713980,
DefinitelyNonNullable: 13893600,
DisjointDomains: 12812284,
UnionOrIntersection: 402653184,
StructuredType: 403701760,
TypeVariable: 34078720,
InstantiableNonPrimitive: 117964800,
InstantiablePrimitive: 14680064,
Instantiable: 132644864,
StructuredOrInstantiable: 536346624,
ObjectFlagsType: 403963917,
Simplifiable: 102760448,
Singleton: 394239,
Narrowable: 536575971,
IncludesMask: 416808959,
IncludesMissingType: 524288,
IncludesNonWideningType: 2097152,
IncludesWildcard: 33554432,
IncludesEmptyObject: 67108864,
IncludesInstantiable: 16777216,
IncludesConstrainedTypeVariable: 536870912,
IncludesError: 1073741824,
NotPrimitiveUnion: 286523411,
} as const;
export const SemanticObjectFlags = {
None: 0,
Class: 1,
Interface: 2,
Reference: 4,
Tuple: 8,
Anonymous: 16,
Mapped: 32,
Instantiated: 64,
ObjectLiteral: 128,
EvolvingArray: 256,
ObjectLiteralPatternWithComputedProperties: 512,
ReverseMapped: 1024,
JsxAttributes: 2048,
JSLiteral: 4096,
FreshLiteral: 8192,
ArrayLiteral: 16384,
PrimitiveUnion: 32768,
ContainsWideningType: 65536,
ContainsObjectOrArrayLiteral: 131072,
NonInferrableType: 262144,
CouldContainTypeVariablesComputed: 524288,
CouldContainTypeVariables: 1048576,
MembersResolved: 2097152,
ClassOrInterface: 3,
RequiresWidening: 196608,
PropagatingFlags: 458752,
InstantiatedMapped: 96,
InstantiationExpressionType: 16777216,
SingleSignatureType: 33554432,
ObjectTypeKindMask: 50332991,
ContainsSpread: 4194304,
ObjectRestType: 8388608,
IsClassInstanceClone: 67108864,
IdenticalBaseTypeCalculated: 134217728,
IdenticalBaseTypeExists: 268435456,
UnresolvedMembers: 536870912,
FromTypeNode: 1073741824,
IsGenericTypeComputed: 4194304,
IsGenericObjectType: 8388608,
IsGenericIndexType: 16777216,
IsGenericType: 25165824,
ContainsIntersections: 33554432,
IsUnknownLikeUnionComputed: 67108864,
IsUnknownLikeUnion: 134217728,
IsNeverIntersectionComputed: 33554432,
IsNeverIntersection: 67108864,
IsConstrainedTypeVariable: 134217728,
} as const;
export const SemanticSymbolFlags = {
None: 0,
FunctionScopedVariable: 1,
BlockScopedVariable: 2,
Property: 4,
EnumMember: 8,
Function: 16,
Class: 32,
Interface: 64,
ConstEnum: 128,
RegularEnum: 256,
ValueModule: 512,
NamespaceModule: 1024,
TypeLiteral: 2048,
ObjectLiteral: 4096,
Method: 8192,
Constructor: 16384,
GetAccessor: 32768,
SetAccessor: 65536,
Signature: 131072,
TypeParameter: 262144,
TypeAlias: 524288,
ExportValue: 1048576,
Alias: 2097152,
Prototype: 4194304,
ExportStar: 8388608,
Optional: 16777216,
Transient: 33554432,
Assignment: 67108864,
ModuleExports: 134217728,
ConstEnumOnlyModule: 268435456,
ReplaceableByMethod: 536870912,
GlobalLookup: 1073741824,
All: 536870912,
Enum: 384,
Variable: 3,
Value: 111551,
Type: 788968,
Namespace: 1920,
Module: 1536,
Accessor: 98304,
FunctionScopedVariableExcludes: 111550,
BlockScopedVariableExcludes: 111551,
ParameterExcludes: 111551,
PropertyExcludes: 13243,
EnumMemberExcludes: 900095,
FunctionExcludes: 110991,
ClassExcludes: 899503,
InterfaceExcludes: 788872,
RegularEnumExcludes: 899327,
ConstEnumExcludes: 899967,
ValueModuleExcludes: 110735,
NamespaceModuleExcludes: 0,
MethodExcludes: 103359,
GetAccessorExcludes: 46011,
SetAccessorExcludes: 78779,
AccessorExcludes: 111547,
TypeParameterExcludes: 526824,
TypeAliasExcludes: 788968,
AliasExcludes: 2097152,
ModuleMember: 2623475,
ExportHasLocal: 944,
BlockScoped: 418,
PropertyOrAccessor: 98308,
ClassMember: 106500,
ExportSupportsDefaultModifier: 112,
ExportDoesNotSupportDefaultModifier: -113,
Classifiable: 2885600,
LateBindingContainer: 6256,
} as const;
export const SemanticSignatureFlags = {
None: 0,
HasRestParameter: 1,
HasLiteralTypes: 2,
Construct: 4,
Abstract: 8,
IsInnerCallChain: 16,
IsOuterCallChain: 32,
IsUntypedSignatureInJSFile: 64,
IsNonInferrable: 128,
IsSignatureCandidateForOverloadFailure: 256,
PropagatingFlags: 335,
CallChainFlags: 48,
} as const;
export const SemanticSignatureKind = {
Call: 0,
Construct: 1,
} as const;
export const SemanticTypePredicateKind = {
This: 0,
Identifier: 1,
AssertsThis: 2,
AssertsIdentifier: 3,
} as const;
export interface SemanticSymbolData {
id: number;
/**
* The project the symbol was first observed in. Used as the default project for
* follow-up lookups that need a project context (e.g. members/exports), since symbols
* are shared snapshot-wide and such lookups can vary by project.
*/
project: string;
name: string;
flags: number;
checkFlags: number;
declarations?: string[];
valueDeclaration?: string;
parent?: number;
exportSymbol?: number;
}
export interface SemanticTypeData {
id: number;
flags: number;
objectFlags?: number;
/** Literal value. BigInt literals are encoded as a decimal string (e.g. "-123") since JSON cannot represent bigint. Absent values are serialized as null. */
value?: string | number | boolean | null;
freshType?: number;
regularType?: number;
target?: number;
typeParameters?: number[];
outerTypeParameters?: number[];
localTypeParameters?: number[];
elementFlags?: number[];
fixedLength?: number;
readonly?: boolean;
objectType?: number;
indexType?: number;
checkType?: number;
extendsType?: number;
baseType?: number;
substConstraint?: number;
texts?: string[];
intrinsicName?: string;
isThisType?: boolean;
aliasTypeArguments?: number[];
aliasSymbol?: number;
symbol?: number;
}
export interface SemanticSignatureData {
id: number;
flags: number;
declaration?: string;
typeParameters?: number[];
parameters?: number[];
thisParameter?: number;
target?: number;
}
export interface SemanticTypePredicateData {
kind: number;
parameterIndex: number;
parameterName?: string;
type?: SemanticTypeData;
}
export interface SemanticIndexInfoData {
keyType: SemanticTypeData;
valueType: SemanticTypeData;
isReadonly?: boolean;
declaration?: string;
}
@@ -8578,6 +8578,12 @@
],
"diags": []
},
"<repo>/tests/corpus/bigint-union-conversion.ts": {
"order": [
"<repo>/tests/corpus/bigint-union-conversion.ts"
],
"diags": []
},
"<repo>/tests/corpus/console-imports.cjs": {
"order": [
"<repo>/tests/corpus/console-imports.cjs"
@@ -8590,6 +8596,12 @@
],
"diags": []
},
"<repo>/tests/corpus/instanceof-class-record-union.ts": {
"order": [
"<repo>/tests/corpus/instanceof-class-record-union.ts"
],
"diags": []
},
"<repo>/tests/corpus/js-declared-fields.cjs": {
"order": [
"<repo>/tests/corpus/js-declared-fields.cjs"
@@ -8,6 +8,8 @@ import { Ts7RpcClient } from "../../src/frontend/ts7/rpc-client.js";
import type { Ts7FileSystem } from "../../src/frontend/ts7/rpc-filesystem.js";
import { spawnTs7Wire } from "../../src/frontend/ts7/rpc-process.js";
import { AstNode } from "../../src/frontend/ts7/ast-node.js";
import { SemanticChecker } from "../../src/frontend/ts7/semantic-checker.js";
import { SemanticNodeHandle, SemanticSignature, SemanticSymbol, SemanticType } from "../../src/frontend/ts7/semantic-model.js";
function tsgoPath(path: string): string {
return process.platform === "win32" ? path.replaceAll("\\", "/") : path;
@@ -68,8 +70,12 @@ test("production RPC bridge preserves pinned SDK parser/checker behavior", () =>
expect(source).toBeInstanceOf(AstNode);
expect(project.program.getSourceFile(path)).toBe(source);
const first = project.checker.getTypeAtPosition(path, source.text.indexOf("answer"));
expect(project.checker).toBeInstanceOf(SemanticChecker);
expect(first).toBeInstanceOf(SemanticType);
expect(project.checker.getTypeAtPosition(path, source.text.indexOf("answer"))).toBe(first);
const symbol = project.checker.getSymbolAtPosition(path, source.text.indexOf("answer"));
expect(symbol).toBeInstanceOf(SemanticSymbol);
expect(symbol!.declarations[0]).toBeInstanceOf(SemanticNodeHandle);
expect(project.checker.getSymbolAtPosition(path, source.text.indexOf("answer"))).toBe(symbol);
expect(symbol!.declarations[0]!.resolve()).toBe(symbol!.declarations[0]!.resolve());
const declaration = symbol!.declarations[0]!.resolve()!;
@@ -123,6 +129,65 @@ test("snapshots share unchanged source files and release independently", () => {
}
});
test("native semantic registries preserve project scope and snapshot lifetimes", () => {
const dir = mkdtempSync(join(tempRoot, "scriptc-ts7-semantic-scopes-"));
const firstConfig = tsgoPath(join(dir, "first.json"));
const secondConfig = tsgoPath(join(dir, "second.json"));
const path = tsgoPath(join(dir, "shared.ts"));
for (const config of [firstConfig, secondConfig]) writeFileSync(config, JSON.stringify({ compilerOptions: { strict: true, target: "esnext", types: [] }, files: [path] }));
const source = "export function identity<T>(value: T): T { return value; }\nexport const answer = 42;\n";
writeFileSync(path, source);
const api = new Ts7Api({ cwd: dir, fs: fallback });
const sdk = new API({ cwd: dir });
function facts(snapshot: Snapshot) {
const a = snapshot.getProject(firstConfig)!;
const b = snapshot.getProject(secondConfig)!;
const answer = source.indexOf("answer");
const symbolA = a.checker.getSymbolAtPosition(path, answer)!;
const symbolB = b.checker.getSymbolAtPosition(path, answer)!;
const typeA = a.checker.getTypeAtPosition(path, answer)!;
const typeB = b.checker.getTypeAtPosition(path, answer)!;
const fileA = a.program.getSourceFile(path)!;
const fileB = b.program.getSourceFile(path)!;
const signatureA = a.checker.getSignatureFromDeclaration(fileA.statements[0]!)!;
const signatureB = b.checker.getSignatureFromDeclaration(fileB.statements[0]!)!;
return {
shape: {
sharedSymbol: symbolA === symbolB, sharedType: typeA === typeB, sharedSignature: signatureA === signatureB,
sameDeclaration: symbolA.declarations[0]!.resolve() === symbolB.declarations[0]!.resolve(),
explicitDeclaration: symbolB.declarations[0]!.resolve(b) === (fileB.statements[1] as import("typescript/unstable/ast").VariableStatement).declarationList.declarations[0],
texts: [a.checker.typeToString(typeA), b.checker.typeToString(typeB)],
parameters: signatureA.getParameters().map((symbol) => symbol.name),
},
symbolA, typeA, signatureA, fileA,
};
}
try {
const first = api.updateSnapshot({ openProjects: [firstConfig, secondConfig] });
const actual = facts(first);
expect(actual.shape).toEqual(facts(sdk.updateSnapshot({ openProjects: [firstConfig, secondConfig] })).shape);
expect(actual.signatureA).toBeInstanceOf(SemanticSignature);
expect(actual.typeA).toBeInstanceOf(SemanticType);
const second = api.updateSnapshot({ openProjects: [] });
const next = facts(second);
expect(next.fileA).toBe(actual.fileA);
expect(next.typeA).not.toBe(actual.typeA);
expect(next.symbolA).not.toBe(actual.symbolA);
expect(next.signatureA).not.toBe(actual.signatureA);
first.dispose();
expect(() => actual.typeA.getSymbol()).toThrow("disposed");
expect(() => actual.symbolA.declarations[0]!.resolve()).toThrow("disposed");
expect(next.signatureA.getParameters()[0]!.name).toBe("value");
expect(second.getProject(firstConfig)!.checker.typeToString(next.typeA)).toBe("42");
second.dispose();
expect(() => next.signatureA.getParameters()).toThrow("disposed");
} finally {
api.close();
sdk.close();
rmSync(dir, { recursive: true, force: true });
}
});
test("virtual filesystem callbacks retain empty, hidden, and fallback files", () => {
const dir = mkdtempSync(join(tempRoot, "scriptc-ts7-rpc-fs-"));
const config = tsgoPath(join(dir, "virtual.json"));
+39
View File
@@ -45,6 +45,45 @@ if (process.argv.includes("--check")) {
writeFileSync(target, output);
}
// The semantic client consumes the same pinned numeric discriminants as
// tsgo. Keep these independent of the SDK's runtime module graph too.
const semanticLines = [
`// Generated from typescript@${version} by scripts/generate-ts7-ast-schema.mjs.`,
"// TypeScript is Copyright Microsoft Corporation, licensed under Apache-2.0.",
"// Regenerate when changing the TypeScript pin; do not edit by hand.",
];
for (const name of ["TypeFlags", "ObjectFlags", "SymbolFlags", "SignatureFlags", "SignatureKind", "TypePredicateKind"]) {
const basename = name[0].toLowerCase() + name.slice(1);
const values = require(join(packageRoot, "dist/enums", `${basename}.js`))[name];
semanticLines.push("", `export const Semantic${name} = {`);
for (const [key, value] of Object.entries(values)) {
if (typeof value === "number") semanticLines.push(` ${key}: ${value},`);
}
semanticLines.push("} as const;");
}
const protoDeclarations = readFileSync(join(packageRoot, "dist/api/proto.d.ts"), "utf8");
const semanticTypes = new Map([
["SymbolResponse", "SemanticSymbolData"],
["TypeResponse", "SemanticTypeData"],
["SignatureResponse", "SemanticSignatureData"],
["TypePredicateResponse", "SemanticTypePredicateData"],
["IndexInfoResponse", "SemanticIndexInfoData"],
]);
for (const [name] of semanticTypes) {
const declaration = protoDeclarations.match(new RegExp(`export interface ${name} \\{[\\s\\S]*?\\n\\}`));
if (!declaration) throw new Error(`Missing TypeScript semantic response: ${name}`);
let text = declaration[0].replace(/\b(Path|__String)\b/g, "string");
for (const [original, generated] of semanticTypes) text = text.replaceAll(original, generated);
semanticLines.push("", text);
}
const semanticOutput = semanticLines.join("\n") + "\n";
const semanticTarget = join(root, "packages/compiler/src/frontend/ts7/semantic-schema.generated.ts");
if (process.argv.includes("--check")) {
if (readFileSync(semanticTarget, "utf8") !== semanticOutput) throw new Error("TypeScript semantic schema is stale; run node scripts/generate-ts7-ast-schema.mjs");
} else {
writeFileSync(semanticTarget, semanticOutput);
}
// Child names alone cannot distinguish arrays from nodes (attributes and
// children can be either). Read that distinction from the pinned client's
// declarations, and require every wire property to have a declared getter.
+37
View File
@@ -0,0 +1,37 @@
type Input = string | number | boolean | bigint | null | undefined;
let evaluations = 0;
function source(value: Input): Input { evaluations++; return value; }
function convert(value: Input): void {
try {
// @ts-expect-error Nullish inputs intentionally exercise BigInt's TypeError.
console.log("value", BigInt(source(value)).toString());
}
catch (error) { if (error instanceof Error) console.log("error", error.name, error.message); }
}
for (const value of ["123456789012345678901234567890", "0xFF", "", "invalid", 42, -0, 1.5, NaN, Infinity, true, false, 123456789012345678901234567890n, null, undefined] as Input[]) convert(value);
console.log("evaluations", evaluations);
class Literal {
readonly value: Input;
constructor(data: { value?: string | number | boolean | null; flags: number }) {
const value = data.value;
if ((data.flags & 1) !== 0 && value != null) {
if (typeof value !== "string") throw new TypeError("expected decimal string");
this.value = BigInt(value);
} else this.value = value ?? undefined;
}
}
console.log("constructor", new Literal({ value: "9007199254740993", flags: 1 }).value);
function captured(): void {
let value: string | undefined = "12";
const mutate = () => { value = undefined; };
if (typeof value === "string") {
mutate();
try { console.log(BigInt(value)); }
catch (error) { if (error instanceof Error) console.log("capture", error.name, error.message); }
}
}
captured();
@@ -0,0 +1,28 @@
// @transform-types
class Base { constructor(readonly value: number) {} }
class Derived extends Base { read(): number { return this.value; } }
class Other { constructor(readonly label: string) {} }
type Input = Base | Other | { value: number; label: string } | number | string | boolean | bigint | symbol | null | undefined;
let reads = 0;
function source(value: Input): Input { reads++; return value; }
function inspect(value: Input): void {
console.log("kinds", source(value) instanceof Base, source(value) instanceof Derived, source(value) instanceof Other);
if (value instanceof Base) console.log("base", value.value);
if (value instanceof Derived) console.log("derived", value.read());
if (value instanceof Other) console.log("other", value.label);
}
const inputs: Input[] = [new Base(1), new Derived(2), new Other("separate"), { value: 3, label: "record" }, 3, "text", false, 2n, Symbol("s"), null, undefined];
for (const input of inputs) inspect(input);
console.log("reads", reads);
class Node { constructor(readonly kind: number) {} get text(): string { return "node"; } }
function location(value?: Node | { document: string; position: number }): string {
const node = value instanceof Node ? value : undefined;
const position = value !== undefined && !(value instanceof Node) ? value : undefined;
return node === undefined ? position?.document ?? "absent" : node.text;
}
console.log(location(new Node(1)), location({ document: "file.ts", position: 0 }), location());
const errors: (Error | { message: string } | undefined)[] = [new Error("base"), new TypeError("type"), { message: "record" }, undefined];
for (const error of errors) console.log("error", error instanceof Error, error instanceof TypeError);
+47 -7
View File
@@ -6,6 +6,10 @@ import { Ts7Wire } from "../../../packages/compiler/src/frontend/ts7/rpc-wire.js
import { AstFile, AstNode } from "../../../packages/compiler/src/frontend/ts7/ast-node.js";
import { AstKind, KIND_NODE_LIST, astChildNames } from "../../../packages/compiler/src/frontend/ts7/ast-schema.generated.js";
import { decodeAstString } from "../../../packages/compiler/src/frontend/ts7/ast-bytes.js";
import { SemanticSnapshot } from "../../../packages/compiler/src/frontend/ts7/semantic-model.js";
import { SemanticChecker } from "../../../packages/compiler/src/frontend/ts7/semantic-checker.js";
import { parseSemanticJson } from "../../../packages/compiler/src/frontend/ts7/semantic-json.js";
import { checkSemanticModel, semanticSource } from "./ts7-semantic-cases.js";
// The harness connects these inherited descriptors straight to native tsgo.
// No JavaScript helper reads, interprets, or relays protocol messages.
@@ -58,6 +62,25 @@ function checkSurrogateBoundary(): string {
}
}
function checkSemanticSurrogateBoundary(): string {
check(parseSemanticJson<string>('"\\ud83c\\udf0d"') === "🌍", "semantic JSON surrogate pair");
check(parseSemanticJson<string>('"\\\\ud800"') === "\\ud800", "semantic JSON escaped backslash");
let refused = 0;
const hex = "0123456789abcdef";
for (let unit = 0xd800; unit <= 0xdfff; unit++) {
try {
const escape = hex.charAt((unit >> 12) & 15) + hex.charAt((unit >> 8) & 15) + hex.charAt((unit >> 4) & 15) + hex.charAt(unit & 15);
const value = parseSemanticJson<string>('"\\u' + escape + '"');
check(value.length === 1 && value.charCodeAt(0) === unit, "lossless semantic JSON code unit");
} catch (error) {
if (!(error instanceof Error) || error.message !== "TypeScript semantic response: runtime cannot preserve lone UTF-16 surrogates") throw error;
refused++;
}
}
check(refused === 0 || refused === 2048, "consistent semantic JSON surrogate boundary");
return refused === 0 ? "preserved" : "refused";
}
function checkAst(ast: AstFile, source: string): void {
check(ast.root.text === source, "decoded source text");
check(ast.root.getSourceFile() === ast.root, "source identity");
@@ -109,7 +132,7 @@ function checkFailures(): void {
let offset = 0;
let closes = 0;
const wire = new Ts7Wire({
read: (buffer, start, _length) => {
read: (buffer, start) => {
if (offset === bytes.length) return 0;
buffer[start] = bytes[offset++]!;
return 1;
@@ -134,15 +157,15 @@ function checkFailures(): void {
let offset = 0;
let closes = 0;
const local = new Ts7RpcClient(new Ts7Wire({
read: (buffer, start, _length) => {
read: (buffer, start) => {
if (offset === bytes.length) return 0;
buffer[start] = bytes[offset++]!;
return 1;
},
write: (buffer, start, _length) => { output.push(buffer[start]!); return 1; },
write: (buffer, start) => { output.push(buffer[start]!); return 1; },
close: () => { closes++; },
}));
local.registerCallback("x", (_payload) => {
local.registerCallback("x", () => {
if (throws) throw new Error("callback failed");
// Reject a nested request before it can put bytes on this stream.
let nestedFailed = false;
@@ -176,6 +199,7 @@ content += '/** Box documentation. */\nexport class Box { readonly value = 2; me
content += 'export const many = [';
for (let index = 0; index < 40; index++) content += `${index},`;
content += '];\nexport const template = `head\\n${answer}tail`;\n';
content += semanticSource();
let reads = 0;
registerTs7FileSystem(client, {
readFile: (path) => {
@@ -190,9 +214,9 @@ registerTs7FileSystem(client, {
return undefined;
},
fileExists: (path) => path === file || path === empty || path === configPath ? true : path === hidden ? false : undefined,
directoryExists: (_path) => undefined,
realpath: (_path) => undefined,
getAccessibleEntries: (_path) => undefined,
directoryExists: () => undefined,
realpath: () => undefined,
getAccessibleEntries: () => undefined,
});
try {
@@ -245,6 +269,21 @@ try {
check(nodeType.id === type.id && nodeSymbol.id === symbol.id, "native AST checker query");
check(tree.resolve(nodeSymbol.declarations![0]!) === declaration, "checker declaration identity");
const semanticSnapshot = new SemanticSnapshot(snapshot.snapshot, {
text: (method, payload) => client.requestText(method, payload),
binary: (method, payload) => client.requestBytes(method, Buffer.from(payload)),
});
const context = semanticSnapshot.addProject(project.id, (path) => path === file || path === tree.root.path ? tree.root : undefined);
const checker = new SemanticChecker(context);
const ownedType = checker.getTypeAtLocation(identifier)!;
const ownedSymbol = checker.getSymbolAtLocation(identifier)!;
check(ownedType.id === type.id && ownedSymbol.id === symbol.id, "semantic model handles");
check(checker.getTypeAtPosition(file, content.indexOf("answer")) === ownedType, "semantic type identity");
check(checker.getSymbolAtPosition(file, content.indexOf("answer")) === ownedSymbol, "semantic symbol identity");
check(ownedSymbol.declarations[0]!.resolve() === declaration, "semantic declaration identity");
check(checker.typeToString(ownedType) === "42" && ownedType.isNumberLiteralType() && ownedType.value === 42, "semantic literal metadata");
checkSemanticModel(semanticSnapshot, checker, tree);
// A server-side refusal completes its request. It must not poison the
// channel: the frontend's checker panic fence relies on this recovery.
let refused = false;
@@ -268,6 +307,7 @@ try {
writeFileSync(report, JSON.stringify({
typeText, symbol: symbol.name, diagnostics: diagnostics.map((diagnostic) => diagnostic.code),
surrogateBoundary: checkSurrogateBoundary(),
semanticSurrogateBoundary: checkSemanticSurrogateBoundary(), semanticModel: true,
echo: true, binaryAst: true, astIdentity: true, virtualFiles: true, retainedSnapshot: true, serverErrorRecovery: true, protocolFailures: true,
}));
} finally {
+223
View File
@@ -0,0 +1,223 @@
import { AstFile, AstNode } from "../../../packages/compiler/src/frontend/ts7/ast-node.js";
import { AstKind } from "../../../packages/compiler/src/frontend/ts7/ast-schema.generated.js";
import { SemanticChecker } from "../../../packages/compiler/src/frontend/ts7/semantic-checker.js";
import { SemanticSignature, SemanticSnapshot, SemanticSymbol, SemanticType } from "../../../packages/compiler/src/frontend/ts7/semantic-model.js";
import { SemanticSignatureKind, SemanticSymbolFlags, SemanticTypeFlags } from "../../../packages/compiler/src/frontend/ts7/semantic-schema.generated.js";
export function semanticSource(): string {
return '\nexport type Choice = string | number | null;\n'
+ 'export type Pair<T> = readonly [T, string];\n'
+ 'export const pair: Pair<number> = [1, "a"];\n'
+ 'export interface Dict { readonly [key: string]: number; }\n'
+ 'export interface Generic<T> { item: T; }\n'
+ 'export type GenericAlias = Generic<number>;\n'
+ 'export class Child extends Box {}\n'
+ '/** Keep the input.\n * @param value input value\n */\n'
+ 'export function identity<T extends { n: number }>(value: T): T { return value; }\n'
+ 'export const called = identity({ n: 1 });\n'
+ 'export function isBox(value: unknown): value is Box { return value instanceof Box; }\n'
+ 'export function withThis(this: Box, ...items: number[]): number { return items.length; }\n'
+ 'export const huge = -123456789012345678901234567890n;\n'
+ 'export const flag = false;\n'
+ 'export const emptyString = "";\n'
+ 'export const ref = { answer };\n'
+ 'export { answer as renamed };\n'
+ 'export enum Mode { Ready = 3 }\n'
+ 'export const enumValue = Mode.Ready;\n'
+ 'export type Nullable = Box | undefined;\n'
+ 'export type Conditional<T> = T extends string ? number : boolean;\n'
+ 'export type Indexed<T extends Generic<number>> = T["item"];\n'
+ 'export type Keys<T> = keyof T;\n'
+ 'export type Template<T extends string> = `prefix${T}suffix`;\n'
+ 'export type Mapped<T extends string> = Uppercase<T>;\n';
}
function check(value: boolean, message: string): void {
if (!value) throw new Error(`Semantic client: ${message}`);
}
function declaration(root: AstNode, name: string): AstNode {
for (const statement of root.statements ?? []) {
if (statement.name?.text === name) return statement;
for (const variable of statement.declarationList?.declarations ?? []) {
if (variable.name?.text === name) return variable;
}
}
throw new Error(`Missing semantic fixture declaration ${name}`);
}
function typeAt(checker: SemanticChecker, node: AstNode): SemanticType {
const type = checker.getTypeAtLocation(node);
if (type === undefined) throw new Error("Missing semantic fixture type");
return type;
}
function symbolAt(checker: SemanticChecker, node: AstNode): SemanticSymbol {
const symbol = checker.getSymbolAtLocation(node);
if (symbol === undefined) throw new Error("Missing semantic fixture symbol");
return symbol;
}
function signatureAt(checker: SemanticChecker, node: AstNode): SemanticSignature {
const signature = checker.getSignatureFromDeclaration(node);
if (signature === undefined) throw new Error("Missing semantic fixture signature");
return signature;
}
/** This runs both in Node and in the C/LLVM native executables. Each fact
* checks relationships within the live native parser/checker's response;
* server-assigned numeric handles never become hard-coded test answers. */
export function checkSemanticModel(snapshot: SemanticSnapshot, checker: SemanticChecker, file: AstFile): void {
const root = file.root;
const answer = declaration(root, "answer");
const answerName = answer.name!;
const answerSymbol = symbolAt(checker, answerName);
const answerType = typeAt(checker, answerName);
const missing = root.endOfFileToken!;
const nodeTypes = checker.getTypeAtLocation([answerName, missing, answerName]);
const nodeSymbols = checker.getSymbolAtLocation([answerName, missing, answerName]);
check(nodeTypes[0] === answerType && nodeTypes[2] === answerType, "batched type identity/order");
check(nodeSymbols[0] === answerSymbol && nodeSymbols[2] === answerSymbol && nodeSymbols[1] === undefined, "batched symbol null/identity/order");
check(checker.getTypeOfSymbol(answerSymbol) === answerType, "symbol type identity");
check(checker.getTypeOfSymbol([answerSymbol, answerSymbol])[1] === answerType, "batch symbol types");
check(checker.getTypeOfSymbolAtLocation(answerSymbol, answerName) === answerType, "location-dependent symbol type");
check(answerSymbol.valueDeclaration?.resolve() === answer && answerSymbol.declarations[0]!.resolve() === answer, "default declaration resolution");
check(answerSymbol.declarations[0]!.resolve(checker.project) === answer, "explicit declaration project");
check(checker.getTypeAtPosition(root.fileName, [answerName.getStart(), answerName.getStart()])[1] === answerType, "position type batch");
check(checker.getSymbolAtPosition(root.fileName, [answerName.getStart(), answerName.getStart()])[1] === answerSymbol, "position symbol batch");
check(checker.getResolvedSymbol(answerName)!.getExportSymbol() === answerSymbol, "resolved local/export symbol identity");
check(checker.resolveName("answer", SemanticSymbolFlags.Value, { document: root.fileName, position: answerName.getStart() }) === answerSymbol, "position name resolution");
check(checker.resolveName("scriptcMissing", SemanticSymbolFlags.Value, answerName) === undefined, "missing name");
const intrinsicNumber = checker.getNumberType();
const intrinsicString = checker.getStringType();
check(checker.getBaseTypeOfLiteralType(answerType) === intrinsicNumber, "literal base type");
check(answerType.getFreshType()!.getRegularType() === answerType.getRegularType(), "fresh/regular literal identity");
const huge = typeAt(checker, declaration(root, "huge").name!);
check(huge.isBigIntLiteralType() && huge.value === -123456789012345678901234567890n, "arbitrary precision bigint response");
check(typeAt(checker, declaration(root, "flag").name!).value === false, "false literal response");
check(typeAt(checker, declaration(root, "emptyString").name!).value === "", "empty string literal response");
const intrinsics = [checker.getAnyType(), intrinsicString, intrinsicNumber, checker.getVoidType(), checker.getUndefinedType(), checker.getNullType(), checker.getNeverType(), checker.getUnknownType(), checker.getBigIntType(), checker.getESSymbolType()];
for (const type of intrinsics) check(type.isIntrinsicType() && !type.isErrorType() && type.intrinsicName !== undefined, "intrinsic metadata");
const boolean = checker.getBooleanType();
check(boolean.isUnionType() && boolean.getTypes()!.every((type) => type.isBooleanLiteralType()), "boolean literal union");
check(checker.isTypeAssignableTo(answerType, intrinsicNumber) && !checker.isTypeAssignableTo(intrinsicNumber, answerType), "assignability");
check(checker.getWidenedType(answerType) !== undefined && checker.getApparentType(intrinsicString)!.isObjectType(), "widened and apparent types");
check(checker.getBaseConstraintOfType(answerType) === undefined, "absent base constraint");
const choice = typeAt(checker, declaration(root, "Choice").type!);
check(choice.isUnionType() && !choice.isIntersectionType() && choice.getTypes()!.length === 3, "union constituents");
check(choice.getTypes()!.some((type) => type === intrinsicString) && choice.getTypes()!.some((type) => type === intrinsicNumber), "constituent identity");
const pair = typeAt(checker, declaration(root, "pair").name!);
check(pair.isTypeReference() && checker.isTupleType(pair) && !checker.isArrayType(pair), "tuple reference classification");
const pairTarget = pair.getTarget()!;
check(pairTarget.isTupleType() && pairTarget.fixedLength === 2 && pairTarget.readonly === true && pairTarget.elementFlags!.length === 2, "tuple target metadata");
check(checker.getTypeArguments(pair)[0] === intrinsicNumber && checker.getTypeArguments(pair)[1] === intrinsicString, "tuple argument identities");
check(pair.getAliasSymbol()!.name === "Pair" && pair.getAliasTypeArguments()[0] === intrinsicNumber, "alias metadata");
const box = declaration(root, "Box");
const boxSymbol = symbolAt(checker, box.name!);
const boxType = checker.getDeclaredTypeOfSymbol(boxSymbol);
check(boxType.isClassOrInterface() && boxType.getSymbol() === boxSymbol, "declared class type");
// The checker instantiates a transient property symbol for a class type;
// the binder's member symbol is distinct but refers to the same AST node.
const boundMember = boxSymbol.getMembers().get("value")!;
const checkedMember = checker.getPropertyOfType(boxType, "value")!;
check(boundMember !== checkedMember && boundMember.declarations[0]!.resolve() === checkedMember.declarations[0]!.resolve(), "bound and instantiated member declarations");
check(boxSymbol.getMembers() === boxSymbol.getMembers() && boxSymbol.getExports() === boxSymbol.getExports(), "symbol table cache");
check(boxType.getBaseTypes()!.length === 0 && checker.getBaseTypes(boxType).length === 0, "empty base types");
const child = checker.getDeclaredTypeOfSymbol(symbolAt(checker, declaration(root, "Child").name!));
check(child.getBaseTypes()![0] === boxType && checker.getBaseTypes(child)[0] === boxType, "base type request identity");
check(checker.getPropertiesOfType(boxType).some((symbol) => symbol.name === "method"), "property query");
check(checker.getPropertyOfType(boxType, "missing") === undefined, "absent property");
const nullable = typeAt(checker, declaration(root, "Nullable").type!);
check(checker.getNonNullableType(nullable) === boxType, "non-nullable type");
const dict = checker.getDeclaredTypeOfSymbol(symbolAt(checker, declaration(root, "Dict").name!));
const index = checker.getIndexInfosOfType(dict)[0]!;
check(index.keyType === intrinsicString && index.valueType === intrinsicNumber && index.isReadonly, "index signature types");
check(index.declaration!.resolve() === declaration(root, "Dict").members![0], "index signature declaration");
const generic = checker.getDeclaredTypeOfSymbol(symbolAt(checker, declaration(root, "Generic").name!));
const genericParameter = generic.getTypeParameters()[0]!;
check(genericParameter.isTypeParameter() && generic.getLocalTypeParameters()[0] === genericParameter && generic.getOuterTypeParameters().length === 0, "interface type parameters");
const genericAlias = typeAt(checker, declaration(root, "GenericAlias").type!);
check(genericAlias.getTarget() === generic && checker.getTypeArguments(genericAlias)[0] === intrinsicNumber, "generic target");
const identity = declaration(root, "identity");
const identitySymbol = symbolAt(checker, identity.name!);
const signature = signatureAt(checker, identity);
const signatureTypes = signature.getTypeParameters();
check(signatureTypes.length === 1 && signatureTypes[0]!.isTypeParameter(), "signature type parameters");
check(signature.getParameters()[0]!.name === "value" && signature.getParameters()[0]!.declarations[0]!.resolve() === identity.parameters![0], "signature parameters");
check(signature.declaration!.resolve() === identity, "signature declaration");
check(checker.getReturnTypeOfSignature(signature) === signatureTypes[0], "generic return identity");
check(checker.getParameterType(signature, 0) === signatureTypes[0], "parameter type query");
const constraint = checker.getConstraintOfTypeParameter(signatureTypes[0]!)!;
check(checker.getPropertyOfType(constraint, "n") !== undefined && checker.getBaseConstraintOfType(signatureTypes[0]!) === constraint, "generic constraints");
check(checker.getSignaturesOfType(typeAt(checker, identity.name!), SemanticSignatureKind.Call)[0] === signature, "signature registry identity");
const resolved = checker.getResolvedSignature(declaration(root, "called").initializer!)!;
check(resolved.getTarget() === signature && resolved.getTypeParameters().length === 0, "instantiated signature target");
const ctor = checker.getSignaturesOfType(checker.getTypeOfSymbol(boxSymbol)!, SemanticSignatureKind.Construct)[0]!;
check(ctor.isConstruct && !ctor.isAbstract && checker.getReturnTypeOfSignature(ctor) === boxType, "construct signature");
const thisSignature = signatureAt(checker, declaration(root, "withThis"));
check(thisSignature.hasRestParameter && !thisSignature.isConstruct && thisSignature.getThisParameter()!.name === "this", "signature this/rest flags");
check(checker.getRestTypeOfSignature(thisSignature) === intrinsicNumber && checker.isArrayLikeType(pair), "array predicates and rest element type");
const predicate = checker.getTypePredicateOfSignature(signatureAt(checker, declaration(root, "isBox")))!;
check(predicate.type === boxType && predicate.parameterIndex === 0 && predicate.parameterName === "value", "type predicate");
check(checker.getTypePredicateOfSignature(signature) === undefined, "absent predicate");
check(identitySymbol.getJsDocTags(checker).some((tag) => tag.name === "param"), "JSDoc tags");
check(identitySymbol.getDocumentationComment(checker).includes("Keep the input"), "JSDoc documentation");
const moduleSymbol = symbolAt(checker, root);
const exports = moduleSymbol.getExports();
const renamed = exports.get("renamed")!;
check(checker.getAliasedSymbol(renamed) === answerSymbol && checker.getImmediateAliasedSymbol(renamed) === answerSymbol, "symbol alias targets");
check(checker.getMemberInModuleExports(moduleSymbol, "answer") === answerSymbol, "module export lookup");
check(checker.getExportsOfModule(moduleSymbol).some((symbol) => symbol === answerSymbol), "module exports identity");
check(answerSymbol.getExportSymbol() === answerSymbol && answerSymbol.getParent() === moduleSymbol, "symbol parent/export handles");
check(!checker.isUnknownSymbol(answerSymbol) && !checker.isUndefinedSymbol(answerSymbol) && !checker.isArgumentsSymbol(answerSymbol), "well-known symbol exclusion");
const undefinedSymbol = checker.resolveName("undefined", SemanticSymbolFlags.Value, answerName)!;
check(checker.isUndefinedSymbol(undefinedSymbol), "well-known undefined symbol");
const shorthand = declaration(root, "ref").initializer!.properties![0]!;
check(checker.getShorthandAssignmentValueSymbol(shorthand) === answerSymbol, "shorthand symbol");
const exportSpecifier = renamed.declarations[0]!.resolve()!;
check(checker.getExportSpecifierLocalTargetSymbol(exportSpecifier) === answerSymbol, "export specifier symbol");
check(checker.getConstantValue(declaration(root, "Mode").members![0]!) === 3, "enum constant");
check(checker.getConstantValue(answerName) === undefined, "missing constant");
check(checker.getContextualType(declaration(root, "pair").initializer!) === pair, "contextual tuple type");
check(checker.getTypeFromTypeNode(declaration(root, "Choice").type!) === choice, "type-node query identity");
check(!checker.isContextSensitive(answerName), "context sensitivity query");
const conditional = typeAt(checker, declaration(root, "Conditional").type!);
check(conditional.isConditionalType() && conditional.getCheckType()!.isTypeParameter() && conditional.getExtendsType() === intrinsicString, "conditional metadata");
check(conditional.getTrueType() === intrinsicNumber && conditional.getFalseType() === checker.getBooleanType(), "conditional lazy branches");
const indexed = typeAt(checker, declaration(root, "Indexed").type!);
check(indexed.isIndexedAccessType() && indexed.getObjectType()!.isTypeParameter() && indexed.getIndexType()!.value === "item", "indexed access type");
const keys = typeAt(checker, declaration(root, "Keys").type!);
check(keys.isIndexType() && keys.getTarget()!.isTypeParameter(), "index type target");
const template = typeAt(checker, declaration(root, "Template").type!);
check(template.isTemplateLiteralType() && template.texts!.join("|") === "prefix|suffix" && template.getTypes()![0]!.isTypeParameter(), "template literal type metadata");
const mapped = typeAt(checker, declaration(root, "Mapped").type!);
check(mapped.isStringMappingType() && mapped.getTarget()!.isTypeParameter(), "string mapping type");
check(checker.typeToTypeNode(intrinsicNumber, answer)!.kind === AstKind.NumberKeyword, "binary type node");
check(checker.signatureToSignatureDeclaration(signature, AstKind.FunctionType, identity)!.kind === AstKind.FunctionType, "binary signature node");
const references = checker.getReferencesToSymbolInFile(root.fileName, answerSymbol);
check(references.length > 0 && references.every((handle) => handle.resolve() !== undefined), "reference node handles");
check(checker.getReferencedSymbolsForNode(root, answerName.getStart()).length === 0, "empty referenced-symbol query");
const usages = checker.getSignatureUsage(identity);
check(usages.length === 1 && usages[0]!.name.resolve()!.text === "identity" && usages[0]!.call!.resolve() === declaration(root, "called").initializer, "signature usage handles");
const completions = checker.getCompletionsAtPosition(root.fileName, root.text!.indexOf("Mode.Ready") + 5, { includeSymbol: true })!;
const ready = completions.entries.find((entry) => entry.name === "Ready")!;
check(ready.symbol === symbolAt(checker, declaration(root, "Mode").members![0]!.name!), "completion symbol identity");
check(checker.getCompletionsAtPosition(root.fileName, answerName.getStart()) === undefined, "absent completions");
check(answerType.flags === SemanticTypeFlags.NumberLiteral, "literal flags pin");
// Disposed object graphs must not fetch a replacement under recycled ids.
snapshot.dispose();
snapshot.dispose();
let refused = false;
try { answerType.getRegularType(); } catch { refused = true; }
check(refused, "type access after disposal");
refused = false;
try { answerSymbol.declarations[0]!.resolve(); } catch { refused = true; }
check(refused, "declaration access after disposal");
}
@@ -92,15 +92,17 @@ for (const backend of ["c", "llvm"] as const) {
expect(built.backend).toBe(backend);
const oracle = await runClient(process.execPath, ["--import", "tsx", entry], dir, join(dir, "oracle.json"));
const native = await runClient(built.binaryPath, [], dir, join(dir, "native.json"));
const { surrogateBoundary: oracleSurrogates, ...oracleFacts } = JSON.parse(oracle);
const { surrogateBoundary: nativeSurrogates, ...nativeFacts } = JSON.parse(native);
const { surrogateBoundary: oracleSurrogates, semanticSurrogateBoundary: oracleSemanticSurrogates, ...oracleFacts } = JSON.parse(oracle);
const { surrogateBoundary: nativeSurrogates, semanticSurrogateBoundary: nativeSemanticSurrogates, ...nativeFacts } = JSON.parse(native);
// The native runtime's documented UTF-16 limit must surface as an
// explicit AST refusal; replacing a checker name would be corruption.
expect(oracleSurrogates).toBe("preserved");
expect(nativeSurrogates).toBe("refused");
expect(oracleSemanticSurrogates).toBe("preserved");
expect(nativeSemanticSurrogates).toBe("refused");
expect(nativeFacts).toEqual(oracleFacts);
expect(nativeFacts).toEqual({
typeText: "42", symbol: "answer", diagnostics: [2322], echo: true, binaryAst: true, astIdentity: true,
typeText: "42", symbol: "answer", diagnostics: [2322], echo: true, binaryAst: true, astIdentity: true, semanticModel: true,
virtualFiles: true, retainedSnapshot: true, serverErrorRecovery: true, protocolFailures: true,
});
} finally {