@informalsystems/quint
Version:
Core tool for the Quint specification language
670 lines • 28.8 kB
JavaScript
;
/*
* 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