272: Pretty Printing

by Sergei Egorov

Status

This SRFI is currently in draft status. Here is an explanation of each status that a SRFI can hold. To provide input on this SRFI, please send email to srfi-272@nospamsrfi.schemers.org. To subscribe to the list, follow these instructions. You can access previous messages via the mailing list archive.

Abstract

This SRFI follows the traditional Scheme model of pretty printing, which treats it as a process distinct from general controlled formatting. While general-purpose formatters often prioritize specialized presentation at the expense of machine-readability, Scheme’s pretty-printers (such as those of SLIB and MIT Scheme) have traditionally treated pretty printing as a variant of write, differing primarily in the insertion of whitespace to make the presentation more palatable to humans. Common Lisp’s pretty-printer, by contrast, fills two roles simultaneously by integrating pretty printing with both its format facility and its generalized write procedures. This unified approach offers great power, but at the cost of complexity that can make it difficult to use effectively. We propose a specialized, layered approach, specifying five libraries of increasing functionality, where all but the first are optional. The libraries are downward-compatible: more powerful libraries satisfy all requirements of the simpler ones while adding new features. Implementors may choose to support a maximum level of functionality appropriate for their systems. Integration with monadic and string-based formatting libraries is supported.

Table of contents

Issues

Currently, none.

Rationale

Pretty printing in Lisp traces its origins to the late 1960s with GRIND [1], a tool designed to ‘grind’ function definitions into human-readable form for inspection and debugging. Because Lisp consists entirely of S-expressions, early pretty printers could rely on straightforward structural analysis of S-expression nesting rather than on complex syntactic knowledge. This regularity made it possible to format code consistently with relatively simple algorithms, establishing pretty printing as a specialized activity distinct from ordinary output.

As Lisp dialects matured, two philosophical approaches emerged. The specialized approach, found in MacLisp, Interlisp, and later in systems such as MIT Scheme and those using SLIB, treated pretty printing as a variant of write whose purpose was to preserve machine-readability while improving human readability through strategic whitespace. Output from these pretty printers could always be read back by the Lisp reader to reconstruct an equal object, giving users predictability, composability, and easy integration with editors like Emacs. This tradition recognized that pretty printing served a narrow purpose: making code and data comprehensible to programmers, not generating reports, tables, or prose.

Common Lisp chose a different path, deeply integrating pretty printing with format and write variants through the *print-pretty* dynamic variable and related printer controls. Influenced by work at Xerox PARC and codified in Common Lisp standard, this integrated approach offered pretty printer dispatch tables, logical blocks for grouping and conditional line breaks, format directives such as ~<..~>, and XP-style constraint-satisfaction formatting that optimized aesthetic criteria when choosing line breaks. The motivation was a single powerful framework capable of handling everything from debug output to complex document generation, allowing users to employ one system regardless of whether they were printing data, generating source code, or producing formatted reports.

That power came at a cost. The interaction between ~<, ~@<, ~:@<, and other format combinations proved notoriously difficult to master, creating a steep learning curve. The generality of the system imposed overhead even for simple cases, and conforming implementations demanded substantial engineering effort that some implementors chose not to undertake. In practice, most programs either used basic write with *print-pretty* set to true or avoided the sophisticated features entirely, leaving much of the machinery unused.

Scheme, adhering to its minimalist philosophy, largely sidestepped this complexity. Early Scheme reports did not specify a pretty printer, leaving implementations to devise their own solutions. MIT Scheme provided pp as a straightforward command for code formatting, SLIB offered portable pretty printing, Chez Scheme included customizable rules for special forms, and Racket developed its own library. The community generally favored specialized tools over monolithic frameworks, reflecting a broader preference for small, composable utilities.

More recently, SRFI 166 introduced monadic formatting inspired by Haskell's printing libraries. It uses functional combinators rather than special variables or dispatch tables, treats formatting as a monadic computation, and can target multiple backends. While it includes pretty printing capabilities, it is fundamentally a formatting framework first and a pretty-printer second, demonstrating the persistent tension between specialized and integrated approaches. Its sophistication requires understanding the combinator model, which may be excessive for users who simply want nicely formatted Scheme code.

These competing visions raise genuine questions about whether pretty printing and general formatting should remain separate. Pretty-printing Scheme code demands machine-readable output, adherence to established conventions like keyword alignment, and focus on code structure. General formatting often prioritizes human readability, custom layouts, and rich text features such as tables and columns, goals that can conflict with code-oriented decisions. A specialized pretty printer can offer a simple API like pp with no need for format strings, combinators, or dispatch mechanisms, making it valuable for quickly inspecting data or formatting code. It can also be highly optimized for S-expressions, using specialized algorithms for width measurement and line breaking without carrying the overhead of generality. Separation of concerns further eases maintenance, allowing code pretty printing conventions to evolve independently of document formatting.

Yet integration has its own merits. A unified system ensures consistent behavior across all output operations, sparing users from learning multiple models. Formatting combinators, dispatch mechanisms, and layout algorithms can be shared between pretty printing and general formatting, reducing overall code size. An integrated system handles hybrid cases more naturally, such as data structures containing both code and human-readable text, or documents that include pretty-printed code examples. Dispatch tables also let users define custom formatting for new data types uniformly, whether those types represent code, data, or documents.

