; Part of Scheme 48 1.9.  See file COPYING for notices and license.

; Authors: John Ramsdell, Richard Kelsey, Jonathan Rees

;;;; A pretty-printer

; This isn't exactly in the spirit of the rest of the Scheme 48
; system.  It's too hairy, and it has unexploited internal generality.
; It really ought to be rewritten.  In addition, it seems to be buggy
; -- it sometimes prints unnecessarily wide lines.  Usually it's
; better than no pretty printer at all, so we tolerate it.

; From: ramsdell@linus.mitre.org
; Date:  Wed, 12 Sep 1990 05:14:49 PDT
;
; As you noted in your comments, pp.scm is not a straight forward
; program.  You could add some comments that would greatly ease the task
; of figuring out what his going on.  In particular, you should describe
; the interface of various objects---most importantly the interface of a
; formatter.  You might also add some description as to what protocol
; they are to follow.

; Other things to implement some day:
;  - LET, LET*, LETREC binding lists should be printed vertically if longer
;    than about 30 characters
;  - COND clauses should all be printed vertically if the COND is vertical
;  - Add an option to lowercase or uppercase symbols and named characters.
;  - Parameters controlling behavior of printer should be passed around
;  - Do something about choosing between #f and ()
;  - Insert line breaks intelligently following head of symbol-headed list,
;    when necessary
;  - Some equivalents of *print-level*, *print-length*, *print-circle*.

; Possible strategies:
;   (foo x y z)     Horizontal = infinity sticky 
;   (foo x y        One sticky + one + body (e.g. named LET)
;     z
;     w)
;   (foo x          One + body
;     y
;     z)
;   (foo x          Two + body
;        y
;     z)
;   (foo x          Big ell = infinity + body (combination)
;	 y
;	 z)
;   (foo            Little ell, zero + body (combination)
;     x
;     y)
;   (foo            Vertical
;    x
;    y)
;
; Available height/width tradeoffs:
;   Combination:
;     Horizontal, big ell, or little ell.
;   Special form:
;     Horizontal, or M sticky + N + body.
;   Random (e.g. vector, improper list, non-symbol-headed list):
;     Horizontal, or vertical.  (Never zero plus body.)

(define (p x . port-option)
  (let ((port (if (pair? port-option) (car port-option)
		  (current-output-port))))
    (pretty-print x port 0)
    (newline port)))

(define *line-width* 80)

(define *single-line-special-form-limit* 30)

; Stream primitives

(define head car)
(define (tail s) (force (cdr s)))

(define (map-stream proc stream)
  (cons (proc (head stream))
	(delay (map-stream proc (tail stream)))))

(define (stream-ref stream n)
  (if (= n 0)
      (head stream)
      (stream-ref (tail stream) (- n 1))))

; Printer

(define (pretty-print obj port pos)
  (let ((node (pp-prescan obj 0)))
;    (if (> (column-of (node-dimensions node)) *line-width*)
;        ;; Eventually add a pass to change format of selected combinations
;        ;; from big-ell to little-ell.
;        (begin (display ";** too wide - ")
;               (write (node-dimensions node))
;               (newline)))
    (print-node node port pos)))

(define make-node list)

(define (node-dimensions node)
  ((car node)))

(define (node-pass-2 node pos)
  ((cadr node) pos))

(define (print-node node port pos)
  ((caddr node) port pos))

