@informalsystems/quint
Version:
Core tool for the Quint specification language
455 lines (454 loc) • 20.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const mocha_1 = require("mocha");
const chai_1 = require("chai");
const inferrer_1 = require("../../src/types/inferrer");
const printing_1 = require("../../src/types/printing");
const errorTree_1 = require("../../src/errorTree");
const util_1 = require("../util");
const typeApplicationResolution_1 = require("../../src/types/typeApplicationResolution");
// Utility used to print update `stringType` values to make
// updating the expected values in the following tests less
// painful.
function _printUpdatedStringTypes(stringTypes) {
console.log('[');
stringTypes.forEach(([n, t]) => console.log(`[${n}n, '${t}'],`));
console.log(']');
}
(0, mocha_1.describe)('inferTypes', () => {
function inferTypesForModules(text) {
const { modules: parsedModules, table } = (0, util_1.parseMockedModule)(text);
// Type inference assumes all type applications (e.g., `Foo[int, str]`) have been resolved.
const resolver = new typeApplicationResolution_1.TypeApplicationResolver(table);
const inferrer = new inferrer_1.TypeInferrer(table);
// Used to collect errors found during type application
let typeAppErrs = new Map();
const modules = parsedModules.map(m => {
const [errs, declarations] = resolver.resolveTypeApplications(m.declarations);
typeAppErrs = new Map([...typeAppErrs, ...errs]);
return { ...m, declarations };
});
const [inferenceErrors, inferenceSchemes] = inferrer.inferTypes(modules.flatMap(m => m.declarations));
const combinedErrors = new Map([...inferenceErrors, ...typeAppErrs]);
return [combinedErrors, inferenceSchemes];
}
function inferTypesForDefs(defs) {
const text = `module wrapper { ${defs.join('\n')} }`;
return inferTypesForModules(text);
}
(0, mocha_1.it)('infers types for basic expressions', () => {
const defs = ['def a = 1 + 2', 'def b(p, q) = p + q', 'val c = val m = 2 { m }', 'def d(S) = S.map(p => p + 10)'];
const [errors, types] = inferTypesForDefs(defs);
chai_1.assert.isEmpty(errors, `Should find no errors, found: ${[...errors.values()].map(errorTree_1.errorTreeToString)}`);
const stringTypes = Array.from(types.entries()).map(([id, type]) => [id, (0, printing_1.typeSchemeToString)(type)]);
// _printUpdatedStringTypes(stringTypes)
chai_1.assert.sameDeepMembers(stringTypes, [
[1n, 'int'],
[2n, 'int'],
[3n, 'int'],
[4n, 'int'],
[5n, 'int'],
[6n, 'int'],
[7n, 'int'],
[8n, 'int'],
[9n, 'int'],
[10n, '(int, int) => int'],
[11n, '(int, int) => int'],
[12n, 'int'],
[13n, 'int'],
[14n, 'int'],
[15n, 'int'],
[16n, 'int'],
[17n, 'Set[int]'],
[18n, 'Set[int]'],
[19n, 'int'],
[20n, 'int'],
[21n, 'int'],
[22n, 'int'],
[23n, '(int) => int'],
[24n, 'Set[int]'],
[25n, '(Set[int]) => Set[int]'],
[26n, '(Set[int]) => Set[int]'],
]);
});
(0, mocha_1.it)('infers types for high-order operators', () => {
const defs = ['def a(f, p) = f(p)', 'def b(g, q) = g(q) + g(not(q))'];
const [errors, types] = inferTypesForDefs(defs);
chai_1.assert.isEmpty(errors, `Should find no errors, found: ${[...errors.values()].map(errorTree_1.errorTreeToString)}`);
const stringTypes = Array.from(types.entries()).map(([id, type]) => [id, (0, printing_1.typeSchemeToString)(type)]);
// _printUpdatedStringTypes(stringTypes)
chai_1.assert.sameDeepMembers(stringTypes, [
[7n, '(bool) => int'],
[8n, 'bool'],
[9n, 'bool'],
[10n, 'int'],
[11n, 'bool'],
[12n, 'bool'],
[13n, 'int'],
[14n, 'int'],
[15n, '((bool) => int, bool) => int'],
[16n, '((bool) => int, bool) => int'],
[1n, '(t_p_2) => _t4'],
[2n, 't_p_2'],
[3n, 't_p_2'],
[4n, '_t4'],
[5n, '((t_p_2) => _t4, t_p_2) => _t4'],
[6n, '∀ t0, t1 . ((t0) => t1, t0) => t1'],
]);
});
(0, mocha_1.it)('infers types for records', () => {
const defs = [
'var x: { f1: int, f2: bool }',
'val m = Set(x, { f1: 1, f2: true })',
'def e(p) = x.with("f1", p.f1)',
'val a = e({ f1: 2 }).fieldNames()',
];
const [errors, types] = inferTypesForDefs(defs);
chai_1.assert.isEmpty(errors, `Should find no errors, found: ${[...errors.values()].map(errorTree_1.errorTreeToString)}`);
const stringTypes = Array.from(types.entries()).map(([id, type]) => [id, (0, printing_1.typeSchemeToString)(type)]);
// _printUpdatedStringTypes(stringTypes)
chai_1.assert.sameDeepMembers(stringTypes, [
[4n, '{ f1: int, f2: bool }'],
[5n, '{ f1: int, f2: bool }'],
[7n, 'str'],
[6n, 'int'],
[9n, 'str'],
[8n, 'bool'],
[10n, '{ f1: int, f2: bool }'],
[11n, 'Set[{ f1: int, f2: bool }]'],
[12n, 'Set[{ f1: int, f2: bool }]'],
[13n, '{ f1: int | tail__t3 }'],
[14n, '{ f1: int, f2: bool }'],
[15n, 'str'],
[16n, '{ f1: int | tail__t3 }'],
[17n, 'str'],
[18n, 'int'],
[19n, '{ f1: int, f2: bool }'],
[20n, '({ f1: int | tail__t3 }) => { f1: int, f2: bool }'],
[21n, '∀ r0 . ({ f1: int | r0 }) => { f1: int, f2: bool }'],
[23n, 'str'],
[22n, 'int'],
[24n, '{ f1: int }'],
[25n, '{ f1: int, f2: bool }'],
[26n, 'Set[str]'],
[27n, 'Set[str]'],
]);
});
(0, mocha_1.it)('infers types for tuples', () => {
const defs = ['def e(p, q) = (p._1, q._2)'];
const [errors, types] = inferTypesForDefs(defs);
chai_1.assert.isEmpty(errors, `Should find no errors, found: ${[...errors.values()].map(errorTree_1.errorTreeToString)}`);
const stringTypes = Array.from(types.entries()).map(([id, type]) => [id, (0, printing_1.typeSchemeToString)(type)]);
// _printUpdatedStringTypes(stringTypes)
chai_1.assert.sameDeepMembers(stringTypes, [
[1n, '(_t0 | tail__t0)'],
[2n, '(tup__t1_0, _t1 | tail__t1)'],
[3n, '(_t0 | tail__t0)'],
[4n, 'int'],
[5n, '_t0'],
[6n, '(tup__t1_0, _t1 | tail__t1)'],
[7n, 'int'],
[8n, '_t1'],
[9n, '(_t0, _t1)'],
[10n, '((_t0 | tail__t0), (tup__t1_0, _t1 | tail__t1)) => (_t0, _t1)'],
[11n, '∀ t0, t1, t2, r0, r1 . ((t0 | r0), (t1, t2 | r1)) => (t0, t2)'],
]);
});
(0, mocha_1.it)('infers types for variants', () => {
const defs = ['type T = A(int) | B', 'val a = variant("A", 3)'];
const [errors, types] = inferTypesForDefs(defs);
chai_1.assert.isEmpty(errors, `Should find no errors, found: ${[...errors.values()].map(errorTree_1.errorTreeToString)}`);
const stringTypes = Array.from(types.entries()).map(([id, type]) => [id, (0, printing_1.typeSchemeToString)(type)]);
// _printUpdatedStringTypes(stringTypes)
chai_1.assert.sameDeepMembers(stringTypes, [
[15n, 'str'],
[16n, 'int'],
[17n, '(A(int) | B(()))'],
[18n, '(A(int) | B(()))'],
[6n, 'int'],
[5n, 'str'],
[7n, 'int'],
[8n, '(A(int) | B(()))'],
[9n, '(int) => (A(int) | B(()))'],
[10n, '(int) => (A(int) | B(()))'],
[11n, 'str'],
[12n, '()'],
[13n, '(B(()) | A(int))'],
[14n, '(B(()) | A(int))'],
]);
});
(0, mocha_1.it)('infers types for different sum type declarations with the same label but different values', () => {
// See https://github.com/informalsystems/quint/issues/1275
const text = `
module A {
type T1 = | A(int)
}
module B {
type T2 = | A(bool)
}
`;
const [errors, _] = inferTypesForModules(text);
chai_1.assert.deepEqual([...errors.entries()], []);
});
(0, mocha_1.it)('infers types for match expression', () => {
const defs = ['type T = A(int) | B', 'val a = variant("A", 3)', 'val nine = match a { A(n) => n * n | B => 9 }'];
const [errors, types] = inferTypesForDefs(defs);
chai_1.assert.isEmpty(errors, `Should find no errors, found: ${[...errors.values()].map(errorTree_1.errorTreeToString)}`);
const stringTypes = Array.from(types.entries()).map(([id, type]) => [id, (0, printing_1.typeSchemeToString)(type)]);
// _printUpdatedStringTypes(stringTypes)
chai_1.assert.sameDeepMembers(stringTypes, [
[15n, 'str'],
[16n, 'int'],
[17n, '(A(int) | B(()))'],
[18n, '(A(int) | B(()))'],
[6n, 'int'],
[5n, 'str'],
[7n, 'int'],
[8n, '(A(int) | B(()))'],
[9n, '(int) => (A(int) | B(()))'],
[10n, '(int) => (A(int) | B(()))'],
[11n, 'str'],
[12n, '()'],
[13n, '(B(()) | A(int))'],
[14n, '(B(()) | A(int))'],
[19n, '(A(int) | B(()))'],
[25n, 'str'],
[27n, 'int'],
[20n, 'int'],
[21n, 'int'],
[22n, 'int'],
[26n, '(int) => int'],
[28n, 'str'],
[30n, '()'],
[23n, 'int'],
[29n, '(()) => int'],
[24n, 'int'],
[31n, 'int'],
]);
});
(0, mocha_1.it)('infers types for match expression with wildcard case', () => {
const defs = ['type T = A(int) | B', 'val nine : int = match B { _ => 9 }'];
const [errors, _] = inferTypesForDefs(defs);
chai_1.assert.isEmpty(errors);
});
(0, mocha_1.it)('reports a type error for match expressions that return inconsitent types in cases', () => {
const defs = [
'type T = A(int) | B',
'val a = variant("A", 3)',
'val nine = match a { A(n) => n * n | B => "not an int" }',
];
const [errors, _] = inferTypesForDefs(defs);
chai_1.assert.isNotEmpty(errors);
chai_1.assert.match([...errors.values()].map(errorTree_1.errorTreeToString)[0], RegExp("Couldn't unify int and str"));
});
(0, mocha_1.it)('reports a type error for match expressions with multiple wildcard cases', () => {
const defs = [
'type T = A(int) | B | C',
'val a = variant("A", 3)',
'val nine = match B { A(n) => "OK" | _ => "first wilcard" | _ => "second, invalid wildcard" }',
];
const [errors, _] = inferTypesForDefs(defs);
chai_1.assert.isNotEmpty(errors);
chai_1.assert.match([...errors.values()].map(errorTree_1.errorTreeToString)[0], RegExp('Invalid wildcard match'));
});
(0, mocha_1.it)('reports a type error for match expressions with non-final wildcard case', () => {
const defs = [
'type T = A(int) | B | C',
'val a = variant("A", 3)',
'val nine = match B { A(n) => "OK" | _ => "invalid, non-final wilcard" | C => "OK" }',
];
const [errors, _] = inferTypesForDefs(defs);
chai_1.assert.isNotEmpty(errors);
chai_1.assert.match([...errors.values()].map(errorTree_1.errorTreeToString)[0], RegExp('Invalid wildcard match'));
});
(0, mocha_1.it)('reports a type error for match expressions on non-variant expressions', () => {
const defs = [
'val notAVariant = "this is not a variant"',
'val invalid = match notAVariant { A(n) => n * n | B => 9 }',
];
const [errors, _] = inferTypesForDefs(defs);
chai_1.assert.isNotEmpty(errors);
chai_1.assert.match([...errors.values()].map(errorTree_1.errorTreeToString)[0], RegExp(`Couldn't unify str and sum`));
});
(0, mocha_1.it)('reports a type error for matchVariant operator with non-label arguments', () => {
const defs = ['type T = A(int) | B', 'val a = variant("A", 3)', 'val nine = matchVariant(a, 3, (_ => 9))'];
const [errors, _] = inferTypesForDefs(defs);
chai_1.assert.isNotEmpty(errors);
chai_1.assert.match([...errors.values()].map(errorTree_1.errorTreeToString)[0], RegExp('Match variant name must be a string literal but it is a int: 3'));
});
(0, mocha_1.it)('reports a type error for a non-applicable case', () => {
const defs = [
'type T = A(int) | B',
'val a = variant("A", 3)',
'val nonExhaustive = match a { A(x) => x * x | B => 9 | NotAVariant => 9 }',
];
const [errors, _] = inferTypesForDefs(defs);
chai_1.assert.isNotEmpty(errors);
chai_1.assert.match([...errors.values()].map(errorTree_1.errorTreeToString)[0], RegExp(`Couldn't unify empty and row`));
});
(0, mocha_1.it)('reports a type error for non-exhaustive match', () => {
const defs = [
'type T = A(int) | B',
'val a = variant("A", 3)',
'val nonExhaustive = match a { NoMatch(x) => x * x }',
];
const [errors, _] = inferTypesForDefs(defs);
chai_1.assert.isNotEmpty(errors);
chai_1.assert.match([...errors.values()].map(errorTree_1.errorTreeToString)[0], RegExp('Incompatible tails for rows'));
});
(0, mocha_1.it)('keeps track of free variables in nested scopes (#966)', () => {
const defs = ['def f(a) = a == "x"', 'def g(b) = val nested = (1,2) { f(b) }'];
const [errors, types] = inferTypesForDefs(defs);
chai_1.assert.isEmpty(errors, `Should find no errors, found: ${[...errors.values()].map(errorTree_1.errorTreeToString)}`);
const stringTypes = Array.from(types.entries()).map(([id, type]) => [id, (0, printing_1.typeSchemeToString)(type)]);
chai_1.assert.includeDeepMembers(stringTypes, [[15n, '(str) => bool']]);
});
(0, mocha_1.it)('considers annotations', () => {
const defs = ['def e(p): (int) => int = p'];
const [errors, types] = inferTypesForDefs(defs);
chai_1.assert.isEmpty(errors, `Should find no errors, found: ${[...errors.values()].map(errorTree_1.errorTreeToString)}`);
const stringTypes = Array.from(types.entries()).map(([id, type]) => [id, (0, printing_1.typeSchemeToString)(type)]);
// _printUpdatedStringTypes(stringTypes)
chai_1.assert.sameDeepMembers(stringTypes, [
[1n, 'int'],
[5n, 'int'],
[6n, '(int) => int'],
[7n, '(int) => int'],
]);
});
(0, mocha_1.it)('keeps track of free names properly (#693)', () => {
const defs = ['val b(x: int -> str): int = val c = x.keys() { 1 }'];
const [errors, types] = inferTypesForDefs(defs);
chai_1.assert.isEmpty(errors, `Should find no errors, found: ${[...errors.values()].map(errorTree_1.errorTreeToString)}`);
const stringTypes = Array.from(types.entries()).map(([id, type]) => [id, (0, printing_1.typeSchemeToString)(type)]);
// _printUpdatedStringTypes(stringTypes)
chai_1.assert.sameDeepMembers(stringTypes, [
[4n, '(int -> str)'],
[6n, '(int -> str)'],
[7n, 'Set[int]'],
[8n, 'Set[int]'],
[9n, 'int'],
[10n, 'int'],
[11n, '((int -> str)) => int'],
[12n, '((int -> str)) => int'],
]);
});
(0, mocha_1.it)('checks annotations', () => {
const defs = ['def e(p): (t) => t = p + 1'];
const [errors] = inferTypesForDefs(defs);
chai_1.assert.sameDeepMembers([...errors.entries()], [
[
4n,
{
location: 'Checking type annotation (t) => t',
children: [
{
location: 'Checking variable t',
message: 'Type annotation is too general: t should be int',
children: [],
},
],
},
],
]);
});
(0, mocha_1.it)('checks correct polymorphic types', () => {
const defs = [
'type Option[a] = Some(a) | None',
'type Result[ok, err] = Ok(ok) | Err(err)',
`def result_map(r: Result[a, e], f: a => b): Result[b, e] =
match r {
| Ok(x) => Ok(f(x))
| Err(_) => r
}`,
`def option_to_result(o: Option[ok], e: err): Result[ok, err] =
match o {
| Some(x) => Ok(x)
| None => Err(e)
}`,
'val nested_type_application: Result[Option[int], str] = Ok(Some(42))',
];
const [errors, _] = inferTypesForDefs(defs);
chai_1.assert.sameDeepMembers([...errors.entries()], []);
});
(0, mocha_1.it)('fails when polymorphic types are not unifiable', () => {
const defs = [
'type Result[ok, err] = Ok(ok) | Err(err)',
`def result_map(r: Result[bool, e]): Result[int, e] =
match r {
| Ok(x) => Ok(x)
| Err(_) => r
}`,
];
const [errors] = inferTypesForDefs(defs);
chai_1.assert.isNotEmpty([...errors.entries()]);
const actualErrors = [...errors.entries()].map(e => (0, errorTree_1.errorTreeToString)(e[1]));
const expectedError = `Couldn't unify bool and int
Trying to unify bool and int
Trying to unify { Ok: bool, Err: _t5 } and { Ok: int, Err: _t5 }
Trying to unify (Ok(bool) | Err(_t5)) and (Ok(int) | Err(_t5))
Trying to unify ((Ok(bool) | Err(_t5))) => (Ok(bool) | Err(_t5)) and ((Ok(bool) | Err(_t5))) => (Ok(int) | Err(_t5))
`;
chai_1.assert.deepEqual(actualErrors, [expectedError]);
});
(0, mocha_1.it)('errors when polymorphic types are applied to invalid numbers of arguments', () => {
const defs = [
'type Result[ok, err] = Ok(ok) | Err(err)',
`val too_many: Result[a, b, c] = Ok(1)`,
`val too_few: Result[a] = Ok(1)`,
];
const [errors] = inferTypesForDefs(defs);
chai_1.assert.isNotEmpty([...errors.entries()]);
const actualErrors = [...errors.entries()].map(e => (0, errorTree_1.errorTreeToString)(e[1]));
const expectedErrors = [
`Couldn't unify sum and app
Trying to unify (Ok(int) | Err(_t3)) and Result[a, b, c]
`,
`too many arguments supplied: Result only accepts 2 parameters
applying type constructor Result to arguments a, b, c
`,
`too few arguments supplied: Result only accepts 2 parameters
applying type constructor Result to arguments a
`,
];
chai_1.assert.deepEqual(actualErrors, expectedErrors);
});
(0, mocha_1.it)('fails when types are not unifiable', () => {
const defs = ['def a = 1.map(p => p + 10)'];
const [errors] = inferTypesForDefs(defs);
chai_1.assert.sameDeepMembers([...errors.entries()], [
[
7n,
{
location: 'Trying to unify (Set[_t1], (_t1) => _t2) => Set[_t2] and (int, (int) => int) => _t3',
children: [
{
location: 'Trying to unify Set[_t1] and int',
message: "Couldn't unify set and int",
children: [],
},
],
},
],
]);
});
(0, mocha_1.it)('prioritizes solving constraints from type annotations', () => {
// Regression test for https://github.com/informalsystems/quint/issues/1177
// The point is that we expect to report an error trying to unify a string with a
const defs = [
`pure def foo(s: str): int = {
val x = 1.in(s) // We SHOULD identify an error here, since s is annotated as a str
val y = s + 1 // and NOT identify an error here, incorrectly expecting s to be a set
y
}`,
];
const [errors] = inferTypesForDefs(defs);
const msgs = [...errors.values()].map(errorTree_1.errorTreeToString);
const expectedMessage = `Couldn't unify set and str
Trying to unify Set[int] and str
Trying to unify (_t0, Set[_t0]) => bool and (int, str) => _t1
`;
chai_1.assert.equal(msgs[0], expectedMessage);
});
});
//# sourceMappingURL=inferrer.test.js.map