@informalsystems/quint
Version:
Core tool for the Quint specification language
244 lines • 12.1 kB
JavaScript
;
/* ----------------------------------------------------------------------------------
* Copyright 2022 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.unifyRows = exports.unify = exports.solveConstraint = void 0;
/**
* Constraint solving for Quint's type system by unifying equality constraints
*
* @author Gabriela Moreira
*
* @module
*/
const either_1 = require("@sweet-monads/either");
const errorTree_1 = require("../errorTree");
const IRprinting_1 = require("../ir/IRprinting");
const quintTypes_1 = require("../ir/quintTypes");
const base_1 = require("./base");
const substitutions_1 = require("./substitutions");
const lodash_1 = require("lodash");
const simplification_1 = require("./simplification");
/*
* Try to solve a constraint by unifying all pairs of types in equality
* constraints inside it.
*
* @param table the lookup table for the module
* @param constraint the constraint to be solved
*
* @returns the substitutions from the unifications, if successful. Otherwise, a
* map from source ids to errors.
*/
function solveConstraint(table, constraint) {
const errors = new Map();
switch (constraint.kind) {
case 'empty':
return (0, either_1.right)([]);
case 'eq':
return unify(table, constraint.types[0], constraint.types[1]).mapLeft(e => {
errors.set(constraint.sourceId, e);
return errors;
});
case 'conjunction': {
// Chain solving of inner constraints, collecting all errors (even after the first failure)
return constraint.constraints
.sort(base_1.compareConstraints)
.reduce((result, con) => {
// If previous result is a failure, try to solve the original constraint
// to gather all errors instead of just propagating the first one
let newCons = con;
result.map(s => {
newCons = (0, substitutions_1.applySubstitutionToConstraint)(table, s, con);
});
return solveConstraint(table, newCons)
.mapLeft(e => {
e.forEach((error, id) => errors.set(id, error));
return errors;
})
.chain(newSubs => result.map(s => (0, substitutions_1.compose)(table, newSubs, s)));
}, (0, either_1.right)([]));
}
case 'isDefined': {
for (const def of table.values()) {
if (def.kind === 'typedef' && def.type) {
const subst = unify(table, def.type, constraint.type);
if (subst.isRight()) {
// We found a defined type unifying with the given schema
return (0, either_1.right)(subst.unwrap());
}
}
}
errors.set(constraint.sourceId, (0, errorTree_1.buildErrorLeaf)(`Looking for defined type unifying with ${(0, IRprinting_1.typeToString)(constraint.type)}`, 'Expected type is not defined'));
return (0, either_1.left)(errors);
}
}
}
exports.solveConstraint = solveConstraint;
/**
* Unifies two Quint types
*
* @param t1 a type to be unified
* @param t2 the type to be unified with
*
* @returns an array of substitutions that unifies both types, when possible.
* Otherwise, an error tree with an error message and its trace.
*/
function unify(table, t1, t2) {
const location = `Trying to unify ${(0, IRprinting_1.typeToString)(t1)} and ${(0, IRprinting_1.typeToString)(t2)}`;
if ((0, IRprinting_1.typeToString)(t1) === (0, IRprinting_1.typeToString)(t2)) {
return (0, either_1.right)([]);
}
else if (t1.kind === 'var') {
return bindType(t1.name, t2).mapLeft(msg => (0, errorTree_1.buildErrorLeaf)(location, msg));
}
else if (t2.kind === 'var') {
return bindType(t2.name, t1).mapLeft(msg => (0, errorTree_1.buildErrorLeaf)(location, msg));
}
else if (t1.kind === 'oper' && t2.kind === 'oper') {
return checkSameLength(location, t1.args, t2.args)
.chain(([args1, args2]) => chainUnifications(table, [...args1, t1.res], [...args2, t2.res]))
.mapLeft(error => (0, errorTree_1.buildErrorTree)(location, error));
}
else if (t1.kind === 'set' && t2.kind === 'set') {
return unify(table, t1.elem, t2.elem);
}
else if (t1.kind === 'list' && t2.kind === 'list') {
return unify(table, t1.elem, t2.elem);
}
else if (t1.kind === 'fun' && t2.kind === 'fun') {
const result = unify(table, t1.arg, t2.arg);
return result.chain(subs => {
const subs2 = unify(table, (0, substitutions_1.applySubstitution)(table, subs, t1.res), (0, substitutions_1.applySubstitution)(table, subs, t2.res));
return subs2.map(s => (0, substitutions_1.compose)(table, subs, s));
});
}
else if (t1.kind === 'tup' && t2.kind === 'tup') {
return unifyRows(table, t1.fields, t2.fields).mapLeft(error => (0, errorTree_1.buildErrorTree)(location, error));
}
else if (t1.kind === 'const') {
return unifyWithAlias(table, t1, t2);
}
else if (t2.kind === 'const') {
return unifyWithAlias(table, t2, t1);
}
else if ((t1.kind === 'rec' && t2.kind === 'rec') || (t1.kind === 'sum' && t2.kind === 'sum')) {
return unifyRows(table, t1.fields, t2.fields).mapLeft(error => (0, errorTree_1.buildErrorTree)(location, error));
}
else {
return (0, either_1.left)((0, errorTree_1.buildErrorLeaf)(location, `Couldn't unify ${t1.kind} and ${t2.kind}`));
}
}
exports.unify = unify;
/**
* Unifies two Quint rows
*
* @param r1 a row to be unified
* @param r2 the row to be unified with
*
* @returns an array of substitutions that unifies both rows, when possible.
* Otherwise, an error tree with an error message and its trace.
*/
function unifyRows(table, r1, r2) {
// The unification algorithm assumes that rows are simplified to a normal form.
// That means that the `other` field is either a row variable or an empty row
// and `fields` is never an empty list
const ra = (0, simplification_1.simplifyRow)(r1);
const rb = (0, simplification_1.simplifyRow)(r2);
const location = `Trying to unify ${(0, IRprinting_1.rowToString)(ra)} and ${(0, IRprinting_1.rowToString)(rb)}`;
// Standard comparison and variable binding
if ((0, IRprinting_1.rowToString)(ra) === (0, IRprinting_1.rowToString)(rb)) {
return (0, either_1.right)([]);
}
else if (ra.kind === 'var') {
return bindRow(ra.name, rb).mapLeft(msg => (0, errorTree_1.buildErrorLeaf)(location, msg));
}
else if (rb.kind === 'var') {
return bindRow(rb.name, ra).mapLeft(msg => (0, errorTree_1.buildErrorLeaf)(location, msg));
}
else if (ra.kind === 'row' && rb.kind === 'row') {
// Both rows are normal rows, so we need to compare their fields
const sharedFieldNames = ra.fields.map(f => f.fieldName).filter(n => rb.fields.some(f => n === f.fieldName));
if (sharedFieldNames.length === 0) {
// No shared fields, so we can just bind the tails, if they exist and are different.
if (ra.other.kind === 'var' && rb.other.kind === 'var' && ra.other.name !== rb.other.name) {
// The result should be { ra.fields + rb.fields, tailVar }
const tailVar = { kind: 'var', name: `$${ra.other.name}$${rb.other.name}` };
const s1 = bindRow(ra.other.name, { ...rb, other: tailVar });
const s2 = bindRow(rb.other.name, { ...ra, other: tailVar });
// These bindings + composition should always succeed. I couldn't find a scenario where they don't.
return s1.chain(sa => s2.map(sb => (0, substitutions_1.compose)(table, sa, sb))).mapLeft(msg => (0, errorTree_1.buildErrorLeaf)(location, msg));
}
else {
return (0, either_1.left)((0, errorTree_1.buildErrorLeaf)(location, `Incompatible tails for rows with disjoint fields: ${(0, IRprinting_1.rowToString)(ra.other)} and ${(0, IRprinting_1.rowToString)(rb.other)}`));
}
}
else {
// There are shared fields.
const uniqueFields1 = ra.fields.filter(f => !sharedFieldNames.includes(f.fieldName));
const uniqueFields2 = rb.fields.filter(f => !sharedFieldNames.includes(f.fieldName));
// Unify the disjoint fields with tail variables
// This call will fit in the above case of row unification
const tailSubs = unifyRows(table, { ...ra, fields: uniqueFields1 }, { ...rb, fields: uniqueFields2 });
// Sort shared fields by field name, and get their types
const fieldTypes = sharedFieldNames.map(n => {
const f1 = ra.fields.find(f => f.fieldName === n);
const f2 = rb.fields.find(f => f.fieldName === n);
return [f1.fieldType, f2.fieldType];
});
// Now, for each shared field, we need to unify the types
const fieldSubs = chainUnifications(table, ...(0, lodash_1.unzip)(fieldTypes));
// Return the composition of the two substitutions
return tailSubs
.chain(subs => fieldSubs.map(s => (0, substitutions_1.compose)(table, subs, s)))
.mapLeft(error => (0, errorTree_1.buildErrorTree)(location, error));
}
}
else {
return (0, either_1.left)((0, errorTree_1.buildErrorLeaf)(location, `Couldn't unify ${ra.kind} and ${rb.kind}`));
}
}
exports.unifyRows = unifyRows;
function unifyWithAlias(table, t1, t2) {
const aliasValue = t1.id ? table.get(t1.id) : undefined;
if (aliasValue?.kind !== 'typedef') {
return (0, either_1.left)((0, errorTree_1.buildErrorLeaf)(`Trying to unify ${t1.name} and ${(0, IRprinting_1.typeToString)(t2)}`, `Couldn't find type alias ${t1.name}`));
}
if (!aliasValue.type) {
return (0, either_1.left)((0, errorTree_1.buildErrorLeaf)(`Trying to unify ${t1.name} and ${(0, IRprinting_1.typeToString)(t2)}`, `Couldn't unify uninterpreted type ${t1.name} with different type`));
}
return unify(table, aliasValue.type, t2);
}
function bindType(name, type) {
if ((0, quintTypes_1.typeNames)(type).typeVariables.has(name)) {
return (0, either_1.left)(`Can't bind ${name} to ${(0, IRprinting_1.typeToString)(type)}: cyclical binding`);
}
else {
return (0, either_1.right)([{ kind: 'type', name: name, value: type }]);
}
}
function bindRow(name, row) {
if ((0, quintTypes_1.rowNames)(row).has(name)) {
return (0, either_1.left)(`Can't bind ${name} to ${(0, IRprinting_1.rowToString)(row)}: cyclical binding`);
}
else {
return (0, either_1.right)([{ kind: 'row', name: name, value: row }]);
}
}
function applySubstitutionsAndUnify(table, subs, t1, t2) {
const newSubstitutions = unify(table, (0, substitutions_1.applySubstitution)(table, subs, t1), (0, substitutions_1.applySubstitution)(table, subs, t2));
return newSubstitutions.map(newSubs => (0, substitutions_1.compose)(table, newSubs, subs));
}
function checkSameLength(location, types1, types2) {
if (types1.length !== types2.length) {
return (0, either_1.left)((0, errorTree_1.buildErrorLeaf)(location, `Expected ${types1.length} arguments, got ${types2.length}`));
}
return (0, either_1.right)([types1, types2]);
}
function chainUnifications(table, types1, types2) {
return types1.reduce((result, t, i) => {
return result.chain(subs => applySubstitutionsAndUnify(table, subs, t, types2[i]));
}, (0, either_1.right)([]));
}
//# sourceMappingURL=constraintSolver.js.map