In this SRFI we propose a bare-bones (srfi 272) pretty printing library, accompanied by optional libraries of increasing functionality: (srfi 272 basic), (srfi 272 intermediate), (srfi 272 advanced), and (srfi 272 fancy). This layered design directly addresses the tension between these approaches by allowing flexible implementation strategies that suit different needs and constraints. For example, the first four libraries could be implemented as ‘views’ of the fifth, re-exporting progressively larger sets of identifiers. Alternatively, the first three libraries could be implemented by remapping their interfaces to an existing library, with the last two libraries omitted. Yet another approach is to implement each library separately, so that no unnecessary code is loaded or linked into an application. This progressive structure acknowledges that different use cases have legitimately different needs rather than forcing all formatting through a single model.

The proposal is intended to cover a wide range of pretty-printing workflows. The bare-bones and basic libraries serve the common case of quickly inspecting data structures or formatting code with a simple API. The intermediate and advanced layers add richer control over presentation, including custom rendering styles for various Scheme constructs. At the highest level, the fancy library provides output to color-capable terminals and other enhanced capabilities. The specification for file-based pretty printing includes provisions for preserving comments, addressing a practical need that simple pretty-printers have historically neglected.

Furthermore, this design does not force integration with general formatting frameworks, but enables it where desired. The make-pprint-generator procedure in the advanced library may be used by SRFI 166 implementations to integrate the proposed advanced pretty printer into its framework. This provides a bridge between the specialized and integrated worlds, allowing Scheme systems to offer a simple pretty printing API for most users while permitting sophisticated composition with monadic formatters when the full power of SRFI 166 is warranted. Below is an example of how such an integration might look:

(define (pprinted obj . kv*)
  (fn (width) ; default width is taken from show env
    (let* ((ppe (append kv* (list pp-width width)))
           (g (apply make-pprint-generator obj ppe)))
      (let lp ((s (g)))
        (if (eof-object? s)
            nothing
            (each s (fn () (lp (g)))))))))

