@informalsystems/quint
Version:
Core tool for the Quint specification language
330 lines (327 loc) • 17.5 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const mocha_1 = require("mocha");
const chai_1 = require("chai");
const fs_1 = require("fs");
const path_1 = require("path");
const json_bigint_1 = __importDefault(require("json-bigint"));
const quintParserFrontend_1 = require("../../src/parsing/quintParserFrontend");
const eol_1 = require("eol");
const idGenerator_1 = require("../../src/idGenerator");
const util_1 = require("../util");
const sourceResolver_1 = require("../../src/parsing/sourceResolver");
const cliHelpers_1 = require("../../src/cliHelpers");
// the name that we are using by default
const defaultSourceName = 'moduleName';
// read a Quint file from the test data directory
function readQuint(name) {
const p = (0, path_1.resolve)(__dirname, '../../testFixture', name + '.qnt');
const content = (0, fs_1.readFileSync)(p).toString('utf8');
return (0, eol_1.lf)(content);
}
// read the expected JSON outcome from the test data directory
function readJson(name) {
const p = (0, path_1.resolve)(__dirname, '../../testFixture', name + '.json');
return json_bigint_1.default.parse((0, fs_1.readFileSync)(p).toString('utf8'));
}
// parse the text and return all parse errors (of phase 1)
function parseErrorsFrom(sourceName, code) {
const gen = (0, idGenerator_1.newIdGenerator)();
return (0, quintParserFrontend_1.parsePhase1fromText)(gen, code, sourceName).errors;
}
// read the Quint file and the expected JSON, parse and compare the results
function parseAndCompare(artifact) {
// read the expected result as JSON
const expected = readJson(artifact);
// We're not interested in testing the contents of the table here
delete expected.table;
let outputToCompare;
// read the input from the data directory and parse it
const gen = (0, idGenerator_1.newIdGenerator)();
const basepath = (0, path_1.resolve)(__dirname, '../../testFixture');
const resolver = (0, sourceResolver_1.fileSourceResolver)(new Map(), (path) => {
// replace the absolute path with a generic mocked path,
// so the same fixtures work across different setups
return path.replace(basepath, 'mocked_path/testFixture');
});
const mainPath = resolver.lookupPath(basepath, `${artifact}.qnt`);
const phase1Result = (0, quintParserFrontend_1.parsePhase1fromText)(gen, readQuint(artifact), mainPath.toSourceName());
const phase2Result = (0, quintParserFrontend_1.parsePhase2sourceResolution)(gen, resolver, mainPath, phase1Result);
const phase3Result = (0, quintParserFrontend_1.parsePhase3importAndNameResolution)(phase2Result);
const { modules, sourceMap, errors } = (0, quintParserFrontend_1.parsePhase4toposort)(phase3Result);
if (errors.length === 0) {
// Only check source map if there are no errors. Otherwise, ids generated for error nodes might be missing from the
// actual modules
const expectedIds = modules.flatMap(m => (0, util_1.collectIds)(m)).sort();
chai_1.assert.sameDeepMembers([...sourceMap.keys()].sort(), expectedIds, 'expected source map to contain all ids');
const expectedSourceMap = readJson(`${artifact}.map`);
const sourceMapResult = json_bigint_1.default.parse(json_bigint_1.default.stringify((0, quintParserFrontend_1.compactSourceMap)(sourceMap)));
chai_1.assert.deepEqual(sourceMapResult, expectedSourceMap, 'expected source maps to be equal');
}
// All phases succeeded, check that the module is correctly output
outputToCompare = {
stage: 'parsing',
warnings: [],
modules: modules,
errors: errors.map((0, cliHelpers_1.mkErrorMessage)(sourceMap)),
};
// run it through stringify-parse to obtain the same json (due to bigints)
const reparsedResult = json_bigint_1.default.parse(json_bigint_1.default.stringify(outputToCompare));
// compare the JSON trees
chai_1.assert.deepEqual(reparsedResult, expected, 'expected --out data to be equal');
}
(0, mocha_1.describe)('parsing', () => {
(0, mocha_1.it)('parses empty module', () => {
const result = (0, quintParserFrontend_1.parsePhase1fromText)((0, idGenerator_1.newIdGenerator)(), readQuint('_0001emptyModule'), 'mocked_path/testFixture/_0001emptyModule.qnt');
const module = { id: 1n, name: 'empty', declarations: [] };
chai_1.assert.deepEqual(result.modules[0], module);
});
(0, mocha_1.it)('parses SuperSpec', () => {
const result = (0, quintParserFrontend_1.parsePhase1fromText)((0, idGenerator_1.newIdGenerator)(), readQuint('SuperSpec'), 'mocked_path/testFixture/SuperSpec.qnt');
chai_1.assert.deepEqual(result.errors, []);
});
(0, mocha_1.it)('parses SuperSpec correctly', () => {
parseAndCompare('SuperSpec');
});
(0, mocha_1.it)('parses out of order definitions', () => {
parseAndCompare('_0099unorderedDefs');
});
(0, mocha_1.it)('parses sum types', () => {
parseAndCompare('_1043sumTypeDecl');
});
(0, mocha_1.it)('parses polymorphic type declarations', () => {
parseAndCompare('_1045polymorphicTypeDecl');
});
(0, mocha_1.it)('parses match expressions', () => {
parseAndCompare('_1044matchExpression');
});
(0, mocha_1.it)('parses tuple destructuring', () => {
const gen = (0, idGenerator_1.newIdGenerator)();
const result = (0, quintParserFrontend_1.parsePhase1fromText)(gen, readQuint('_1080tupleDestructuring'), defaultSourceName);
chai_1.assert.isEmpty(result.errors, 'expected no parse errors');
});
(0, mocha_1.it)('parses record destructuring', () => {
const gen = (0, idGenerator_1.newIdGenerator)();
const result = (0, quintParserFrontend_1.parsePhase1fromText)(gen, readQuint('_1090recordDestructuring'), defaultSourceName);
chai_1.assert.isEmpty(result.errors, 'expected no parse errors');
});
});
// instead of testing how errors are produced in json,
// we only test the error messages
(0, mocha_1.describe)('syntax errors', () => {
(0, mocha_1.it)('unbalanced module definition', () => {
const code = 'module empty {';
const errors = parseErrorsFrom(defaultSourceName, code);
chai_1.assert.equal(errors.length, 1);
chai_1.assert.equal(errors[0].message, `mismatched input '<EOF>' expecting {'}', 'const', 'var', 'assume', 'val', 'pure', 'type', 'def', 'action', 'run', 'temporal', 'nondet', 'import', 'export', DOCCOMMENT}`);
chai_1.assert.equal(errors[0].code, 'QNT000');
});
(0, mocha_1.it)('something unexpected in a module definition', () => {
const code = 'module empty { something }';
const errors = parseErrorsFrom(defaultSourceName, code);
chai_1.assert.equal(errors.length, 1);
chai_1.assert.equal(errors[0].message, `extraneous input 'something' expecting {'}', 'const', 'var', 'assume', 'val', 'pure', 'type', 'def', 'action', 'run', 'temporal', 'nondet', 'import', 'export', DOCCOMMENT}`);
chai_1.assert.equal(errors[0].code, 'QNT000');
});
(0, mocha_1.it)('error in a constant definition', () => {
const code = 'module err { const broken }';
const errors = parseErrorsFrom(defaultSourceName, code);
chai_1.assert.equal(errors.length, 1);
chai_1.assert.equal(errors[0].message, `mismatched input '}' expecting {':', '::'}`);
chai_1.assert.equal(errors[0].code, 'QNT000');
});
(0, mocha_1.it)('error on unexpected symbol after expression', () => {
// syntax error on: p !! name
const code = 'module unexpectedExpr { def access(p) = p !! name }';
const errors = parseErrorsFrom(defaultSourceName, code);
chai_1.assert.isAtLeast(errors.length, 1);
chai_1.assert.equal(errors[0].message, `token recognition error at: '!!'`);
chai_1.assert.equal(errors[0].code, 'QNT000');
});
(0, mocha_1.it)('error on unexpected token', () => {
// ~ is an unexpected token
const code = 'module unexpectedToken { def access(p) = { p ~ name } }';
const errors = parseErrorsFrom(defaultSourceName, code);
chai_1.assert.isAtLeast(errors.length, 1);
chai_1.assert.equal(errors[0].message, `token recognition error at: '~'`);
chai_1.assert.equal(errors[0].code, 'QNT000');
});
(0, mocha_1.it)('error on unexpected hash', () => {
// # is an unexpected token
const code = 'module unexpectedToken { def access(p) = { p # name } }';
const errors = parseErrorsFrom(defaultSourceName, code);
chai_1.assert.isAtLeast(errors.length, 1);
chai_1.assert.equal(errors[0].message, `token recognition error at: '# '`);
chai_1.assert.equal(errors[0].code, 'QNT000');
});
(0, mocha_1.it)('error on unexpected hashbang', () => {
// hashbang '#!' is only valid at the beginning of a file
const code = 'module unexpectedToken { def access(p) = { p #! name } }';
const errors = parseErrorsFrom(defaultSourceName, code);
chai_1.assert.isAtLeast(errors.length, 1);
chai_1.assert.equal(errors[0].message, `token recognition error at: '#! name } }'`);
chai_1.assert.equal(errors[0].code, 'QNT000');
});
(0, mocha_1.it)('error on multiple hashbangs', () => {
// only a single hashbang '#!' is valid at the beginning of a file
const code = '#!foo\n#!bar\nmodule unexpectedToken { def access = { true } }';
const errors = parseErrorsFrom(defaultSourceName, code);
chai_1.assert.isAtLeast(errors.length, 1);
chai_1.assert.equal(errors[0].message, `extraneous input '#!bar\\n' expecting {'module', DOCCOMMENT}`);
chai_1.assert.equal(errors[0].code, 'QNT000');
});
(0, mocha_1.it)('error on unexpected token "="', () => {
// "=" is unexpected
const code = 'module unexpectedEq { val errorHere = x = 1 }';
const errors = parseErrorsFrom(defaultSourceName, code);
chai_1.assert.equal(errors.length, 1);
chai_1.assert.equal(errors[0].message, `unexpected '=', did you mean '=='?`);
chai_1.assert.equal(errors[0].code, 'QNT006');
});
(0, mocha_1.it)('error on hanging operator name', () => {
// the keyword cardinality is hanging
const code = 'module hangingCardinality { def one(S) = { S cardinality } }';
const errors = parseErrorsFrom(defaultSourceName, code);
chai_1.assert.isAtLeast(errors.length, 1);
chai_1.assert.equal(errors[0].message, `extraneous input '(' expecting {'}', 'const', 'var', 'assume', 'val', 'pure', 'type', 'def', 'action', 'run', 'temporal', 'nondet', 'import', 'export', DOCCOMMENT}`);
chai_1.assert.equal(errors[0].code, 'QNT000');
});
(0, mocha_1.it)('error on double spread', () => {
const code = `module spreadError {
val rec = { a: 1, b: true }
val errorRec = { ...rec, ...rec }
}`;
const errors = parseErrorsFrom(defaultSourceName, code);
chai_1.assert.equal(errors.length, 1);
chai_1.assert.equal(errors[0].message, '... may be used once in { ...record, <fields> }');
chai_1.assert.equal(errors[0].code, 'QNT012');
});
(0, mocha_1.it)('error on single quotes in import', () => {
// we should use double quotes
const code = `module singleQuotes { import I.* from './_1025importeeWithError' }`;
const errors = parseErrorsFrom(defaultSourceName, code);
chai_1.assert.isAtLeast(errors.length, 1);
chai_1.assert.equal(errors[0].message, `mismatched input ''' expecting STRING`);
chai_1.assert.equal(errors[0].code, 'QNT000');
});
(0, mocha_1.it)('error on type declarations with undeclared variables', () => {
const code = `module singleQuotes { type T = (List[a], Set[b]) }`;
const [error] = parseErrorsFrom(defaultSourceName, code);
chai_1.assert.deepEqual(error.code, 'QNT014');
chai_1.assert.deepEqual(error.message, `the type variables a, b are unbound.
E.g., in
type T = List[a]
type variable 'a' is unbound. To fix it, write
type T[a] = List[a]`);
});
(0, mocha_1.it)('error on type declarations with undeclared variables', () => {
const code = `module mapSyntax { type WrongMap = Map[int, str] }`;
const [error] = parseErrorsFrom(defaultSourceName, code);
chai_1.assert.deepEqual(error.code, 'QNT015');
chai_1.assert.deepEqual(error.message, `Use 'int -> str' instead of 'Map[int, str]' for map types`);
chai_1.assert.deepEqual(error.data?.fix, {
kind: 'replace',
original: 'Map[int, str]',
replacement: 'int -> str',
});
});
(0, mocha_1.it)('error on using reserved keywords', () => {
const code = `module reservedKeyword { type Reserved = { and: int, export: bool } }`;
const [error1, error2] = parseErrorsFrom(defaultSourceName, code);
chai_1.assert.deepEqual(error1.code, 'QNT008');
chai_1.assert.deepEqual(error1.message, `Reserved keyword 'and' cannot be used as an identifier.`);
chai_1.assert.deepEqual(error2.code, 'QNT008');
chai_1.assert.deepEqual(error2.message, `Reserved keyword 'export' cannot be used as an identifier.`);
});
(0, mocha_1.it)('error on duplicate record fields', () => {
const code = `module duplicateFields { val a = { foo: 1, foo: 2 } }`;
const errors = parseErrorsFrom(defaultSourceName, code);
chai_1.assert.equal(errors.length, 1);
chai_1.assert.equal(errors[0].code, 'QNT016');
chai_1.assert.equal(errors[0].message, 'Field foo cannot be defined more than once');
});
(0, mocha_1.it)('error on duplicate record fields with spread operator', () => {
const code = `module duplicateFields { val a = {foo: 1, bar: 2} val b = {...a, bar: 2, bar: 3 } }`;
const errors = parseErrorsFrom(defaultSourceName, code);
chai_1.assert.equal(errors.length, 1);
chai_1.assert.equal(errors[0].code, 'QNT016');
chai_1.assert.equal(errors[0].message, 'Field bar cannot be defined more than once');
});
(0, mocha_1.it)('error on multiple duplicated record fields', () => {
const code = `module duplicateFields { val a = {foo: 1, bar: 2, foo: 1, bar: 2} }`;
const errors = parseErrorsFrom(defaultSourceName, code);
chai_1.assert.equal(errors.length, 1);
chai_1.assert.equal(errors[0].code, 'QNT016');
chai_1.assert.equal(errors[0].message, 'Fields foo, bar cannot be defined more than once');
});
});
// Test the JSON error output. Most of the tests here should migrate to the
// above test suite called 'syntax errors' or other test suites for the later
// phases. It is sufficient to have a few tests that compare JSON. For the
// rest, we are interested in checking the error messages, not the JSON files.
(0, mocha_1.describe)('parse errors', () => {
(0, mocha_1.it)('error on unresolved name', () => {
parseAndCompare('_1010undefinedName');
});
(0, mocha_1.it)('error on unresolved scoped name', () => {
parseAndCompare('_1011nameOutOfScope');
});
(0, mocha_1.it)('error on unresolved type alias', () => {
parseAndCompare('_1012unknownType');
});
(0, mocha_1.it)('error on unresolved type alias inside let', () => {
parseAndCompare('_1013unknownTypeLetIn');
});
(0, mocha_1.it)('error on conflicting names', () => {
parseAndCompare('_1014conflictingNames');
});
(0, mocha_1.it)('parses single import from ', () => {
parseAndCompare('_1021importee1');
});
(0, mocha_1.it)('parses import from transtively', () => {
parseAndCompare('_1020importFrom');
});
(0, mocha_1.it)('errors on incorrect import', () => {
parseAndCompare('_1023importFromUnresolved');
});
(0, mocha_1.it)('errors on cyclic imports', () => {
parseAndCompare('_1026importCycleA');
});
(0, mocha_1.it)('a module with a constant', () => {
parseAndCompare('_1030const');
});
(0, mocha_1.it)('an instance', () => {
parseAndCompare('_1031instance');
});
(0, mocha_1.it)('docstrings', () => {
parseAndCompare('_1032docstrings');
});
// The test below needs a fix, see:
//
// https://github.com/informalsystems/quint/issues/378
//it('error on top-level nondet', () => {
// parseAndCompare('_1015noToplevelNondet')
//})
(0, mocha_1.it)('error on overriding values that are not constants', () => {
parseAndCompare('_1016nonConstOverride');
});
(0, mocha_1.it)('success on keywords that are allowed as identifiers', () => {
parseAndCompare('_1017keywordsAsIdentifiers');
});
(0, mocha_1.it)('error on cyclic definitions', () => {
parseAndCompare('_0100cyclicDefs');
});
(0, mocha_1.it)('error on accidental recursion', () => {
parseAndCompare('_0101noRecursion');
});
(0, mocha_1.it)('errors on invalid record fields', () => {
parseAndCompare('_1042qualifiersInRecordsFieldsError');
});
(0, mocha_1.it)('error on map syntax', () => {
parseAndCompare('_1070mapSyntax');
});
});
//# sourceMappingURL=quintParserFrontend.test.js.map