UNPKG

@informalsystems/quint

Version:

Core tool for the Quint specification language

285 lines 11.3 kB
"use strict"; /* ---------------------------------------------------------------------------------- * Copyright 2022 Informal Systems * Licensed under the Apache License, Version 2.0. * See LICENSE in the project root for license information. * --------------------------------------------------------------------------------- */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ModeChecker = void 0; /** * Check that all definitions in a module have correctly annotated modes * according to inferred effects * * @author Gabriela Moreira * * @module */ const lodash_isequal_1 = __importDefault(require("lodash.isequal")); const IRprinting_1 = require("../ir/IRprinting"); const IRVisitor_1 = require("../ir/IRVisitor"); const util_1 = require("../util"); const base_1 = require("./base"); const printing_1 = require("./printing"); class ModeChecker { /** * Constructs a new instance of ModeChecker with optional suggestions. * * @constructor * @param suggestions - Optional map of suggestions for annotations that could * be more strict. To be used as a starting point */ constructor(suggestions) { this.errors = new Map(); this.suggestions = new Map(); this.effects = new Map(); if (suggestions) { this.suggestions = suggestions; } } /** * Matches annotated modes for each declaration with its inferred effect. Returns * errors for incorrect annotations and suggestions for annotations that could * be more strict. * * @param decls: the list of declarations to be checked * @param effects: the map from expression ids to their inferred effects * * @returns The mode errors, if any is found. Otherwise, a map with potential suggestions. */ checkModes(decls, effects) { this.effects = effects; decls.forEach(decl => (0, IRVisitor_1.walkDeclaration)(this, decl)); return [this.errors, this.suggestions]; } exitOpDef(def) { const effect = this.effects.get(def.id); if (!effect) { return; } const [mode, explanation] = modeForEffect(effect, def.qualifier); if (mode === def.qualifier) { return; } if (isMoreGeneral(mode, def.qualifier)) { this.errors.set(def.id, { code: 'QNT200', message: `${(0, IRprinting_1.qualifierToString)(def.qualifier)} operators ${modeConstraint(def.qualifier, mode)}, but operator \`${def.name}\` ${explanation}. Use ${(0, IRprinting_1.qualifierToString)(mode)} instead.`, reference: def.id, data: { fix: { kind: 'replace', original: (0, IRprinting_1.qualifierToString)(def.qualifier), replacement: (0, IRprinting_1.qualifierToString)(mode) }, }, }); } else { this.suggestions.set(def.id, mode); } } exitInstance(def) { // For each override, check that the value a pure val def.overrides.forEach(([name, ex]) => { const effect = this.effects.get(ex.id); if (!effect) { return; } const [mode, explanation] = modeForEffect(effect, 'puredef'); if (mode === 'pureval' || mode === 'puredef') { return; } this.errors.set(ex.id, { code: 'QNT201', message: `Instance overrides must be pure, but the value for ${name.name} ${explanation}`, reference: ex.id, data: {}, }); }); } } exports.ModeChecker = ModeChecker; function modeConstraint(mode, expectedMode) { switch (mode) { case 'pureval': if (expectedMode === 'puredef') { return 'may not have parameters'; } else { return 'may not interact with state variables'; } case 'puredef': return 'may not interact with state variables'; case 'val': if (expectedMode === 'def') { return 'may not have parameters'; } else { return 'may only read state variables'; } case 'def': return 'may only read state variables'; case 'action': return 'may only read and update state variables'; case 'temporal': return 'may not update state variables'; case 'nondet': case 'run': return '[not supported by the mode checker]'; } } const componentKindPriority = ['temporal', 'update', 'read']; const componentDescription = new Map([ ['temporal', 'performs temporal operations over'], ['update', 'updates'], ['read', 'reads'], ]); const modesForArrow = new Map([ ['temporal', 'temporal'], ['update', 'action'], ['read', 'def'], ]); const modesForConcrete = new Map([ ['temporal', 'temporal'], ['update', 'action'], ['read', 'val'], ]); function modeForEffect(scheme, annotatedMode) { const effect = scheme.effect; const nonFreeVars = scheme.entityVariables; switch (effect.kind) { case 'concrete': { const kind = componentKindPriority.find(kind => { return effect.components.some(c => { if (c.kind !== kind) { return false; } const nonFreeEntities = (0, base_1.entityNames)(c.entity) .filter(v => nonFreeVars.has(v)) .concat((0, base_1.stateVariables)(c.entity).map(v => v.name)); return nonFreeEntities && nonFreeEntities.length > 0; }); }); if (!kind) { return ['pureval', "doesn't read or update any state variable"]; } const components = effect.components.filter(c => c.kind === kind); return [ modesForConcrete.get(kind), `${componentDescription.get(kind)} variables ${components.map(c => (0, printing_1.entityToString)(c.entity))}`, ]; } case 'arrow': { const r = effect.result; if (r.kind === 'arrow') { throw new Error(`Unexpected arrow found in operator result: ${(0, printing_1.effectToString)(effect)}`); } const parametersMessage = `has ${effect.params.length} parameter${effect.params.length > 1 ? 's' : ''}`; if (r.kind === 'variable') { return ['puredef', parametersMessage]; } const entitiesByComponentKind = paramEntitiesByEffect(effect); const addedEntitiesByComponentKind = new Map(); r.components.forEach(c => { const paramEntities = entitiesByComponentKind.get(c.kind) ?? []; addedEntitiesByComponentKind.set(c.kind, addedEntities(paramEntities, c.entity)); }); const kind = componentKindPriority.find(kind => { const entities = addedEntitiesByComponentKind.get(kind); const nonFreeEntities = entities?.flatMap(vs => { return (0, base_1.entityNames)(vs) .filter(v => nonFreeVars.has(v)) .concat((0, base_1.stateVariables)(vs).map(v => v.name)); }); return nonFreeEntities && nonFreeEntities.length > 0; }); if (annotatedMode === 'val' && (!kind || kind === 'read')) { return ['def', parametersMessage]; } if (!kind) { return ['puredef', parametersMessage]; } return [ modesForArrow.get(kind), `${componentDescription.get(kind)} variables ${addedEntitiesByComponentKind.get(kind).map(printing_1.entityToString)}`, ]; } case 'variable': { return ['pureval', "doesn't read or update any state variable"]; } } } /* * If there is a entity with an effect in the results that is not present on the * parameters, then the operator is adding that and this should * promote the operators mode. * * For example, an operator with the effect `(Read[v]) => Read[v, 'x']` is adding `Read['x']` * to the parameter effect and should be promoted to `def`. */ function addedEntities(paramEntities, resultEntity) { switch (resultEntity.kind) { case 'union': return resultEntity.entities.flatMap(entity => addedEntities(paramEntities, entity)); case 'concrete': { const vars = resultEntity.stateVariables.filter(v => !paramEntities.some(p => (0, lodash_isequal_1.default)(p, v))); if (vars.length === 0) { return []; } return [{ kind: 'concrete', stateVariables: vars }]; } case 'variable': return !paramEntities.some(p => (0, lodash_isequal_1.default)(p, resultEntity)) ? [resultEntity] : []; } } function paramEntitiesByEffect(effect) { const entitiesByComponentKind = new Map(); effect.params.forEach(p => { switch (p.kind) { case 'concrete': { p.components.forEach(c => { const existing = entitiesByComponentKind.get(c.kind) || []; entitiesByComponentKind.set(c.kind, concatEntity(existing, c.entity)); }); } break; case 'arrow': { const nested = paramEntitiesByEffect(p); nested.forEach((entities, kind) => { const existing = entitiesByComponentKind.get(kind) || []; entitiesByComponentKind.set(kind, entities.reduce(concatEntity, existing)); }); if (p.result.kind === 'concrete') { p.result.components.forEach(c => { const existing = entitiesByComponentKind.get(c.kind) || []; entitiesByComponentKind.set(c.kind, concatEntity(existing, c.entity)); }); } } break; case 'variable': // Nothing to gather break; default: (0, util_1.unreachable)(p); } }); return entitiesByComponentKind; } function concatEntity(list, entity) { switch (entity.kind) { case 'union': return entity.entities.reduce(concatEntity, list); case 'concrete': case 'variable': return list.concat(entity); } } const modeOrder = ['pureval', 'val', 'puredef', 'def', 'nondet', 'action', 'temporal', 'run']; function isMoreGeneral(m1, m2) { const p1 = modeOrder.findIndex(elem => elem === m1); const p2 = modeOrder.findIndex(elem => elem === m2); return p1 > p2; } //# sourceMappingURL=modeChecker.js.map