(show #t (pprinted x pp-code #t pp-max-tab 5))

Integration with SRFI 48 can be achieved by substituting pp for write in the implementation of the ~Y directive.

Note: Although the exact details of how S-expressions are pretty-printed are not specified, many points in this SRFI implicitly assume that a conventional Lisp/Scheme structural layout is used. Its main features are: a single space for horizontal separation, no whitespace after an opening parenthesis or before a closing parenthesis, and indentation that follows the nesting of subexpressions. The References section contains links to relevant style guidelines.

Specification

While naming the libraries, we follow R7RS conventions; for R6RS systems, the numerical name segment should be prefixed with a colon, e.g. (srfi :272 basic).

The (srfi 272) library

This library provides a minimalist view on pretty printing functionality.

(ppobj ⟩)   procedure
(ppobj ⟩ ⟨port ⟩)   procedure
Writes a pretty-printed representation of ⟨obj ⟩ to the given port. If the port argument is missing, the value of (current-output-port) is used. Whitespace is used to format the output within a system-dependent column width. This width may depend on characteristics of the output port, such as the current width of a terminal window, and may thus vary between calls. Alternatively, it may be a fixed internal value. This width is a soft limit; the printer may write past the margin when printing indivisible tokens (such as large exact numbers or symbols).

This library provides no guarantee of termination when printing objects that contain circular references. For non-circular objects, it provides the same machine-readability guarantees as the implementation’s write procedure. If an object can be written by write and then read back by read to produce an equal object, then pp must also generate output that can be read back to produce an equal object.

When producing its output, pp can assume that it is called right after a newline or at the very first position of the output port. If this condition does not hold, the output may be aligned incorrectly. The output is always followed by a terminating newline ending the last line of the ⟨obj ⟩ representation.

The (srfi 272 basic) library

This optional library provides all the functionality of the (srfi 272) library, adding the procedures and parameters described below. The pp procedure maintains the same requirements and guarantees as described above in matters not related to the right margin and printing of shared and circular references. For these matters, it behaves as specified in the parameter descriptions below.

pp-width   parameter
If set to an exact positive integer, this parameter specifies a soft right margin for pp, measured in columns. For non-fixed-width fonts, a column is approximated by a standard unit (e.g., ‘en’, the width of an en-dash). Some implementations may support values other than exact positive integers; their interpretation is implementation-dependent. The default value is implementation-dependent.
    Note: Width values observed in existing pretty printers are in 72–80 range.

pp-graph   parameter
If set to a true value, this parameter forces pp to use datum labels (i.e. #N= and #N#) to represent both shared and circular references. The default value is #f.

pp-circle   parameter
This parameter is ignored if the pp-graph parameter is true. If pp-graph is #f and pp-circle is true, pp is forced to use datum labels to represent circular references only. If both pp-graph and pp-circle are set to #f, pp may fail to terminate when printing an object containing circular references. The default value is #t.
    Note: Some existing pretty printers may not be able to print only circular references; they always print circular and shared references together. This behavior is conforming for R6RS-based systems, but not for R7RS-based ones.

(pprintobj ⟩)   procedure
(pprintobj ⟩ ⟨port ⟩)   procedure
Behaves as R7RS write with respect to shared and circular references (but see the Note above). This is analogous to pp called in a context where pp-graph is #f and pp-circle is true. Current values of the pp-graph and pp-circle are neither used nor affected.

(pprint-sharedobj ⟩)   procedure
(pprint-sharedobj ⟩ ⟨port ⟩)   procedure
Behaves as R7RS write-shared with respect to shared and circular references. Analogous to pp called with pp-graph set to true. Current values of the pp-graph and pp-circle are neither used nor affected.
    This procedure is usually faster than pprint.

(pprint-simpleobj ⟩)   procedure
(pprint-simpleobj ⟩ ⟨port ⟩)   procedure
Behaves as R7RS write-simple with respect to shared and circular references. Analogous to pp called with pp-graph and pp-circle set to #f. Current values of the pp-graph and pp-circle are neither used nor affected.
    This procedure may loop indefinitely on circular objects. The reason it is offered is speed: potentially, it is the fastest of the three.

The (srfi 272 intermediate) library

This optional library provides all the functionality of the (srfi 272 basic) library, adding the arguments and parameters described below. The set of available functionality is an intersection of what is usually included in intermediate pretty-printers and, in many cases, can be mapped directly to existing implementation-specific libraries. The pp procedure maintains the same requirements and guarantees as specified previously, except in matters related to the printing of numbers and the imposition of level/length limits on the output. For these matters, it behaves as specified in the parameter descriptions below.

(ppobj ⟩ ⟨keyword argument…⟩)   procedure
(ppobj ⟩ ⟨port ⟩ ⟨keyword argument…⟩)   procedure
This extended variant of pp accepts additional keyword arguments in the form of alternating key and value expressions. Each key expression must evaluate to a parameter object available through this library (normally, they are just names of the parameters). Each value expression must evaluate to an object acceptable as a value for the preceding parameter.
    When determining the value for a parameter, pp first scans its keyword arguments from left to right. If a keyword equal to the parameter is found, the value following it is used; otherwise, the value stored in the parameter itself is used.
    Rationale: This provides an alternative to wrapping a call to pp in parameterize. It enables procedural abstractions, such as bundling parameters into configuration lists.

(pp*obj ⟩ ⟨keyword argument…⟩ ⟨keyword argument list ⟩)   procedure
(pp*obj ⟩ ⟨port ⟩ ⟨keyword argument…⟩ ⟨keyword argument list ⟩)   procedure
This variant of pp accepts keyword arguments followed by a final expression that must evaluate to a proper list of alternating keys and values. This final list is effectively ‘spliced’ into the call, as if its contents were passed to pp via apply. Example:

(define my-pp-config (list pp-width 120 pp-graph #t))
(pp* x port my-pp-config)

; ... has the same effect as:
(apply pp x port my-pp-config)

; .. and as:
(pp x port pp-width 120 pp-graph #t)

; and as (no procedural abstraction):
(parameterize ([pp-width 120] [pp-graph #t])
  (pp x port))

Note: The extended versions of pp gain no new printing behavior from the keyword argument mechanism itself; changes in behavior depend only on the parameter values, not on the method by which they are supplied.

(pprintobj ⟩ ⟨keyword argument…⟩)   procedure
(pprintobj ⟩ ⟨port ⟩ ⟨keyword argument…⟩)   procedure
(pprint-sharedobj ⟩ ⟨keyword argument…⟩)   procedure
(pprint-sharedobj ⟩ ⟨port ⟩ ⟨keyword argument…⟩)   procedure
(pprint-simpleobj ⟩ ⟨keyword argument…⟩)   procedure
(pprint-simpleobj ⟩ ⟨port ⟩ ⟨keyword argument…⟩)   procedure
The pprint, pprint-shared, and pprint-simple procedures in this library are also extended to accept keyword arguments (but not a final keyword argument list; apply should be used for that purpose). The values for pp-graph and pp-circle that are hardwired into these procedures take precedence over any values supplied via keyword arguments or parameters.

(pprint-fileinfile ⟩ ⟨keyword argument…⟩)   procedure
(pprint-fileinfile ⟩ ⟨outfile ⟩ ⟨keyword argument…⟩)   procedure
This procedure reads code expressions from a text file named ⟨infile ⟩ (a string), and pretty prints the expressions it reads to a new text file named ⟨outfile ⟩ (also a string). If ⟨outfile ⟩ is not specified, the output is directed to the value of (current-output-port). This procedure can be configured with keyword arguments and parameters in the same manner as the pprint procedure.
    An implementation may preserve some original whitespace and comments from the input file, but is not required to do so. If it does not preserve top-level comments or whitespace, it should separate top-level expressions with a single empty line.

pp-radix   parameter
If set to one of the exact integers 2, 8, 10, or 16, this parameter forces pp to print exact numbers in the specified radix. For non-decimal radices, the corresponding #b, #o, or #x prefix is automatically prepended to ensure the output is machine-readable. The effect of a non-decimal radix on inexact numbers, as well as the effect of radixes other than those explicitly listed, must be such that reading the printed number back in the same implementation produces an eqv? numerical value. (A relaxed rule applies to NaNs: if the number being printed is a NaN, it may be read back as a NaN with a different payload).
    An implementation may support non-numerical values for pp-radix to facilitate, among other things, preservation of the original lexical representation of numbers from a source file, or to allow elements of a bytevector to be printed in hexadecimal while other numbers are printed in decimal. The default is an implementation-specific value that makes pp follow conventions of the implementation’s write procedure.

pp-level   parameter
pp-length   parameter
The pp-level parameter limits how deeply the printer descends into nested structures. Its value must be either #f (the default, meaning no limit) or an exact nonnegative integer. The object passed to the printer is at level 0; if it is a list or vector, its elements are at level 1, their sub-elements at level 2, and so on. When the printer encounters a component at a level equal to or exceeding this bound, it prints a ‘stub’ instead of recursing further. This restriction applies only to objects with list-like syntax; atomic values such as booleans, symbols, strings, and numbers are unaffected.
    The pp-length parameter limits how many elements are shown inside any one list, vector, or other list-like object. Its value must also be either #f (the default, meaning no limit) or an exact non-negative integer. If an object contains more elements than permitted, the printer writes the leading elements followed by a stub representing the remainder. For dotted lists, if the sequence contains exactly as many elements as the limit, the terminating atom is printed instead of a stub.
    The appearance of these stubs is implementation-dependent. Some printers, following Common Lisp tradition, use # for level-limit stubs, while others may use ‘shells’ of objects with their contents replaced by an ellipsis (...). The length-limit stub may also vary but is usually an ellipsis (...). The (srfi 272 intermediate) library offers no control over the appearance of these stubs.

Note: Perhaps counterintuitively, most existing pretty-printers do not treat reader macros such as 'obj, `obj, etc. the same way as their unabbreviated forms. The obj following a reader macro prefix is often considered to be at the same nesting level as the macro form itself. The intermediate library neither requires nor forbids this behavior, nor does it provide parameters to control it. The only requirement is to treat read macros consistently with respect to shared references and level/length limits. In particular, no #N= datum label should be printed if all its references are invisible due to presentation limits, and no #N# reference should be printed without its corresponding definition label being visible. This rule should apply to all compound objects, not only to the ones printed as reader macros.

(pretty-stylesymbol ⟩)   procedure
(pretty-stylesymbol ⟩ ⟨style ⟩)   procedure
When printing Scheme code, the printer may follow established conventions for code presentation, as found in tools like Emacs. Many special forms are formatted in idiosyncratic ways dictated by their semantics, where different subforms play different roles. This procedure provides control over the styling by allowing a user to associate a custom style with a form’s leading symbol by copying a style associated with an existing symbol.
    If called with a single argument, this procedure returns the style object associated with that symbol, or #f if no style is associated. A style is an opaque object, distinct from #f.
    If called with two arguments, the procedure associates the given style (which may be either a style object returned from a one-argument call or #f) with the given symbol. Providing #f as the style value resets the formatting for forms starting with that symbol to an implementation-specific default—the same default that is used for printing unknown forms.
    Although implementations are not required to implement custom styling control via this mechanism (they can always return #f), they are encouraged to do so. If they do so, they must ensure that their style objects are reusable and do not depend on the length of the symbols with which they are associated.

The (srfi 272 advanced) library

This optional library provides all the functionality of the (srfi 272 intermediate) library, adding the procedures and parameters described below. The functionality provided is a union of features commonly found in intermediate to advanced pretty-printers, and in many cases, implementation-specific libraries can be mapped directly to this one.

The pp, pp*, pprint, pprint-shared, and pprint-simple procedures maintain the same requirements and guarantees as specified for the intermediate library, except where modified by the functionality described below.

The pprint-file procedure is enhanced in this library: it is required to pass line comments and whitespace between top-level forms from the input to the output verbatim, unless the pp-decorate parameter is set to #f (see detailed description below).

The storage behind the pretty-style procedure is made explicit; the style objects are no longer opaque. The details follow the description of the pp-styles parameter below.

There is a new pretty-hooks procedure and pp-hooks parameter for extending the domain of the pretty printer to new objects or customizing printing of existing objects; see details below.

pp-lines   parameter
This parameter limits the total number of lines printed. Its value must be either #f (the default, meaning no limit) or an exact non-negative integer. If the limit is reached, printing is terminated with a lines stub.

pp-level-stub   parameter
pp-length-stub   parameter
pp-lines-stub   parameter
These parameters provide explicit control over the appearance of the stubs printed when output is truncated due to the pp-level, pp-length, and pp-lines limits, respectively. All three parameters accept string values (e.g. "#" for level, "..." for length, and ".." for lines to achieve a Common Lisp-like appearance). The pp-level-stub can also accept #t, which instructs the pretty-printer to use ‘shell’ representations of compound objects for the level stubs. For a level stub, the object’s contents are replaced by the pp-length-stub string, as if the objects are printed with pp-length set to 0. If the pp-lines-stub parameter is set to #f, the pp-length-stub string is used instead. Following Common Lisp convention, all outstanding closing parentheses must be printed immediately after a line stub, before the terminating newline (if any).

pp-newline   parameter
If set to #f, this parameter suppresses the final newline that is normally printed at the end of the output. The default value is #t.

pp-inline-width   parameter
pp-miser-width   parameter
These parameters accept #f or an exact nonnegative integer. If pp-inline-width is an integer, it acts as a soft limit on the width of complete subexpressions that would otherwise be printed on a single line. This can be set to a value less than pp-width to produce more natural-looking layouts. If it is set to #f, single-line expressions receive no special treatment.
    If pp-miser-width is an integer, it specifies a ‘miser mode’ zone near the right margin. Expressions printed within this zone are formatted with minimal indentation to save horizontal space. If it is set to #f, expressions near the right margin receive no special treatment.
    The default values for both parameters are implementation-dependent.

pp-indent   parameter
If set to an exact non-negative integer, this parameter informs the printer that the output starts at the specified column. This is useful when embedding pretty-printed text within a line that already contains content. All subsequent lines of the pretty-printed output will be left-padded with spaces to ensure correct alignment relative to this initial indentation. Implementations that track port columns may support non-numeric values for this parameter. The default value is either 0 or an implementation-dependent value that instructs the printer to use the current port column.

pp-tab   parameter
pp-max-tab   parameter
These parameters, which accept exact non-negative integers, control indentation. The pp-tab parameter specifies the number of columns by which the printer indents the bodies of most forms (such as let expressions), relative to the start of the form’s keyword. Note that since an opening parenthesis adds one column of indentation, the total indent from the parenthesis will be (pp-tab) + 1. A value of 0 aligns the body with the keyword.
    When formatting code, short keywords like if may trigger a special layout where the first subexpression after the keyword appears on the same line as the keyword, with subsequent subexpressions aligned under it. This can lead to offsets greater than what pp-tab would dictate. The pp-max-tab parameter puts an upper limit on these special offsets, preventing long keywords from causing excessive rightward shifts. As before, offsets are counted from the start column of the form’s keyword or first expression, not from its opening parenthesis/bracket.
    The default values for both parameters are implementation-dependent.

pp-code   parameter
pp-brackets   parameter
These boolean parameters provide hints  to trigger specialized code-formatting behavior. If pp-code is true, it signals that the input is an S-expression representing Scheme code, allowing the printer to apply conventional code layout rules. Otherwise, no such special formatting is assumed. Conforming implementations are encouraged, but not required, to act on this hint. The default value is implementation-dependent.
    If pp-brackets is true, it hints that R6RS-style square brackets [] should be selectively used as an alternative to parentheses when printing in code mode. Implementations not supporting this feature in the reader are free to ignore the hint. To ensure portability, a value of #f should disable bracket printing on all implementations. The default is implementation-dependent.
    Note: See R6RS Appendix C for guidelines on the use of square brackets.

pp-pretty   parameter
This boolean parameter is true by default. If set to #f, all runs of continuous whitespace normally introduced by the pretty-printer are collapsed into single spaces, producing a style similar to that of write. Other pretty printing parameters not related to whitespace remain in effect. The terminating newline is also omitted, regardless of the pp-newline setting.

pp-color   parameter
If the implementation supports color-capable ports (e.g., ports connected to ANSI X3.64 / ISO/IEC 6429 compatible terminals), this parameter can be used to request that the printer use syntax highlighting when printing Scheme code (see pp-code for discussion on code vs. data printing). If set to #t, some built-in implementation-dependent color theme will be used. For more control over colors, see the description of the (srfi 272 colorize) library below. The default value of this parameter is #f (do not use color).
    Implementations are encouraged to assess local color capabilities to make sure the colors will be displayed faithfully, e.g. by inspecting local environment variables. If an implementation cannot detect any color support, it should not by default attempt to use color and just print without it (but see the colorize library for ways to control this behavior).
    Note that it can be useful to emit color sequences to ports not connected to a terminal. One might want to aggregate output in string or file ports to be later sent to a color-capable port, so implementations should not make decisions based solely on capabilities of the port passed to the pretty-printer as an argument.

pp-decorate   parameter
If set to #f, this parameter commands pprint-file to ignore all comments in the input file, and to separate printed top-level expressions with a single empty line. If set to #t (the default), pprint-file must preserve line comments and whitespace between top-level expressions, passing it to the output verbatim. There is no requirement to preserve block or S-expression comments, but implementations are free to do so.
    Implementations are free to support non-boolean values for this parameter to facilitate more faithful reproduction of some aspects of the original source code.

pp-emit   parameter
The value of this parameter must be a procedure accepting two arguments: a string and a port. This procedure is used for all conventional output of the pretty printer, which is all output except the special character sequences added by the colorizer; in most cases, all output is conventional. The default value of the parameter is the standard write-string procedure, but a user can substitute a different procedure to perform custom post-processing of the output, such as adding line numbers.

pp-tint   parameter
The value of this parameter must be a procedure accepting two arguments: a string and a port. This procedure is used for all unconventional output of the pretty printer, which is limited to special character sequences added by the colorizer. The default value of the parameter is the standard write-string procedure, but a user can substitute a different procedure to perform custom post-processing of the output, such as filtering out the coloring.

pp-styles   parameter
This parameter holds the internal style registry, which is managed by pretty-style, making it explicit and directly accessible. It holds an opaque style registry object that can be manipulated functionally via add-pp-style. This allows for side-effect-free manipulation and enables capturing a snapshot of the registry for later use.

(add-pp-stylestyle registry ⟩ ⟨symbol ⟩ ⟨style ⟩)   procedure
This procedure functionally updates a style registry object. It returns a new registry object reflecting the change, leaving the original arguments unmodified. If the style argument is #f, it returns a registry where the symbol has no associated style. Otherwise, style must be a valid style object, and the returned registry will contain this new association, replacing any previous one for the given symbol.

(lookup-pp-stylestyle registry ⟩ ⟨symbol ⟩)   procedure
This procedure returns the style object associated with the given symbol in the style registry object, or #f if no association exists.

This library makes the structure of a style object explicit. It is an improper list matching the grammar below. Although the grammar is given as a printed representation, a style is a list, not a string.

style ⟩ ⟶   ( _ . ⟨fmt-tail ⟩ )
fmt-tail ⟩ ⟶   body | fill | dc* | ec* | fc* | lc* | ( i? . ⟨fmt-tail ⟩ ) | (fmt ⟩ . ⟨fmt-tail ⟩ )
fmt ⟩ ⟶   i | d | e | f | l | h | dc | ec | fc | lc | ⟨fmt-tail ⟩

A style object acts as a pattern that is matched against an input form. If the form does not match the pattern, the non-matching part is printed as a plain datum. All terminals in the grammar (except i?) match any subform, and the printer will attempt to render that subform according to the specified method (see table below). The i? terminal is special: if the corresponding subform is an identifier, i? is treated as i; otherwise, the terminal is ignored, and matching continues with the same subform and the rest of the style pattern.

iprint as identifier (a bound variable)
dprint as datum (uncolored, no special formatting)
eprint as an expression
fprint as a formal parameter or a (possibly improper) list of formals
lprint as a literal or a list of literals
hprint as a definition head form (i or MIT-style list)
dcprint as a datum-expressions clause
ecprint as an expression-expressions clause (as in cond)
fcprint as a formals-expressions clause (as in case-lambda)
lcprint as a literals-expressions clause (as in case)
dc*print as a list of dc clauses
ec*print as a list of ec clauses
fc*print as a list of fc clauses
lc*print as a list of lc clauses
bodyprint as list of expressions, typically indented (as in lambda)
fillprint as compact list of expressions, filling lines (as in applications)

Examples of common styles:

define(_ h . body)
lambda(_ f . body)
let(_ i? fc* . body)
if(_ . body)
or(_ . fill)
do(_ fc* ec . body)
cond(_ . ec*)
case(_ e . lc*)
when(_ e . body)
guard(_ (f . ec*) . body)
case-lambda(_ . fc*)

(pretty-hooktest ⟩)   procedure
(pretty-hooktest ⟩ ⟨hook ⟩)   procedure
This procedure provides an interface for managing global hooks—special handlers for types of Scheme objects that require custom printing logic (e.g., boxes or homogeneous numeric vectors). It maintains a hook registry, an opaque object stored in the pp-hooks parameter.
    If called with one argument, it returns the hook object associated with the test procedure, or #f if no association exists. A return value of #t indicates that test is not a simple predicate but a procedure that returns a hook object upon a successful match.
    If called with two arguments, it associates a hook object with a test procedure by changing the value of the pp-hooks parameter. If hook is #t, it signifies that test is a procedure that returns a hook object.

pp-hooks   parameter
The hook registry managed by pretty-hook is stored in this parameter. It can be manipulated functionally via add-pp-hook, allowing for side-effect-free updates and snapshots of the registry state.

(add-pp-hookhook registry ⟩ ⟨test ⟩ ⟨hook ⟩)   procedure
This procedure functionally updates a hook registry. If hook is #f, it returns a new registry with no hook associated with the test procedure. If hook is a hook object, test is treated as a predicate, and the returned registry contains this new association. If hook is #t, test is expected to be a procedure that returns a hook on successful matches.
    New associations are added to the beginning of the registry’s testing order; existing associations for the same test are replaced in place. The original arguments are not modified.

(lookup-pp-hookhook registry ⟩ ⟨test ⟩)   procedure
This procedure returns the hook object associated with the given test in the hook registry. It returns #t if the test is registered as a hook-returning procedure, and #f if no association exists.

(glst-pp-hookprefix ⟩ ⟨cf ⟩ ⟨xf ⟩ ⟨suffix ⟩)   procedure
(bvec-pp-hookprefix ⟩ ⟨lf ⟩ ⟨rf ⟩ ⟨suffix ⟩)   procedure
(atom-pp-hookshareable? ⟩ ⟨wf ⟩ ⟨df ⟩)   procedure
(rmac-pp-hookprefix ⟩ ⟨ef ⟩ ⟨xf ⟩)   procedure
These procedures construct hook objects. A hook ‘explains’ a custom data type to the printer in terms it can understand, by providing prefix/suffix strings and functions that allow the printer to treat the object as an abstract sequence or an atomic value.
    The glst-pp-hook constructor creates a hook that views an input object in terms of a generalized list. It provides a prefix string (e.g., "#["), a cf  (conversion function) to convert the object into a Scheme list, an xf  (a reverse conversion function) to recreate the original object from a list, and a suffix string (e.g., "]").
    The bvec-pp-hook constructor creates a hook that views an input object in terms of a binary vector. It provides a prefix string, an lf  (length function), an rf  (a vector-ref-like reference function) returning a number, and a suffix string.
    The atom-pp-hook constructor creates a hook that views an input object as an indivisible ‘atom.’ It provides a boolean flag indicating whether the object can be labeled with a datum label when printed in pp-graph mode, a wf  (width function) to calculate its printed length, and a df  (display function) that accepts an object and a radix, returning a list of strings and color symbols for printing. This list should contain one or more strings, each string optionally preceded by a symbol identifying its semantic color  (more on this below).
    The rmac-pp-hook constructor creates a hook that views an input object as a read macro (like 'a standing for (quote a)). It provides a prefix string (e.g., "'"), an ef  (extraction function, e.g., cadr), and an xf  (a reverse of the extraction function, e.g., (lambda (x) (list 'quote x)).
    The choice of which hook to use must be based on the desired printed representation, not the object’s internal structure. For example, bit vectors that should be printed as #*010110110 cannot be described with glist-pp-hook or bvec-pp-hook, because those hooks assume space-separated elements. Instead, such an object must be described with atom-pp-hook, even though it is internally a sequence. The same principle applies to strings.

(make-pprint-generatorobj ⟩ ⟨keyword argument…⟩)   procedure
This procedure returns a SRFI 158-compatible generator, delivering the pretty-printed representation of ⟨obj ⟩ one line at a time. All lines but the last one are strings ending in a newline; whether the last string ends in a newline depends on the value of the pp-newline parameter. Keyword parameters are processed in the usual way (see pprint for details).
    If pp-color parameter is set to a non-false value, the lines may include color sequences; to filter them out post factum, one can override the pp-tint parameter.

Note: the make-pprint-generator procedure may serve as an integration point for plugging the proposed pretty printer into the SRFI 166 framework.

The (srfi 272 fancy) library

This optional library provides all the functionality of the (srfi 272 advanced) library, adding new features to the pprint-file procedure, and introducing a variant that generates HTML output.

In this library, the pprint-file procedure is further enhanced. If the pp-decorate parameter is true, pprint-file is expected to preserve as many comments as possible, including line comments within expressions, unless they interfere with line breaking decisions made by the printer. Implementations are encouraged, but not required, to consider the location of existing internal line comments when choosing where to insert line breaks. If pp-decorate is #f, pprint-file must ignore all comments and separate top-level expressions with a single empty line.

One way to preserve internal comments is to use a custom reader to collect their locations and pass the resulting comment map to the printer via a custom value for the pp-decorate parameter. The same effect can be achieved by using a reader that produces annotated abstract syntax trees (ASTs) and having the printer accept this format alongside regular S-expressions.

While processing the head of the input file (lines before the first S-expression), pprint-file in decoration mode must recognize a special ‘magic comment’ that follows the Emacs convention for file-local variables, for example:

;; -*- mode: scheme; fill-column: 75; coding: utf-8; -*-

It must parse the first such line it encounters and extract key-value pairs that can be interpreted as pretty-printer parameters. Recognized keys include both standard Emacs variables and pp- prefixed parameter names. The minimum supported set of mappings from file variable keys to parameters is shown below as key-parameter pairs:

pp-width:pp-width
fill-column:pp-width
pp-inline-width:pp-inline-width
pp-miser-width:pp-miser-width
pp-tab:pp-tab
pp-max-tab:pp-max-tab
pp-brackets:pp-brackets

The values for these variables should be numbers or the special symbols t (for #t) and nil (for #f). These values are converted to regular Scheme values and used to establish a file-scoped configuration. This configuration takes precedence over the dynamic values of the parameters but can be overridden by keyword arguments explicitly passed to pprint-file.

(pprint-file/htmlinfile ⟩ ⟨keyword argument…⟩)   procedure
(pprint-file/htmlinfile ⟩ ⟨outfile ⟩ ⟨keyword argument…⟩)   procedure
This procedure reads code expressions from a text file specified by ⟨infile ⟩ (a string), and pretty-prints the expressions it reads to a new HTML file specified by ⟨outfile ⟩ (also a string). If ⟨outfile ⟩ is not specified, the output is directed to the value of (current-output-port). This procedure can be configured with keyword arguments and parameters in the same manner as pprint-file.
    The requirements for preserving whitespace and comments are the same as for this library’s pprint-file. The pp-color parameter is treated specially: if color printing is requested, the coloring engine switches to emitting HTML <span> tags with appropriate CSS classes or inline styles instead of terminal color sequences. The color theme and text attributes are adapted for presentation in HTML.

The (srfi 272 colorize) library

This optional library provides a comprehensive framework for adding color and text attributes to the pretty printer’s output, primarily for ANSI-compatible terminals. It introduces facilities for detecting terminal capabilities, generating appropriate SGR (Select Graphic Rendition) control sequences, and mapping high-level semantic categories to specific visual styles.

This library can be imported alongside either the (srfi 272 advanced) or (srfi 272 fancy) library, which accepts semantic color mappers, as defined by this library, as legal values of the pp-color parameter.

The library employs a heuristic-based system to determine the host terminal’s color capabilities. This logic is encapsulated in the detect-sgr-tier procedure and exposed through the sgr-support-tier parameter.

(detect-sgr-tier)  procedure
Probes the environment (inspecting variables like TERM, COLORTERM, NO_COLOR, and CLICOLOR_FORCE) to determine the terminal’s SGR support level. It returns one of the following values:

#fNo ANSI SGR support is detected or it is explicitly disabled
0Basic 16-color support (ANSI escape codes 30-37, 90-97)
1256-color palette support
224-bit true color support (16 million colors)
3Extended SGR support with additional attributes (colored underlines, etc.)

sgr-support-tier   parameter
Holds the result of (detect-sgr-tier) by default. Users can parameterize this to override the detected tier for a specific context, forcing the printer to generate SGR sequences for a different capability level.

A set of procedures is provided to generate the raw SGR parameter strings for each support tier. They have many optional parameters, shown in square brackets in the prototypes below:

(sgr0fg16 ⟩ [⟨bold? ⟩ ⟨uline? ⟩ ⟨bg16 ⟩])  procedure
(sgr1fg256 ⟩ [⟨bold? ⟩ ⟨uline? ⟩ ⟨bg256 ⟩])  procedure
(sgr2fgtrue ⟩ [⟨bold? ⟩ ⟨uline? ⟩ ⟨italic? ⟩ ⟨strike? ⟩ ⟨bgtrue ⟩])  procedure
(sgr3fgtrue ⟩ [⟨bold? ⟩ ⟨dim? ⟩ ⟨uline ⟩ ⟨italic? ⟩ ⟨strike? ⟩ ⟨bgtrue ⟩])  procedure
These procedures construct SGR parameter strings (the part between ESC[ and m) for tiers 0, 1, 2, and 3, respectively. They accept color specifications ( 0..15 for ⟨fg16 ⟩/⟨bg16 ⟩, 0..255 for ⟨fg256 ⟩/⟨bg256 ⟩, 0x000000..0xFFFFFF for ⟨fgtrue ⟩/⟨bgtrue ⟩ #RRGGBB colors) and boolean flags for attributes like bold and underline. The sgr3 procedure accepts an advanced ⟨uline ⟩ values for styled underlines:

0 | offdisable underline
1 | #t | straight | _straight underline
2 | double | =double underline
3 | curly | ~curly underline
4 | dotted | :dotted underline
5 | dashed | -dashed underline
(type ⟩ . ⟨fgtrue ⟩)underline with specified type (see values above) and RGB color

To manage styles across different terminal capabilities, this library introduces the concept of an ANSI SGR Bundle (ASB). An ASB is a vector of four SGR parameter strings, corresponding to tiers 0 through 3.

(make-asbs0 ⟩ [⟨s1 ⟩ ⟨s2 ⟩ ⟨s3 ⟩])  procedure
(asb->sgr-stringasb ⟩ [⟨tier ⟩])  procedure
The make-asb procedure constructs an ASB. If fewer than four arguments are provided, the last argument is used to fill the remaining slots.
    The asb->sgr-string procedure renders an ASB into a full SGR escape sequence (e.g., "\x1B[31;1m") appropriate for the given tier (or the current value of sgr-support-tier). It returns an empty string if color support is disabled.

(make-asb-paletteasb ⟩ …)   procedure
(asb-palette-refpalette ⟩ ⟨i ⟩)   procedure
default-asb-palette  constant
The library assembles color bundles into palettes and maps semantic categories (such as comment or number) to entries in those palettes. The make-asb-palette procedure accepts either 8 or 16 ASBs; if the rightmost 8 arguments are missing, the leftmost 8 arguments are copied over. It returns an opaque palette object. The asb-palette-ref procedure returns the ASB stored at index ⟨i ⟩ (015). A default 16-color palette, default-asb-palette, is provided.

(semantic-color-mapper?obj ⟩)  procedure
(make-semantic-color-mapperpalette ⟩ [⟨palette-mapper ⟩])  procedure
default-semantic-color-mapper  constant
A semantic color mapper is a procedure that receives a semantic color symbol (e.g., string, keyword) and a start?  flag and returns a string to be sent to the output port verbatim.
    The make-semantic-color-mapper procedure creates such a mapper by combining a palette with a ⟨palette-mapper ⟩ that maps semantic symbols to palette indices (if not given, a default mapping is used, shown in the table below). The resulting mapper produces SGR escape sequences.
    A default-semantic-color-mapper is provided, which implements a reasonable default theme.

The table below shows semantic color symbols, their description, and palette indices for these symbols, as mapped by the default ⟨palette-mapper ⟩.

commentComments8 (gray)
char, stringCharacter and string literals10 (green)
escape, char-escape, string-escapeEscape sequences11 (yellow)
char-meta, string-metaMetadata in chars/strings2 (green)
symbol-escapeEscaped symbols11 (yellow)
formalFormal parameters7 (light gray)
variableVariablesdefault (no styling)
definedVariables being definedbold default
keywordKeywords of special forms12 (blue)
numberNumeric literals13 (purple)
literalLiteral values14 (teal)
literal-meta, metaMetadata6 (cyan)
directiveDirectives13 (purple)
parenParentheses7 (light gray)
bracketBracketsdefault
warningWarnings1 (red)

Note that users may create semantic color mappers manually, bypassing make-semantic-color-mapper. Manually created mappers are not limited to returning ANSI SGR sequences; for example, this is the mapper that may be used to produce HTML markup:

(lambda (sc start?)
  (if start?
      (string-append "<span class=\"" (symbol->string sc) "\">")
      "</span>"))

(semantic-color->start-stringsc ⟩ [⟨mapper ⟩ ⟨tier ⟩])  procedure
(semantic-color->end-stringsc ⟩ [⟨mapper ⟩ ⟨tier ⟩])  procedure
These are the high-level procedures used by the pretty printer’s emission layer. Given a semantic color symbol ⟨sc ⟩ (e.g., comment), they return the appropriate start sequence (e.g., "\x1B[90m") and end sequence ("\x1B[0m") for the current support tier and color mapper. This is the primary interface for integrating colorization into the printer’s internal color emission routines. Note that coloring sequences are not passed to pp-emit, but sent to the output port via pp-tint.

The (srfi 272 measure) library

If this library is supported, users may plug in their own versions of char width measurement procedures by changing the value of the char-width-procedure parameter it exports.

char-width-procedure   parameter
This parameter holds a procedure that accepts a character and returns either the character width in columns it takes on a terminal (0, 1, or 2), or #f if this character is not printable. The exact behavior of this procedure is implementation-dependent, but implementations are encouraged to mimic the behavior of the wcwidth C procedure on their system, preferably by wrapping it directly.

References

[1] Goldstein, I. Pretty-printing, converting list to linear structure.
Artificial Intelligence Laboratory Memo. No. 279, M.I.T., Cambridge, Mass., 1973.

[2] An Introduction to Scheme and its Implementation.
https://docs.scheme.org/schintro/schintro_24.html

[3] Heinrich Taube. Lisp Style Tips for the Beginner.
https://ccrma.stanford.edu/CCRMA/Courses/AlgoComp/cm/doc/contrib/lispstyle.html

[4] Scheme code conventions (part of Cyclone documentation).
https://justinethier.github.io/cyclone/docs/Scheme-code-conventions.html

[5] Taylor R. Campbell. Riastradh's Lisp Style Rules.
https://mumble.net/~campbell/scheme/style.txt

[6] Scheme Style Guide (Cornell University).
https://www.cs.cornell.edu/courses/cs212/1999FA/handouts/tips.html

Implementation

Sample portable R7RS implementation is available. Its only external dependency is (srfi 39) to make sure that parameters may be set globally (R7RS does not provide a method of doing this).

Source for the sample implementation.

Acknowledgements

This proposal builds upon and is inspired by the work of countless contributors spanning the early Lisp era to the present day. Acknowledging each individual by name would be impossible without risk of omission.

© 2026 Sergei Egorov

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


Editor: Arthur A. Gleckler