UNPKG

@informalsystems/quint

Version:

Core tool for the Quint specification language

285 lines 13.6 kB
"use strict"; /* ---------------------------------------------------------------------------------- * Copyright 2022-2023 Informal Systems * Licensed under the Apache License, Version 2.0. * See LICENSE in the project root for license information. * --------------------------------------------------------------------------------- */ Object.defineProperty(exports, "__esModule", { value: true }); exports.NameCollector = void 0; const quintIr_1 = require("../ir/quintIr"); const base_1 = require("./base"); const importErrors_1 = require("./importErrors"); const lodash_1 = require("lodash"); /** * Collects all top-level definitions in Quint modules. Used internally by * `NameResolver`. Also handles imports, instances and exports, collecting * definitions from those statements and managing their level of visibility. */ class NameCollector { constructor() { this.definitionsByName = new Map(); this.definitionsByModule = new Map(); this.errors = []; this.table = new Map(); // the current depth of operator definitions: top-level defs are depth 0 // FIXME(#1279): The walk* functions update this value, but they need to be // initialized to -1 here for that to work on all scenarios. this.definitionDepth = -1; this.currentModuleName = ''; } switchToModule(moduleName) { this.currentModuleName = moduleName; this.definitionsByName = this.definitionsByModule.get(moduleName) ?? new Map(); } enterModule(module) { this.currentModuleName = module.name; this.definitionsByName = new Map(); if (this.definitionsByModule.has(module.name)) { const message = `Module with name '${module.name}' was already defined`; this.errors.push({ code: 'QNT102', message, reference: module.id, data: {} }); } } exitModule(module) { this.definitionsByModule.set(module.name, this.definitionsByName); } enterVar(def) { this.collectDefinition(def); } enterConst(def) { this.collectDefinition(def); } enterOpDef(def) { // FIXME (#1013): This should collect the type annotation, but something // breaks in the type checker if we do. We should fix that and then ensure // that we collect type annotations here. if (this.definitionDepth === 0) { // collect only top-level definitions this.collectDefinition({ ...def, typeAnnotation: undefined, depth: this.definitionDepth }); } } enterTypeDef(def) { this.collectDefinition(def); } enterAssume(def) { this.collectDefinition(def); } enterInstance(decl) { // Copy defs from the module being instantiated if (decl.protoName === this.currentModuleName) { // Importing current module this.errors.push((0, importErrors_1.selfReferenceError)(decl)); return; } const moduleTable = this.definitionsByModule.get(decl.protoName); if (!moduleTable) { // Instantiating a non-existing module this.errors.push((0, importErrors_1.moduleNotFoundError)(decl)); return; } const instanceTable = (0, lodash_1.cloneDeep)(moduleTable); if (decl.qualifiedName) { // Add the qualifier to `definitionsMyModule` map with a copy of the // definitions, so if there is an export of that qualifier, we know which // definitions to export this.definitionsByModule.set(decl.qualifiedName, instanceTable); } // For each override, check if the name exists in the instantiated module and is a constant. // If so, update the value definition to point to the expression being overridden decl.overrides.forEach(([param, _ex]) => { // Constants are always top-level const constDef = (0, base_1.getTopLevelDef)(instanceTable, param.name); if (!constDef) { this.errors.push((0, importErrors_1.paramNotFoundError)(decl, param)); return; } if (constDef.kind !== 'const') { this.errors.push((0, importErrors_1.paramIsNotAConstantError)(decl, param)); return; } constDef.hidden = false; }); // All names from the instanced module should be acessible with the instance namespace // So, copy them to the current module's lookup table const newDefs = (0, base_1.copyNames)(instanceTable, decl.qualifiedName, true); this.collectTopLevelDefinitions(newDefs, decl); } enterImport(decl) { if (decl.protoName === this.currentModuleName) { // Importing current module this.errors.push((0, importErrors_1.selfReferenceError)(decl)); return; } const moduleTable = (0, lodash_1.cloneDeep)(this.definitionsByModule.get(decl.protoName)); if (!moduleTable) { // Importing non-existing module this.errors.push((0, importErrors_1.moduleNotFoundError)(decl)); return; } if (decl.qualifiedName) { // Add the qualifier to `definitionsMyModule` map with a copy of the // definitions, so if there is an export of that qualifier, we know which // definitions to export const newTable = new Map([...moduleTable.entries()]); this.definitionsByModule.set(decl.qualifiedName, newTable); } const importableDefinitions = (0, base_1.copyNames)(moduleTable, (0, quintIr_1.qualifier)(decl), true); if (!decl.defName || decl.defName === '*') { // Imports all definitions this.collectTopLevelDefinitions(importableDefinitions, decl); return; } // Tries to find a specific definition, reporting an error if not found const newDef = (0, base_1.getTopLevelDef)(importableDefinitions, decl.defName); if (!newDef) { this.errors.push((0, importErrors_1.nameNotFoundError)(decl)); return; } this.collectDefinition(newDef, decl); } // Imported names are copied with a scope since imports are not transitive by // default. Exporting needs to turn those names into unhidden ones so, when // the current module is imported, the names are accessible. Note that it is // also possible to export names that were not previously imported via `import`. enterExport(decl) { if (decl.protoName === this.currentModuleName) { // Exporting current module this.errors.push((0, importErrors_1.selfReferenceError)(decl)); return; } const moduleTable = (0, lodash_1.cloneDeep)(this.definitionsByModule.get(decl.protoName)); if (!moduleTable) { // Exporting non-existing module this.errors.push((0, importErrors_1.moduleNotFoundError)(decl)); return; } const exportableDefinitions = (0, base_1.copyNames)(moduleTable, (0, quintIr_1.qualifier)(decl)); if (!decl.defName || decl.defName === '*') { // Export all definitions this.collectTopLevelDefinitions(exportableDefinitions, decl); return; } // Tries to find a specific definition, reporting an error if not found const newDef = (0, base_1.getTopLevelDef)(exportableDefinitions, decl.defName); if (!newDef) { this.errors.push((0, importErrors_1.nameNotFoundError)(decl)); return; } this.collectDefinition(newDef, decl); } /** Public interface to manipulate the collected definitions. Used by * `NameResolver` to add and remove scoped definitions */ /** * Collects a definition. If the identifier is an underscore or a built-in * name, the definition is not collected. If the identifier conflicts with a * previous definition, a conflict is recorded. * * @param def - The definition object to collect. * @param name - An optional name for the definition, if the name is different * than `def.name` (i.e. in import-like statements). * @param source - An optional source identifier for the definition, if the * source is different than `def.id` (i.e. in import-like statements). * * @returns The definition object that was collected. */ collectDefinition(def, importedFrom) { const identifier = importedFrom?.defName ?? def.name; const source = importedFrom?.id ?? def.id; if (identifier === '_') { // Don't collect underscores, as they are special identifiers that allow no usage return def; } if (base_1.builtinNames.includes(identifier)) { // Conflict with a built-in name this.recordConflict(identifier, undefined, source); return def; } def.depth ??= 0; const namespaces = importedFrom ? this.namespaces(importedFrom) : []; if (!this.definitionsByName.has(identifier)) { // No existing defs with this name. Create an entry with a single def. this.definitionsByName.set(identifier, [{ ...(0, base_1.addNamespacesToDef)(def, namespaces), importedFrom }]); return def; } // Else: There are exiting defs. We need to check for conflicts const existingEntries = this.definitionsByName.get(identifier); // Entries conflict if they have different ids, but the same depth. // Entries with different depths are ok, because one is shadowing the // other. const conflictingEntries = existingEntries.filter(entry => entry.id !== def.id && entry.depth === def.depth); // Record potential errors and move on conflictingEntries.forEach(existingEntry => { this.recordConflict(identifier, existingEntry.id, source); }); // Keep entries with different ids. DON'T keep the whole // `existingEntries` since those may contain the same exact defs, but // hidden. const newDef = { ...(0, base_1.addNamespacesToDef)(def, namespaces), importedFrom, shadowing: existingEntries.length > 0 }; this.definitionsByName.set(identifier, existingEntries.filter(entry => entry.id !== def.id).concat([newDef])); return newDef; } /** * Deletes the definition with the given identifier from the collected definitions. * * @param identifier - The identifier of the definition to delete. */ deleteDefinition(identifier) { this.definitionsByName.get(identifier)?.pop(); return; } /** * Gets the definition with the given name, in the current (visiting) scope * * @param identifier - The identifier of the definition to retrieve. * * @returns The definition object for the given identifier, or undefined if a * definitions with that identifier was never collected. */ getDefinition(identifier) { const defs = this.definitionsByName.get(identifier); if (defs === undefined || defs.length === 0) { return; } return defs[defs.length - 1]; } namespaces(decl) { if (decl.kind === 'instance') { return (0, lodash_1.compact)([decl.qualifiedName ?? decl.protoName, this.currentModuleName]); } const namespace = (0, quintIr_1.qualifier)(decl); return namespace ? [namespace] : []; } collectTopLevelDefinitions(newDefs, importedFrom) { const namespaces = importedFrom ? this.namespaces(importedFrom) : []; const newEntries = (0, lodash_1.compact)([...newDefs.keys()].map(identifier => { const def = (0, base_1.getTopLevelDef)(newDefs, identifier); if (!def) { return; } const existingEntries = this.definitionsByName.get(identifier); if (existingEntries) { const conflictingEntries = existingEntries.filter(entry => entry.id !== def.id); conflictingEntries.forEach(existingEntry => { this.recordConflict(identifier, existingEntry.id, def.id); }); // Keep conflicting entries and add the new one. DON'T keep the whole // `existingEntries` since those may contain the same exact defs, but // hidden. return [identifier, conflictingEntries.concat([{ ...(0, base_1.addNamespacesToDef)(def, namespaces), importedFrom }])]; } return [identifier, [{ ...(0, base_1.addNamespacesToDef)(def, namespaces), importedFrom }]]; })); this.definitionsByName = new Map([...this.definitionsByName.entries(), ...newEntries]); } recordConflict(identifier, exisitingSource, newSource) { // exisitingSource is undefined when the conflict is with a built-in name const message = exisitingSource ? `Conflicting definitions found for name '${identifier}' in module '${this.currentModuleName}'` : `Built-in name '${identifier}' is redefined in module '${this.currentModuleName}'`; if (exisitingSource) { this.errors.push({ code: 'QNT101', message, reference: exisitingSource, data: {} }); } this.errors.push({ code: 'QNT101', message, reference: newSource, data: {} }); } } exports.NameCollector = NameCollector; //# sourceMappingURL=collector.js.map