UNPKG

@informalsystems/quint

Version:

Core tool for the Quint specification language

244 lines 12.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. * --------------------------------------------------------------------------------- */ 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