@informalsystems/quint
Version:
Core tool for the Quint specification language
213 lines • 13.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.matchConstraints = exports.variantConstraints = exports.itemConstraints = exports.tupleConstructorConstraints = exports.withConstraints = exports.fieldNamesConstraints = exports.fieldConstraints = exports.recordConstructorConstraints = void 0;
/**
* Special constraint cases for Quint operators that we are not able to type in our system,
* including record and tuple related operators
*
* @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 lodash_1 = require("lodash");
function recordConstructorConstraints(id, args, resultTypeVar) {
// A record constructor has the normal form Rec('field1', value1, 'field2', value2, ...)
// So we iterate over the arguments in pairs (chunks of size 2)
//
// - We can ignore the _keyType because we are verifying here that every key is a string litteral
// - We can ignore the _value because we are only doing type checking
const fields = (0, lodash_1.chunk)(args, 2).map(([[key, _keyType], [_value, valueType]]) => {
if (key.kind !== 'str') {
return (0, either_1.left)((0, errorTree_1.buildErrorLeaf)(`Generating record constraints for ${args.map(a => (0, IRprinting_1.expressionToString)(a[0]))}`, `Record field name must be a name expression but is ${key.kind}: ${(0, IRprinting_1.expressionToString)(key)}`));
}
return (0, either_1.right)({ fieldName: key.value, fieldType: valueType });
});
return (0, either_1.mergeInMany)(fields).map(fs => {
const t = { kind: 'rec', fields: { kind: 'row', fields: fs, other: { kind: 'empty' } } };
const constraint = { kind: 'eq', types: [t, resultTypeVar], sourceId: id };
return [constraint];
});
}
exports.recordConstructorConstraints = recordConstructorConstraints;
function fieldConstraints(id, args, resultTypeVar) {
// We can ignore the _fieldNameType because we are verifying here that every key is a string litteral
const [[_rec, recType], [fieldName, _fieldNameType]] = args;
if (fieldName.kind !== 'str') {
return (0, either_1.left)((0, errorTree_1.buildErrorLeaf)(`Generating record constraints for ${args.map(a => (0, IRprinting_1.expressionToString)(a[0]))}`, `Record field name must be a string expression but is ${fieldName.kind}: ${(0, IRprinting_1.expressionToString)(fieldName)}`));
}
const generalRecType = {
kind: 'rec',
fields: {
kind: 'row',
fields: [{ fieldName: fieldName.value, fieldType: resultTypeVar }],
other: { kind: 'var', name: `tail_${resultTypeVar.name}` },
},
};
const constraint = { kind: 'eq', types: [recType, generalRecType], sourceId: id };
return (0, either_1.right)([constraint]);
}
exports.fieldConstraints = fieldConstraints;
function fieldNamesConstraints(id, args, resultTypeVar) {
const [[_rec, recType]] = args;
const generalRecType = { kind: 'rec', fields: { kind: 'var', name: `rec_${resultTypeVar.name}` } };
const c1 = { kind: 'eq', types: [resultTypeVar, { kind: 'set', elem: { kind: 'str' } }], sourceId: id };
const c2 = { kind: 'eq', types: [recType, generalRecType], sourceId: id };
return (0, either_1.right)([c1, c2]);
}
exports.fieldNamesConstraints = fieldNamesConstraints;
function withConstraints(id, args, resultTypeVar) {
// We can ignore the _fieldNameType because we are verifying here that every key is a string litteral
const [[_rec, recType], [fieldName, _fieldNameType], [_value, valueType]] = args;
if (fieldName.kind !== 'str') {
return (0, either_1.left)((0, errorTree_1.buildErrorLeaf)(`Generating record constraints for ${args.map(a => (0, IRprinting_1.expressionToString)(a[0]))}`, `Record field name must be a string expression but is ${fieldName.kind}: ${(0, IRprinting_1.expressionToString)(fieldName)}`));
}
const generalRecType = {
kind: 'rec',
fields: {
kind: 'row',
fields: [{ fieldName: fieldName.value, fieldType: valueType }],
other: { kind: 'var', name: `tail_${resultTypeVar.name}` },
},
};
const c1 = { kind: 'eq', types: [recType, generalRecType], sourceId: id };
const c2 = { kind: 'eq', types: [resultTypeVar, generalRecType], sourceId: id };
return (0, either_1.right)([c1, c2]);
}
exports.withConstraints = withConstraints;
function tupleConstructorConstraints(id, args, resultTypeVar) {
const fields = args.map(([_, type], i) => {
return { fieldName: `${i}`, fieldType: type };
});
const t2 = { kind: 'tup', fields: { kind: 'row', fields: fields, other: { kind: 'empty' } } };
const c = { kind: 'eq', types: [t2, resultTypeVar], sourceId: id };
return (0, either_1.right)([c]);
}
exports.tupleConstructorConstraints = tupleConstructorConstraints;
function itemConstraints(id, args, resultTypeVar) {
// We can ignore the _itemNameType because we are verifying here that every key is a string litteral
const [[_tup, tupType], [itemName, _itemNameType]] = args;
if (itemName.kind !== 'int') {
return (0, either_1.left)((0, errorTree_1.buildErrorLeaf)(`Generating record constraints for ${args.map(a => (0, IRprinting_1.expressionToString)(a[0]))}`, `Tup field index must be an int expression but is ${itemName.kind}: ${(0, IRprinting_1.expressionToString)(itemName)}`));
}
// A tuple with item acess of N should have at least N fields
// Fill previous fileds with type variables
const fields = (0, lodash_1.times)(Number(itemName.value - 1n)).map(i => {
return { fieldName: `${i}`, fieldType: { kind: 'var', name: `tup_${resultTypeVar.name}_${i}` } };
});
fields.push({ fieldName: `${itemName.value - 1n}`, fieldType: resultTypeVar });
const generalTupType = {
kind: 'tup',
fields: {
kind: 'row',
fields: fields,
other: { kind: 'var', name: `tail_${resultTypeVar.name}` },
},
};
const constraint = { kind: 'eq', types: [tupType, generalTupType], sourceId: id };
return (0, either_1.right)([constraint]);
}
exports.itemConstraints = itemConstraints;
// The rule for the `variant` operator:
//
// Γ ⊢ e: (t, c) Γ ⊢ 'l' : str v is fresh
// --------------------------------------------------------------------
// Γ ⊢ variant('l', e) : (s, c /\ s ~ < l : t | v > /\ isDefined(s))
//
// Where < ... > is a row-based variant.
//
// This rule is explained in /doc/rfcs/rfc001-sum-types/rfc001-sum-types.org
function variantConstraints(id, args, resultTypeVar) {
// `_valuEx : valueType` is checked already via the constraintGenerator
const [[labelExpr, labelType], [_valueEx, valueType]] = args;
if (labelExpr.kind !== 'str') {
return (0, either_1.left)((0, errorTree_1.buildErrorLeaf)(`Generating variant constraints for ${args.map(a => (0, IRprinting_1.expressionToString)(a[0]))}`, `Variant label must be a string expression but is ${labelType.kind}: ${(0, IRprinting_1.expressionToString)(labelExpr)}`));
}
// A tuple with item acess of N should have at least N fields
// Fill previous fields with type variables
const variantField = { fieldName: labelExpr.value, fieldType: valueType };
const generalVarType = {
kind: 'sum',
fields: {
kind: 'row',
fields: [variantField],
other: { kind: 'var', name: `tail_${resultTypeVar.name}` },
},
};
const isDefinedConstraint = { kind: 'isDefined', type: generalVarType, sourceId: id };
const propagateResultConstraint = { kind: 'eq', types: [resultTypeVar, generalVarType], sourceId: id };
return (0, either_1.right)([isDefinedConstraint, propagateResultConstraint]);
}
exports.variantConstraints = variantConstraints;
// The rule for the `match` operator:
//
// Γ ⊢ e:(s, c)
// Γ, x1:(t1, c1') ⊢ e1:(t, c1) ... Γ, xn:(tn, cn') ⊢ en:(t, cn)
// ------------------------------------------------------------------------------
// Γ ⊢ match e { i1 : x1 => e1, ..., in : xn => en } : (t, c /\
// c1 /\ c1' /\ ... /\ cn /\ cn' /\
// s ~ < i1 : t1, ..., in : tn > )
//
// Where
//
// - < ... > is a row-based variant.
// - {l1 : x1 => e1, ..., ln : xn => en } coordinates label `li` with an eliminator `xi => ei`
// for each label in sum-type `s`.
//
// This rule is explained in /doc/rfcs/rfc001-sum-types/rfc001-sum-types.org
function matchConstraints(id, args, resultTypeVar) {
// A match eliminator has the normal form `matchVariant(expr, 'field1', elim1,..., 'fieldn', elimn)`.
// We separate the `expr` we are matching against into the `_variantExpr` and the
// `labelsAndCases`, which holds the pairs of field labels and eliminators.
const [[_variantExpr, variantType], ...labelsAndcases] = args;
// We group the `labelsAndCases` in chunks of size 2 fur subsequent analysis.
const labelAndElimPairs = (0, lodash_1.chunk)(labelsAndcases, 2);
// Now we verify that each label is a string literal and each eliminator is a unary operator.
// This is a well-formedness check prior to checking that the match expression fits the
// `sumType` of the value we are matching on.
const validatedFields = [];
const fieldValidationError = (msg) => validatedFields.push((0, either_1.left)((0, errorTree_1.buildErrorLeaf)(`Generating match constraints for ${labelsAndcases.map(a => (0, IRprinting_1.expressionToString)(a[0]))}`, msg)));
let wildCardMatch = false;
for (const [[labelExpr, _labelType], [elimExpr, elimType]] of labelAndElimPairs) {
labelExpr.kind !== 'str'
? fieldValidationError(`Match variant name must be a string literal but it is a ${labelExpr.kind}: ${(0, IRprinting_1.expressionToString)(labelExpr)}`)
: elimType.kind !== 'oper'
? fieldValidationError(`Match case eliminator must be an operator expression but it is a ${elimType.kind}: ${(0, IRprinting_1.expressionToString)(elimExpr)}`)
: elimType.args.length !== 1
? fieldValidationError(`Match case eliminator must be a unary operator but it is an operator of ${elimType.args.length} arguments: ${(0, IRprinting_1.expressionToString)(elimExpr)}`)
: !wildCardMatch && labelExpr.value === '_'
? (wildCardMatch = true) // The wildcard case, `_ => foo`, means we can match anything else
: wildCardMatch // There should only ever be 1 wildcard match, and it should be the last case
? fieldValidationError(`Invalid wildcard match ('_') in match expression: ${(0, IRprinting_1.expressionToString)(elimExpr)}. Only one wildcard can appear and it must be the final case of a match.`)
: validatedFields.push((0, either_1.right)([labelExpr.value, elimType.args[0]])); // label and associated type of a variant case
}
// TODO: Support more expressive and informative type errors.
// This will require tying into the constraint checking system.
// See https://github.com/informalsystems/quint/issues/1231
return (0, either_1.mergeInMany)(validatedFields).map((fields) => {
// Form a constraint ensuring that the match expression fits the sum-type it is applied to:
const matchCaseType = wildCardMatch ? (0, quintTypes_1.sumType)(fields, `${resultTypeVar.name}_wildcard`) : (0, quintTypes_1.sumType)(fields); // The sum-type implied by the match elimination cases.
// s ~ < i1 : t1, ..., in : tn > )
const variantTypeIsMatchCaseType = { kind: 'eq', types: [variantType, matchCaseType], sourceId: id };
// Form a set of constraints ensuring that all the result types of the match cases are the same:
// We extract just the types of the eliminator operators, which we've ensured are operators during field validation.
const eliminatorOpResultTypes = labelAndElimPairs.map(([_, elim]) => elim[1].res);
// These equality constraints implement the fact that all return values of cases, and the value of
// the match expression as a whole, are of the same type, `t`
const resultTypesAreEqual = [];
for (const resultType of eliminatorOpResultTypes) {
resultTypesAreEqual.push({ kind: 'eq', types: [resultTypeVar, resultType], sourceId: id });
}
return [variantTypeIsMatchCaseType, ...resultTypesAreEqual];
});
}
exports.matchConstraints = matchConstraints;
//# sourceMappingURL=specialConstraints.js.map