(define (pp-prescan obj hang)
  (cond ((symbol? obj)
         (make-leaf (string-length (symbol->string obj))
                    obj hang))
        ((number? obj)
         (make-leaf (string-length (number->string obj))
                    obj hang))
        ((boolean? obj)
         (make-leaf 2 obj hang))
        ((string? obj)
         ;;++ Should count number of backslashes and quotes
         (make-leaf (+ (string-length obj) 2) obj hang))
        ((char? obj)
         (make-leaf (case obj
                      ((#\space) 7)
                      ((#\newline) 9)
                      (else 3))
                    obj hang))
        ((pair? obj)
         (pp-prescan-pair obj hang))
        ((vector? obj)
         (pp-prescan-vector obj hang))
	(else
	 (pp-prescan-random obj hang))))

(define (make-leaf width obj hang)
  (let ((width (+ width hang)))
    (make-node (lambda () width)
	       (lambda (pos)
		 (+ pos width))
	       (lambda (port pos)
		 (write obj port)
		 (do ((i 0 (+ i 1)))
		     ((>= i hang) (+ pos width))
		   (write-char #\) port))))))

(define (make-prefix-node string node)
  (let ((len (string-length string)))
    (make-node (lambda ()
		 (+ (node-dimensions node) len))
	       (lambda (pos)
		 (node-pass-2 node (+ pos len)))
	       (lambda (port pos)
		 (display string port)
		 (print-node node port (+ pos len))))))

(define (pp-prescan-vector obj hang)
  (if (= (vector-length obj) 0)
      (make-leaf 3 obj hang)
      (make-prefix-node "#" (pp-prescan-list (vector->list obj) #t hang))))

; Improve later.

(define (pp-prescan-random obj hang)
  (let ((l (disclose obj)))
    (if (list? l)
	(make-prefix-node "#." (pp-prescan-list l #t hang))
	(make-leaf 25 obj hang))))  ;Very random number

(define (pp-prescan-pair obj hang)
  (cond ((read-macro-inverse obj)
         =>
         (lambda (inverse)
	   (make-prefix-node inverse (pp-prescan (cadr obj) hang))))
        (else
         (pp-prescan-list obj #f hang))))

(define (pp-prescan-list obj random? hang)
  (let loop ((l obj) (z '()))
    (if (pair? (cdr l))
	(loop (cdr l)
	      (cons (pp-prescan (car l) 0) z))
	(make-list-node
	  (reverse
	    (if (null? (cdr l))
		(cons (pp-prescan (car l) (+ hang 1)) z)
		(cons (make-prefix-node ". " (pp-prescan (cdr l) (+ hang 1)))
		      (cons (pp-prescan (car l) 0) z))))
	  obj
	  (or random? (not (null? (cdr l))))))))

; Is it sufficient to tell parent node:
;  At a cost of X line breaks, I can make myself narrower by Y columns. ?
; Then how do we decide whether we narrow ourselves or some of our children?

(define (make-list-node node-list obj random?)
  (let* ((random? (or random?
		      ;; Heuristic for things like do, cond, let, ...
		      (not (symbol? (car obj)))
		      (eq? (car obj) 'else)))
	 (probe (if (not random?)
		    (indentation-for (car obj))
		    #f))
	 (format horizontal-format)
	 (dimensions (compute-dimensions node-list format))
	 (go-non-horizontal
	  (lambda (col)
	    (set! format
		  (cond (random? vertical-format)
			(probe (probe obj))
			(else big-ell-format)))
	    (let* ((start-col (+ col 1))
		   (col (node-pass-2 (car node-list) start-col))
		   (final-col
		       (format (cdr node-list) 
			       (lambda (node col target-col)
				 (node-pass-2 node target-col))
			       start-col
			       (+ col 1)
			       col)))
	      (set! dimensions (compute-dimensions node-list format))
	      final-col))))
    (if (> dimensions
	   (if probe
	       *single-line-special-form-limit*
	       *line-width*))
	(go-non-horizontal 0))
    (make-node (lambda () dimensions)
	       (lambda (col) ;Pass 2: if necessary, go non-horizontal
		 (let ((defacto (+ col (column-of dimensions))))
		   (if (> defacto *line-width*)
		       (go-non-horizontal col)
		       defacto)))
	       (lambda (port pos)
		 (write-char #\( port)
		 (let* ((pos (+ pos 1))
			(start-col (column-of pos))
			(pos (print-node (car node-list) port pos)))
		   (format (cdr node-list) 
			   (lambda (node pos target-col)
			     (let ((pos (go-to-column target-col
						      port pos)))
			       (print-node node port pos)))
			   start-col
			   (+ (column-of pos) 1)
			   pos))))))

(define (compute-dimensions node-list format)
  (let* ((start-col 1)			;open paren
	 (pos (+ (make-position start-col 0)
		 (node-dimensions (car node-list)))))
    (format (cdr node-list)
	    (lambda (node pos target-col)
	      (let* ((dims (node-dimensions node))
		     (lines (+ (line-of pos) (line-of dims)))
		     (width (+ target-col (column-of dims))))
		(if (>= (column-of pos) target-col)
		    ;; Line break required
		    (make-position
		     (max (column-of pos) width)
		     (+ lines 1))
		    (make-position width lines))))
	    start-col
	    (+ (column-of pos) 1)	;first-col
	    pos)))

; Three positions are significant
;   (foo baz ...)
;    ^   ^  ^
;    |   |  +--- (column-of pos)
;    |   +------ first-col
;    +---------- start-col

; Separators

(define on-same-line
  (lambda (start-col first-col pos)
    start-col first-col ;ignored
    (+ (column-of pos) 1)))

(define indent-under-first
  (lambda (start-col first-col pos)
    start-col ;ignored
    first-col))

(define indent-for-body
  (lambda (start-col first-col pos)
    first-col ;ignored
    (+ start-col 1)))

(define indent-under-head
  (lambda (start-col first-col pos)
    first-col ;ignored
    start-col))

; Format constructors

(define (once separator format)
  (lambda (tail proc start-col first-col pos)
    (if (null? tail)
	pos
	(let ((target-col (separator start-col first-col pos)))
	  (format (cdr tail)
		  proc
		  start-col
		  first-col
		  (proc (car tail) pos target-col))))))

(define (indefinitely separator)
  (letrec ((self (once separator	;eta
		       (lambda (tail proc start-col first-col pos)
			 (self tail proc start-col first-col pos)))))
    self))

(define (repeatedly separator count format)
  (do ((i 0 (+ i 1))
       (format format
	       (once separator format)))
      ((>= i count) format)))

; Particular formats

(define vertical-format
  (indefinitely indent-under-head))

(define horizontal-format
  (indefinitely on-same-line))

(define big-ell-format
  (indefinitely indent-under-first))

(define little-ell-format
  (indefinitely indent-for-body))

(define format-for-named-let
  (repeatedly on-same-line 2 (indefinitely indent-for-body)))

(define hook-formats
  (letrec ((stream (cons little-ell-format
			 (delay (map-stream (lambda (format)
					      (once indent-under-first format))
					    stream)))))
    stream))

; Hooks for special forms.
; A hook maps an expression to a format.

(define (compute-let-indentation exp)
  (if (and (not (null? (cdr exp)))
	   (symbol? (cadr exp)))
      format-for-named-let
      (stream-ref hook-formats 1)))

(define hook
  (let ((hooks (map-stream (lambda (format)
			     (lambda (exp) exp ;ignored
			       format))
			   hook-formats)))
    (lambda (n)
      (stream-ref hooks n))))


; Table of indent hooks.

(define indentations (make-table))

(define (indentation-for name)
  (table-ref indentations name))

(define (define-indentation name n)
  (table-set! indentations
	      name
	      (if (integer? n) (hook n) n)))

; Indent hooks for Revised^n Scheme.

(for-each (lambda (name)
	    (define-indentation name 1))
	  '(lambda define define-syntax let* letrec let-syntax letrec-syntax
	    case call-with-values call-with-input-file
	    call-with-output-file with-input-from-file
	    with-output-to-file syntax-rules))

(define-indentation 'do            2)
(define-indentation 'call-with-current-continuation 0)

(define-indentation 'let           compute-let-indentation)

; Kludge to force vertical printing (do AND and OR as well?)
(define-indentation 'if            (lambda (exp) big-ell-format))
(define-indentation 'cond          (lambda (exp) big-ell-format))


; Other auxiliaries

(define (go-to-column target-col port pos) ;=> pos
  ;; Writes at least one space or newline
  (let* ((column (column-of pos))
	 (line (if (>= column target-col)
		   (+ (line-of pos) 1)
		   (line-of pos))))
    (do ((column (if (>= column target-col)
		     (begin (newline port) 0)
		     column)
		 (+ column 1)))
	((>= column target-col)
	 (make-position column line))
      (write-char #\space port))))

(define (make-position column line)
  (+ column (* line 1000)))

(define (column-of pos)
  (remainder pos 1000))

(define (line-of pos)
  (quotient pos 1000))

(define (read-macro-inverse x)
  (cond ((and (pair? x)
              (pair? (cdr x))
              (null? (cddr x)))
         (case (car x)
           ((quote)            "'")
           ((quasiquote)       "`")
           ((unquote)          ",")
           ((unquote-splicing) ",@")
           (else #f)))
        (else #f)))

; For the command processor:

;(define-command 'p "<exp>" "pretty-print" '(expression)
;  (p (eval expression (user-package)) (command-output)))
