PreviousUpNext

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

## prettyprint-buffer-g.pkg
## All rights reserved.
## 2007-09-10 CrT: Completely rewritten.  Twice. ;-)

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


# The implementation of prettyprint
# streams -- this is where all the
# action is.
#
# 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.
#

# 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."



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:  ((file::Output_Stream, Stream))
              ->
              Void;
}
{
    package d =  device;
    package t =  token;
    package q =  queue;                 # queue is from   src/lib/src/queue.pkg

    Device =  d::Device;                # Handles device-specific aspects of writing to ansi terminal, plain ascii stream or whatever.
    Style  =  t::Style;                 # Text attributes like color, underline, blink etc.
    Token  =  t::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
                =
                file::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, ... }   ) =  d::newline device;
    fun write         (STREAM { device, ... }, s) =  d::string (device, s);



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

        blanks (STREAM { device, ... }, n)
            =>
            d::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,  ... } ) =>  d::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 (file::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, ... };
                                     d::push_style (device, style);
                                     per_token( rest, column );
                                 };


                             POP_STYLE
                                 =>
                                 {   stream ->   STREAM { device, ... };
                                     d::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;

            d::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 =  t::style token;
          
            if   (d::same_style (current_style stream, token_style))
                
                 add_string (stream, t::string token, t::size token);
            else
                 add_token (stream, PUSH_STYLE token_style);
                 add_string (stream, t::string token, t::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 (d::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 (d::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);
};


## COPYRIGHT (c) 2005 John Reppy (http://www.cs.uchicago.edu/~jhr)
## Subsequent changes by Jeff Prothero Copyright (c) 2010-2012,
## released under Gnu Public Licence version 3.


Comments and suggestions to: bugs@mythryl.org

PreviousUpNext