UNPKG

@informalsystems/quint

Version:

Core tool for the Quint specification language

398 lines 17.1 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.unifyEntities = exports.stateVariables = exports.entityNames = exports.toScheme = exports.effectNames = exports.unify = void 0; /** * Essentials for handling Effects: types, interfaces and unification * * @author Gabriela Moreira * * @module */ const printing_1 = require("./printing"); const either_1 = require("@sweet-monads/either"); const substitutions_1 = require("./substitutions"); const errorTree_1 = require("../errorTree"); const simplification_1 = require("./simplification"); const lodash_isequal_1 = __importDefault(require("lodash.isequal")); const lodash_1 = require("lodash"); /** * Unifies two effects by matching effect types and unifying their entities. * * @param ea an effect to be unified * @param eb the effect to be unified with * * @returns an array of substitutions that unifies both effects, when possible. * Otherwise, an error tree with an error message and its trace. */ function unify(ea, eb) { const location = `Trying to unify ${(0, printing_1.effectToString)(ea)} and ${(0, printing_1.effectToString)(eb)}`; const [e1, e2] = [ea, eb].map(simplification_1.simplify); if ((0, printing_1.effectToString)(e1) === (0, printing_1.effectToString)(e2)) { return (0, either_1.right)([]); } else if (e1.kind === 'arrow' && e2.kind === 'arrow') { return unifyArrows(location, e1, e2); } else if (e1.kind === 'concrete' && e2.kind === 'concrete') { return unifyConcrete(location, e1, e2).mapLeft(err => (0, errorTree_1.buildErrorTree)(location, err)); } else if (e1.kind === 'variable') { return bindEffect(e1.name, e2).mapLeft(msg => (0, errorTree_1.buildErrorLeaf)(location, msg)); } else if (e2.kind === 'variable') { return bindEffect(e2.name, e1).mapLeft(msg => (0, errorTree_1.buildErrorLeaf)(location, msg)); } else { return (0, either_1.left)({ location, message: "Can't unify different types of effects", children: [], }); } } exports.unify = unify; /** * Finds all names that occur in an effect * * @param effect the Effect to have its names found * * @returns the set of effect variables and the set of entity variables */ function effectNames(effect) { switch (effect.kind) { case 'concrete': return { effectVariables: new Set(), entityVariables: new Set(effect.components.flatMap(c => entityNames(c.entity))), }; case 'arrow': { const nested = effect.params.concat(effect.result).flatMap(effectNames); return nested.reduce((acc, { effectVariables: effectVariables, entityVariables: entityVariables }) => ({ effectVariables: new Set([...acc.effectVariables, ...effectVariables]), entityVariables: new Set([...acc.entityVariables, ...entityVariables]), }), { effectVariables: new Set(), entityVariables: new Set() }); } case 'variable': return { effectVariables: new Set([effect.name]), entityVariables: new Set() }; } } exports.effectNames = effectNames; /** * Converts an effect to an effect scheme without any quantification * * @param effect the effect to be converted * * @returns an effect scheme with that effect and no quantification */ function toScheme(effect) { return { effect: effect, effectVariables: new Set(), entityVariables: new Set(), }; } exports.toScheme = toScheme; function bindEffect(name, effect) { if (effectNames(effect).effectVariables.has(name)) { return (0, either_1.left)(`Can't bind ${name} to ${(0, printing_1.effectToString)(effect)}: cyclical binding`); } else { return (0, either_1.right)([{ kind: 'effect', name, value: effect }]); } } function bindEntity(name, entity) { switch (entity.kind) { case 'concrete': case 'variable': return [{ kind: 'entity', name, value: entity }]; case 'union': if (entityNames(entity).includes(name)) { // An entity variable (which always stands for a set of state variables) // unifies with the union of n sets of entities that may include itself, // iff it unifies with each set. // // I.e.: // // (v1 =.= v1 ∪ ... ∪ vn) <=> (v1 =.= ... =.= vn) // // We call this function recursively because, in the general case, // occurrences of `v1` may be nested, as in: // // v1 =.= v1 ∪ (v2 ∪ (v3 ∪ v1)) ∪ v4 // // In practice, we are flattening unions before we call this function, // but solving the general case ensures we preserve correct behavior // even if this function is used on its own, without incurring any // notable overhead when `entities` is already flat. return entity.entities.flatMap(e => bindEntity(name, e)); } else { // Otherwise, the variable may be bound to the union of the entities // without qualification. return [{ kind: 'entity', name, value: entity }]; } } } /** * Finds all entity names referred to by an entity * * @param entity the entity to be searched * * @returns a list of names */ function entityNames(entity) { switch (entity.kind) { case 'concrete': return []; case 'variable': return [entity.name]; case 'union': return entity.entities.flatMap(entityNames); } } exports.entityNames = entityNames; /** * Finds all state variables referred to by an entity * * @param entity the entity to be searched * * @returns a list of state entities */ function stateVariables(entity) { switch (entity.kind) { case 'variable': return []; case 'concrete': return entity.stateVariables; case 'union': return entity.entities.flatMap(stateVariables); } } exports.stateVariables = stateVariables; function unifyArrows(location, e1, e2) { const paramsTuple = [e1.params, e2.params]; return (0, either_1.right)(paramsTuple) .chain(([p1, p2]) => { if (p1.length === p2.length) { return (0, either_1.right)([p1, p2, []]); } else { return (0, either_1.left)((0, errorTree_1.buildErrorLeaf)(location, `Expected ${p1.length} arguments, got ${p2.length}`)); } }) .chain(([p1, p2, unpackingSubs]) => { const e1r = (0, substitutions_1.applySubstitution)(unpackingSubs, e1.result); const e2r = (0, substitutions_1.applySubstitution)(unpackingSubs, e2.result); return (0, either_1.mergeInMany)([e1r, e2r]) .chain(([e1result, e2result]) => { const [arrow1, subs1] = simplifyArrowEffect(p1, e1result); const [arrow2, subs2] = simplifyArrowEffect(p2, e2result); const subs = (0, substitutions_1.compose)(subs1, subs2).chain(s => (0, substitutions_1.compose)(s, unpackingSubs)); return arrow1.params .reduce((result, e, i) => { return result.chain(subs => applySubstitutionsAndUnify(subs, e, arrow2.params[i])); }, subs) .chain(subs => applySubstitutionsAndUnify(subs, arrow1.result, arrow2.result)); }) .mapLeft(err => (0, errorTree_1.buildErrorTree)(location, err)); }); } const compatibleComponentKinds = [ ['read', 'update'], ['read', 'temporal'], ]; function canCoexist(c1, c2) { return compatibleComponentKinds.some(kinds => kinds.includes(c1.kind) && kinds.includes(c2.kind)); } function isDominant(c1, c2) { return c1.kind === 'update' && c2.kind === 'temporal'; } function unifyConcrete(location, e1, e2) { const generalResult = e1.components.reduce((result, ca) => { return e2.components.reduce((innerResult, cb) => { return innerResult .chain(subs => { const c1 = { ...ca, entity: (0, substitutions_1.applySubstitutionToEntity)(subs, ca.entity) }; const c2 = { ...cb, entity: (0, substitutions_1.applySubstitutionToEntity)(subs, cb.entity) }; if (c1.kind === c2.kind) { return unifyEntities(c1.entity, c2.entity); } else if (canCoexist(c1, c2)) { return (0, either_1.right)([]); } else if (isDominant(c1, c2)) { // The dominated component has to be nullified return unifyEntities({ kind: 'concrete', stateVariables: [] }, c2.entity); } else if (isDominant(c2, c1)) { // The dominated component has to be nullified return unifyEntities(c1.entity, { kind: 'concrete', stateVariables: [] }); } else { // We should never reach this. Instead, one of the kinds should // dominate the other, and then we nullify the dominated component return (0, either_1.left)({ location, message: `Can't unify ${c1.kind} ${(0, printing_1.effectComponentToString)(c1)} and ${c2.kind} ${(0, printing_1.effectComponentToString)(c2)}`, children: [], }); } }) .chain(s => innerResult.chain(s2 => (0, substitutions_1.compose)(s2, s))); }, result); }, (0, either_1.right)([])); return nullifyUnmatchedComponents(e1.components, e2.components) .chain(s1 => nullifyUnmatchedComponents(e2.components, e1.components).chain(s2 => (0, substitutions_1.compose)(s1, s2))) .chain(s1 => generalResult.chain(s2 => (0, substitutions_1.compose)(s1, s2))); } function unifyEntities(va, vb) { const v1 = (0, simplification_1.flattenUnions)(va); const v2 = (0, simplification_1.flattenUnions)(vb); const location = `Trying to unify entities [${(0, printing_1.entityToString)(v1)}] and [${(0, printing_1.entityToString)(v2)}]`; if (v1.kind === 'concrete' && v2.kind === 'concrete') { if ((0, lodash_isequal_1.default)(new Set(v1.stateVariables.map(v => v.name)), new Set(v2.stateVariables.map(v => v.name)))) { return (0, either_1.right)([]); } else { return (0, either_1.left)({ location, message: `Expected [${v1.stateVariables.map(v => v.name)}] and [${v2.stateVariables.map(v => v.name)}] to be the same`, children: [], }); } } else if (v1.kind === 'variable' && v2.kind === 'variable' && v1.name === v2.name) { return (0, either_1.right)([]); } else if (v1.kind === 'variable') { return (0, either_1.right)(bindEntity(v1.name, v2)); } else if (v2.kind === 'variable') { return (0, either_1.right)(bindEntity(v2.name, v1)); } else if ((0, lodash_isequal_1.default)(v1, v2)) { return (0, either_1.right)([]); } else if (v1.kind === 'union' && v2.kind === 'concrete') { return (0, either_1.mergeInMany)(v1.entities.map(v => unifyEntities(v, v2))) .map(subs => subs.flat()) .mapLeft(err => (0, errorTree_1.buildErrorTree)(location, err)); } else if (v1.kind === 'concrete' && v2.kind === 'union') { return unifyEntities(v2, v1); } else if (v1.kind === 'union' && v2.kind === 'union') { const intersection = (0, lodash_1.intersectionWith)(v1.entities, v2.entities, lodash_isequal_1.default); if (intersection.length > 0) { const s1 = { ...v1, entities: (0, lodash_1.differenceWith)(v1.entities, intersection, lodash_isequal_1.default) }; const s2 = { ...v2, entities: (0, lodash_1.differenceWith)(v2.entities, intersection, lodash_isequal_1.default) }; // There was an intersection, try to unify the remaining entities return unifyEntities(s1, s2); } // At least one of the entities is a union // Unifying sets is complicated and we should never have to do this in Quint's // use case for this effect system return (0, either_1.left)({ location, message: 'Unification for unions of entities is not implemented', children: [], }); } else { throw new Error(`Impossible: all cases should be covered`); } } exports.unifyEntities = unifyEntities; /** Check if there are any components in c1 that are not in c2 * If so, they have to be nullified */ function nullifyUnmatchedComponents(components1, components2) { return components1.reduce((result, c2) => { return result.chain(subs => { if (!components2.some(c1 => c1.kind === c2.kind)) { const newSubs = unifyEntities(c2.entity, { kind: 'concrete', stateVariables: [] }); return newSubs.chain(s => (0, substitutions_1.compose)(subs, s)); } return (0, either_1.right)(subs); }); }, (0, either_1.right)([])); } function applySubstitutionsAndUnify(subs, e1, e2) { return (0, either_1.mergeInMany)([(0, substitutions_1.applySubstitution)(subs, e1), (0, substitutions_1.applySubstitution)(subs, e2)]) .chain(effectsWithSubstitutions => unify(...effectsWithSubstitutions)) .chain(newSubstitutions => (0, substitutions_1.compose)(newSubstitutions, subs)); } /** * Simplifies effects of the form (Read[v0, ..., vn]) => Read[v0, ..., vn] into * (Read[v0#...#vn]) => Read[v0#...#vn] so the entities can be unified with other * sets of entities. All of the entities v0, ..., vn are bound to the single entity * named v0#...#vn. E.g., for entities v0, v1, v2, we will produce the new unique entity * v0#v1#v2 and the bindings v1 |-> v0#v1#v2, v1 |-> v0#v1#v2, v2 |-> v0#v1#v2. * This new name could be a fresh name, but we use an unique name since there is no * fresh name generation in this pure unification environment. * * @param params the arrow effect parameters * @param result the arrow effect result * @returns an arrow effect with the new format and the substitutions with binded entities */ function simplifyArrowEffect(params, result) { if (params.length === 1 && (0, printing_1.effectToString)(params[0]) === (0, printing_1.effectToString)(result) && params[0].kind === 'concrete') { const effect = params[0]; const hashedComponents = effect.components.map(c => ({ kind: c.kind, entity: hashEntity(c.entity) })); const arrow = { kind: 'arrow', params: [{ kind: 'concrete', components: hashedComponents }], result }; const subs = effect.components.flatMap(c => { return entityNames(c.entity).map(n => { return { kind: 'entity', name: n, value: hashEntity(c.entity) }; }); }); return [arrow, subs]; } return [{ kind: 'arrow', params, result }, []]; } function hashEntity(va) { switch (va.kind) { case 'concrete': return va; case 'variable': return va; case 'union': { const nested = va.entities.map(hashEntity); // Separate variables from the rest const [name, entities] = nested.reduce(([name, entities], v) => { if (v.kind === 'variable') { name.push(v.name); } else { entities.push(v); } return [name, entities]; }, [[], []]); // Consider all cases of name and entities being empty or not if (name.length === 0) { if (entities.length === 0) { return { kind: 'concrete', stateVariables: [] }; } else { return { kind: 'union', entities: entities }; } } else { if (entities.length === 0) { return { kind: 'variable', name: name.join('#') }; } else { return { kind: 'union', entities: [{ kind: 'variable', name: name.join('#') }, ...entities] }; } } } } } //# sourceMappingURL=base.js.map