@informalsystems/quint
Version:
Core tool for the Quint specification language
398 lines • 17.1 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.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