PreviousUpNext

15.4.833  src/lib/prettyprint/big/src/prettyprint-buffer-g.pkg

## prettyprint-buffer-g.pkg
#
# The implementation of prettyprint
# streams -- this is where all the
# action is.
#
# I believe this is the file I'm currently trying to
# establish as a replacement for:
#
#     
#
# See ../README.
#
# Concise overview:
#
# -> The only purpose of the prettyprinter is to decide where
#    to put newlines, whitespace and indentation.
#
# -> The prettyprinter views its input stream as consisting
#    of known-width 'tokens' containing the useful text to be
#    printed, and of 'breaks', which mark places where it is
#    allowed to insert a newline.
#
# -> The desired structure is represented as a set of nested
#    'boxes', where a box essentially indicates the newline
#    insertion policy to be followed for some stretch of text.
#      Each box also has some default indentation for each line,
#    which will typically increase with box nesting level.
#
# -> The simplest box type is 'h' (horizontal)', in which
#    breaks are -never- converted to newlines.
#
# -> The next simplest box type is 'v' (vertical)', in which
#    breaks are -always- converted to newlines.
#
# -> The 'line' box type is slightly more sophisticated:  It
#    behaves as an 'h' box if the result will fit on a line,
#    else as a v box:  In simple cases, this results in the
#    tokens in the box all being placed in a line, either
#    horizontal or vertical.  In an line box, either all the
#    breaks produce newlines, or else none do.
#
# -> The 'wrap' box type is the most commonly used, and implements
#    the familiar word-wrap algorithm:  A break produces a newline
#    iff this is required to keep the line length within width limits.
#
## 2007-09-10 CrT: Completely rewritten.  Twice. ;-)
#
# See also:
#     src/lib/prettyprint/big/src/prettyprint-stream-g.pkg

# Compiled by:
#     src/lib/prettyprint/big/src/prettyprinting.sublib



# This generic does not currently appear to be invoked anywhere. <---



#               "The difference between the true hacker
#                and the mere power user is that the
#                limits of what a power user can achieve
#                are set by his tools, but the hacker is
#                master of his tools, and lives in a
#                world without limits.
#
#               "Every true hacker is necessarily a compiler
#                hacker at need, as well as an editor hacker,
#                kernel hacker, and so forth.
#
#               "True hackers are a rare breed.  For every
#                one of them, you will find a thousand
#                power users posing as hackers, more often
#                than not without even realizing the difference."



stipulate
    package fil =  file__premicrothread;                                # file__premicrothread  is from   src/lib/std/src/posix/file--premicrothread.pkg
