@informalsystems/quint
Version:
Core tool for the Quint specification language
283 lines • 16.1 kB
JavaScript
;
/*
* A collection of CLI functions that implement color output and pseudo-graphics.
*
* Igor Konnov, 2023
*
* Copyright 2023 Informal Systems
* Licensed under the Apache License, Version 2.0.
* See LICENSE in the project root for license information.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.printTrace = exports.printExecutionFrameRec = exports.prettyQuintType = exports.prettyTypeScheme = exports.prettyQuintDeclaration = exports.prettyQuintEx = exports.terminalWidth = void 0;
const assert_1 = require("assert");
const chalk_1 = __importDefault(require("chalk"));
const prettierimp_1 = require("./prettierimp");
const quintIr_1 = require("./ir/quintIr");
const idGenerator_1 = require("./idGenerator");
const quintTypes_1 = require("./ir/quintTypes");
const printing_1 = require("./types/printing");
const IRprinting_1 = require("./ir/IRprinting");
const simplification_1 = require("./types/simplification");
/**
* Find out the terminal width for text formatting.
* Since this number may change while running, it is a function.
* Also, when the output is redirected, the number of columns is undefined.
*/
const terminalWidth = () => {
const cols = process.stdout.columns;
return cols !== undefined && cols > 0 ? cols : 80;
};
exports.terminalWidth = terminalWidth;
// convert a Quint expression to a colored pretty-printed doc, tuned for REPL
function prettyQuintEx(ex) {
switch (ex.kind) {
case 'bool':
return (0, prettierimp_1.richtext)(chalk_1.default.yellow, ex.value.toString());
case 'int':
return (0, prettierimp_1.richtext)(chalk_1.default.yellow, ex.value.toString());
case 'str':
return (0, prettierimp_1.richtext)(chalk_1.default.green, `"${ex.value}"`);
case 'app':
switch (ex.opcode) {
case 'Set':
return (0, prettierimp_1.group)([(0, prettierimp_1.richtext)(chalk_1.default.green, 'Set'), nary((0, prettierimp_1.text)('('), ex.args.map(prettyQuintEx), (0, prettierimp_1.text)(')'))]);
case 'Map': {
const ps = ex.args.map(tup => {
if (tup.kind === 'app' && tup.opcode === 'Tup' && tup.args.length === 2) {
const [k, v] = tup.args;
return (0, prettierimp_1.group)([prettyQuintEx(k), (0, prettierimp_1.richtext)(chalk_1.default.gray, ' ->'), (0, prettierimp_1.nest)(' ', [(0, prettierimp_1.line)(), prettyQuintEx(v)])]);
}
else {
return (0, prettierimp_1.text)('<expected-pair>');
}
});
return (0, prettierimp_1.group)([(0, prettierimp_1.richtext)(chalk_1.default.green, 'Map'), nary((0, prettierimp_1.text)('('), ps, (0, prettierimp_1.text)(')'))]);
}
case 'Tup':
return nary((0, prettierimp_1.text)('('), ex.args.map(prettyQuintEx), (0, prettierimp_1.text)(')'));
case 'List': {
return nary((0, prettierimp_1.text)('['), ex.args.map(prettyQuintEx), (0, prettierimp_1.text)(']'));
}
case 'Rec': {
const kvs = [];
for (let i = 0; i < ex.args.length / 2; i++) {
const key = ex.args[2 * i];
if (key && key.kind === 'str') {
const value = prettyQuintEx(ex.args[2 * i + 1]);
kvs.push((0, prettierimp_1.group)([(0, prettierimp_1.text)(key.value), (0, prettierimp_1.richtext)(chalk_1.default.gray, ':'), (0, prettierimp_1.nest)(' ', [(0, prettierimp_1.line)(), value])]));
}
}
return nary((0, prettierimp_1.text)('{'), kvs, (0, prettierimp_1.text)('}'), (0, prettierimp_1.line)());
}
case 'variant': {
const labelExpr = ex.args[0];
(0, assert_1.strict)(labelExpr.kind === 'str', 'malformed variant operator');
const label = (0, prettierimp_1.richtext)(chalk_1.default.green, labelExpr.value);
const valueExpr = ex.args[1];
const value = valueExpr.kind === 'app' && valueExpr.opcode === 'Tup' && valueExpr.args.length === 0
? [] // A payload with the empty tuple is shown as a bare label
: [(0, prettierimp_1.text)('('), prettyQuintEx(valueExpr), (0, prettierimp_1.text)(')')];
return (0, prettierimp_1.group)([label, ...value]);
}
default:
// instead of throwing, show it in red
return (0, prettierimp_1.richtext)(chalk_1.default.red, `unsupported operator: ${ex.opcode}(...)`);
}
case 'lambda': {
const params = (0, prettierimp_1.parens)((0, prettierimp_1.docJoin)([(0, prettierimp_1.text)(','), (0, prettierimp_1.line)()], ex.params.map(p => (0, prettierimp_1.text)(p.name))));
return (0, prettierimp_1.braces)((0, prettierimp_1.group)((0, prettierimp_1.textify)([params, (0, prettierimp_1.line)(), '=>', (0, prettierimp_1.line)(), '...'])));
}
default:
return (0, prettierimp_1.richtext)(chalk_1.default.red, `unsupported operator: ${ex.kind}`);
}
}
exports.prettyQuintEx = prettyQuintEx;
function prettyQuintDeclaration(decl, includeBody = true, type) {
const typeAnnotation = (0, quintIr_1.isAnnotatedDef)(decl)
? [(0, prettierimp_1.text)(': '), prettyQuintType(decl.typeAnnotation)]
: type // If annotation is not present, but type is, use the type
? [(0, prettierimp_1.text)(': '), prettyTypeScheme(type)]
: [];
switch (decl.kind) {
case 'def': {
const header = (0, prettierimp_1.group)([
(0, prettierimp_1.richtext)(chalk_1.default.magenta, (0, IRprinting_1.qualifierToString)(decl.qualifier)),
(0, prettierimp_1.text)(' '),
(0, prettierimp_1.richtext)(chalk_1.default.blue, decl.name),
...typeAnnotation,
]);
return includeBody ? (0, prettierimp_1.group)([header, (0, prettierimp_1.text)(' = '), prettyQuintEx(decl.expr)]) : header;
}
case 'typedef': {
const header = (0, prettierimp_1.group)([(0, prettierimp_1.text)('type '), (0, prettierimp_1.richtext)(chalk_1.default.blue, decl.name)]);
if (decl.type) {
return (0, prettierimp_1.group)([header, (0, prettierimp_1.text)(' = '), prettyQuintType(decl.type)]);
}
return header;
}
default:
// TODO, not used for now
return (0, prettierimp_1.text)((0, IRprinting_1.declarationToString)(decl));
}
}
exports.prettyQuintDeclaration = prettyQuintDeclaration;
function prettyTypeScheme(scheme) {
const [vars, type] = (0, printing_1.canonicalTypeScheme)(scheme);
const varsDoc = vars.length > 0 ? (0, prettierimp_1.group)([(0, prettierimp_1.text)('∀ '), (0, prettierimp_1.docJoin)([(0, prettierimp_1.text)(','), (0, prettierimp_1.line)()], vars.map(prettierimp_1.text)), (0, prettierimp_1.text)(' . ')]) : (0, prettierimp_1.text)('');
return (0, prettierimp_1.group)([varsDoc, prettyQuintType(type)]);
}
exports.prettyTypeScheme = prettyTypeScheme;
function prettyQuintType(type) {
switch (type.kind) {
case 'bool':
case 'int':
case 'str':
return (0, prettierimp_1.richtext)(chalk_1.default.yellow, type.kind);
case 'const':
case 'var':
return (0, prettierimp_1.richtext)(chalk_1.default.blue, type.name);
case 'set':
return (0, prettierimp_1.group)([(0, prettierimp_1.richtext)(chalk_1.default.green, 'Set'), (0, prettierimp_1.text)('['), prettyQuintType(type.elem), (0, prettierimp_1.text)(']')]);
case 'list':
return (0, prettierimp_1.group)([(0, prettierimp_1.richtext)(chalk_1.default.green, 'List'), (0, prettierimp_1.text)('['), prettyQuintType(type.elem), (0, prettierimp_1.text)(']')]);
case 'fun':
return (0, prettierimp_1.group)([prettyQuintType(type.arg), (0, prettierimp_1.richtext)(chalk_1.default.gray, ' -> '), prettyQuintType(type.res)]);
case 'oper': {
const args = type.args.map(prettyQuintType);
return (0, prettierimp_1.group)([nary((0, prettierimp_1.text)('('), args, (0, prettierimp_1.text)(')')), (0, prettierimp_1.text)(' => '), prettyQuintType(type.res)]);
}
case 'tup':
return (0, prettierimp_1.group)([(0, prettierimp_1.text)('('), prettyRow(type.fields, false), (0, prettierimp_1.text)(')')]);
case 'rec': {
if ((0, IRprinting_1.rowToString)(type.fields) === '{}') {
return (0, prettierimp_1.text)('{}');
}
return (0, prettierimp_1.group)([(0, prettierimp_1.text)('{ '), prettyRow(type.fields), (0, prettierimp_1.text)('}')]);
}
case 'sum': {
return prettySumRow(type.fields);
}
case 'app': {
const args = type.args.map(prettyQuintType);
return (0, prettierimp_1.group)([prettyQuintType(type), (0, prettierimp_1.text)('['), ...args, (0, prettierimp_1.text)(']')]);
}
}
}
exports.prettyQuintType = prettyQuintType;
function prettyRow(r, showFieldName = true) {
const row = (0, simplification_1.simplifyRow)(r);
const fields = row.kind === 'row' ? row.fields : [];
const other = row.kind === 'row' ? row.other : undefined;
const fieldsDocs = fields.map(f => {
const prefix = showFieldName ? `${f.fieldName}: ` : '';
return (0, prettierimp_1.group)([(0, prettierimp_1.text)(prefix), prettyQuintType(f.fieldType)]);
});
const otherDoc = other?.kind === 'var' ? [(0, prettierimp_1.text)(` | ${other.name}`)] : [];
return (0, prettierimp_1.group)([(0, prettierimp_1.nest)(' ', [prettierimp_1.linebreak, (0, prettierimp_1.docJoin)([(0, prettierimp_1.text)(','), (0, prettierimp_1.line)()], fieldsDocs)]), ...otherDoc, prettierimp_1.linebreak]);
}
function prettySumRow(r) {
const row = (0, simplification_1.simplifyRow)(r);
const fields = row.kind === 'row' ? row.fields : [];
const other = row.kind === 'row' ? row.other : undefined;
const fieldsDocs = fields.map(f => {
if (other?.kind === 'empty') {
return (0, prettierimp_1.group)((0, prettierimp_1.text)(f.fieldName));
}
else if ((0, quintTypes_1.isUnitType)(f.fieldType)) {
// Print the variant `Foo({})`
return (0, prettierimp_1.group)([(0, prettierimp_1.text)(`${f.fieldName}`)]);
}
else {
return (0, prettierimp_1.group)([(0, prettierimp_1.text)(`${f.fieldName}(`), prettyQuintType(f.fieldType), (0, prettierimp_1.text)(')')]);
}
});
return (0, prettierimp_1.group)([(0, prettierimp_1.nest)(' ', [prettierimp_1.linebreak, (0, prettierimp_1.docJoin)([(0, prettierimp_1.text)('|'), (0, prettierimp_1.line)()], fieldsDocs)]), prettierimp_1.linebreak]);
}
/**
* Print an execution frame and its children recursively.
* Since this function is printing a tree, we need precise text alignment.
* Using a tree here with an optional line break would produce incorrect output.
*
* @param box the box to print in
* @param frame the frame to print
* @param isLast the array of booleans, one per ancestor, that indicates whether
* an ancestor does not have siblings to the right, the last index
* corresponds to the direct parent.
*/
function printExecutionFrameRec(box, frame, isLast) {
// convert the arguments and the result to strings
const args = (0, prettierimp_1.docJoin)([(0, prettierimp_1.text)(','), (0, prettierimp_1.line)()], frame.args.map(a => prettyQuintEx(a.toQuintEx(idGenerator_1.zerog))));
const r = frame.result.isLeft() ? (0, prettierimp_1.text)('none') : prettyQuintEx(frame.result.value.toQuintEx(idGenerator_1.zerog));
const depth = isLast.length;
// generate the tree ASCII graphics for this frame
let treeArt = isLast
.map((il, i) => i < depth - 1
? // continue the ancestor's branch, unless it's the last one
il
? ' '
: '│ '
: // close or close & continue the leaf branch,
// depending on whether this frame is the last one
il
? '└─ '
: '├─ ')
.join('');
box.out(treeArt);
// format the call with its arguments and place it right after the tree art
let argsDoc = frame.args.length === 0 ? (0, prettierimp_1.text)('') : (0, prettierimp_1.group)((0, prettierimp_1.textify)(['(', (0, prettierimp_1.nest)(' ', [prettierimp_1.linebreak, (0, prettierimp_1.group)(args)]), prettierimp_1.linebreak, ')']));
// draw proper branches in the indentation
const indentTreeArt = isLast.map(il => (il ? ' ' : '│ ')).join('');
// pretty print the arguments and the result
const callDoc = (0, prettierimp_1.group)((0, prettierimp_1.nest)(indentTreeArt, [(0, prettierimp_1.text)(frame.app.opcode), (0, prettierimp_1.group)([argsDoc, (0, prettierimp_1.nest)(' ', (0, prettierimp_1.group)([(0, prettierimp_1.text)(' =>'), (0, prettierimp_1.line)(), r]))])]));
box.out((0, prettierimp_1.format)(box.width, treeArt.length, callDoc));
box.out('\n');
const n = frame.subframes.length;
// visualize the children
frame.subframes.forEach((f, i) => printExecutionFrameRec(box, f, isLast.concat([i === n - 1])));
}
exports.printExecutionFrameRec = printExecutionFrameRec;
/**
* Print a trace with chalk.
*/
function printTrace(console, states, frames, hideVars = []) {
const b = chalk_1.default.bold;
states.forEach((state, index) => {
(0, assert_1.strict)(state.kind === 'app' && state.opcode === 'Rec' && state.args.length % 2 === 0);
if (index < frames.length) {
// be lenient to broken input
console.out(`[${b('Frame ' + index)}]\n`);
printExecutionFrameRec(console, frames[index], []);
console.out('\n');
}
// Filter out hidden variables from the state display
let filteredState = state;
if (hideVars.length > 0 && state.kind === 'app' && state.opcode === 'Rec') {
// A record is represented as [key1, value1, key2, value2, ...]
const filteredArgs = [];
for (let i = 0; i < state.args.length; i += 2) {
const key = state.args[i];
const value = state.args[i + 1];
// Only include this key-value pair if the key is not in hideVars
if (key.kind === 'str' && !hideVars.includes(key.value)) {
filteredArgs.push(key);
filteredArgs.push(value);
}
}
filteredState = { ...state, args: filteredArgs };
}
const stateDoc = [(0, prettierimp_1.group)([(0, prettierimp_1.brackets)((0, prettierimp_1.richtext)(b, `State ${index}`)), (0, prettierimp_1.line)()]), prettyQuintEx(filteredState)];
console.out((0, prettierimp_1.format)(console.width, 0, stateDoc));
console.out('\n\n');
});
}
exports.printTrace = printTrace;
// a helper function to produce specific indentation
function nary(left, args, right, padding = prettierimp_1.linebreak) {
const as = (0, prettierimp_1.group)([(0, prettierimp_1.nest)(' ', [padding, (0, prettierimp_1.docJoin)([(0, prettierimp_1.text)(','), (0, prettierimp_1.line)()], args)]), padding]);
return (0, prettierimp_1.group)([left, as, right]);
}
//# sourceMappingURL=graphics.js.map