@informalsystems/quint
Version:
Core tool for the Quint specification language
285 lines • 11.3 kB
JavaScript
;
/* ----------------------------------------------------------------------------------
* 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