herein

    generic package   prettyprint_buffer_g   (
        #             ====================
        #
        package token:   Prettyprint_Token;             # Prettyprint_Token     is from   src/lib/prettyprint/big/src/prettyprint-token.api
        package device:  Prettyprint_Device;    # Prettyprint_Device    is from   src/lib/prettyprint/big/src/prettyprint-device.api

        sharing token::Style == device::Style;
    )
    : (weak)
    api {
        include Prettyprint_Buffer;                     # Prettyprint_Buffer    is from   src/lib/prettyprint/big/src/prettyprint-buffer.api

        dump:  (fil::Output_Stream, Stream)
                  ->
                  Void;
    }
    {
        stipulate
            package dvc =  device;
            package tkn =  token;
        herein
            Device =  dvc::Device;              # Handles device-specific aspects of writing to ansi terminal, plain ascii stream or whatever.
            Style  =  tkn::Style;                       # Text attributes like color, underline, blink etc.
            Token  =  tkn::Token;                       # A token will usually contain some text plus whatever Style information is required to render it on the Device.

            Indent = BOX_RELATIVE  Int          # Indent relative to enclosing box.
                   | CURSOR_RELATIVE  Int;      # Indent relative to where box starts on current line.      

            # *** DATA STRUCTURES ***
            Prettyprint_Token
                = TEXT { string: String, length: Int }
                                                #  Raw text.  This includes tokens.  The 
                                                #  width and style information is taken 
                                                #  care of when they are inserted in 
                                                #  queue. 
                | NONBREAKABLE_SPACES  Int      #  Some number of non-breakable spaces 
                | BREAK  { wrap: Ref Bool,   spaces: Int,   indent_on_wrap: Int }
                | PUSH_STYLE  Style
                | POP_STYLE
                | NEWLINE
                | CONTROL  (Device -> Void)             #  Device control operation 
                | BOX Box
                | LINE (List Prettyprint_Token)


            also
            Wrap_Policy

                = NONE                                  # All on one line -- break never rendered as newline.
                | ALL                                   # One line each -- every break rendered as newline.
                | ALL_OR_NONE                           # NONE if it fits, else ALL.
                | AS_NEEDED                             # Normal wordwrap:  break rendered as newline only when necessary.

            withtype
            Box
                =
                { indent:              Indent,
                  width:               Int,             # We try to fit box contents into this width.
                  wrap_policy:         Wrap_Policy,
                  id:                  Int,             # Unique id number per box.  Only used for debugging/display.

                  first_line_length:   Ref Int,         # Length of contents if newline free, else length of first line.
                  final_line_length:   Ref Int,         # Length of contents if newline free, else length of last line (zero if box contents end with newline)
                  has_newlines:        Ref Bool,        # TRUE iff there's a NEWLINE somewhere inside.
                  contents:            Ref List Prettyprint_Token
                };


            Stream
                =
                STREAM  {
                    device:                   Device,           # The underlying device 
                    device_is_closed:         Ref( Bool ),      # TRUE iff the stream is closed. 


                    # We build up a prettyprint expression as a tree
                    # of nested_boxes until we are flushed, at which
                    # point we actually format and print it.
                    #
                    # At any given time, the currently open box is 'box',
                    # the one enclosing it is first in the nested_boxes
                    # list, and the root of the box tree is last in the
                    # nested_boxes list.  (Keeping the top of the stack
                    # in a separate variable lets us communicate to the
                    # type system that we always have at least one box
                    # on the stack, and thus avoid a lot of spurious
                    # checks for stack-empty.)
                    box:           Ref Box,
                    nested_boxes:  Ref (List Box),
                    box_nesting:   Ref Int,                     # Current depth of 'nested_boxes'. Used only to catch infinite loops.

                    next_box_id:   Ref Int,

                    # We don't actually use the style_stack
                    # for anything in this module -- it is
                    # purely an opaque-to-us customer
                    # convenience:

                    style_stack
                        :
                        Ref(  List(  Style ) )
                };



            max_box_nesting =   1000;                   # Purely to catch prettyprint infinite recursions. 

            exception  PRETTYPRINT_MAX_DEPTH_EXCEEDED;  # Raised when above is exceeded;

            default_box_width = 60;                     # There is currently no way to change box widths.


            # *** DEBUGGING FUNCTIONS ***

            package f = sfprintf;                       # sfprintf      is from   src/lib/src/sfprintf.pkg

            fun wrap_policy_to_string NONE        =>  "NONE";
                wrap_policy_to_string ALL         =>  "ALL";
                wrap_policy_to_string ALL_OR_NONE =>  "ALL_OR_NONE";
                wrap_policy_to_string AS_NEEDED   =>  "AS_NEEDED";
            end;


            fun indent_to_string (BOX_RELATIVE    n) =>  cat ["BOX_RELATIVE ",    int::to_string n];
                indent_to_string (CURSOR_RELATIVE n) =>  cat ["CURSOR_RELATIVE ", int::to_string n];
            end;




            fun dump (out_stream, STREAM stream)
                =
                {   fun print string
                        =
                        fil::write (out_stream, string);


                    fun printf' (format, items)
                        =
                        print (f::sprintf' format items);


        #           fun format_box_stack_element_to_string (wrap_policy, box_indent, box_width)
        #               =
        #               f::sprintf'   "(%s, %d, %d)"   [f::STR (wrap_policy_to_string  wrap_policy), f::INT box_indent, f::INT box_width];


                    fun print_list format_element []
                            =>
                            print "[]";

                        print_list format_element my_list
                            =>
                            print (
                                list_to_string::list_to_string'

                                    {  first     => "[\n    ",
                                       last      => "]",
                                       between   => "\n    ",
                                       to_string => format_element
                                    }

                                    my_list
                            );
                    end;

                    fun print_box   (box as { id, indent, width, first_line_length, final_line_length, has_newlines, wrap_policy, contents })   prefix
                        =
                        {   print (prefix + "Box");
                            print ("   id = "           + (int::to_string id));
                            print ("   indent = "       + (indent_to_string indent));
                            print ("   width = "        + (int::to_string width));
                            print ("   first_line_length = " + (int::to_string *first_line_length));
                            print ("   final_line_length = " + (int::to_string *final_line_length));
                            print ("   has_newlines = " + (*has_newlines ?? "TRUE" :: "FALSE"));
                            print ("   wrap_policy = "  + (wrap_policy_to_string  wrap_policy));
                            print ("   contents len = " + (int::to_string (list::length *contents)));
                            print  ":\n";

                            print_tokens  *contents  (prefix + "    ")
                            where
                                fun print_tokens [] _
                                        =>
                                        ();

                                    print_tokens  (token ! rest)  prefix
                                        =>
                                        {   case token

                                                 TEXT { string, length }
                                                     =>
                                                     print (prefix + "TEXT (" + (int::to_string length) + ") '" + string + "'\n");

                                                 NONBREAKABLE_SPACES  int
                                                     =>
                                                     print (prefix + "NONBREAKABLE_SPACES " + (int::to_string int) + "\n");

                                                 BREAK  { wrap,   spaces,   indent_on_wrap }
                                                     =>
                                                     {   print (prefix + "BREAK");
                                                         print ("   wrap = "    + (*wrap ?? "TRUE" :: "FALSE"));
                                                         print ("   spaces = "  + (int::to_string spaces));
                                                         print ("   indent_on_wrap = "  + (int::to_string indent_on_wrap));
                                                         print  "\n";
                                                     };

                                                 PUSH_STYLE _
                                                     =>
                                                     print (prefix + "PUSH_STYLE ...\n");

                                                 POP_STYLE
                                                     =>
                                                     print (prefix + "POP_STYLE\n");

                                                 NEWLINE
                                                     =>
                                                     print (prefix + "NEWLINE\n");

                                                 CONTROL _
                                                     =>
                                                     print (prefix + "CONTROL ...\n");

                                                 BOX box
                                                     =>
                                                     {   print (prefix + "BOX:\n");
                                                         print_box  box  (prefix + "    "); 
                                                     };

                                                 LINE tokens
                                                     =>
                                                     {   print (prefix + "LINE");
                                                         print ("   length = "  + (int::to_string (list::length tokens)));
                                                         print  ":\n";
                                                         print_tokens  tokens  (prefix + "    "); 
                                                     };
                                            esac;

                                            print_tokens  rest  prefix;
                                        };
                                end;                            # fun print_tokens
                            end;                                # where
                        };

                    print  ("BEGIN\n");
                    printf' (
                        "box_nesting = %3d\n",
                        [   f::INT *stream.box_nesting
                        ]
                    );



                    print   "Expression:\n";

                    case *stream.nested_boxes

                         [] => print_box *stream.box "";
                         x  => case (reverse x)

                                    bot ! rest => print_box bot "";
                                    _          => raise exception FAIL "impossible";
                               esac;
                    esac;
                    print "\n";


                    print  ("END\n");
                };



            # *** UTILITY FUNCTIONS ***

            too_long =  8888;   # A box-length value picked to be large
                                # enough to not fit in any plausible box,
                                # but small enough that adding a few
                                # together won't produce integer overflow.


            # Output functions 
            fun write_newline (STREAM { device, ... }   ) =  dvc::newline device;
            fun write         (STREAM { device, ... }, s) =  dvc::string (device, s);



            fun blanks (_, 0)
                    =>
                    ();

                blanks (STREAM { device, ... }, n)
                    =>
                    dvc::space (device, n);
            end;















            # Return the current style of the prettyprint stream,
            # which is the top entry on the style stack, or else
            # the default style if the stack is empty:

            fun current_style (STREAM { style_stack => REF [], device,  ... } ) =>  dvc::default_style device;
                current_style (STREAM { style_stack => REF (style ! _), ... } ) =>  style;
            end;



            # Break up the 'contents' list of tokens in a box
            # into LINEs terminated by NEWLINE tokens
            # (except perhaps for the last):

            fun make_lines  stream   (box as { contents, id,  wrap_policy, has_newlines, width, indent, first_line_length, final_line_length })
                =
                {
                    box_lines
                        =
                        make_lines'  (
                            *contents,
                            [],                 # tokens
                            []                  # lines
                        );

                    box.contents     :=   box_lines;  
                }
                where
                    fun make_lines'  ([],  tokens,  lines)
                            =>
                            {
                                tokens =   reverse tokens;
                                line   =   LINE tokens;
                                lines  =   line ! lines;

                                reverse lines;
                            };

                        make_lines'  (token ! rest,  tokens,  lines)
                            =>
                            case token

                                 NEWLINE
                                     =>
                                     {
                                         tokens =   reverse (token ! tokens);
                                         make_lines'  (rest,  /*tokens=*/ [],   LINE tokens ! lines);
                                     };

                                 BOX box
                                     =>
                                     {
                                         box_contents
                                             =
                                             make_lines'  (
                                                 *box.contents,
                                                 [],                    # token accumulator
                                                 []                     # line  accumulator
                                             );

                                         box.contents     :=   box_contents;

                                         make_lines'  (
                                             rest,
                                             token          !  tokens,
                                             lines
                                         );
                                     };

                                 LINE _
                                     =>
                                     {   print "Internal error: LINE in make_lines' input?!\n";
                                         dump (fil::stdout, stream);
        #                                 raise exception FAIL "Internal error: LINE in make_lines' input";
                                         make_lines'  (rest,  tokens,  lines);
                                     };

                                 _   =>
                                     {
                                         make_lines'  (rest,  token ! tokens,  lines);
                                     };

                            esac;
                    end; 
                end



            # Given the list of tokens in a LINE,
            # wrap either all BREAKs or none of them.
            #
            # Any embedded boxes have already been
            # wrap_box()'d, so they have valid values
            # of first_line_length, final_line_length and has_newlines.
            #

            also
            fun wrap_all_or_none  (
                    width,              # Current box width
                    tokens,             # List of tokens in current line.
                    column,             # Current column, relative to box left margin
                    wrap_policy         # One of ALL, NONE, ALL_OR_NONE
                )
                =
                {
                    line_has_newlines = REF FALSE;
                    first_line_length = REF -1;

                    # Should we change all BREAKs to newlines, or none or them?
                    wrap_them
                        =
                        case wrap_policy

                             ALL  =>  TRUE;
                             NONE =>  FALSE;

                             ALL_OR_NONE
                                 =>
                                 {   unwrapped_length
                                         =
                                         tot_length (tokens, 0)
                                         where
                                             fun tot_length ([], result) => result;
                                                 tot_length (((TEXT                { length, ... }) ! rest), result) =>   tot_length (rest, result +  length);
                                                 tot_length (((NONBREAKABLE_SPACES   n          ) ! rest), result) =>   tot_length (rest, result +  n     );
                                                 tot_length (((BREAK               { spaces, ... }) ! rest), result) =>   tot_length (rest, result +  spaces);

                                                 tot_length (((BOX { first_line_length, has_newlines, ... }) ! rest), result)
                                                     =>
                                                     if   *has_newlines     too_long;
                                                                         else   tot_length (rest, result + *first_line_length);   fi;
                                                 tot_length ((_ ! rest), result)
                                                     =>
                                                     tot_length (rest, result);
                                             end;
                                         end;

                                     if  (unwrapped_length > width   )   TRUE;
                                                                    else   FALSE;   fi;
                                 };

                             AS_NEEDED => raise exception FAIL "wrap_all_or_none: wrap_policy == AS_NEEDED!?";
                        esac;


                    fun per_token ([], column) =>   column;

                        per_token (token ! rest, column)
                            =>
                            case token

                                 BREAK  { wrap, spaces, indent_on_wrap, ... }
                                     =>
                                     if   wrap_them

                                          if   (*first_line_length == -1   )   first_line_length := column;   fi;
                                          wrap := TRUE;
                                          line_has_newlines := TRUE;
                                          per_token  (rest, indent_on_wrap);
                                     else
                                          per_token  (rest, column + spaces);
                                     fi;

                                 NEWLINE
                                     =>
                                     {   if   (*first_line_length == -1   )   first_line_length := column;   fi;
                                         line_has_newlines := TRUE;
                                         per_token  (rest, 0);
                                     };

                                 TEXT { length, ... }  =>   per_token  (rest, column + length        );
                                 NONBREAKABLE_SPACES n =>   per_token  (rest, column + n             );
                                 PUSH_STYLE _          =>   per_token  (rest, column                 );
                                 POP_STYLE             =>   per_token  (rest, column                 );
                                 CONTROL _             =>   per_token  (rest, column                 );

                                 BOX { final_line_length, indent, ... }
                                     =>
                                     case indent

                                          BOX_RELATIVE    i =>    per_token  (rest, *final_line_length + i         );
                                          CURSOR_RELATIVE i =>    per_token  (rest, *final_line_length + i + column);
                                     esac;

                                 LINE _  =>  raise exception FAIL "per_token: LINE within line?!";
                            esac;
                    end;                                                        # fun per_token

                    column =   per_token (tokens, column);

                    if   (*first_line_length == -1   )   first_line_length := column;   fi;

                    (*first_line_length, column, *line_has_newlines);
                }                                                               # fun wrap_all_or_none


            # Here we implement a conventional word-wrap
            # style algorithm where we wrap a line at
            # a BREAK iff it is our last chance to avoid
            # exceeding our assigned box width.

            also
            fun wrap_as_needed  (box_width, tokens, column)
                =
                {
                    line_has_newlines = REF FALSE;
                    first_line_length = REF -1;



                    # To decide whether to wrap a line at a break point,
                    # we must compute whether this is our last chance to
                    # avoid exceeding our allowed box width, which involves
                    # computing the text length from this BREAK to the
                    # next BREAK or NEWLINE (or end of token list).
                    # That's our job here:

                    fun forced_follow_on  tokens
                        =
                        forced_follow_on'  (tokens, 0)
                        where 
                            fun forced_follow_on'  ([], column)
                                    =>
                                    column;

                                forced_follow_on'  (token ! rest,  column)
                                    =>
                                    case token

                                         TEXT { length, ... }
                                             =>
                                             forced_follow_on'  (rest,  column + length);

                                         NONBREAKABLE_SPACES n
                                             =>
                                             forced_follow_on'  (rest,  column + n);

                                         (PUSH_STYLE _ | POP_STYLE | CONTROL _)
                                             =>
                                             forced_follow_on'  (rest,  column);

                                         (NEWLINE | BREAK _)
                                             =>
                                             column;

                                         BOX { first_line_length, has_newlines, ... }
                                             =>
                                             # If a box contains newlines, then 'first_line_length'
                                             # is the length of its first line, and we've
                                             # reached the end of our forced follow-on,
                                             # otherwise 'first_line_length' is the total box
                                             # length, and we need to keep on iterating:

                                             if *has_newlines

                                                  column + *first_line_length;
                                             else
                                                  forced_follow_on'  (rest,  column + *first_line_length);
                                             fi;

                                         LINE _
                                             =>
                                             {   /*raise exception FAIL*/ print "Internal error: forced_follow_on token is a LINE?!\n";
                                                 column;
                                             };
                                    esac;
                            end;                        # fun forced_follow_on'
                    end;                                # fun forced_follow_on


                    # Scan the tokens in a LINE setting
                    # BREAKs to wrap as appropriate.

                    fun per_token  ([], column)
                            =>
                            column;


                        per_token  (token ! rest,   column)
                            =>
                            case token

                                 (PUSH_STYLE _
                                 | POP_STYLE
                                 | CONTROL _)          =>   per_token( rest,   column          );
                                 TEXT { length, ... }  =>   per_token( rest,   column + length );
                                 NONBREAKABLE_SPACES n =>   per_token( rest,   column + n      );

                                 NEWLINE
                                     =>
                                     {   line_has_newlines :=  TRUE;
                                         per_token( rest, /*column=*/ 0 );
                                     };

                                 BREAK  { wrap, spaces, indent_on_wrap }
                                     =>
                                     {
                                         # If next BREAK or NEWLINE
                                         # would be beyond right margin
                                         # of box, then we need to wrap:

                                         if   (column  +  spaces  +  forced_follow_on rest  >  box_width)

                                              if   (*first_line_length == -1   )   first_line_length := column;   fi;
                                              wrap := TRUE;
                                              line_has_newlines :=  TRUE;
                                              per_token( rest, /*column=*/ indent_on_wrap);
                                         else
                                              per_token( rest,   column + spaces );
                                         fi;
                                     };

                                 BOX (box as { id, first_line_length, indent, final_line_length, ... })
                                     =>
                                     {   column
                                             =
                                             case indent

                                                  BOX_RELATIVE    i =>   *final_line_length + i;
                                                  CURSOR_RELATIVE i =>   *final_line_length + i + column;
                                             esac;

                                         per_token( rest, column );
                                     };

                                 LINE _
                                     =>
                                     {   /*raise exception FAIL*/ print "Internal error: wrap_all_or_none token is a LINE?!\n";
                                         per_token( rest,   column );
                                     };
                            esac;

                    end;                # fun per_token

                    column =   per_token (tokens, column);

                    if   (*first_line_length == -1   )   first_line_length := column;   fi;

                    (*first_line_length, column, *line_has_newlines);
                }                                               # fun wrap_as_needed

            also
            fun wrap_line  (width, tokens, column, wrap_policy)
                =
                case wrap_policy

                     AS_NEEDED  =>  wrap_as_needed   (width, tokens, column );
                     _          =>  wrap_all_or_none (width, tokens, column, wrap_policy );
                esac

            also
            fun wrap_box {
                    box as { id, indent, width, first_line_length, final_line_length, has_newlines, wrap_policy, contents },
                    column
                }
                =
                {   # Start by recursively wrapping all sub-boxes
                    # of this box.  When this is done, we know for
                    # each subbox whether it contains newlines (which
                    # may be either NEWLINEs or BREAKs which wrapped)
                    # and also the lengths of its first and last lines:

                    {   per_line *contents
                        where
                            fun per_line  ((LINE tokens) ! rest)
                                    =>
                                    {    per_token tokens;
                                         per_line  rest;
                                    };
                                per_line  (_ ! rest) =>   raise exception FAIL "wrap_subboxes_lines: Non-LINE arg?!";
                                per_line        []   =>   ();
                            end

                            also
                            fun per_token  ((BOX box) ! rest)
                                    =>
                                    {   wrap_box { box, column => 0};
                                        per_token  rest;
                                    };
                                per_token (_ ! rest) =>   per_token  rest;
                                per_token       []   =>   ();
                            end;
                        end;
                    };

                    # With the wrap decisions for our sub-boxes all
                    # made, we now have enough information in hand
                    # to make those decisions for our own box:

                    per_line  (*contents, /*column=*/0, /*first_line=*/ TRUE)
                    where
                        fun per_line ([], column, first_line)
                                =>
                                ();

                            per_line (line ! rest,  column, first_line)
                                =>
                                {   case line

                                         LINE tokens
                                             =>
                                             {   (wrap_line (width, tokens, column, wrap_policy ))
                                                     ->
                                                     (initial_line_length, last_line_length, line_has_newlines);

                                                 if   first_line      first_line_length :=  initial_line_length;   fi;

                                                 final_line_length :=  last_line_length;                # Gets overwritten unless this -is- the last LINE.
                                                 has_newlines :=  (*has_newlines or line_has_newlines);
                                             };

                                         _   =>
                                             {   /*raise exception FAIL*/ print "Internal error: per_line arg wasn't a LINE\n";
                                                 ();
                                             };
                                    esac;

                                    per_line (rest,  column, /*first_line=*/ FALSE);
                                };
                        end;
                    end;
                }                                               # fun wrap_box


            also
            fun print_lines (
                    box,
                    column,             # 0-based column relative to device (not box!) left margin
                    left_margin,        # Left margin of current box.
                    stream
                )
                =
                {   box ->   { indent, width, wrap_policy, contents, ... };

                    left_margin
                        =
                        case indent

                             BOX_RELATIVE    i =>   left_margin + i;
                             CURSOR_RELATIVE i =>   column      + i;
                        esac;

                    per_line (*contents, column)
                    where
                        fun per_token ([], column)
                                =>
                                column;

                            per_token  (token ! rest, column)
                                =>
                                case token

                                     TEXT { length, string }
                                         =>
                                         {   write (stream, string);
                                             per_token( rest, column + length );
                                         };

                                     NONBREAKABLE_SPACES n
                                         =>
                                         {   blanks (stream, n);
                                             per_token( rest, column + n );
                                         };

                                     BREAK { wrap, spaces, indent_on_wrap }
                                         =>
                                         if *wrap

                                              column = left_margin + indent_on_wrap;
                                              write_newline stream;
                                              blanks (stream, column);
                                              per_token( rest, column );
                                         else
                                              blanks  (stream, spaces);
                                              per_token( rest, column + spaces );
                                         fi;

                                     NEWLINE
                                         =>
                                         {   column = left_margin;
                                             write_newline stream;
                                             blanks (stream, column);
                                             per_token( rest, column );
                                         };


                                     BOX box
                                         =>
                                         {   column =  print_lines (box, column, left_margin, stream );
                                             per_token( rest, column );
                                         };


                                     PUSH_STYLE style
                                         =>
                                         {   stream ->   STREAM { device, ... };
                                             dvc::push_style (device, style);
                                             per_token( rest, column );
                                         };


                                     POP_STYLE
                                         =>
                                         {   stream ->   STREAM { device, ... };
                                             dvc::pop_style device;
                                             per_token( rest, column );
                                         };


                                     CONTROL ctl_g
                                         =>
                                         {   stream ->   STREAM { device, ... };
                                             ctl_g device;
                                             per_token( rest, column );
                                         };

                                     LINE _
                                         =>
                                         {   /*raise exception FAIL*/ print "Internal error: per_token encountered LINE within LINE token list\n";
                                             per_token( rest, column );
                                         };
                                esac;
                        end;                            # fun per_token

                        fun per_line  ([], column)
                                =>
                                column;

                            per_line  (line ! rest, column)
                                =>
                                case line

                                     LINE tokens
                                         =>
                                         {   column =  per_token  (tokens, column);
                                             per_line  (rest, column);
                                         };

                                     _   =>
                                         {   /*raise exception FAIL*/ print "Internal error: per_line arg wasn't a LINE\n";
                                             per_line  (rest, column);
                                         };
                                esac;
                        end;
                    end;
                };                              # fun print_lines




            # Here's the heart of the module.
            # We prettyprint in four passes:
            # 1) Build up the box tree -- complete by the time we get here.
            # 2) Break the contents of each box up into
            #    NEWLINE-delimited lines, and precompute box lengths:   make_lines
            # 3) Decide which breaks to wrap (change to newlines):      wrap_box
            # 4) Print the result out:                                  print_lines

            fun prettyprint_box (stream, box)
                =
                {   make_lines   stream   box;
                    wrap_box             { box,   column => 0};
                    print_lines          (box,   /*column=*/ 0,   /*left_margin=*/    0,   stream    );
                };




            #  Add a token to the contents of currently-open box:

            fun add_token (STREAM { box as REF box', ... }, token)
                =
                {   box' -> { contents, ... };

                    contents
                        :=
                        token ! *contents;
                };


            fun add_string (stream, string, length)
                =
                add_token (stream, TEXT { string, length } );



            fun prettyprint_open_box (stream as STREAM { box, nested_boxes, box_nesting, next_box_id, /*uneeded*/  device, device_is_closed, style_stack }, indent, wrap_policy)
                =
                {   id = *next_box_id;

                    next_box_id := id + 1;

                    # Set up empty record for new box:
                    new_box
                        =
                        { indent,
                          wrap_policy,
                          width             => default_box_width,
                          id,

                          has_newlines      => REF FALSE,
                          first_line_length => REF 0,
                          final_line_length => REF 0,
                          contents          => REF []
                        };


                    # Add new child box to contents
                    # of previously open box: 
                    {   (*box) ->   { contents, ... }; 

                        contents
                            := 
                                BOX new_box
                                !
                                *contents;
                    }; 

                    nested_boxes :=   *box ! *nested_boxes;             # Push currently open box on stack.
                    box_nesting  :=   *box_nesting + 1;                 # Remember new stack depth.
                    box          :=   new_box;                          # Establish new (empty) currently-open box. 


                    if (*box_nesting > max_box_nesting)                 # Catch prettyprint infinite loops.
                         raise exception     PRETTYPRINT_MAX_DEPTH_EXCEEDED;
                    fi;
                };



            fun prettyprint_end_box (stream as  STREAM {  nested_boxes as REF [], ... } )
                    =>
                    {   /*raise exception FAIL*/ print "User error: Attempted to close nonexistent box!";
                        ();
                    };

                prettyprint_end_box (stream as  STREAM { nested_boxes as REF (topbox ! rest),
                                                           box as REF { contents, ... },
                                                           box_nesting,
                                                           ...
                                                         }
                    )
                    =>
                    {   # We've accumulated the box contents
                        # in reverse order.  Now that we're
                        # done accumulating stuff for this
                        # box, put the contents in their
                        # proper order:

                        contents     :=   reverse *contents;


                        # Pop box stack:
                        box          :=   topbox;
                        nested_boxes :=   rest;
                        box_nesting  :=   *box_nesting - 1;
                    };
            end;


            fun prettyprint_break  (stream as STREAM { box as REF { contents, ... }, ... },  { spaces, indent_on_wrap } )
                =
                contents
                    :=
                        (BREAK { spaces,  indent_on_wrap,  wrap => REF FALSE } )
                        !
                        *contents;


            fun prettyprint_newline (stream as STREAM { box as REF { contents, ... }, ... })
                =
                contents
                    :=
                    NEWLINE ! *contents;


            fun prettyprint_flush (stream as STREAM { box, nested_boxes, device, next_box_id, ... }, with_newline)
                =
                {   end_boxes ()
                    where
                        fun end_boxes ()
                            =
                            case *nested_boxes

                                 []     # NB: To avoid special cases, we always leave one box on the stack.
                                     =>
                                     {   (*box) ->    { contents, first_line_length, final_line_length, has_newlines, ... };

                                         # Box contents accumulate in reverse order.
                                         # Normally we correct for this by reversing
                                         # the contents list when we close a box, but
                                         # the root box never gets closed, so we have
                                         # to reverse the contents here, right before
                                         # prettyprinting them:
                                         contents :=   reverse *contents;

                                         prettyprint_box (stream, *box);

                                         # Clear out the prettyprint stuff, so
                                         # we don't wind up printing it again: 
                                         contents          :=  [];
                                         first_line_length :=  0;
                                         final_line_length :=  0;
                                         has_newlines      :=  FALSE;
                                         next_box_id       :=  1;
                                     };

                                 topbox ! rest
                                     =>
                                     {   prettyprint_end_box  stream;
                                         end_boxes ();
                                     };
                            esac; 
                    end;

                    if   with_newline      write_newline  stream;   fi;

                    dvc::flush device;
                };


            # *** USER FUNCTIONS ***

            fun open_stream d
                =
                STREAM {
                  device           =>  d,
                  device_is_closed =>  REF FALSE,
                  style_stack      =>  REF [],
                  box_nesting      =>  REF 0,
                  next_box_id      =>  REF 1,
                  nested_boxes     =>  REF [],
                  box              =>  REF { indent            =>   BOX_RELATIVE 0,
                                             width             =>   default_box_width,
                                             wrap_policy       =>   AS_NEEDED,
                                             id                =>       0,
                                             first_line_length =>   REF 0,
                                             final_line_length =>   REF 0,
                                             has_newlines      =>   REF FALSE,
                                             contents          =>   REF []
                                           }
                };


            fun flush_stream stream
                =
                prettyprint_flush (stream, FALSE);


            fun close_stream (stream as STREAM { device_is_closed, ... } )
                =
                {   flush_stream stream;
                    device_is_closed := TRUE;
                };


            fun get_device (STREAM { device, ... } )
                =
                device;


            fun begin_horizontal_box     stream   = prettyprint_open_box (stream,  BOX_RELATIVE 4, NONE);
            fun begin_vertical_box     stream   = prettyprint_open_box (stream, (BOX_RELATIVE 4), ALL);
            fun begin_horizontal_else_vertical_box  stream   = prettyprint_open_box (stream, (BOX_RELATIVE 4), ALL_OR_NONE);
            fun begin_wrap_box  stream   = prettyprint_open_box (stream, (BOX_RELATIVE 4), AS_NEEDED);
            fun begin_wrap'_box stream   = prettyprint_open_box (stream, (BOX_RELATIVE 4), AS_NEEDED);

            fun begin_indented_vertical_box      stream indent  = prettyprint_open_box (stream, indent, ALL);
            fun begin_indented_horizontal_else_vertical_box   stream indent  = prettyprint_open_box (stream, indent, ALL_OR_NONE);
            fun begin_indented_wrap_box   stream indent  = prettyprint_open_box (stream, indent, AS_NEEDED);
            fun begin_indented_wrap'_box  stream indent  = prettyprint_open_box (stream, indent, AS_NEEDED);

            fun end_box   stream 
                =
                prettyprint_end_box   stream;

            fun horizontal_box     stream thunk =   { begin_horizontal_box     stream;   thunk();   end_box stream; };
            fun vertical_box     stream thunk =   { begin_vertical_box     stream;   thunk();   end_box stream; };
            fun horizontal_else_vertical_box  stream thunk =   { begin_horizontal_else_vertical_box  stream;   thunk();   end_box stream; };
            fun wrap_box  stream thunk =   { begin_wrap_box  stream;   thunk();   end_box stream; };
            fun wrap'_box stream thunk =   { begin_wrap'_box stream;   thunk();   end_box stream; };


            fun token (stream as STREAM { device, ... } ) token
                =
                {   token_style =  tkn::style token;

                    if (dvc::same_style (current_style stream, token_style))
                        #
                        add_string (stream, tkn::string token, tkn::size token);
                    else
                        add_token (stream, PUSH_STYLE token_style);
                        add_string (stream, tkn::string token, tkn::size token);
                        add_token (stream, POP_STYLE);
                    fi;
                };


            fun string stream s
                =
                add_string (stream, s, size s);


            fun push_style (stream as STREAM { style_stack, ... }, sty)
                =
                {   if (not (dvc::same_style (current_style stream, sty)))
                        #
                        add_token (stream, PUSH_STYLE sty);
                    fi;

                    style_stack :=   sty ! *style_stack;
                };


            fun pop_style (stream as STREAM { style_stack, ... } )
                =
                case *style_stack
                    #
                    [] => {  /*raise exception FAIL*/ print "User error: pp: unmatched pop_style\n";
                          };

                    (sty ! rest)
                        =>
                        {   style_stack := rest;

                            if (not (dvc::same_style (current_style stream, sty)))
                                #
                                 add_token (stream, POP_STYLE);
                            fi;
                        };
                esac;


            fun break               stream arg =  prettyprint_break (stream, arg);
            fun space               stream n   =  break stream { spaces => n, indent_on_wrap => 0 };
            fun cut                 stream     =  break stream { spaces => 0, indent_on_wrap => 0 };
            fun newline             stream     =  prettyprint_newline stream;
            fun nonbreakable_spaces stream n   =  add_token (stream, NONBREAKABLE_SPACES n );

            fun control stream control_g
                =
                add_token (stream, CONTROL control_g);
        end;                                                                                            # stipulate
    };
end;

## COPYRIGHT (c) 2005 John Reppy (http://www.cs.uchicago.edu/~jhr)
## All rights reserved.
## Subsequent changes by Jeff Prothero Copyright (c) 2010-2013,
## released per terms of SMLNJ-COPYRIGHT.


Comments and suggestions to: bugs@mythryl.org

PreviousUpNext