UNPKG

@informalsystems/quint

Version:

Core tool for the Quint specification language

299 lines (296 loc) 15.5 kB
"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 contens 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 accross 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 correclty 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'); }); }); // 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', 'type', 'val', 'def', 'pure', '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', 'type', 'val', 'def', 'pure', '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', 'type', 'val', 'def', 'pure', '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.`); }); }); // 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