UNPKG

@informalsystems/quint

Version:

Core tool for the Quint specification language

670 lines 28.8 kB
"use strict"; /* * REPL for quint. * * Igor Konnov, Gabriela Moreira, 2022-2023. * * Copyright 2022-2023 Informal Systems * Licensed under the Apache License, Version 2.0. * See LICENSE in the project root for license information. */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.quintRepl = exports.newCompilationState = exports.settings = void 0; const readline = __importStar(require("readline")); const fs_1 = require("fs"); const maybe_1 = require("@sweet-monads/maybe"); const chalk_1 = __importDefault(require("chalk")); const prettierimp_1 = require("./prettierimp"); const quintIr_1 = require("./ir/quintIr"); const errorReporter_1 = require("./errorReporter"); const trace_1 = require("./runtime/trace"); const quintParserFrontend_1 = require("./parsing/quintParserFrontend"); const graphics_1 = require("./graphics"); const verbosity_1 = require("./verbosity"); const rng_1 = require("./rng"); const version_1 = require("./version"); const sourceResolver_1 = require("./parsing/sourceResolver"); const process_1 = require("process"); const idGenerator_1 = require("./idGenerator"); const IRprinting_1 = require("./ir/IRprinting"); const cliHelpers_1 = require("./cliHelpers"); const evaluator_1 = require("./runtime/impl/evaluator"); const IRVisitor_1 = require("./ir/IRVisitor"); const quintAnalyzer_1 = require("./quintAnalyzer"); const resolver_1 = require("./names/resolver"); // tunable settings exports.settings = { prompt: '>>> ', continuePrompt: '... ', }; /* An empty initial compilation state */ function newCompilationState() { return { idGen: (0, idGenerator_1.newIdGenerator)(), sourceCode: new Map(), modules: [], sourceMap: new Map(), analysisOutput: { types: new Map(), effects: new Map(), modes: new Map(), }, }; } exports.newCompilationState = newCompilationState; /** * The internal state of the REPL. */ class ReplState { constructor(verbosityLevel, rng) { const recorder = (0, trace_1.newTraceRecorder)(verbosityLevel, rng); this.moduleHist = ''; this.exprHist = []; this.lastLoadedFileAndModule = [undefined, undefined]; this.compilationState = newCompilationState(); this.evaluator = new evaluator_1.Evaluator(new Map(), recorder, rng); this.nameResolver = new resolver_1.NameResolver(); this.inputCounter = 0; } clone() { const copy = new ReplState(this.verbosity, (0, rng_1.newRng)(this.rng.getState())); copy.moduleHist = this.moduleHist; copy.exprHist = this.exprHist; copy.lastLoadedFileAndModule = this.lastLoadedFileAndModule; copy.compilationState = this.compilationState; copy.inputCounter = this.inputCounter; return copy; } addReplModule() { const replModule = { name: '__repl__', declarations: [], id: 0n }; this.compilationState.modules.push(replModule); this.compilationState.mainName = '__repl__'; this.moduleHist += (0, IRprinting_1.moduleToString)(replModule); } clear() { const rng = (0, rng_1.newRng)(this.rng.getState()); const recorder = (0, trace_1.newTraceRecorder)(this.verbosity, rng); this.moduleHist = ''; this.exprHist = []; this.compilationState = newCompilationState(); this.evaluator = new evaluator_1.Evaluator(new Map(), recorder, rng); this.nameResolver = new resolver_1.NameResolver(); this.inputCounter = 0; } get recorder() { // ReplState always passes TraceRecorder in the evaluation state return this.evaluator.recorder; } get rng() { return this.recorder.rng; } get verbosity() { return this.recorder.verbosityLevel; } set verbosity(level) { this.recorder.verbosityLevel = level; } get seed() { return this.rng.getState(); } set seed(newSeed) { this.rng.setState(newSeed); } } // The default exit terminates the process. // Since it is inconvenient for testing, do not use it in tests :) function defaultExit() { process.exit(0); } // the entry point to the REPL function quintRepl(input, output, options = { verbosity: verbosity_1.verbosity.defaultLevel, }, exit = defaultExit) { // output a line of text, no line feed is introduced const out = (text) => output.write(text); const prompt = (text) => { return verbosity_1.verbosity.hasReplPrompt(options.verbosity) ? text : ''; }; if (verbosity_1.verbosity.hasReplBanners(options.verbosity)) { out(chalk_1.default.gray(`Quint REPL ${version_1.version}\n`)); out(chalk_1.default.gray('Type ".exit" to exit, or ".help" for more information\n')); } // create a readline interface const rl = readline.createInterface({ input, output, prompt: prompt(exports.settings.prompt), }); // the state const state = new ReplState(options.verbosity, (0, rng_1.newRng)()); // we let the user type a multiline string, which is collected here: let multilineText = ''; // when recyclingOwnOutput is true, REPL is receiving its older output let recyclingOwnOutput = false; // when the number of open braces or parentheses is positive, // we enter the multiline mode let nOpenBraces = 0; let nOpenParen = 0; let nOpenComments = 0; // Ctrl-C handler rl.on('SIGINT', () => { // if the user is stuck and presses Ctrl-C, reset the multiline mode multilineText = ''; nOpenBraces = 0; nOpenParen = 0; nOpenComments = 0; rl.setPrompt(prompt(exports.settings.prompt)); out(chalk_1.default.yellow(' <cancelled>\n')); // clear the line and show the prompt rl.write(null, { ctrl: true, name: 'u' }); rl.prompt(); }); // next line handler function nextLine(line) { const [nob, nop, noc] = countBraces(line); nOpenBraces += nob; nOpenParen += nop; nOpenComments += noc; if (multilineText === '') { // if the line starts with a non-empty prompt, // we assume it is multiline code that was copied from a REPL prompt recyclingOwnOutput = exports.settings.prompt !== '' && line.trim().indexOf(exports.settings.prompt) === 0; if (nOpenBraces > 0 || nOpenParen > 0 || nOpenComments > 0 || recyclingOwnOutput) { // Enter a multiline mode. // If the text is copy-pasted from the REPL output, // trim the REPL decorations. multilineText = trimReplDecorations(line); rl.setPrompt(prompt(exports.settings.continuePrompt)); } else { if (line.trim() !== '') { tryEvalAndClearRecorder(out, state, line + '\n'); } } } else { const trimmedLine = line.trim(); const continueOwnOutput = exports.settings.continuePrompt !== '' && trimmedLine.indexOf(exports.settings.continuePrompt) === 0; if ((trimmedLine.length === 0 && nOpenBraces <= 0 && nOpenParen <= 0 && nOpenComments <= 0) || (recyclingOwnOutput && !continueOwnOutput)) { // End the multiline mode. // If recycle own output, then the current line is, most likely, // older input. Ignore it. tryEvalAndClearRecorder(out, state, multilineText); multilineText = ''; recyclingOwnOutput = false; rl.setPrompt(prompt(exports.settings.prompt)); } else { // Continue the multiline mode. // It may happen that the text is copy-pasted from the REPL output. // In this case, we have to trim the leading '... '. multilineText += '\n' + trimReplDecorations(trimmedLine); } } } // load the code from a filename and optionally import a module function load(filename, moduleName) { state.clear(); const newState = loadFromFile(out, state, filename); if (!newState) { return; } state.lastLoadedFileAndModule[0] = filename; const moduleNameToLoad = moduleName ?? getMainModuleAnnotation(newState.moduleHist); if (!moduleNameToLoad) { // No module to load, introduce the __repl__ module newState.addReplModule(); } if (tryEvalModule(out, newState, moduleNameToLoad ?? '__repl__')) { state.lastLoadedFileAndModule[1] = moduleNameToLoad; } else { out(chalk_1.default.yellow('Pick the right module name and import it (the file has been loaded)\n')); return; } if (newState.exprHist) { const exprHist = newState.exprHist; newState.exprHist = []; if (exprHist.length > 0) { replayExprHistory(newState, filename, exprHist); } } state.moduleHist = newState.moduleHist; state.exprHist = newState.exprHist; state.compilationState = newState.compilationState; state.evaluator = newState.evaluator; state.nameResolver = newState.nameResolver; } function replayExprHistory(state, filename, exprHist) { if (verbosity_1.verbosity.hasReplBanners(options.verbosity)) { out(chalk_1.default.gray(`Evaluating expression history in ${filename}\n`)); } exprHist.forEach(expr => { if (verbosity_1.verbosity.hasReplPrompt(options.verbosity)) { out(exports.settings.prompt); out(expr.replaceAll('\n', `\n${exports.settings.continuePrompt}`)); out('\n'); } tryEvalAndClearRecorder(out, state, expr); }); } function consumeLine(line) { const r = (s) => { return chalk_1.default.red(s); }; const g = (s) => { return chalk_1.default.gray(s); }; // The continue prompt is handled by `nextLine` as the input may be acopy // and paste from the reply itself. if (!line.startsWith('.') || line.startsWith(exports.settings.continuePrompt)) { // an input to evaluate nextLine(line); } else { // a special command to REPL, extract the command name const m = line.match(/^\s*\.(\w+)/); if (m === null) { out(r(`Unexpected command: ${line}\n`)); out(g(`Type .help to see the list of commands\n`)); } else { switch (m[1]) { case 'help': out(`${r('.clear')}\tClear the history\n`); out(`${r('.exit')}\tExit the REPL\n`); out(`${r('.help')}\tPrint this help message\n`); out(`${r('.load')} <filename> ${g('[<module>]')}\tClear the history,\n`); out(' \tload the code from a file into the REPL session\n'); out(' \tand optionally import all definitions from <module>\n'); out(`${r('.reload')}\tClear the history, load and (optionally) import the last loaded file.\n`); out(' \t^ a productivity hack\n'); out(`${r('.save')} <filename>\tSave the accumulated definitions to a file\n`); out(`${r('.verbosity')}=[0-5]\tSet the output level (0 = quiet, 5 = very detailed).\n`); out(`${r('.seed')}[=<number>]\tSet or get the random seed.\n`); out('\nType an expression and press Enter to evaluate it.\n'); out('When the REPL switches to multiline mode "...", finish it with an empty line.\n'); out('\nPress Ctrl+C to abort current expression, Ctrl+D to exit the REPL\n'); break; case 'exit': exit(); break; case 'clear': out('\n'); // be nice to external programs state.clear(); break; case 'load': { const args = line.trim().split(/\s+/); const [filename, moduleName] = [args[1], args[2]]; if (!filename) { out(r('.load requires a filename\n')); } else { load(filename, moduleName); } } break; case 'reload': if (state.lastLoadedFileAndModule[0] !== undefined) { load(state.lastLoadedFileAndModule[0], state.lastLoadedFileAndModule[1]); } else { out(r('Nothing to reload. Use: .load filename [moduleName].\n')); } break; case 'save': { const args = line.trim().split(/\s+/); if (!args[1]) { out(r('.save requires a filename\n')); } else { saveToFile(out, state, args[1]); } } break; case 'verbosity': { // similar to yargs, accept: .verbosity n, .verbosity=n, .verbosity = n const m = line.match(/^\.verbosity\s*=?\s*([0-5])$/); if (m === null) { out(r('.verbosity requires a level from 0 to 5\n')); } else { state.verbosity = Number(m[1]); if (verbosity_1.verbosity.hasReplPrompt(state.verbosity)) { out(g(`.verbosity=${state.verbosity}\n`)); } rl.setPrompt(prompt(exports.settings.prompt)); } } break; case 'seed': { // accept: .seed n, .seed=n, .seed = n const m = line.match(/^\.seed\s*=?\s*((0x[0-9a-f]+|[0-9]*))$/); if (m === null) { out(r('.seed requires an integer, or no argument\n')); } else { if (m[1].trim() === '') { out(g(`.seed=${state.seed}\n`)); } else { state.seed = BigInt(m[1]); if (verbosity_1.verbosity.hasReplPrompt(state.verbosity)) { out(g(`.seed=${state.seed}\n`)); } } } } break; default: out(r(`Unexpected command: ${line}\n`)); out(g(`Type .help to see the list of commands\n`)); break; } } } } // the read-eval-print loop rl.on('line', line => { consumeLine(line); rl.prompt(); }).on('close', () => { out('\n'); exit(); }); // Everything is registered. Optionally, load a module. if (options.preloadFilename) { load(options.preloadFilename, options.importModule); } // Evaluate the repl's command input before starting the interactive loop if (options.replInput && options.replInput.length > 0) { out(prompt(exports.settings.prompt)); options.replInput.forEach(input => input.split('\n').forEach(part => { const line = `${part}\n`; // put \n back in out(prompt(line)); consumeLine(line); out(prompt(rl.getPrompt())); })); } rl.prompt(); return rl; } exports.quintRepl = quintRepl; function saveToFile(out, state, filename) { // 1. Write the previously loaded modules. // 2. Write the definitions in the loaded module (or in __repl__ if no module was loaded). // 3. Wrap expressions into special comments. try { const mainModuleAnnotation = state.moduleHist.startsWith('// @mainModule') ? '' : `// @mainModule ${state.lastLoadedFileAndModule[1] ?? '__repl__'}\n`; const text = mainModuleAnnotation + `${state.moduleHist}` + state.exprHist.map(s => `/*! ${s} !*/\n`).join('\n'); (0, fs_1.writeFileSync)(filename, text); out(`Session saved to: ${filename}\n`); } catch (error) { out(chalk_1.default.red(error)); out('\n'); } } function loadFromFile(out, state, filename) { try { const modules = (0, fs_1.readFileSync)(filename, 'utf8'); const newState = state.clone(); newState.moduleHist = modules + newState.moduleHist; // unwrap the expressions from the specially crafted comments const exprs = Array.from((modules ?? '').matchAll(/\/\*! (.*?) !\*\//gms) ?? []).map(groups => groups[1]); // and replay them one by one newState.exprHist = exprs; return newState; } catch (error) { out(chalk_1.default.red(error)); out('\n'); return; } } function tryEvalModule(out, state, mainName) { const modulesText = state.moduleHist; const mainPath = (0, sourceResolver_1.fileSourceResolver)(state.compilationState.sourceCode).lookupPath((0, process_1.cwd)(), 'repl.ts'); state.compilationState.sourceCode.set(mainPath.toSourceName(), modulesText); // FIXME(#1052): We should build a proper sourceCode map from the files we previously loaded const sourceCode = new Map(); const idGen = (0, idGenerator_1.newIdGenerator)(); const { modules, table, resolver, sourceMap, errors } = (0, quintParserFrontend_1.parse)(idGen, mainPath.toSourceName(), mainPath, modulesText, sourceCode); // On errors, we'll produce the computational context up to this point const [analysisErrors, analysisOutput] = (0, quintAnalyzer_1.analyzeModules)(table, modules); state.compilationState = { idGen, sourceCode, modules, sourceMap, analysisOutput }; if (errors.length > 0 || analysisErrors.length > 0) { printErrorMessages(out, state, 'syntax error', errors); printErrorMessages(out, state, 'static analysis error', analysisErrors); return false; } resolver.switchToModule(mainName); state.nameResolver = resolver; state.evaluator.updateTable(table); return true; } // Try to evaluate the expression in a string and print it, if successful. // After that, clear the recorded stack. function tryEvalAndClearRecorder(out, state, newInput) { const result = tryEval(out, state, newInput); state.recorder.clear(); return result; } // try to evaluate the expression in a string and print it, if successful function tryEval(out, state, newInput) { const columns = (0, graphics_1.terminalWidth)(); if (state.compilationState.modules.length === 0) { state.addReplModule(); tryEvalModule(out, state, '__repl__'); } // Generate a unique source name for this input to avoid line number conflicts in the source map const inputSource = `<input-${state.inputCounter}>`; state.compilationState.sourceCode.set(inputSource, newInput); state.inputCounter++; const parseResult = (0, quintParserFrontend_1.parseExpressionOrDeclaration)(newInput, inputSource, state.compilationState.idGen, state.compilationState.sourceMap); if (parseResult.kind === 'error') { printErrorMessages(out, state, 'syntax error', parseResult.errors); out('\n'); // be nice to external programs return false; } if (parseResult.kind === 'none') { // a comment or whitespaces return true; } // evaluate the input, depending on its type if (parseResult.kind === 'expr') { (0, IRVisitor_1.walkExpression)(state.nameResolver, parseResult.expr); if (state.nameResolver.errors.length > 0) { printErrorMessages(out, state, 'static analysis error', state.nameResolver.errors); state.nameResolver.errors = []; return false; } state.evaluator.updateTable(state.nameResolver.table); const [analysisErrors, _analysisOutput] = (0, quintAnalyzer_1.analyzeInc)(state.compilationState.analysisOutput, state.nameResolver.table, [ { kind: 'def', qualifier: 'action', name: 'q::input', expr: parseResult.expr, id: state.compilationState.idGen.nextId(), }, ]); if (analysisErrors.length > 0) { printErrorMessages(out, state, 'static analysis error', analysisErrors); return false; } state.exprHist.push(newInput.trim()); const evalResult = state.evaluator.evaluate(parseResult.expr); evalResult.map(ex => { out((0, prettierimp_1.format)(columns, 0, (0, graphics_1.prettyQuintEx)(ex))); out('\n'); if (ex.kind === 'bool' && ex.value) { // A Boolean expression may be an action or a run. // Save the state, if there were any updates to variables. const [shifted, missing] = state.evaluator.shiftAndCheck(); if (shifted && verbosity_1.verbosity.hasDiffs(state.verbosity)) { console.log(state.evaluator.trace.renderDiff((0, graphics_1.terminalWidth)(), { collapseThreshold: 2 })); } if (missing.length > 0) { out(chalk_1.default.yellow('[warning] some variables are undefined: ' + missing.join(', ') + '\n')); } } return ex; }); if (verbosity_1.verbosity.hasUserOpTracking(state.verbosity)) { const trace = state.recorder.currentFrame; if (trace.subframes.length > 0) { out('\n'); trace.subframes.forEach((f, i) => { out(`[Frame ${i}]\n`); (0, graphics_1.printExecutionFrameRec)({ width: columns, out }, f, []); out('\n'); }); } } if (evalResult.isLeft()) { printErrorMessages(out, state, 'runtime error', [evalResult.value]); return false; } return true; } if (parseResult.kind === 'declaration') { parseResult.decls.forEach(decl => { (0, IRVisitor_1.walkDeclaration)(state.nameResolver.collector, decl); (0, IRVisitor_1.walkDeclaration)(state.nameResolver, decl); }); if (state.nameResolver.errors.length > 0) { printErrorMessages(out, state, 'static analysis error', state.nameResolver.errors); out('\n'); parseResult.decls.forEach(decl => { if ((0, quintIr_1.isDef)(decl)) { state.nameResolver.collector.deleteDefinition(decl.name); } }); state.nameResolver.errors = []; return false; } const [analysisErrors, analysisOutput] = (0, quintAnalyzer_1.analyzeInc)(state.compilationState.analysisOutput, state.nameResolver.table, parseResult.decls); if (analysisErrors.length > 0) { printErrorMessages(out, state, 'static analysis error', analysisErrors); parseResult.decls.forEach(decl => { if ((0, quintIr_1.isDef)(decl)) { state.nameResolver.collector.deleteDefinition(decl.name); state.compilationState.analysisOutput.effects.delete(decl.id); state.compilationState.analysisOutput.modes.delete(decl.id); } }); return false; } state.compilationState.analysisOutput = analysisOutput; state.moduleHist = state.moduleHist.slice(0, state.moduleHist.length - 1) + newInput + '\n}'; // update the history out('\n'); } return true; } // print error messages with proper colors function printErrorMessages(out, state, kind, errors, color = chalk_1.default.red) { const modulesText = state.moduleHist; const messages = errors.map((0, cliHelpers_1.mkErrorMessage)(state.compilationState.sourceMap)); // Contents in `moudulesText` can come from multiple files, but we don't keep track of that in our // `sourceCode` map. So we use a fallback here to '<modules>' const sourceCode = new Map([['<modules>', modulesText], ...state.compilationState.sourceCode.entries()]); const finders = (0, errorReporter_1.createFinders)(sourceCode); messages.forEach(e => { const error = { ...e, locs: e.locs.map(loc => ({ ...loc, source: finders.has(loc.source) ? loc.source : '<modules>' })), }; const msg = (0, errorReporter_1.formatError)(sourceCode, finders, error, (0, maybe_1.none)()); out(color(`${kind}: ${msg}\n`)); }); } // if a line start with '>>> ' or '... ', trim these markers function trimReplDecorations(line) { // we are not using settings.prompt and settings.continuePrompt, // as ... are interpreted as three characters. const match = /^\s*(>>> |\.\.\. )(.*)/.exec(line); if (match && match[2] !== undefined) { return match[2]; } else { return line; } } // count the difference between the number of: // - '{' and '}' // - '(' and ')' // - '/*' and '*/' function countBraces(str) { let nOpenBraces = 0; let nOpenParen = 0; let nOpenComments = 0; for (let i = 0; i < str.length; i++) { switch (str[i]) { case '{': nOpenBraces++; break; case '}': nOpenBraces--; break; case '(': nOpenParen++; break; case ')': nOpenParen--; break; case '/': if (i + 1 < str.length && str[i + 1] === '*') { nOpenComments++; i++; } break; case '*': if (i + 1 < str.length && str[i + 1] === '/') { nOpenComments--; i++; } break; default: } } return [nOpenBraces, nOpenParen, nOpenComments]; } function getMainModuleAnnotation(moduleHist) { const moduleName = moduleHist.match(/^\/\/ @mainModule\s+(\w+)\n/); return moduleName?.at(1); } //# sourceMappingURL=repl.js.map