UNPKG

@informalsystems/quint

Version:

Core tool for the Quint specification language

302 lines 10.8 kB
"use strict"; /* * 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