by Sergei Egorov
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.
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.
Currently, none.
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.
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).
(srfi 272) libraryThis library provides a minimalist view on pretty printing functionality.
(pp ⟨obj ⟩)
procedure
(pp ⟨obj ⟩ ⟨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.
(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.
(pprint ⟨obj ⟩)
procedure
(pprint ⟨obj ⟩ ⟨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-shared ⟨obj ⟩)
procedure
(pprint-shared ⟨obj ⟩ ⟨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-simple ⟨obj ⟩)
procedure
(pprint-simple ⟨obj ⟩ ⟨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.
(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.
(pp ⟨obj ⟩ ⟨keyword argument…⟩)
procedure
(pp ⟨obj ⟩ ⟨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.
(pprint ⟨obj ⟩ ⟨keyword argument…⟩)
procedure
(pprint ⟨obj ⟩ ⟨port ⟩ ⟨keyword argument…⟩)
procedure
(pprint-shared ⟨obj ⟩ ⟨keyword argument…⟩)
procedure
(pprint-shared ⟨obj ⟩ ⟨port ⟩ ⟨keyword argument…⟩)
procedure
(pprint-simple ⟨obj ⟩ ⟨keyword argument…⟩)
procedure
(pprint-simple ⟨obj ⟩ ⟨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-file ⟨infile ⟩ ⟨keyword argument…⟩)
procedure
(pprint-file ⟨infile ⟩ ⟨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-style ⟨symbol ⟩)
procedure
(pretty-style ⟨symbol ⟩ ⟨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.
(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-style ⟨style 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-style ⟨style 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.
i | print as identifier (a bound variable) |
d | print as datum (uncolored, no special formatting) |
e | print as an expression |
f | print as a formal parameter or a (possibly improper) list of formals |
l | print as a literal or a list of literals |
h | print as a definition head form (i or MIT-style list) |
dc | print as a datum-expressions clause |
ec | print as an expression-expressions clause (as in cond) |
fc | print as a formals-expressions clause (as in case-lambda) |
lc | print 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 |
body | print as list of expressions, typically indented (as in lambda) |
fill | print 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-hook ⟨test ⟩)
procedure
(pretty-hook ⟨test ⟩ ⟨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-hook ⟨hook 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-hook ⟨hook 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-hook ⟨prefix ⟩ ⟨cf ⟩ ⟨xf ⟩ ⟨suffix ⟩)
procedure
(bvec-pp-hook ⟨prefix ⟩ ⟨lf ⟩ ⟨rf ⟩ ⟨suffix ⟩)
procedure
(atom-pp-hook ⟨shareable? ⟩ ⟨wf ⟩ ⟨df ⟩)
procedure
(rmac-pp-hook ⟨prefix ⟩ ⟨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-generator ⟨obj ⟩ ⟨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.
(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/html ⟨infile ⟩ ⟨keyword argument…⟩)
procedure
(pprint-file/html ⟨infile ⟩ ⟨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.
(srfi 272 colorize) libraryThis 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:
#f | No ANSI SGR support is detected or it is explicitly disabled |
0 | Basic 16-color support (ANSI escape codes 30-37, 90-97) |
1 | 256-color palette support |
2 | 24-bit true color support (16 million colors) |
3 | Extended 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:
(sgr0 ⟨fg16 ⟩ [⟨bold? ⟩ ⟨uline? ⟩ ⟨bg16 ⟩]) procedure
(sgr1 ⟨fg256 ⟩ [⟨bold? ⟩ ⟨uline? ⟩ ⟨bg256 ⟩]) procedure
(sgr2 ⟨fgtrue ⟩ [⟨bold? ⟩ ⟨uline? ⟩ ⟨italic? ⟩ ⟨strike? ⟩ ⟨bgtrue ⟩]) procedure
(sgr3 ⟨fgtrue ⟩ [⟨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 | off | disable 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-asb ⟨s0 ⟩ [⟨s1 ⟩ ⟨s2 ⟩ ⟨s3 ⟩]) procedure
(asb->sgr-string ⟨asb ⟩ [⟨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-palette ⟨asb ⟩ …)
procedure
(asb-palette-ref ⟨palette ⟩ ⟨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 ⟩
(0–15).
A default 16-color palette, default-asb-palette, is provided.
(semantic-color-mapper? ⟨obj ⟩) procedure
(make-semantic-color-mapper ⟨palette ⟩ [⟨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 ⟩.
comment | Comments | 8 (gray) |
char, string | Character and string literals | 10 (green) |
escape, char-escape, string-escape | Escape sequences | 11 (yellow) |
char-meta, string-meta | Metadata in chars/strings | 2 (green) |
symbol-escape | Escaped symbols | 11 (yellow) |
formal | Formal parameters | 7 (light gray) |
variable | Variables | default (no styling) |
defined | Variables being defined | bold default |
keyword | Keywords of special forms | 12 (blue) |
number | Numeric literals | 13 (purple) |
literal | Literal values | 14 (teal) |
literal-meta, meta | Metadata | 6 (cyan) |
directive | Directives | 13 (purple) |
paren | Parentheses | 7 (light gray) |
bracket | Brackets | default |
warning | Warnings | 1 (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-string ⟨sc ⟩ [⟨mapper ⟩ ⟨tier ⟩]) procedure
(semantic-color->end-string ⟨sc ⟩ [⟨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.
(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.
[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
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).
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.