@informalsystems/quint
Version:
Core tool for the Quint specification language
302 lines • 10.8 kB
JavaScript
;
/*
* A reimplementation of the prettier printer
* (originally, introduced by Philip Wadler for Haskell).
*
* Our implementation has two important features:
*
* - It is compatible with ANSI color codes. This is why we could not use
* prettier-printer from npm.
* - It is written in TypeScript.
*
* This implementation is following the non-lazy algorithm for OCaml by:
*
* Christian Lindig. Strictly Pretty. March 6, 2000.
*
* https://lindig.github.io/papers/strictly-pretty-2000.pdf
*
* We further adopt the algorithm to TypeScript, by using
* an immutable stack to simulate recursion.
*
* Igor Konnov, 2023
*
* Copyright 2023 Informal Systems
* Licensed under the Apache License, Version 2.0.
* See LICENSE in the project root for license information.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.brackets = exports.parens = exports.braces = exports.docJoin = exports.group = exports.nest = exports.space = exports.linebreak = exports.line = exports.textify = exports.richtext = exports.text = exports.format = void 0;
const immutable_1 = require("immutable");
/**
* Layout a document. This is the end function that produces produces a string
* for the best document layout.
*
* @param maxWidth the maximum width
* @param firstColumn the column to use as the starting position
* @param doc the document to format
* @returns a resulting string
*/
const format = (maxWidth, firstColumn, doc) => {
const fits = {
indentText: [],
indentLen: 0,
mode: 'flat',
doc: (0, exports.group)(doc),
};
return formatInternal(maxWidth, firstColumn, fits).join('');
};
exports.format = format;
/**
* A type guard to distinguish an array document from other documents.
* @param d the document to test
* @returns the type predicate that evaluates to true on Doc[]
*/
function isDocArray(d) {
return Array.isArray(d);
}
/**
* Create a document that carries an indivisible piece of text
* @param text a string-like text
* @returns a new document that contains the text
*/
const text = (text) => {
return { kind: 'text', text: text };
};
exports.text = text;
/**
* Create a document that carries decorated text, e.g., adding color or emphasis.
* It is your responsibility to make sure that the number of columns occupied by
* `decorator(s)` equals to the number of columns occupied by `s`.
* For example, adding ANSI color does not affect the space, but it changes
* string length.
*
* @param decorator a function that decorates plain text
* @param s the string to decorate
* @returns a string-like object
*/
const richtext = (decorator, s) => {
return (0, exports.text)({
length: s.length,
toString: () => decorator(s),
});
};
exports.richtext = richtext;
/**
* A convenience operator that wraps all strings with `text(...)` while keeping
* all documents untouched.
*
* @param ds an array of documents and strings
* @returns an array of documents, in which all strings are wrapped with `text(...)`
*/
const textify = (ds) => {
return ds.map(d => (typeof d === 'string' ? (0, exports.text)(d) : d));
};
exports.textify = textify;
/**
* Create an optional line feed.
* @param lf the text to use as a line feed
* @param space the text to use a space
* @returns a new document that represents a line break
*/
const line = (lf = '\n', space = ' ') => {
return { kind: 'line', linefeed: lf, space };
};
exports.line = line;
/**
* A potential line break that is either a line feed, or an empty string.
*/
exports.linebreak = { kind: 'line', linefeed: '\n', space: '' };
/**
* Simply a non-breaking space, that is, `' '`.
*/
exports.space = (0, exports.text)(' ');
/**
* Create a document that introduces `indent` spaces after every line break.
* @param indent the text to use for indentation, e.g., a few spaces
* @param doc the document to decorate with indentation
* @returns a new document that decorates doc
*/
const nest = (indent, doc) => {
return { kind: 'nest', indent, child: doc };
};
exports.nest = nest;
/**
* Create a group document. If all of its children fit on a single line,
* it should turn all line breaks into spaces. Otherwise, the group stacks
* its non-group children vertically.
* @param doc the document to group
* @returns a new document that groups doc
*/
const group = (child) => {
return { kind: 'group', child };
};
exports.group = group;
/**
* Join documents with a separator.
* @param separator the separator to use
* @param docs documents to join
* @returns a new document that contains the elements joined with the separator
*/
const docJoin = (separator, docs) => {
const j = (l, d) => {
return l.length === 0 ? [d] : l.concat([separator, d]);
};
return docs.reduce(j, []);
};
exports.docJoin = docJoin;
/**
* Enclose a document in braces `{ ... }`
* @param doc the document to enclose
* @returns the document `{doc}`
*/
const braces = (doc) => {
return [(0, exports.text)('{'), doc, (0, exports.text)('}')];
};
exports.braces = braces;
/**
* Enclose a document in parentheses `( ... )`
* @param doc the document to enclose
* @returns the document `(doc)`
*/
const parens = (doc) => {
return [(0, exports.text)('('), doc, (0, exports.text)(')')];
};
exports.parens = parens;
/**
* Enclose a document in brackets `[ ... ]`
* @param doc the document to enclose
* @returns the document `[doc]`
*/
const brackets = (doc) => {
return [(0, exports.text)('['), doc, (0, exports.text)(']')];
};
exports.brackets = brackets;
/**
* Test whether documents fit into `width` columns.
* We have to manually implement tail recursion via stack.
* Otherwise, recursive calls may produce a stack overflow in JS.
*
* @param width the number of columns to fit into
* @param inputStack the elements to fit in, as a stack
* @returns true if and only if `docs` fit into `w` columns
*/
const fits = (width, inputStack) => {
let columnBudget = width;
let stack = inputStack;
while (!stack.isEmpty() && columnBudget >= 0) {
const elem = stack.first();
stack = stack.shift();
if (isDocArray(elem.doc)) {
// push the children on the stack with the same indentation and mode
stack = stack.unshiftAll(elem.doc.map(d => {
return { ...elem, doc: d };
}));
}
else {
switch (elem.doc.kind) {
case 'text':
// consume the columns required by the text
columnBudget -= elem.doc.text.length;
break;
case 'line':
if (elem.mode === 'flat') {
// consume the columns required by the space
columnBudget -= elem.doc.space.length;
}
else {
// Although the paper says that this case is imposssible,
// it is triggered by their own test. Following the authors,
// we return true immediately.
return true;
}
break;
case 'nest':
// increase the indentation level and check again (in the next iteration)
stack = stack.unshift({
...elem,
indentText: elem.indentText.concat([elem.doc.indent.toString()]),
indentLen: elem.indentLen + elem.doc.indent.length,
doc: elem.doc.child,
});
break;
case 'group':
// assume that the group fits and check further
stack = stack.unshift({
...elem,
mode: 'flat',
doc: elem.doc.child,
});
break;
}
}
}
return columnBudget >= 0;
};
/**
* A stack-based implementation of `format`. Since it is using FitsElem,
* we do not expose this function to the user.
*
* In the future, we should consider using generators, as this function
* produces an array of strings in memory, whereas the documents could
* be consumed by an output function directly.
*
* @param maxWidth the intended width in columns
* @param start the number of columns already consumed on the current line
* @param elems the elements to format
*/
const formatInternal = (maxWidth, start, elem) => {
let columnBudget = maxWidth;
let consumedOnLine = start;
let output = [];
let stack = (0, immutable_1.Stack)([elem]);
while (!stack.isEmpty()) {
const elem = stack.first();
stack = stack.shift();
if (isDocArray(elem.doc)) {
stack = stack.unshiftAll(elem.doc.map(d => {
return { ...elem, doc: d };
}));
}
else {
switch (elem.doc.kind) {
case 'text':
output.push(elem.doc.text.toString());
consumedOnLine += elem.doc.text.length;
break;
case 'nest':
stack = stack.unshift({
...elem,
indentText: elem.indentText.concat([elem.doc.indent.toString()]),
indentLen: elem.indentLen + elem.doc.indent.length,
doc: elem.doc.child,
});
break;
case 'line':
if (elem.mode === 'flat') {
output.push(elem.doc.space.toString());
consumedOnLine += elem.doc.space.length;
}
else {
output.push(elem.doc.linefeed.toString());
output.push(elem.indentText.join(''));
consumedOnLine = elem.indentLen;
}
break;
case 'group': {
const first = { ...elem, mode: 'flat', doc: elem.doc.child };
if (fits(columnBudget - consumedOnLine, stack.unshift(first))) {
// the whole group can be printed without a break
stack = stack.unshift(first);
}
else {
// the group needs a break on every line, but subgroups may flow
stack = stack.unshift({ ...first, mode: 'break' });
}
break;
}
}
}
}
return output;
};
//# sourceMappingURL=prettierimp.js.map