diff --git a/.vscode/launch.json b/.vscode/launch.json index ed3d5cbab8c..47305fe2f1b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -119,8 +119,18 @@ "request": "launch", "args": ["--extensionDevelopmentPath=${workspaceFolder}/packages/cadl-vscode"], "env": { - "CADL_DEVELOPMENT_MODE": "true", - "CADL_SERVER_NODE_OPTIONS": "--nolazy --inspect-brk=4242" + // Log elapsed time for each call to server. + //"CADL_SERVER_LOG_TIMING": "true", + + // Save .cpuprofile for last run of each server function here + // NOTE: This will add a lot of lag so don't trust logged timing if also enabled above. + //"CADL_SERVER_PROFILE_DIR": "${workspaceRoot}/temp", + + // Use empty node options and don't debug while profiling to get the most accurate timing + //"CADL_SERVER_NODE_OPTIONS": "", + + "CADL_SERVER_NODE_OPTIONS": "--nolazy --inspect-brk=4242", + "CADL_DEVELOPMENT_MODE": "true" }, "presentation": { "hidden": true diff --git a/common/changes/@cadl-lang/compiler/reuse-parse-and-bind_2022-08-22-18-59.json b/common/changes/@cadl-lang/compiler/reuse-parse-and-bind_2022-08-22-18-59.json new file mode 100644 index 00000000000..81ddbf7099b --- /dev/null +++ b/common/changes/@cadl-lang/compiler/reuse-parse-and-bind_2022-08-22-18-59.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@cadl-lang/compiler", + "comment": "Perf: Reuse unchanged files and programs in language server.", + "type": "minor" + } + ], + "packageName": "@cadl-lang/compiler" +} \ No newline at end of file diff --git a/common/changes/@cadl-lang/versioning/reuse-parse-and-bind_2022-08-22-18-59.json b/common/changes/@cadl-lang/versioning/reuse-parse-and-bind_2022-08-22-18-59.json new file mode 100644 index 00000000000..b925a7f965b --- /dev/null +++ b/common/changes/@cadl-lang/versioning/reuse-parse-and-bind_2022-08-22-18-59.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@cadl-lang/versioning", + "comment": "", + "type": "none" + } + ], + "packageName": "@cadl-lang/versioning" +} \ No newline at end of file diff --git a/common/changes/cadl-vscode/reuse-parse-and-bind_2022-08-22-18-59.json b/common/changes/cadl-vscode/reuse-parse-and-bind_2022-08-22-18-59.json new file mode 100644 index 00000000000..bc1c3e2f36c --- /dev/null +++ b/common/changes/cadl-vscode/reuse-parse-and-bind_2022-08-22-18-59.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "cadl-vscode", + "comment": "", + "type": "none" + } + ], + "packageName": "cadl-vscode" +} \ No newline at end of file diff --git a/packages/cadl-vscode/src/extension.ts b/packages/cadl-vscode/src/extension.ts index 86c00018017..a7e2c9d1c41 100644 --- a/packages/cadl-vscode/src/extension.ts +++ b/packages/cadl-vscode/src/extension.ts @@ -79,7 +79,7 @@ async function resolveCadlServer(context: ExtensionContext): Promise const script = context.asAbsolutePath("../compiler/dist/server/server.js"); // we use CLI instead of NODE_OPTIONS environment variable in this case // because --nolazy is not supported by NODE_OPTIONS. - const options = nodeOptions?.split(" ") ?? []; + const options = nodeOptions?.split(" ").filter((o) => o) ?? []; return { command: "node", args: [...options, script, ...args] }; } diff --git a/packages/compiler/config/config-loader.ts b/packages/compiler/config/config-loader.ts index b2169232599..d6b9da22199 100644 --- a/packages/compiler/config/config-loader.ts +++ b/packages/compiler/config/config-loader.ts @@ -1,6 +1,6 @@ import jsyaml from "js-yaml"; import { getDirectoryPath, joinPaths, resolvePath } from "../core/path-utils.js"; -import { SchemaValidator } from "../core/schema-validator.js"; +import { createJSONSchemaValidator } from "../core/schema-validator.js"; import { CompilerHost, Diagnostic } from "../core/types.js"; import { deepClone, deepFreeze, doIO, loadFile } from "../core/util.js"; import { CadlConfigJsonSchema } from "./config-schema.js"; @@ -8,7 +8,7 @@ import { CadlConfig } from "./types.js"; export const CadlConfigFilename = "cadl-project.yaml"; -const defaultConfig: CadlConfig = deepFreeze({ +export const defaultConfig: CadlConfig = deepFreeze({ diagnostics: [], emitters: {}, }); @@ -88,7 +88,7 @@ export async function loadCadlConfigFile( }; } -const configValidator = new SchemaValidator(CadlConfigJsonSchema); +const configValidator = createJSONSchemaValidator(CadlConfigJsonSchema); async function loadConfigFile( host: CompilerHost, diff --git a/packages/compiler/core/binder.ts b/packages/compiler/core/binder.ts index ced611f4f64..3c293796e01 100644 --- a/packages/compiler/core/binder.ts +++ b/packages/compiler/core/binder.ts @@ -24,8 +24,8 @@ import { TemplateParameterDeclarationNode, UnionStatementNode, UsingStatementNode, - Writable, } from "./types.js"; +import { mutate } from "./util.js"; // Use a regular expression to define the prefix for Cadl-exposed functions // defined in JavaScript modules @@ -34,6 +34,20 @@ const DecoratorFunctionPattern = /^\$/; const SymbolTable = class extends Map implements SymbolTable { duplicates = new Map>(); + constructor(source?: SymbolTable) { + super(); + + if (source) { + for (const [key, value] of source) { + // Note: shallow copy of value here so we can mutate flags on set. + super.set(key, { ...value }); + } + for (const [key, value] of source.duplicates) { + this.duplicates.set(key, new Set(value)); + } + } + } + // First set for a given key wins, but record all duplicates for diagnostics. set(key: string, value: Sym) { const existing = super.get(key); @@ -41,7 +55,7 @@ const SymbolTable = class extends Map implements SymbolTable { super.set(key, value); } else { if (existing.flags & SymbolFlags.Using) { - existing.flags |= SymbolFlags.DuplicateUsing; + mutate(existing).flags |= SymbolFlags.DuplicateUsing; } const duplicateArray = this.duplicates.get(existing); @@ -56,24 +70,17 @@ const SymbolTable = class extends Map implements SymbolTable { }; export interface Binder { - bindSourceFile(sourceFile: CadlScriptNode): void; + bindSourceFile(script: CadlScriptNode): void; bindJsSourceFile(sourceFile: JsSourceFileNode): void; - bindNode(node: Node): void; } -export function createSymbolTable(): SymbolTable { - return new SymbolTable(); +export function createSymbolTable(source?: SymbolTable): SymbolTable { + return new SymbolTable(source); } -export interface BinderOptions { - // Configures the initial parent node to use when calling bindNode. This is - // useful for binding Cadl fragments outside the context of a full script node. - initialParentNode?: Node; -} - -export function createBinder(program: Program, options: BinderOptions = {}): Binder { +export function createBinder(program: Program): Binder { let currentFile: CadlScriptNode; - let parentNode: Node | undefined = options?.initialParentNode; + let parentNode: Node | undefined; let fileNamespace: NamespaceStatementNode | undefined; let scope: ScopeNode; @@ -83,7 +90,6 @@ export function createBinder(program: Program, options: BinderOptions = {}): Bin return { bindSourceFile, - bindNode, bindJsSourceFile, }; @@ -95,9 +101,17 @@ export function createBinder(program: Program, options: BinderOptions = {}): Bin return name.replace(DecoratorFunctionPattern, ""); } - function bindJsSourceFile(sourceFile: Writable) { + function bindJsSourceFile(sourceFile: JsSourceFileNode) { + // cast because it causes TS to make the type of .symbol never other + if ((sourceFile.symbol as any) !== undefined) { + return; + } fileNamespace = undefined; - sourceFile.symbol = createSymbol(sourceFile, sourceFile.file.path, SymbolFlags.SourceFile); + mutate(sourceFile).symbol = createSymbol( + sourceFile, + sourceFile.file.path, + SymbolFlags.SourceFile + ); const rootNs = sourceFile.esmExports["namespace"]; for (const [key, member] of Object.entries(sourceFile.esmExports)) { @@ -142,15 +156,15 @@ export function createBinder(program: Program, options: BinderOptions = {}): Bin containerSymbol = existingBinding; } else { // we have some conflict, lets report a duplicate binding error. - containerSymbol.exports!.set( + mutate(containerSymbol.exports)!.set( part, createSymbol(sourceFile, part, SymbolFlags.Namespace, containerSymbol) ); } } else { const sym = createSymbol(sourceFile, part, SymbolFlags.Namespace, containerSymbol); - sym.exports = createSymbolTable(); - containerSymbol.exports!.set(part, sym); + mutate(sym).exports = createSymbolTable(); + mutate(containerSymbol.exports!).set(part, sym); containerSymbol = sym; } } @@ -160,25 +174,30 @@ export function createBinder(program: Program, options: BinderOptions = {}): Bin } else { sym = createSymbol(sourceFile, name, SymbolFlags.Function, containerSymbol); } - sym.value = member as any; - containerSymbol.exports!.set(sym.name, sym); + mutate(sym).value = member as any; + mutate(containerSymbol.exports)!.set(sym.name, sym); } } } - function bindSourceFile(sourceFile: Writable) { - sourceFile.symbol = createSymbol(sourceFile, sourceFile.file.path, SymbolFlags.SourceFile); - sourceFile.symbol.exports = createSymbolTable(); + function bindSourceFile(script: CadlScriptNode) { + if (script.locals !== undefined) { + return; + } + + mutate(script).locals = createSymbolTable(); + mutate(script).symbol = createSymbol(script, script.file.path, SymbolFlags.SourceFile); + mutate(script.symbol).exports = createSymbolTable(); fileNamespace = undefined; - currentFile = sourceFile; - scope = sourceFile; - bindNode(sourceFile); + currentFile = script; + scope = script; + bindNode(script); } function bindNode(node: Node) { if (!node) return; // set the node's parent since we're going for a walk anyway - node.parent = parentNode; + mutate(node).parent = parentNode; switch (node.kind) { case SyntaxKind.ModelStatement: @@ -246,8 +265,8 @@ export function createBinder(program: Program, options: BinderOptions = {}): Bin parentNode = prevParent; } - function bindProjection(node: Writable) { - node.locals = new SymbolTable(); + function bindProjection(node: ProjectionNode) { + mutate(node).locals = createSymbolTable(); } /** @@ -264,7 +283,7 @@ export function createBinder(program: Program, options: BinderOptions = {}): Bin * multiple times for the same symbol. * */ - function bindProjectionStatement(node: Writable) { + function bindProjectionStatement(node: ProjectionStatementNode) { const name = node.id.sv; const table: SymbolTable = (scope as NamespaceStatementNode | CadlScriptNode).symbol.exports!; let sym: Sym; @@ -275,13 +294,13 @@ export function createBinder(program: Program, options: BinderOptions = {}): Bin declareSymbol(node, SymbolFlags.Projection); return; } - sym.declarations.push(node); + mutate(sym.declarations).push(node); } else { sym = createSymbol(node, name, SymbolFlags.Projection, scope.symbol); - table.set(name, sym); + mutate(table).set(name, sym); } - node.symbol = sym; + mutate(node).symbol = sym; if ( node.selector.kind !== SyntaxKind.Identifier && @@ -322,8 +341,8 @@ export function createBinder(program: Program, options: BinderOptions = {}): Bin declareSymbol(node, SymbolFlags.FunctionParameter); } - function bindProjectionLambdaExpression(node: Writable) { - node.locals = new SymbolTable(); + function bindProjectionLambdaExpression(node: ProjectionLambdaExpressionNode) { + mutate(node).locals = new SymbolTable(); } function bindTemplateParameterDeclaration(node: TemplateParameterDeclarationNode) { @@ -333,40 +352,40 @@ export function createBinder(program: Program, options: BinderOptions = {}): Bin function bindModelStatement(node: ModelStatementNode) { declareSymbol(node, SymbolFlags.Model); // Initialize locals for type parameters - node.locals = new SymbolTable(); + mutate(node).locals = new SymbolTable(); } function bindInterfaceStatement(node: InterfaceStatementNode) { declareSymbol(node, SymbolFlags.Interface); - node.locals = new SymbolTable(); + mutate(node).locals = new SymbolTable(); } function bindUnionStatement(node: UnionStatementNode) { declareSymbol(node, SymbolFlags.Union); - node.locals = new SymbolTable(); + mutate(node).locals = new SymbolTable(); } function bindAliasStatement(node: AliasStatementNode) { declareSymbol(node, SymbolFlags.Alias); // Initialize locals for type parameters - node.locals = new SymbolTable(); + mutate(node).locals = new SymbolTable(); } function bindEnumStatement(node: EnumStatementNode) { declareSymbol(node, SymbolFlags.Enum); } - function bindNamespaceStatement(statement: Writable) { + function bindNamespaceStatement(statement: NamespaceStatementNode) { // check if there's an existing symbol for this namespace const existingBinding = (scope as NamespaceStatementNode).symbol.exports!.get(statement.id.sv); if (existingBinding && existingBinding.flags & SymbolFlags.Namespace) { - statement.symbol = existingBinding; + mutate(statement).symbol = existingBinding; // locals are never shared. - statement.locals = createSymbolTable(); - existingBinding.declarations.push(statement); + mutate(statement).locals = createSymbolTable(); + mutate(existingBinding.declarations).push(statement); } else { // Initialize locals for non-exported symbols - statement.locals = createSymbolTable(); + mutate(statement).locals = createSymbolTable(); declareSymbol(statement, SymbolFlags.Namespace); } @@ -383,17 +402,17 @@ export function createBinder(program: Program, options: BinderOptions = {}): Bin } function bindUsingStatement(statement: UsingStatementNode) { - (currentFile.usings as UsingStatementNode[]).push(statement); + mutate(currentFile.usings).push(statement); } function bindOperationStatement(statement: OperationStatementNode) { if (scope.kind !== SyntaxKind.InterfaceStatement) { declareSymbol(statement, SymbolFlags.Operation); - statement.locals = new SymbolTable(); + mutate(statement).locals = createSymbolTable(); } } - function declareSymbol(node: Writable, flags: SymbolFlags) { + function declareSymbol(node: Declaration, flags: SymbolFlags) { switch (scope.kind) { case SyntaxKind.NamespaceStatement: return declareNamespaceMember(node, flags); @@ -402,13 +421,13 @@ export function createBinder(program: Program, options: BinderOptions = {}): Bin return declareScriptMember(node, flags); default: const symbol = createSymbol(node, node.id.sv, flags, scope.symbol); - node.symbol = symbol; - scope.locals!.set(node.id.sv, symbol); + mutate(node).symbol = symbol; + mutate(scope.locals!).set(node.id.sv, symbol); return symbol; } } - function declareNamespaceMember(node: Writable, flags: SymbolFlags) { + function declareNamespaceMember(node: Declaration, flags: SymbolFlags) { if ( flags & SymbolFlags.Namespace && mergeNamespaceDeclarations(node as NamespaceStatementNode, scope) @@ -416,12 +435,12 @@ export function createBinder(program: Program, options: BinderOptions = {}): Bin return; } const symbol = createSymbol(node, node.id.sv, flags, scope.symbol); - node.symbol = symbol; - scope.symbol.exports!.set(node.id.sv, symbol); + mutate(node).symbol = symbol; + mutate(scope.symbol.exports)!.set(node.id.sv, symbol); return symbol; } - function declareScriptMember(node: Writable, flags: SymbolFlags) { + function declareScriptMember(node: Declaration, flags: SymbolFlags) { const effectiveScope = fileNamespace ?? scope; if ( flags & SymbolFlags.Namespace && @@ -430,18 +449,18 @@ export function createBinder(program: Program, options: BinderOptions = {}): Bin return; } const symbol = createSymbol(node, node.id.sv, flags, fileNamespace?.symbol); - node.symbol = symbol; - effectiveScope.symbol.exports!.set(node.id.sv, symbol); + mutate(node).symbol = symbol; + mutate(effectiveScope.symbol.exports!).set(node.id.sv, symbol); return symbol; } - function mergeNamespaceDeclarations(node: Writable, scope: ScopeNode) { + function mergeNamespaceDeclarations(node: NamespaceStatementNode, scope: ScopeNode) { // we are declaring a namespace in either global scope, or a blockless namespace. const existingBinding = scope.symbol.exports!.get(node.id.sv); if (existingBinding) { // we have an existing binding, so just push this node to its declarations - existingBinding!.declarations.push(node); - node.symbol = existingBinding; + mutate(existingBinding!.declarations).push(node); + mutate(node).symbol = existingBinding; return true; } return false; diff --git a/packages/compiler/core/checker.ts b/packages/compiler/core/checker.ts index 669a4165812..16ee5ffbc69 100644 --- a/packages/compiler/core/checker.ts +++ b/packages/compiler/core/checker.ts @@ -105,9 +105,8 @@ import { UnionStatementNode, UnionVariant, UnionVariantNode, - Writable, } from "./types.js"; -import { isArray } from "./util.js"; +import { isArray, Mutable, mutate } from "./util.js"; export interface TypeNameOptions { namespaceFilter: (ns: Namespace) => boolean; @@ -266,11 +265,14 @@ const TypeInstantiationMap = class type StdTypeName = IntrinsicModelName | "Array" | "Record"; +let currentSymbolId = 0; + export function createChecker(program: Program): Checker { - let currentSymbolId = 0; const stdTypes: Partial> = {}; const symbolLinks = new Map(); const mergedSymbols = new Map(); + const augmentedSymbolTables = new Map(); + const typePrototype: TypePrototype = { get projections(): ProjectionStatementNode[] { return (projectionsByTypeKind.get((this as Type).kind) || []).concat( @@ -330,10 +332,7 @@ export function createChecker(program: Program): Checker { cadlNamespaceNode = cadlNamespaceBinding.declarations[1] as NamespaceStatementNode; initializeCadlIntrinsics(); for (const file of program.sourceFiles.values()) { - for (const [name, binding] of cadlNamespaceBinding.exports!) { - const usedSym = createUsingSymbol(binding); - file.locals!.set(name, usedSym); - } + addUsingSymbols(cadlNamespaceBinding.exports!, file.locals); } } @@ -376,7 +375,7 @@ export function createChecker(program: Program): Checker { function initializeCadlIntrinsics() { // a utility function to log strings or numbers - cadlNamespaceBinding!.exports!.set("log", { + mutate(cadlNamespaceBinding!.exports)!.set("log", { flags: SymbolFlags.Function, name: "log", value(p: Program, str: string): Type { @@ -433,19 +432,42 @@ export function createChecker(program: Program): Checker { continue; } usedUsing.add(namespaceSym); - - for (const [name, binding] of sym.exports!) { - parentNs.locals!.set(name, createUsingSymbol(binding)); - } + addUsingSymbols(sym.exports!, parentNs.locals!); } if (cadlNamespaceNode) { - for (const [name, binding] of cadlNamespaceBinding!.exports!) { - file.locals!.set(name, createUsingSymbol(binding)); - } + addUsingSymbols(cadlNamespaceBinding!.exports!, file.locals); + } + } + + function addUsingSymbols(source: SymbolTable, destination: SymbolTable): void { + const augmented = getOrCreateAugmentedSymbolTable(destination); + for (const symbolSource of source.values()) { + const sym: Sym = { + flags: SymbolFlags.Using, + declarations: [], + name: symbolSource.name, + symbolSource: symbolSource, + }; + augmented.set(sym.name, sym); } } + /** + * We cannot inject symbols into the symbol tables hanging off syntax tree nodes as + * syntax tree nodes can be shared by other programs. This is called as a copy-on-write + * to inject using and late-bound symbols, and then we use the copy when resolving + * in the table. + */ + function getOrCreateAugmentedSymbolTable(table: SymbolTable): Mutable { + let augmented = augmentedSymbolTables.get(table); + if (!augmented) { + augmented = createSymbolTable(table); + augmentedSymbolTables.set(table, augmented); + } + return mutate(augmented); + } + /** * Create the link for the given type to the symbol links. * If currently instantiating a template it will link to the instantiations. @@ -1428,10 +1450,9 @@ export function createChecker(program: Program): Checker { function getSymbolId(s: Sym) { if (s.id === undefined) { - s.id = currentSymbolId++; + mutate(s).id = currentSymbolId++; } - - return s.id; + return s.id!; } function resolveIdentifierInTable( @@ -1442,6 +1463,8 @@ export function createChecker(program: Program): Checker { if (!table) { return undefined; } + + table = augmentedSymbolTables.get(table) ?? table; let sym; if (resolveDecorator) { sym = table.get("@" + node.sv); @@ -1599,6 +1622,7 @@ export function createChecker(program: Program): Checker { if (!table) { return; } + table = augmentedSymbolTables.get(table) ?? table; for (const [key, sym] of table) { if (sym.flags & SymbolFlags.DuplicateUsing) { const duplicates = table.duplicates.get(sym)!; @@ -1663,23 +1687,17 @@ export function createChecker(program: Program): Checker { } let scope: Node | undefined = node.parent; - let binding; + let binding: Sym | undefined; while (scope && scope.kind !== SyntaxKind.CadlScript) { if (scope.symbol && "exports" in scope.symbol) { const mergedSymbol = getMergedSymbol(scope.symbol); binding = resolveIdentifierInTable(node, mergedSymbol.exports, resolveDecorator); - if (binding) return binding; } if ("locals" in scope) { - if ("duplicates" in scope.locals!) { - binding = resolveIdentifierInTable(node, scope.locals, resolveDecorator); - } else { - binding = resolveIdentifierInTable(node, scope.locals, resolveDecorator); - } - + binding = resolveIdentifierInTable(node, scope.locals, resolveDecorator); if (binding) return binding; } @@ -2128,7 +2146,7 @@ export function createChecker(program: Program): Checker { switch (type.kind) { case "Model": type.symbol = createSymbol(type.node, type.name, SymbolFlags.Model | SymbolFlags.LateBound); - type.symbol.type = type; + mutate(type.symbol).type = type; break; case "Interface": type.symbol = createSymbol( @@ -2136,64 +2154,54 @@ export function createChecker(program: Program): Checker { type.name, SymbolFlags.Interface | SymbolFlags.LateBound ); - type.symbol.type = type; + mutate(type.symbol).type = type; break; case "Union": if (!type.name) return; // don't make a symbol for anonymous unions type.symbol = createSymbol(type.node, type.name, SymbolFlags.Union | SymbolFlags.LateBound); - type.symbol.type = type; + mutate(type.symbol).type = type; break; } } function lateBindMembers(type: Type, containerSym: Sym) { + let containerMembers: Mutable | undefined; + switch (type.kind) { case "Model": for (const prop of walkPropertiesInherited(type)) { - const sym = createSymbol( - prop.node, - prop.name, - SymbolFlags.ModelProperty | SymbolFlags.LateBound - ); - sym.type = prop; - containerSym.members!.set(prop.name, sym); + lateBindMember(prop, SymbolFlags.ModelProperty); } break; case "Enum": for (const member of type.members) { - const sym = createSymbol( - member.node, - member.name, - SymbolFlags.EnumMember | SymbolFlags.LateBound - ); - sym.type = member; - containerSym.members!.set(member.name, sym); + lateBindMember(member, SymbolFlags.EnumMember); } - break; case "Interface": for (const member of type.operations.values()) { - const sym = createSymbol( - member.node, - member.name, - SymbolFlags.InterfaceMember | SymbolFlags.LateBound - ); - sym.type = member; - containerSym.members!.set(member.name, sym); + lateBindMember(member, SymbolFlags.InterfaceMember); } break; case "Union": for (const variant of type.variants.values()) { - // don't bind anything for union expressions - if (!variant.node || typeof variant.name === "symbol") continue; - const sym = createSymbol( - variant.node, - variant.name, - SymbolFlags.UnionVariant | SymbolFlags.LateBound - ); - sym.type = variant; - containerSym.members!.set(variant.name, sym); + lateBindMember(variant, SymbolFlags.UnionVariant); } + break; + } + + function lateBindMember( + member: Type & { node?: Node; name: string | symbol }, + kind: SymbolFlags + ) { + if (!member.node || typeof member.name !== "string") { + // don't bind anything for union expressions + return; + } + const sym = createSymbol(member.node, member.name, kind | SymbolFlags.LateBound); + mutate(sym).type = member; + containerMembers ??= getOrCreateAugmentedSymbolTable(containerSym.members!); + containerMembers.set(member.name, sym); } } @@ -2894,10 +2902,10 @@ export function createChecker(program: Program): Checker { for (const [sym, duplicates] of source.duplicates) { const targetSet = target.duplicates.get(sym); if (targetSet === undefined) { - target.duplicates.set(sym, new Set([...duplicates])); + mutate(target.duplicates).set(sym, new Set([...duplicates])); } else { for (const duplicate of duplicates) { - targetSet.add(duplicate); + mutate(targetSet).add(duplicate); } } } @@ -2912,18 +2920,18 @@ export function createChecker(program: Program): Checker { existingBinding = { ...sourceBinding, }; - target.set(key, existingBinding); + mutate(target).set(key, existingBinding); mergedSymbols.set(sourceBinding, existingBinding); } else if (existingBinding.flags & SymbolFlags.Namespace) { - existingBinding.declarations.push(...sourceBinding.declarations); + mutate(existingBinding.declarations).push(...sourceBinding.declarations); mergedSymbols.set(sourceBinding, existingBinding); - mergeSymbolTable(sourceBinding.exports!, existingBinding.exports!); + mergeSymbolTable(sourceBinding.exports!, mutate(existingBinding.exports!)); } else { // this will set a duplicate error - target.set(key, sourceBinding); + mutate(target).set(key, sourceBinding); } } else { - target.set(key, sourceBinding); + mutate(target).set(key, sourceBinding); } } } @@ -2939,21 +2947,21 @@ export function createChecker(program: Program): Checker { pos: 0, end: 0, sv: "__GLOBAL_NS", - symbol: undefined as any, + symbol: undefined!, flags: NodeFlags.Synthetic, }; - const nsNode: Writable = { + const nsNode: NamespaceStatementNode = { kind: SyntaxKind.NamespaceStatement, decorators: [], pos: 0, end: 0, id: nsId, - symbol: undefined as any, + symbol: undefined!, locals: createSymbolTable(), flags: NodeFlags.Synthetic, }; - nsNode.symbol = createSymbol(nsNode, "__GLOBAL_NS", SymbolFlags.Namespace); + mutate(nsNode).symbol = createSymbol(nsNode, "__GLOBAL_NS", SymbolFlags.Namespace); return nsNode; } @@ -4231,10 +4239,6 @@ function isErrorType(type: Type): type is ErrorType { return type.kind === "Intrinsic" && type.name === "ErrorType"; } -function createUsingSymbol(symbolSource: Sym): Sym { - return { flags: SymbolFlags.Using, declarations: [], name: symbolSource.name, symbolSource }; -} - const numericRanges = { int64: [BigInt("-9223372036854775807"), BigInt("9223372036854775808"), { int: true }], int32: [-2147483648, 2147483647, { int: true }], diff --git a/packages/compiler/core/library.ts b/packages/compiler/core/library.ts index 285f654de5a..843cdbaa949 100644 --- a/packages/compiler/core/library.ts +++ b/packages/compiler/core/library.ts @@ -1,5 +1,12 @@ import { createDiagnosticCreator } from "./diagnostics.js"; -import { CadlLibrary, CadlLibraryDef, CallableMessage, DiagnosticMessages } from "./types.js"; +import { createJSONSchemaValidator } from "./schema-validator.js"; +import { + CadlLibrary, + CadlLibraryDef, + CallableMessage, + DiagnosticMessages, + JSONSchemaValidator, +} from "./types.js"; const globalLibraryUrlsLoadedSym = Symbol.for("CADL_LIBRARY_URLS_LOADED"); if ((globalThis as any)[globalLibraryUrlsLoadedSym] === undefined) { @@ -37,6 +44,8 @@ export function createCadlLibrary< T extends { [code: string]: DiagnosticMessages }, E extends Record >(lib: Readonly>): CadlLibrary { + let emitterOptionValidator: JSONSchemaValidator; + const { reportDiagnostic, createDiagnostic } = createDiagnosticCreator(lib.diagnostics, lib.name); function createStateSymbol(name: string): symbol { return Symbol.for(`${lib.name}.${name}`); @@ -44,7 +53,20 @@ export function createCadlLibrary< const caller = getCaller(); loadedUrls.add(caller); - return { ...lib, reportDiagnostic, createDiagnostic, createStateSymbol }; + return { + ...lib, + reportDiagnostic, + createDiagnostic, + createStateSymbol, + get emitterOptionValidator() { + if (!emitterOptionValidator && lib.emitter?.options) { + emitterOptionValidator = createJSONSchemaValidator(lib.emitter.options, { + coerceTypes: true, + }); + } + return emitterOptionValidator; + }, + }; } export function paramMessage( diff --git a/packages/compiler/core/options.ts b/packages/compiler/core/options.ts index 8c81fb78b7d..4f22e59ca22 100644 --- a/packages/compiler/core/options.ts +++ b/packages/compiler/core/options.ts @@ -1,4 +1,4 @@ -import { LogLevel } from "./types"; +import { LogLevel, ParseOptions } from "./types"; export interface CompilerOptions { miscOptions?: any; @@ -16,4 +16,6 @@ export interface CompilerOptions { * analysis in the language server. */ designTimeBuild?: boolean; + + parseOptions?: ParseOptions; } diff --git a/packages/compiler/core/parser.ts b/packages/compiler/core/parser.ts index ef1ca275991..5783f0d850a 100644 --- a/packages/compiler/core/parser.ts +++ b/packages/compiler/core/parser.ts @@ -1,4 +1,3 @@ -import { createSymbolTable } from "./binder.js"; import { codePointBefore, isIdentifierContinue } from "./charcode.js"; import { compilerAssert } from "./diagnostics.js"; import { CompilerDiagnostics, createDiagnostic } from "./messages.js"; @@ -48,6 +47,7 @@ import { NumericLiteralNode, OperationSignature, OperationStatementNode, + ParseOptions, ProjectionBlockExpressionNode, ProjectionEnumSelectorNode, ProjectionExpression, @@ -80,9 +80,8 @@ import { UnionVariantNode, UsingStatementNode, VoidKeywordNode, - Writable, } from "./types.js"; -import { isArray } from "./util.js"; +import { isArray, mutate } from "./util.js"; /** * Callback to parse each element in a delimited list @@ -240,13 +239,6 @@ namespace ListKind { } as const; } -export interface ParseOptions { - /** - * Include comments in resulting output. - */ - comments?: boolean; -} - export function parse(code: string | SourceFile, options: ParseOptions = {}): CadlScriptNode { let parseErrorInNextFinishedNode = false; let previousTokenEnd = -1; @@ -276,11 +268,12 @@ export function parse(code: string | SourceFile, options: ParseOptions = {}): Ca } as any, namespaces: [], usings: [], - locals: createSymbolTable(), + locals: undefined!, inScopeNamespaces: [], parseDiagnostics, comments, printable: treePrintable, + parseOptions: options, ...finishNode(0), }; } @@ -295,7 +288,7 @@ export function parse(code: string | SourceFile, options: ParseOptions = {}): Ca const directives = parseDirectiveList(); const decorators = parseDecoratorList(); const tok = token(); - let item: Writable; + let item: Statement; switch (tok) { case Token.ImportKeyword: reportInvalidDecorators(decorators, "import statement"); @@ -340,7 +333,7 @@ export function parse(code: string | SourceFile, options: ParseOptions = {}): Ca break; } - item.directives = directives; + mutate(item).directives = directives; if (isBlocklessNamespace(item)) { if (seenBlocklessNs) { @@ -375,7 +368,7 @@ export function parse(code: string | SourceFile, options: ParseOptions = {}): Ca const decorators = parseDecoratorList(); const tok = token(); - let item: Writable; + let item: Statement; switch (tok) { case Token.ImportKeyword: reportInvalidDecorators(decorators, "import statement"); @@ -428,7 +421,7 @@ export function parse(code: string | SourceFile, options: ParseOptions = {}): Ca item = parseInvalidStatement(pos, decorators); break; } - item.directives = directives; + mutate(item).directives = directives; stmts.push(item); } @@ -480,6 +473,7 @@ export function parse(code: string | SourceFile, options: ParseOptions = {}): Ca kind: SyntaxKind.NamespaceStatement, decorators, id: nsSegments[0], + locals: undefined!, statements, ...finishNode(pos), @@ -491,6 +485,7 @@ export function parse(code: string | SourceFile, options: ParseOptions = {}): Ca decorators: [], id: nsSegments[i], statements: outerNs, + locals: undefined!, ...finishNode(pos), }; } @@ -1933,7 +1928,7 @@ export function parse(code: string | SourceFile, options: ParseOptions = {}): Ca break; } - let item: Writable; + let item: T; if (kind.invalidDecoratorTarget) { item = (parseItem as ParseListItem)(); } else { @@ -1941,7 +1936,7 @@ export function parse(code: string | SourceFile, options: ParseOptions = {}): Ca } items.push(item); - item.directives = directives; + mutate(item).directives = directives; const delimiter = token(); const delimiterPos = tokenPos(); @@ -2484,24 +2479,24 @@ export function hasParseError(node: Node) { return node.flags & NodeFlags.DescendantHasError; } -function checkForDescendantErrors(node: Writable) { +function checkForDescendantErrors(node: Node) { if (node.flags & NodeFlags.DescendantErrorsExamined) { return; } - node.flags |= NodeFlags.DescendantErrorsExamined; + mutate(node).flags |= NodeFlags.DescendantErrorsExamined; - visitChildren(node, (child: Writable) => { + visitChildren(node, (child: Node) => { if (child.flags & NodeFlags.ThisNodeHasError) { - node.flags |= NodeFlags.DescendantHasError | NodeFlags.DescendantErrorsExamined; + mutate(node).flags |= NodeFlags.DescendantHasError | NodeFlags.DescendantErrorsExamined; return true; } checkForDescendantErrors(child); if (child.flags & NodeFlags.DescendantHasError) { - node.flags |= NodeFlags.DescendantHasError | NodeFlags.DescendantErrorsExamined; + mutate(node).flags |= NodeFlags.DescendantHasError | NodeFlags.DescendantErrorsExamined; return true; } - child.flags |= NodeFlags.DescendantErrorsExamined; + mutate(child).flags |= NodeFlags.DescendantErrorsExamined; return false; }); diff --git a/packages/compiler/core/program.ts b/packages/compiler/core/program.ts index a799da89397..ca57924f45f 100644 --- a/packages/compiler/core/program.ts +++ b/packages/compiler/core/program.ts @@ -16,7 +16,6 @@ import { CompilerOptions } from "./options.js"; import { isImportStatement, parse } from "./parser.js"; import { getDirectoryPath, joinPaths, resolvePath } from "./path-utils.js"; import { createProjector } from "./projector.js"; -import { SchemaValidator } from "./schema-validator.js"; import { CadlLibrary, CadlScriptNode, @@ -41,7 +40,7 @@ import { SyntaxKind, Type, } from "./types.js"; -import { doIO, findProjectRoot, loadFile } from "./util.js"; +import { deepEquals, doIO, findProjectRoot, loadFile, mapEquals } from "./util.js"; export interface Program { compilerOptions: CompilerOptions; @@ -203,7 +202,8 @@ interface CadlLibraryReference { export async function createProgram( host: CompilerHost, mainFile: string, - options: CompilerOptions = {} + options: CompilerOptions = {}, + oldProgram?: Program // NOTE: deliberately separate from options to avoid memory leak by chaining all old programs together. ): Promise { const validateCbs: any = []; const stateMaps = new Map>(); @@ -280,6 +280,17 @@ export async function createProgram( await loadEmitters(resolvedMain, emitters); } + if ( + oldProgram && + mapEquals(oldProgram.sourceFiles, program.sourceFiles) && + deepEquals(oldProgram.compilerOptions, program.compilerOptions) + ) { + return oldProgram; + } + + // let GC reclaim old program, we do not reuse it beyond this point. + oldProgram = undefined; + program.checker = createChecker(program); program.checker.checkProgram(); @@ -431,13 +442,13 @@ export async function createProgram( sv: "", pos: 0, end: 0, - symbol: undefined as any, + symbol: undefined!, flags: NodeFlags.Synthetic, }, esmExports: exports, file, namespaceSymbols: [], - symbol: undefined as any, + symbol: undefined!, pos: 0, end: 0, flags: NodeFlags.None, @@ -451,24 +462,33 @@ export async function createProgram( const file = await loadJsFile(path, diagnosticTarget); if (file !== undefined) { program.jsSourceFiles.set(path, file); - if (file.symbol === undefined) { - binder.bindJsSourceFile(file); - } + binder.bindJsSourceFile(file); } } - async function loadCadlScript(cadlScript: SourceFile): Promise { + async function loadCadlScript(file: SourceFile): Promise { // This is not a diagnostic because the compiler should never reuse the same path. // It's the caller's responsibility to use unique paths. - if (program.sourceFiles.has(cadlScript.path)) { - throw new RangeError("Duplicate script path: " + cadlScript); + if (program.sourceFiles.has(file.path)) { + throw new RangeError("Duplicate script path: " + file.path); } - const sourceFile = parse(cadlScript); - program.reportDiagnostics(sourceFile.parseDiagnostics); - program.sourceFiles.set(cadlScript.path, sourceFile); - binder.bindSourceFile(sourceFile); - await loadScriptImports(sourceFile); - return sourceFile; + + const script = parseOrReuse(file); + program.reportDiagnostics(script.parseDiagnostics); + program.sourceFiles.set(file.path, script); + binder.bindSourceFile(script); + await loadScriptImports(script); + return script; + } + + function parseOrReuse(file: SourceFile): CadlScriptNode { + const old = oldProgram?.sourceFiles.get(file.path) ?? host?.parseCache?.get(file); + if (old?.file === file && deepEquals(old.parseOptions, options.parseOptions)) { + return old; + } + const script = parse(file, options.parseOptions); + host.parseCache?.set(file, script); + return script; } async function loadScriptImports(file: CadlScriptNode) { @@ -565,11 +585,8 @@ export async function createProgram( } if (emitterFunction !== undefined) { if (libDefinition?.emitter?.options) { - const optionValidator = new SchemaValidator(libDefinition.emitter?.options, { - coerceTypes: true, - }); - const diagnostics = optionValidator.validate(options, NoTarget); - if (diagnostics.length > 0) { + const diagnostics = libDefinition?.emitterOptionValidator?.validate(options, NoTarget); + if (diagnostics && diagnostics.length > 0) { program.reportDiagnostics(diagnostics); return; } @@ -965,9 +982,10 @@ export async function createProgram( export async function compile( mainFile: string, host: CompilerHost, - options?: CompilerOptions + options?: CompilerOptions, + oldProgram?: Program ): Promise { - return await createProgram(host, mainFile, options); + return await createProgram(host, mainFile, options, oldProgram); } function computeEmitters( diff --git a/packages/compiler/core/projection-members.ts b/packages/compiler/core/projection-members.ts index 2092443722c..6e1bcd04eba 100644 --- a/packages/compiler/core/projection-members.ts +++ b/packages/compiler/core/projection-members.ts @@ -81,7 +81,7 @@ export function createProjectionMembers(checker: Checker): { name, optional: false, decorators: [], - node: undefined as any, + node: undefined!, default: defaultT, type, }) @@ -264,7 +264,7 @@ export function createProjectionMembers(checker: Checker): { createType({ kind: "Operation", name, - node: undefined as any, + node: undefined!, parameters, returnType, decorators: [], @@ -333,7 +333,7 @@ export function createProjectionMembers(checker: Checker): { enum: base, name, decorators: [], - node: undefined as any, + node: undefined!, value: type ? type.value : undefined, }) ); diff --git a/packages/compiler/core/schema-validator.ts b/packages/compiler/core/schema-validator.ts index bdcea338fdb..6a4477ce656 100644 --- a/packages/compiler/core/schema-validator.ts +++ b/packages/compiler/core/schema-validator.ts @@ -1,29 +1,24 @@ -import Ajv, { ErrorObject, JSONSchemaType } from "ajv"; +import Ajv, { ErrorObject } from "ajv"; import { compilerAssert } from "./diagnostics.js"; -import { NoTarget } from "./index.js"; -import { Diagnostic, SourceFile } from "./types.js"; +import { Diagnostic, JSONSchemaType, JSONSchemaValidator, NoTarget, SourceFile } from "./types.js"; -export interface SchemaValidatorOptions { +export interface JSONSchemaValidatorOptions { coerceTypes?: boolean; } -export class SchemaValidator { - private ajv: Ajv; - public constructor(private schema: JSONSchemaType, options: SchemaValidatorOptions = {}) { - this.ajv = new Ajv({ - strict: true, - coerceTypes: options.coerceTypes, - }); - } +export function createJSONSchemaValidator( + schema: JSONSchemaType, + options: JSONSchemaValidatorOptions = {} +): JSONSchemaValidator { + const ajv = new Ajv({ + strict: true, + coerceTypes: options.coerceTypes, + }); + + return { validate }; - /** - * Validate the config is valid - * @param config Configuration - * @param target @optional file for errors tracing. - * @returns Validation - */ - public validate(config: unknown, target: SourceFile | typeof NoTarget): Diagnostic[] { - const validate = this.ajv.compile(this.schema); + function validate(config: unknown, target: SourceFile | typeof NoTarget): Diagnostic[] { + const validate = ajv.compile(schema); const valid = validate(config); compilerAssert( !valid || !validate.errors, diff --git a/packages/compiler/core/types.ts b/packages/compiler/core/types.ts index 406f7ed1462..014cffd356d 100644 --- a/packages/compiler/core/types.ts +++ b/packages/compiler/core/types.ts @@ -379,56 +379,56 @@ export interface TemplateParameter extends BaseType { } export interface Sym { - flags: SymbolFlags; + readonly flags: SymbolFlags; /** * Nodes which contribute to this declaration */ - declarations: Node[]; + readonly declarations: readonly Node[]; /** * The name of the symbol */ - name: string; + readonly name: string; /** * A unique identifier for this symbol. Used to look up the symbol links. */ - id?: number; + readonly id?: number; /** * The symbol containing this symbol, if any. E.g. for things declared in * a namespace, this refers to the namespace. */ - parent?: Sym; + readonly parent?: Sym; /** * Externally visible symbols contained inside this symbol. E.g. all declarations * in a namespace, or members of an enum. */ - exports?: SymbolTable; + readonly exports?: SymbolTable; /** * Symbols for members of this symbol which must be referenced off the parent symbol * and cannot be referenced by other means (i.e. by unqualified lookup of the symbol * name). */ - members?: SymbolTable; + readonly members?: SymbolTable; /** * For using symbols, this is the used symbol. */ - symbolSource?: Sym; + readonly symbolSource?: Sym; /** * For late-bound symbols, this is the type referenced by the symbol. */ - type?: Type; + readonly type?: Type; /** * For decorator and function symbols, this is the JS function implementation. */ - value?: (...args: any[]) => any; + readonly value?: (...args: any[]) => any; } export interface SymbolLinks { @@ -440,11 +440,11 @@ export interface SymbolLinks { instantiations?: TypeInstantiationMap; } -export interface SymbolTable extends Map { +export interface SymbolTable extends ReadonlyMap { /** * Duplicate */ - readonly duplicates: Map>; + readonly duplicates: ReadonlyMap>; } // prettier-ignore @@ -599,7 +599,7 @@ export const enum NodeFlags { export interface BaseNode extends TextRange { readonly kind: SyntaxKind; - parent?: Node; + readonly parent?: Node; readonly directives?: readonly DirectiveExpressionNode[]; readonly flags: NodeFlags; /** @@ -611,7 +611,7 @@ export interface BaseNode extends TextRange { export interface TemplateDeclarationNode { readonly templateParameters: readonly TemplateParameterDeclarationNode[]; - locals?: SymbolTable; + readonly locals?: SymbolTable; } export type Node = @@ -647,10 +647,14 @@ export type Node = export type Comment = LineComment | BlockComment; export interface LineComment extends TextRange { - kind: SyntaxKind.LineComment; + readonly kind: SyntaxKind.LineComment; } export interface BlockComment extends TextRange { - kind: SyntaxKind.BlockComment; + readonly kind: SyntaxKind.BlockComment; +} + +export interface ParseOptions { + readonly comments?: boolean; } export interface CadlScriptNode extends DeclarationNode, BaseNode { @@ -664,6 +668,7 @@ export interface CadlScriptNode extends DeclarationNode, BaseNode { readonly parseDiagnostics: readonly Diagnostic[]; readonly printable: boolean; // If this ast tree can safely be printed/formatted. readonly locals: SymbolTable; + readonly parseOptions: ParseOptions; // Options used to parse this file } export type Statement = @@ -681,7 +686,7 @@ export type Statement = | ProjectionStatementNode; export interface DeclarationNode { - id: IdentifierNode; + readonly id: IdentifierNode; } export type Declaration = @@ -786,7 +791,7 @@ export interface MemberExpressionNode extends BaseNode { export interface NamespaceStatementNode extends BaseNode, DeclarationNode { readonly kind: SyntaxKind.NamespaceStatement; readonly statements?: readonly Statement[] | NamespaceStatementNode; - readonly decorators: DecoratorExpressionNode[]; + readonly decorators: readonly DecoratorExpressionNode[]; readonly locals?: SymbolTable; } @@ -821,7 +826,7 @@ export interface ModelStatementNode extends BaseNode, DeclarationNode, TemplateD readonly properties: readonly (ModelPropertyNode | ModelSpreadPropertyNode)[]; readonly extends?: Expression; readonly is?: Expression; - readonly decorators: DecoratorExpressionNode[]; + readonly decorators: readonly DecoratorExpressionNode[]; } export interface InterfaceStatementNode extends BaseNode, DeclarationNode, TemplateDeclarationNode { @@ -1061,7 +1066,7 @@ export interface ProjectionModelPropertyNode extends BaseNode { readonly kind: SyntaxKind.ProjectionModelProperty; readonly id: IdentifierNode | StringLiteralNode; readonly value: ProjectionExpression; - readonly decorators: DecoratorExpressionNode[]; + readonly decorators: readonly DecoratorExpressionNode[]; readonly optional: boolean; readonly default?: ProjectionExpression; } @@ -1204,13 +1209,13 @@ export interface TextRange { * The starting position of the ranger measured in UTF-16 code units from the * start of the full string. Inclusive. */ - pos: number; + readonly pos: number; /** * The ending position measured in UTF-16 code units from the start of the * full string. Exclusive. */ - end: number; + readonly end: number; } export interface SourceLocation extends TextRange { @@ -1274,6 +1279,11 @@ export interface CompilerHost { // read a utf-8 encoded file readFile(path: string): Promise; + /** + * Optional cache to reuse the results of parsing and binding across programs. + */ + parseCache?: WeakMap; + /** * Write the file. * @param path Path to the file. @@ -1431,32 +1441,25 @@ export interface CadlLibraryDef< export type JSONSchemaType = AjvJSONSchemaType; -export interface CadlLibrary< - T extends { [code: string]: DiagnosticMessages }, - E extends Record = Record -> { +export interface JSONSchemaValidator { /** - * Name of the library. Must match the package.json name. - */ - readonly name: string; - - /** - * Map of potential diagnostics that can be emitted in this library where the key is the diagnostic code. - */ - readonly diagnostics: DiagnosticMap; - - /** - * List of other library that should be imported when this is used as an emitter. - * Compiler will emit an error if the libraryes are not explicitly imported. + * Validate the configuration against its JSON Schema. + * + * @param config Configuration to validate. + * @param target Source file target to use for diagnostics. + * @returns Diagnostics produced by schema validation of the configuration. */ - readonly requireImports?: readonly string[]; + validate(config: unknown, target: SourceFile | typeof NoTarget): Diagnostic[]; +} +export interface CadlLibrary< + T extends { [code: string]: DiagnosticMessages }, + E extends Record = Record +> extends CadlLibraryDef { /** - * Emitter configuration if library is an emitter. + * JSON Schema validator for emitter options */ - readonly emitter?: { - options?: JSONSchemaType; - }; + readonly emitterOptionValidator?: JSONSchemaValidator; reportDiagnostic( program: Program, @@ -1535,8 +1538,3 @@ export interface Logger { error(message: string): void; log(log: LogInfo): void; } - -/** - * Remove the readonly properties on an object. - */ -export type Writable = { -readonly [P in keyof T]: T[P] }; diff --git a/packages/compiler/core/util.ts b/packages/compiler/core/util.ts index ba453232124..3c13c82e302 100644 --- a/packages/compiler/core/util.ts +++ b/packages/compiler/core/util.ts @@ -16,14 +16,23 @@ import { NoTarget, SourceFile, SourceFileKind, + Sym, + SymbolTable, } from "./types.js"; export { cadlVersion } from "./manifest.js"; export { NodeHost } from "./node-host.js"; +/** + * Recursively calls Object.freeze such that all objects and arrays + * referenced are frozen. + * + * Does not support cycles. Intended to be used only on plain data that can + * be directly represented in JSON. + */ export function deepFreeze(value: T): T { if (Array.isArray(value)) { - value.map(deepFreeze); + value.forEach(deepFreeze); } else if (typeof value === "object") { for (const prop in value) { deepFreeze(value[prop]); @@ -33,6 +42,12 @@ export function deepFreeze(value: T): T { return Object.freeze(value); } +/** + * Deeply clones an object. + * + * Does not support cycles. Intended to be used only on plain data that can + * be directly represented in JSON. + */ export function deepClone(value: T): T { if (Array.isArray(value)) { return value.map(deepClone) as any; @@ -49,6 +64,78 @@ export function deepClone(value: T): T { return value; } +/** + * Checks if two objects are deeply equal. + * + * Does not support cycles. Intended to be used only on plain data that can + * be directly represented in JSON. + */ +export function deepEquals(left: unknown, right: unknown): boolean { + if (left === right) { + return true; + } + if (left === null || right === null || typeof left !== "object" || typeof right !== "object") { + return false; + } + if (Array.isArray(left)) { + return Array.isArray(right) ? arrayEquals(left, right, deepEquals) : false; + } + return mapEquals(new Map(Object.entries(left)), new Map(Object.entries(right)), deepEquals); +} + +export type EqualityComparer = (x: T, y: T) => boolean; + +/** + * Check if two arrays have the same elements. + * + * @param equals Optional callback for element equality comparison. + * Default is to compare by identity using `===`. + */ +export function arrayEquals( + left: T[], + right: T[], + equals: EqualityComparer = (x, y) => x === y +): boolean { + if (left === right) { + return true; + } + if (left.length !== right.length) { + return false; + } + for (let i = 0; i < left.length; i++) { + if (!equals(left[i], right[i])) { + return false; + } + } + + return true; +} + +/** + * Check if two maps have the same entries. + * + * @param equals Optional callback for value equality comparison. + * Default is to compare by identity using `===`. + */ +export function mapEquals( + left: Map, + right: Map, + equals: EqualityComparer = (x, y) => x === y +): boolean { + if (left === right) { + return true; + } + if (left.size !== right.size) { + return false; + } + for (const [key, value] of left) { + if (!right.has(key) || !equals(value, right.get(key)!)) { + return false; + } + } + return true; +} + export async function getNormalizedRealPath(host: CompilerHost, path: string) { return normalizePath(await host.realpath(path)); } @@ -184,6 +271,28 @@ export async function findProjectRoot( } } +/** + * The mutable equivalent of a type. + */ +//prettier-ignore +export type Mutable = + T extends SymbolTable ? T & { set(key: string, value: Sym): void } : + T extends ReadonlyMap ? Map : + T extends ReadonlySet ? Set : + T extends readonly (infer V)[] ? V[] : + // brand to force explicit conversion. + { -readonly [P in keyof T]: T[P] } & { __writableBrand: never }; + +/** + * Casts away readonly typing. + * + * Use it like this when it is safe to override readonly typing: + * mutate(item).prop = value; + */ +export function mutate(value: T): Mutable { + return value as Mutable; +} + export function getSourceFileKindFromExt(path: string): SourceFileKind | undefined { const ext = getAnyExtensionFromPath(path); if (ext === ".js" || ext === ".mjs") { diff --git a/packages/compiler/init/init.ts b/packages/compiler/init/init.ts index d76282bba1c..afbf478d4bd 100644 --- a/packages/compiler/init/init.ts +++ b/packages/compiler/init/init.ts @@ -6,7 +6,7 @@ import { CadlConfigFilename } from "../config/config-loader.js"; import { logDiagnostics } from "../core/diagnostics.js"; import { formatCadl } from "../core/formatter.js"; import { getBaseFileName, joinPaths } from "../core/path-utils.js"; -import { SchemaValidator } from "../core/schema-validator.js"; +import { createJSONSchemaValidator } from "../core/schema-validator.js"; import { CompilerHost, SourceFile } from "../core/types.js"; import { readUrlOrPath, resolveRelativeUrlOrPath } from "../core/util.js"; import { InitTemplate, InitTemplateDefinitionsSchema, InitTemplateFile } from "./init-template.js"; @@ -272,7 +272,7 @@ function validateTemplateDefinitions( templates: unknown, file: SourceFile ): asserts templates is Record { - const validator = new SchemaValidator(InitTemplateDefinitionsSchema); + const validator = createJSONSchemaValidator(InitTemplateDefinitionsSchema); const diagnostics = validator.validate(templates, file); if (diagnostics.length > 0) { logDiagnostics(diagnostics, host.logSink); diff --git a/packages/compiler/lib/decorators.ts b/packages/compiler/lib/decorators.ts index 17deb9ca59b..6b74066a1ec 100644 --- a/packages/compiler/lib/decorators.ts +++ b/packages/compiler/lib/decorators.ts @@ -43,7 +43,7 @@ function setTemplatedStringProperty( program: Program, target: Type, text: string, - sourceObject: Type + sourceObject?: Type ) { // TODO: replace with built-in decorator validation https://github.com/Azure/cadl-azure/issues/1022 @@ -95,7 +95,7 @@ const docsKey = createStateSymbol("docs"); * * @doc can be specified on any language element -- a model, an operation, a namespace, etc. */ -export function $doc(context: DecoratorContext, target: Type, text: string, sourceObject: Type) { +export function $doc(context: DecoratorContext, target: Type, text: string, sourceObject?: Type) { setTemplatedStringProperty(docsKey, context.program, target, text, sourceObject); } diff --git a/packages/compiler/server/server.ts b/packages/compiler/server/server.ts index af082fbebe2..a129d89a8d7 100644 --- a/packages/compiler/server/server.ts +++ b/packages/compiler/server/server.ts @@ -1,4 +1,8 @@ import { Console } from "console"; +import { writeFile } from "fs/promises"; +import inspector from "inspector"; +import mkdirp from "mkdirp"; +import { join } from "path"; import { fileURLToPath } from "url"; import { TextDocument } from "vscode-languageserver-textdocument"; import { @@ -13,6 +17,10 @@ import { createServer, Server, ServerHost } from "./serverlib.js"; let server: Server | undefined = undefined; +const profileDir = process.env.CADL_SERVER_PROFILE_DIR; +const logTiming = process.env.CADL_SERVER_LOG_TIMING === "true"; +let profileSession: inspector.Session | undefined; + process.on("unhandledRejection", fatalError); try { main(); @@ -50,6 +58,12 @@ function main() { s.log("Process ID", process.pid); s.log("Command Line", process.argv); + if (profileDir) { + s.log("CPU profiling enabled", profileDir); + profileSession = new inspector.Session(); + profileSession.connect(); + } + connection.onInitialize(async (params) => { if (params.capabilities.workspace?.workspaceFolders) { clientHasWorkspaceFolderCapability = true; @@ -64,19 +78,19 @@ function main() { s.initialized(params); }); - connection.onDidChangeWatchedFiles(s.watchedFilesChanged); - connection.onDefinition(s.gotoDefinition); - connection.onCompletion(s.complete); - connection.onReferences(s.findReferences); - connection.onRenameRequest(s.rename); - connection.onPrepareRename(s.prepareRename); - connection.onFoldingRanges(s.getFoldingRanges); - connection.onDocumentSymbol(s.getDocumentSymbols); - connection.onDocumentHighlight(s.findDocumentHighlight); - connection.languages.semanticTokens.on(s.buildSemanticTokens); + connection.onDidChangeWatchedFiles(profile(s.watchedFilesChanged)); + connection.onDefinition(profile(s.gotoDefinition)); + connection.onCompletion(profile(s.complete)); + connection.onReferences(profile(s.findReferences)); + connection.onRenameRequest(profile(s.rename)); + connection.onPrepareRename(profile(s.prepareRename)); + connection.onFoldingRanges(profile(s.getFoldingRanges)); + connection.onDocumentSymbol(profile(s.getDocumentSymbols)); + connection.onDocumentHighlight(profile(s.findDocumentHighlight)); + connection.languages.semanticTokens.on(profile(s.buildSemanticTokens)); - documents.onDidChangeContent(s.checkChange); - documents.onDidClose(s.documentClosed); + documents.onDidChangeContent(profile(s.checkChange)); + documents.onDidClose(profile(s.documentClosed)); documents.listen(connection); connection.listen(); @@ -93,3 +107,42 @@ function fatalError(e: unknown) { console.error(e); process.exit(1); } + +function profile any>(func: T): T { + const name = func.name; + + if (logTiming) { + func = time(func); + } + + if (!profileDir) { + return func; + } + + return (async (...args: any[]) => { + profileSession!.post("Profiler.enable", () => { + // eslint-disable-next-line @typescript-eslint/no-misused-promises + profileSession!.post("Profiler.start", async () => { + const ret = await func.apply(undefined!, args); + // eslint-disable-next-line @typescript-eslint/no-misused-promises + profileSession!.post("Profiler.stop", async (err, args) => { + if (!err && args.profile) { + await mkdirp(profileDir!); + await writeFile(join(profileDir!, name + ".cpuprofile"), JSON.stringify(args.profile)); + } + }); + return ret; + }); + }); + }) as T; +} + +function time any>(func: T): T { + return (async (...args: any[]) => { + const start = Date.now(); + const ret = await func.apply(undefined!, args); + const end = Date.now(); + server!.log(func.name, end - start + " ms"); + return ret; + }) as T; +} diff --git a/packages/compiler/server/serverlib.ts b/packages/compiler/server/serverlib.ts index ff885036316..7384b923769 100644 --- a/packages/compiler/server/serverlib.ts +++ b/packages/compiler/server/serverlib.ts @@ -41,7 +41,8 @@ import { WorkspaceFolder, WorkspaceFoldersChangeEvent, } from "vscode-languageserver/node.js"; -import { loadCadlConfigForPath } from "../config/config-loader.js"; +import { defaultConfig, findCadlConfigPath, loadCadlConfigFile } from "../config/config-loader.js"; +import { CadlConfig } from "../config/types.js"; import { compilerAssert, createSourceFile, @@ -49,7 +50,7 @@ import { getSourceLocation, } from "../core/diagnostics.js"; import { CompilerOptions } from "../core/options.js"; -import { getNodeAtPosition, parse, visitChildren } from "../core/parser.js"; +import { getNodeAtPosition, visitChildren } from "../core/parser.js"; import { ensureTrailingDirectorySeparator, getAnyExtensionFromPath, @@ -77,9 +78,9 @@ import { Node, SourceFile, StringLiteralNode, - Sym, SymbolFlags, SyntaxKind, + TextRange, Type, } from "../core/types.js"; import { @@ -93,6 +94,7 @@ import { getDoc, isDeprecated, isIntrinsic } from "../lib/decorators.js"; export interface ServerHost { compilerHost: CompilerHost; + throwInternalErrors?: boolean; getOpenDocumentByURL(url: string): TextDocument | undefined; sendDiagnostics(params: PublishDiagnosticsParams): void; log(message: string): void; @@ -101,6 +103,7 @@ export interface ServerHost { export interface Server { readonly pendingMessages: readonly string[]; readonly workspaceFolders: readonly ServerWorkspaceFolder[]; + compile(document: TextDocument | TextDocumentIdentifier): Promise; initialize(params: InitializeParams): Promise; initialized(params: InitializedParams): void; workspaceFoldersChanged(e: WorkspaceFoldersChangeEvent): Promise; @@ -121,7 +124,7 @@ export interface Server { } export interface ServerSourceFile extends SourceFile { - // Keep track of the open doucment (if any) associated with a source file. + // Keep track of the open document (if any) associated with a source file. readonly document?: TextDocument; } @@ -167,6 +170,7 @@ export interface SemanticToken { interface CachedFile { type: "file"; file: SourceFile; + version?: number; // Cache additional data beyond the raw text of the source file. Currently // used only for JSON.parse result of package.json. @@ -177,6 +181,7 @@ interface CachedError { type: "error"; error: unknown; data?: any; + version?: undefined; } interface KeywordArea { @@ -189,6 +194,9 @@ interface KeywordArea { const serverOptions: CompilerOptions = { noEmit: true, designTimeBuild: true, + parseOptions: { + comments: true, + }, }; const keywords = [ @@ -220,7 +228,7 @@ export function createServer(host: ServerHost): Server { // could give us back an equivalent but non-identical URL but the original // URL is used as a key into the opened documents and so we must reproduce // it exactly. - const pathToURLMap: Map = new Map(); + const pathToURLMap = new Map(); // Cache all file I/O. Only open documents are sent over the LSP pipe. When // the compiler reads a file that isn't open, we use this cache to avoid @@ -229,6 +237,8 @@ export function createServer(host: ServerHost): Server { const fileSystemCache = createFileSystemCache(); const compilerHost = createCompilerHost(); + const oldPrograms = new Map(); + let workspaceFolders: ServerWorkspaceFolder[] = []; let isInitialized = false; let pendingMessages: string[] = []; @@ -240,6 +250,7 @@ export function createServer(host: ServerHost): Server { get workspaceFolders() { return workspaceFolders; }, + compile, initialize, initialized, workspaceFoldersChanged, @@ -371,7 +382,7 @@ export function createServer(host: ServerHost): Server { ): Promise { const path = await getPath(document); const mainFile = await getMainFileForDocument(path); - const config = await loadCadlConfigForPath(compilerHost, mainFile); + const config = await getConfig(mainFile, path); const options = { ...serverOptions, @@ -384,7 +395,8 @@ export function createServer(host: ServerHost): Server { let program: Program; try { - program = await createProgram(compilerHost, mainFile, options); + program = await createProgram(compilerHost, mainFile, options, oldPrograms.get(mainFile)); + oldPrograms.set(mainFile, program); if (!upToDate(document)) { return undefined; } @@ -392,7 +404,8 @@ export function createServer(host: ServerHost): Server { if (mainFile !== path && !program.sourceFiles.has(path)) { // If the file that changed wasn't imported by anything from the main // file, retry using the file itself as the main file. - program = await createProgram(compilerHost, path, options); + program = await createProgram(compilerHost, path, options, oldPrograms.get(path)); + oldPrograms.set(path, program); } if (!upToDate(document)) { @@ -410,6 +423,9 @@ export function createServer(host: ServerHost): Server { return program; } catch (err: any) { + if (host.throwInternalErrors) { + throw err; + } host.sendDiagnostics({ uri: document.uri, diagnostics: [ @@ -427,9 +443,34 @@ export function createServer(host: ServerHost): Server { } } + async function getConfig(mainFile: string, path: string): Promise { + const configPath = await findCadlConfigPath(compilerHost, mainFile); + if (!configPath) { + return defaultConfig; + } + + const cached = await fileSystemCache.get(configPath); + if (cached?.data) { + return cached.data; + } + + const config = await loadCadlConfigFile(compilerHost, configPath); + await fileSystemCache.setData(configPath, config); + return config; + } + + async function getScript(document: TextDocument | TextDocumentIdentifier) { + const file = await compilerHost.readFile(await getPath(document)); + const cached = compilerHost.parseCache?.get(file); + return cached ?? (await compile(document, (_, __, script) => script)); + } + async function getFoldingRanges(params: FoldingRangeParams): Promise { - const file = await compilerHost.readFile(await getPath(params.textDocument)); - const ast = parse(file, { comments: true }); + const ast = await getScript(params.textDocument); + if (!ast) { + return []; + } + const file = ast.file; const ranges: FoldingRange[] = []; let rangeStartSingleLines = -1; for (let i = 0; i < ast.comments.length; i++) { @@ -504,8 +545,11 @@ export function createServer(host: ServerHost): Server { } async function getDocumentSymbols(params: DocumentSymbolParams): Promise { - const file = await compilerHost.readFile(await getPath(params.textDocument)); - const ast = parse(file); + const ast = await getScript(params.textDocument); + if (!ast) { + return []; + } + const file = ast.file; const symbols: SymbolInformation[] = []; visitChildren(ast, addSymbolsForNode); @@ -528,21 +572,20 @@ export function createServer(host: ServerHost): Server { async function findDocumentHighlight( params: DocumentHighlightParams ): Promise { - const file = await compilerHost.readFile(await getPath(params.textDocument)); - const identifiers = await compile(params.textDocument, (program, document, file) => - findReferenceIdentifiers(program, file, document.offsetAt(params.position), false) - ); - if (identifiers === undefined) { - return []; - } - return identifiers.map((identifier) => { - const start = file.getLineAndCharacterOfPosition(identifier.pos); - const end = file.getLineAndCharacterOfPosition(identifier.end); - return { - range: Range.create(start, end), + let highlights: DocumentHighlight[] = []; + await compile(params.textDocument, (program, document, file) => { + const identifiers = findReferenceIdentifiers( + program, + file, + document.offsetAt(params.position), + [file] + ); + highlights = identifiers.map((identifier) => ({ + range: getRange(identifier, file.file), kind: DocumentHighlightKind.Read, - }; + })); }); + return highlights; } async function checkChange(change: TextDocumentChangeEvent) { @@ -646,7 +689,7 @@ export function createServer(host: ServerHost): Server { async function findReferences(params: ReferenceParams): Promise { const identifiers = await compile(params.textDocument, (program, document, file) => - findReferenceIdentifiers(program, file, document.offsetAt(params.position), true) + findReferenceIdentifiers(program, file, document.offsetAt(params.position)) ); return getLocations(identifiers); } @@ -664,8 +707,7 @@ export function createServer(host: ServerHost): Server { const identifiers = findReferenceIdentifiers( program, file, - document.offsetAt(params.position), - true + document.offsetAt(params.position) ); for (const id of identifiers) { const location = getLocation(id); @@ -683,28 +725,11 @@ export function createServer(host: ServerHost): Server { return { changes }; } - function addReferenceIdentifiers( - program: Program, - file: CadlScriptNode, - sym: Sym, - references: IdentifierNode[] - ) { - visitChildren(file, function visit(node) { - if (node.kind === SyntaxKind.Identifier) { - const s = program.checker.resolveIdentifier(node); - if (s === sym || (sym.type && s?.type === sym.type)) { - references.push(node); - } - } - visitChildren(node, visit); - }); - } - function findReferenceIdentifiers( program: Program, file: CadlScriptNode, pos: number, - wholeProgram: boolean + searchFiles: Iterable = program.sourceFiles.values() ): IdentifierNode[] { const id = getNodeAtPosition(file, pos); if (id?.kind !== SyntaxKind.Identifier) { @@ -717,12 +742,16 @@ export function createServer(host: ServerHost): Server { } const references: IdentifierNode[] = []; - if (wholeProgram) { - for (const script of program.sourceFiles.values() ?? []) { - addReferenceIdentifiers(program, script, sym, references); - } - } else { - addReferenceIdentifiers(program, file, sym, references); + for (const searchFile of searchFiles) { + visitChildren(searchFile, function visit(node) { + if (node.kind === SyntaxKind.Identifier) { + const s = program.checker.resolveIdentifier(node); + if (s === sym || (sym.type && s?.type === sym.type)) { + references.push(node); + } + } + visitChildren(node, visit); + }); } return references; } @@ -901,9 +930,13 @@ export function createServer(host: ServerHost): Server { async function getSemanticTokens(params: SemanticTokensParams): Promise { const ignore = -1; const defer = -2; - const file = await compilerHost.readFile(await getPath(params.textDocument)); + + const ast = await getScript(params.textDocument); + if (!ast) { + return []; + } + const file = ast.file; const tokens = mapTokens(); - const ast = parse(file); classifyNode(ast); return Array.from(tokens.values()).filter((t) => t.kind !== undefined); @@ -1071,7 +1104,7 @@ export function createServer(host: ServerHost): Server { sendDiagnostics(change.document, []); } - function getLocations(targets: DiagnosticTarget[] | undefined): Location[] { + function getLocations(targets: readonly DiagnosticTarget[] | undefined): Location[] { return targets?.map(getLocation).filter((x): x is Location => !!x) ?? []; } @@ -1081,14 +1114,18 @@ export function createServer(host: ServerHost): Server { return undefined; } - const start = location.file.getLineAndCharacterOfPosition(location.pos); - const end = location.file.getLineAndCharacterOfPosition(location.end); return { uri: getURL(location.file.path), - range: Range.create(start, end), + range: getRange(location, location.file), }; } + function getRange(location: TextRange, file: SourceFile): Range { + const start = file.getLineAndCharacterOfPosition(location.pos); + const end = file.getLineAndCharacterOfPosition(location.end); + return Range.create(start, end); + } + function convertSeverity(severity: "warning" | "error"): DiagnosticSeverity { switch (severity) { case "warning": @@ -1165,10 +1202,10 @@ export function createServer(host: ServerHost): Server { let mainFile = "main.cadl"; let pkg: any; const pkgPath = joinPaths(dir, "package.json"); - const cached = (await fileSystemCache.get(pkgPath))?.data; + const cached = await fileSystemCache.get(pkgPath); if (cached) { - pkg = cached; + pkg = cached.data; } else { [pkg] = await loadFile( compilerHost, @@ -1177,7 +1214,7 @@ export function createServer(host: ServerHost): Server { logMainFileSearchDiagnostic, options ); - (await fileSystemCache.get(pkgPath))!.data = pkg ?? {}; + await fileSystemCache.setData(pkgPath, pkg ?? {}); } if (typeof pkg?.cadlMain === "string") { @@ -1258,6 +1295,12 @@ export function createServer(host: ServerHost): Server { set(path: string, entry: CachedFile | CachedError) { cache.set(path, entry); }, + async setData(path: string, data: any) { + const entry = await this.get(path); + if (entry) { + entry.data = data; + } + }, notify(changes: FileEvent[]) { changes.push(...changes); }, @@ -1268,30 +1311,35 @@ export function createServer(host: ServerHost): Server { const base = host.compilerHost; return { ...base, + parseCache: new WeakMap(), readFile, stat, getSourceFileKind, }; async function readFile(path: string): Promise { - // Try open files sent from client over LSP const document = getOpenDocument(path); - if (document) { - return { - document, - ...createSourceFile(document.getText(), path), - }; - } - - // Try file system cache const cached = await fileSystemCache.get(path); - if (cached) { + + // Try cache + if (cached && (!document || document.version === cached.version)) { if (cached.type === "error") { throw cached.error; } return cached.file; } + // Try open document, although this is cheap, the instance still needs + // to be cached so that the compiler can reuse parse and bind results. + if (document) { + const file = { + document, + ...createSourceFile(document.getText(), path), + }; + fileSystemCache.set(path, { type: "file", file, version: document.version }); + return file; + } + // Hit the disk and cache try { const file = await base.readFile(path); diff --git a/packages/compiler/test/binder.test.ts b/packages/compiler/test/binder.test.ts index 8c4ec7a0a19..58a85b87234 100644 --- a/packages/compiler/test/binder.test.ts +++ b/packages/compiler/test/binder.test.ts @@ -496,13 +496,13 @@ function createJsSourceFile(exports: any): JsSourceFileNode { sv: "", pos: 0, end: 0, - symbol: undefined as any, + symbol: undefined!, flags: NodeFlags.Synthetic, }, esmExports: exports, file, namespaceSymbols: [], - symbol: undefined as any, + symbol: undefined!, pos: 0, end: 0, flags: NodeFlags.None, diff --git a/packages/compiler/test/checker/using.test.ts b/packages/compiler/test/checker/using.test.ts index 76ad61e8841..decf9bddc21 100644 --- a/packages/compiler/test/checker/using.test.ts +++ b/packages/compiler/test/checker/using.test.ts @@ -1,7 +1,11 @@ -import { match, rejects, strictEqual } from "assert"; -import { getSourceLocation } from "../../core/index.js"; +import { rejects, strictEqual } from "assert"; import { Model } from "../../core/types.js"; -import { createTestHost, expectDiagnosticEmpty, TestHost } from "../../testing/index.js"; +import { + createTestHost, + expectDiagnosticEmpty, + expectDiagnostics, + TestHost, +} from "../../testing/index.js"; describe("compiler: using statements", () => { let testHost: TestHost; @@ -197,7 +201,7 @@ describe("compiler: using statements", () => { expectDiagnosticEmpty(diagnostics); }); - it("report ambigous diagnostics when using name present in multiple using", async () => { + it("report ambiguous diagnostics when using name present in multiple using", async () => { testHost.addCadlFile( "main.cadl", ` @@ -227,13 +231,14 @@ describe("compiler: using statements", () => { model B extends A {} ` ); - const diagnostics = await testHost.diagnose("./"); - strictEqual(diagnostics.length, 1); - strictEqual(diagnostics[0].code, "ambiguous-symbol"); - strictEqual( - diagnostics[0].message, - '"A" is an ambiguous name between N.A, M.A. Try using fully qualified name instead: N.A, M.A' - ); + const diagnostics = await testHost.diagnose("./", { nostdlib: true }); + expectDiagnostics(diagnostics, [ + { + code: "ambiguous-symbol", + message: + '"A" is an ambiguous name between N.A, M.A. Try using fully qualified name instead: N.A, M.A', + }, + ]); }); it("ambigous use doesn't affect other files", async () => { @@ -277,13 +282,14 @@ describe("compiler: using statements", () => { ` ); const diagnostics = await testHost.diagnose("./"); - strictEqual(diagnostics.length, 1); - strictEqual(diagnostics[0].code, "ambiguous-symbol"); - strictEqual( - diagnostics[0].message, - '"A" is an ambiguous name between N.A, M.A. Try using fully qualified name instead: N.A, M.A' - ); - match(getSourceLocation(diagnostics[0].target)?.file.path!, /ambiguous\.cadl$/); + expectDiagnostics(diagnostics, [ + { + code: "ambiguous-symbol", + message: + '"A" is an ambiguous name between N.A, M.A. Try using fully qualified name instead: N.A, M.A', + file: /ambiguous\.cadl$/, + }, + ]); }); it("resolves 'local' decls over usings", async () => { diff --git a/packages/compiler/test/config/config.test.ts b/packages/compiler/test/config/config.test.ts index 123d4448df5..f02fbda6ff8 100644 --- a/packages/compiler/test/config/config.test.ts +++ b/packages/compiler/test/config/config.test.ts @@ -5,7 +5,7 @@ import { CadlConfigJsonSchema } from "../../config/config-schema.js"; import { CadlRawConfig, loadCadlConfigForPath } from "../../config/index.js"; import { createSourceFile } from "../../core/diagnostics.js"; import { NodeHost } from "../../core/node-host.js"; -import { SchemaValidator } from "../../core/schema-validator.js"; +import { createJSONSchemaValidator } from "../../core/schema-validator.js"; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -69,7 +69,7 @@ describe("compiler: config file loading", () => { }); describe("validation", () => { - const validator = new SchemaValidator(CadlConfigJsonSchema); + const validator = createJSONSchemaValidator(CadlConfigJsonSchema); const file = createSourceFile("", ""); function validate(data: CadlRawConfig) { diff --git a/packages/compiler/test/parser.test.ts b/packages/compiler/test/parser.test.ts index d6bf2659e2c..6121f8e2b1c 100644 --- a/packages/compiler/test/parser.test.ts +++ b/packages/compiler/test/parser.test.ts @@ -800,11 +800,6 @@ export function dumpAST(astNode: Node, file?: SourceFile) { return undefined; } - if (key === "locals" && value.size === 0) { - // this will be an empty symbol table after parsing, hide it - return undefined; - } - if (Array.isArray(value) && value.length === 0) { // hide empty arrays too return undefined; diff --git a/packages/compiler/test/server/reuse.test.ts b/packages/compiler/test/server/reuse.test.ts new file mode 100644 index 00000000000..39f8a545846 --- /dev/null +++ b/packages/compiler/test/server/reuse.test.ts @@ -0,0 +1,150 @@ +import { ok } from "assert"; +import { Program, SymbolTable, visitChildren } from "../../core/index.js"; +import { mutate } from "../../core/util.js"; +import { + createTestServerHost, + expectDiagnosticEmpty, + resolveVirtualPath, +} from "../../testing/index.js"; + +describe("server: reuse", () => { + it("reuses uchanged programs", async () => { + const host = await createTestServerHost(); + const document = host.addOrUpdateDocument("main.cadl", "model M {}"); + const oldProgram = await host.server.compile(document); + ok(oldProgram); + expectDiagnosticEmpty(oldProgram.diagnostics); + const newProgram = await host.server.compile(document); + ok(newProgram); + expectSameProgram(oldProgram, newProgram); + }); + + it("reuses unchanged files", async () => { + const source = `import "./other.cadl"; model M extends N {}`; + const otherSource = `model N {}`; + + const host = await createTestServerHost(); + const document = host.addOrUpdateDocument("main.cadl", source); + + host.addOrUpdateDocument("other.cadl", otherSource); + + const oldProgram = await host.server.compile(document); + ok(oldProgram); + expectDiagnosticEmpty(oldProgram.diagnostics); + + host.addOrUpdateDocument("other.cadl", otherSource + "// force change"); + const newProgram = await host.server.compile(document); + ok(newProgram); + expectDiagnosticEmpty(newProgram.diagnostics); + + expectNotSameProgram(oldProgram, newProgram); + expectNotSameSourceFile(oldProgram, newProgram, "other.cadl"); + expectSameSourceFile(oldProgram, newProgram, "main.cadl"); + }); + + it("does not mutate symbols when reusing unchanged files", async () => { + // trigger features that add symbols during checking: using statements, member references. + const source = ` + import "./other.cadl"; + + using OtherNamespace; + + namespace N { + model M extends OtherModel { + a: string; + b: string; + } + + union U { + a: string, + b: int32, + } + + enum E { + A, + B, + } + + interface I { + a(): void; + b(): void; + } + + model LateBoundReferences { + a: N.M.a; + b: N.U.a; + c: N.E.A; + d: N.I.b; + } + }`; + + const otherSource = ` + namespace OtherNamespace { + model OtherModel {} + }`; + + const host = await createTestServerHost(); + host.addOrUpdateDocument("other.cadl", otherSource); + const document = host.addOrUpdateDocument("main.cadl", source); + const oldProgram = await host.server.compile(document); + ok(oldProgram); + expectDiagnosticEmpty(oldProgram.diagnostics); + + freezeSymbolTables(oldProgram); + + host.addOrUpdateDocument("other.cadl", otherSource + "// force change"); + const newProgram = await host.server.compile(document); + ok(newProgram); + expectDiagnosticEmpty(newProgram.diagnostics); + + expectNotSameProgram(oldProgram, newProgram); + expectNotSameSourceFile(oldProgram, newProgram, "other.cadl"); + expectSameSourceFile(oldProgram, newProgram, "main.cadl"); + }); +}); + +function expectSameProgram(oldProgram: Program, newProgram: Program) { + ok(newProgram === oldProgram, "Programs are not identical but should be."); +} + +function expectNotSameProgram(oldProgram: Program, newProgram: Program) { + ok(newProgram !== oldProgram, "Programs are identical but should not be."); +} + +function expectSameSourceFile(oldProgram: Program, newProgram: Program, path: string) { + path = resolveVirtualPath(path); + const oldFile = oldProgram.sourceFiles.get(path); + const newFile = newProgram.sourceFiles.get(path); + ok(oldFile === newFile, `Source files for ${path} are not identical but should be.`); +} + +function expectNotSameSourceFile(oldProgram: Program, newProgram: Program, path: string) { + path = resolveVirtualPath(path); + const oldFile = oldProgram.sourceFiles.get(path); + const newFile = newProgram.sourceFiles.get(path); + ok(oldFile !== newFile, `Source files for ${path} are identical but should not be.`); +} + +function freezeSymbolTables(program: Program) { + for (const file of program.sourceFiles.values()) { + freezeSymbolTable(file.locals); + visitChildren(file, function visit(child) { + if ("locals" in child) { + freezeSymbolTable(child.locals); + } + if (child.symbol) { + freezeSymbolTable(child.symbol.exports); + freezeSymbolTable(child.symbol.members); + } + visitChildren(child, visit); + }); + } +} + +function freezeSymbolTable(table: SymbolTable | undefined) { + if (table) { + mutate(table).set = () => { + throw new Error("SymbolTable is frozen"); + }; + } +} diff --git a/packages/compiler/test/server/server-file-handling.test.ts b/packages/compiler/test/server/server-file-handling.test.ts index 75b8a9bf214..a8b63edb445 100644 --- a/packages/compiler/test/server/server-file-handling.test.ts +++ b/packages/compiler/test/server/server-file-handling.test.ts @@ -15,7 +15,6 @@ describe("compiler: server: main file", () => { host.addCadlFile("./main.cadl", 'import "./common.cadl"; import "./subdir/subfile.cadl";'); host.addCadlFile("./common.cadl", "model Base {}"); - host.addCadlFile("./subdir/empty.cadl", ""); // to force virtual file system to create directory const document = host.addOrUpdateDocument("./subdir/subfile.cadl", "model Sub extends Base {}"); await host.server.checkChange({ document }); diff --git a/packages/compiler/testing/test-server-host.ts b/packages/compiler/testing/test-server-host.ts index 10679b7ae06..aa1a5910de3 100644 --- a/packages/compiler/testing/test-server-host.ts +++ b/packages/compiler/testing/test-server-host.ts @@ -32,6 +32,7 @@ export async function createTestServerHost(options?: TestHostOptions) { const serverHost: TestServerHost = { ...fileSystem, + throwInternalErrors: true, server: undefined!, // initialized later due to cycle logMessages, getOpenDocumentByURL(url) { @@ -42,9 +43,19 @@ export async function createTestServerHost(options?: TestHostOptions) { }, addOrUpdateDocument(path: string, content: string) { const url = this.getURL(path); - const version = documents.get(url)?.version ?? 1; + + let version = 1; + const oldDocument = documents.get(url); + if (oldDocument) { + version = oldDocument.version; + if (oldDocument.getText() !== content) { + version++; + } + } + const document = TextDocument.create(url, "cadl", version, content); documents.set(url, document); + fileSystem.addCadlFile(path, ""); // force virtual file system to create directory where document lives. return document; }, getDiagnostics(path) { diff --git a/packages/versioning/src/versioning.ts b/packages/versioning/src/versioning.ts index 34fedeaa7f2..450ca694c86 100644 --- a/packages/versioning/src/versioning.ts +++ b/packages/versioning/src/versioning.ts @@ -423,7 +423,7 @@ export function getVersions(p: Program, t: Type): [Namespace, VersionMap] | [] { } else if (t.namespace) { return cacheVersion(t, getVersions(p, t.namespace)); } else { - return cacheVersion(t, [t, undefined as any]); + return cacheVersion(t, [t, undefined!]); } } else if ( t.kind === "Operation" ||