;;;; This software is part of the SBCL system. See the README file for
;;;; more information.
;;;;
;;;; This software is derived from the CMU CL system, which was
;;;; written at Carnegie Mellon University and released into the
;;;; public domain. The software is in the public domain and is
;;;; provided with absolutely no warranty. See the COPYING and CREDITS
;;;; files for more information.

(in-package "SB-FORMAT")

;;;; TOKENIZE-CONTROL-STRING

;;; The case for caching is to speed up out-of-line calls that use a fixed
;;; control string in a loop, not to avoid re-tokenizing all strings that
;;; happen to be STRING= to that string.
;;; (Might we want to bypass the cache when compile-time tokenizing?)
#+sb-xc-host
(defun tokenize-control-string (string)
  (combine-directives
   (%tokenize-control-string string 0 (length string) nil)
   t))
#-sb-xc-host
(defun-cached (tokenize-control-string
               :memoizer memoize
               :hash-bits 7
               :hash-function (lambda (string)
                                (ash (get-lisp-obj-address string)
                                     #.(- sb-vm:n-lowtag-bits))))
              ((string eq))
  (declare (simple-string string))
  (macrolet ((compute-it ()
               `(combine-directives
                 (%tokenize-control-string string 0 (length string) nil)
                 t)))
    (if (test-header-data-bit string
                              ;; shareable = readonly
                              (ash (logior sb-vm:+vector-shareable+
                                           sb-vm:+vector-shareable-nonstd+)
                                   sb-vm:array-flags-data-position))
        (memoize (compute-it))
        (compute-it))))

;;; If at some point I can figure out how to *CORRECTLY* utilize
;;; non-simple strings, then the INDEX and END will bound the parse.
;;; [Tokenization is the easy part, it's the substring extraction
;;; and processing, and error reporting, that become very complicated]
(defun %tokenize-control-string (string index end symbols)
  (declare (simple-string string))
  (let ((result nil)
        ;; FIXME: consider rewriting this 22.3.5.2-related processing
        ;; using specials to maintain state and doing the logic inside
        ;; the directive expanders themselves.
        (block)
        (pprint)
        (semicolon)
        (justification-semicolon))
    (loop
      (let ((next-directive (or (position #\~ string :start index :end end) end)))
        (when (> next-directive index)
          (push (possibly-base-stringize-to-heap (subseq string index next-directive))
                result))
        (when (= next-directive end)
          (return))
        (let* ((directive (parse-directive string next-directive symbols))
               (char (directive-character directive)))
          ;; this processing is required by CLHS 22.3.5.2
          (cond
            ((char= char #\<) (push directive block))
            ((and block (char= char #\;) (directive-colonp directive))
             (setf semicolon directive))
            ((char= char #\>)
             (unless block
               (format-error-at string next-directive
                                "~~> without a matching ~~<"))
             (cond
               ((directive-colonp directive)
                (unless pprint
                  (setf pprint (car block)))
                (setf semicolon nil))
               (semicolon
                (unless justification-semicolon
                  (setf justification-semicolon semicolon))))
             (pop block))
            ;; block cases are handled by the #\< expander/interpreter
            ((not block)
             (case char
               ((#\W #\I #\_) (unless pprint (setf pprint directive)))
               (#\T (when (and (directive-colonp directive)
                               (not pprint))
                      (setf pprint directive))))))
          (push directive result)
          (when (char= (directive-character directive) #\/)
            (pop symbols))
          (setf index (directive-end directive)))))
    (when (and pprint justification-semicolon)
      (let ((pprint-offset (1- (directive-end pprint)))
            (justification-offset
             (1- (directive-end justification-semicolon))))
        (format-error-at*
         string (min pprint-offset justification-offset)
         "Misuse of justification and pprint directives" '()
         :second-relative (- (max pprint-offset justification-offset)
                             (min pprint-offset justification-offset)
                             1)
         :references '((:ansi-cl :section (22 3 5 2))))))
    (nreverse result)))

(eval-when (#-sb-xc :compile-toplevel :load-toplevel :execute)
(defun parse-directive (string start symbols)
  (let ((posn (1+ start)) (params nil) (colonp nil) (atsignp nil)
        (end (length string)))
    (flet ((get-char ()
             (if (= posn end)
                 (format-error-at string start
                                  "String ended before directive was found")
                 (schar string posn)))
           (check-ordering ()
             (when (or colonp atsignp)
               (format-error-at*
                string posn
                "Parameters found after #\\: or #\\@ modifier" '()
                :references '((:ansi-cl :section (22 3)))))))
      (loop
        (let ((char (get-char)))
          (cond ((or (char<= #\0 char #\9) (char= char #\+) (char= char #\-))
                 (check-ordering)
                 (multiple-value-bind (param new-posn)
                     (parse-integer string :start posn :junk-allowed t)
                   (push (cons posn param) params)
                   (setf posn new-posn)
                   (case (get-char)
                     (#\,)
                     ((#\: #\@)
                      (decf posn))
                     (t
                      (return)))))
                ((or (char= char #\v)
                     (char= char #\V))
                 (check-ordering)
                 (push (cons posn :arg) params)
                 (incf posn)
                 (case (get-char)
                   (#\,)
                   ((#\: #\@)
                    (decf posn))
                   (t
                    (return))))
                ((char= char #\#)
                 (check-ordering)
                 (push (cons posn :remaining) params)
                 (incf posn)
                 (case (get-char)
                   (#\,)
                   ((#\: #\@)
                    (decf posn))
                   (t
                    (return))))
                ((char= char #\')
                 (check-ordering)
                 (incf posn)
                 (push (cons posn (get-char)) params)
                 (incf posn)
                 (unless (char= (get-char) #\,)
                   (decf posn)))
                ((char= char #\,)
                 (check-ordering)
                 (push (cons posn nil) params))
                ((char= char #\:)
                 (if colonp
                     (format-error-at*
                      string posn "Too many colons supplied" '()
                      :references '((:ansi-cl :section (22 3))))
                     (setf colonp t)))
                ((char= char #\@)
                 (if atsignp
                     (format-error-at*
                      string posn "Too many #\\@ characters supplied" '()
                      :references '((:ansi-cl :section (22 3))))
                     (setf atsignp t)))
                (t
                 (when (and (char= (schar string (1- posn)) #\,)
                            (or (< posn 2)
                                (char/= (schar string (- posn 2)) #\')))
                   (check-ordering)
                   (push (cons (1- posn) nil) params))
                 (return))))
        (incf posn))
      (let ((char (get-char)))
        (when (char= char #\/)
          (let ((closing-slash (position #\/ string :start (1+ posn))))
            (if closing-slash
                (setf posn closing-slash)
                (format-error-at string posn "No matching closing slash"))))
        (make-format-directive
         string start (1+ posn)
         (nreverse params) colonp atsignp (char-upcase char)
         (when (eql char #\/) (car symbols))))))))

;;;; specials used to communicate information

;;; Used both by the expansion stuff and the interpreter stuff. When it is
;;; non-NIL, up-up-and-out (~:^) is allowed. Otherwise, ~:^ isn't allowed.
(defvar *up-up-and-out-allowed* nil)

;;; Used by the interpreter stuff. When it's non-NIL, it's a function
;;; that will invoke PPRINT-POP in the right lexical environemnt.
(defvar *logical-block-popper* nil)
(declaim (type (or null function) *logical-block-popper*)
         (always-bound *logical-block-popper*))

;;; Used by the expander stuff. This is bindable so that ~<...~:>
;;; can change it.
(defvar *expander-next-arg-macro* 'expander-next-arg)

;;; Used by the expander stuff. Initially starts as T, and gets set to NIL
;;; if someone needs to do something strange with the arg list (like use
;;; the rest, or something).
(defvar *only-simple-args*)

;;; Used by the expander stuff. We do an initial pass with this as NIL.
;;; If someone doesn't like this, they (THROW 'NEED-ORIG-ARGS NIL) and we try
;;; again with it bound to T. If this is T, we don't try to do anything
;;; fancy with args.
(defvar *orig-args-available* nil)

;;; Used by the expander stuff. List of (symbol . offset) for simple args.
(defvar *simple-args*)

;;; Make a few simplifications to the directive list in INPUT,
;;; including translation of ~% to literal newline.
;;; I think that this does not have implications on conditional newlines
;;; vis a vis "When a line break is inserted by any type of conditional
;;; newline, any blanks that immediately precede the conditional newline
;;; are omitted". i.e. one could argue that nonliteral blanks are not
;;; quite the same as literal blanks, i.e. not subject to removal,
;;; but I don't think that's true, and in fact that is the source of
;;; an extremely subtle bug that writing the #\space character as a
;;; physical space character followed by a conditional newline is,
;;; if taken literally, supposed to remove the desired output.
(defun combine-directives (input literalize-tilde)
  (let (output)
    (labels ((emit-string (string)
               (if (stringp (car output))
                   (rplaca output (concat (car output) string))
                   (push string output)))
             (emit-placeholder-p ()
               ;; Return T if an otherwise empty string inside ~{ ... ~} must be
               ;; kept in order to avoid turning "~{~0|~}" (etc) into "~{~}".
               ;; The latter control string means something completely different.
               (and (format-directive-p (car output))
                    (eql #\{ (directive-character (car output)))))
             (emit-directive (directive)
               ;; If the head of output is the empty string and DIRECTIVE is
               ;; NOT a closing curly brace, we can reuse the first cons cell.
               (if (and (typep (car output) '(string 0))
                        (char/= (directive-character directive) #\}))
                   (rplaca output directive)
                   (push directive output)))
             (concat (first second)
               ;; The result is a base-string iff both inputs are base-string.
               ;; %TOKENIZE-CONTROL-STRING calls POSSIBLY-BASE-STRINGIZE
               ;; on all pieces of the input.
               ;; I suspect that this may be permissible for CONCATENATE generally
               ;; when given 'STRING as its output type with base-string as inputs.
               ;; But as with all things string, the spec is unclear as usual imho.
               (cond ((and (typep first 'base-string)
                           (typep second 'base-string))
                      (concatenate 'base-string first second))
                     (t
                      (concatenate 'string first second)))))
      (loop
        (unless input (return (nreverse output)))
        (let ((item (pop input)))
          (etypecase item
            (string
             (aver (plusp (length item)))
             (emit-string item))
            ;;  - Handle tilde-newline immediately
            ;;  - Turn "~%" into literal newline (for parameter N <=127)
            ;;  - Unless x-compiling, turn "~|" into literal form-feed (ditto)
            ;;  - Optionally turn "~~" into literal tilde (ditto)
            (format-directive
             (let ((params (directive-params item))
                   (colon (directive-colonp item))
                   (atsign (directive-atsignp item))
                   (char (directive-character item)))
               (block nil
                 (case char
                   (#\Newline
                    ;; tilde newline wants no params, and not both colon+atsign
                    (when (and (not params) (not (and colon atsign)))
                      ;; atsign = preserve newline / colon = preserve whitespace
                      (let ((s (concat (if atsign
                                           #.(make-string 1 :initial-element #\Newline)
                                           "")
                                       (if (and (not colon) (stringp (car input)))
                                           (string-left-trim
                                            ;; #\Tab is a nonstandard char
                                            `(#-sb-xc-host ,(code-char tab-char-code)
                                              #\space #\newline)
                                            (pop input))
                                           ""))))
                        (when (or (plusp (length s)) (emit-placeholder-p))
                          (emit-string s)))
                      (return)))
                   ;; TODO: #\& with a literal parameter of 0 is equivalent
                   ;; to the empty string.
                   ((#\% #\~ #-sb-xc-host #\|) ; #\Page is a nonstandard char
                    (let ((n (or (cdar params) 1)))
                      (when (and (not (or colon atsign))
                                 (or (null params) (singleton-p params))
                                 (typep n '(mod 128))
                                 ;; Don't insert literal tilde when parsing/
                                 ;; unparsing to create a FMT-CONTROL instance.
                                 (or (not (eql char #\~)) literalize-tilde))
                        (when (or (plusp n) (emit-placeholder-p))
                          (let ((char (case char
                                        (#\% #\Newline)
                                        #-sb-xc-host (#\| (code-char form-feed-char-code))
                                        (t char))))
                            (emit-string (make-string n :initial-element char))))
                        (return)))))
                 (emit-directive item))))))))))

;;;; FORMATTER stuff

(sb-xc:defmacro formatter (control-string)
  `#',(%formatter control-string))

(defun %formatter (control-string &optional (arg-count 0) (need-retval t)
                   &aux (lambda-name
                         (logically-readonlyize
                          (format nil "fmt$~36R"
                                  (#+sb-xc-host %sxhash-simple-string
                                   #-sb-xc-host sxhash control-string)))))
  ;; ARG-COUNT is supplied only when the use of this formatter is in a literal
  ;; call to FORMAT, in which case we can possibly elide &optional parsing.
  ;; But we can't in general, because FORMATTER may be called by users
  ;; to obtain functions that may be invoked in random wrong ways.
  ;; NEED-RETVAL signifies that the caller wants back the list of
  ;; unconsumed arguments. This is the default assumption.
  (block nil
    (catch 'need-orig-args
      (let* ((*simple-args* nil)
             (*only-simple-args* t)
             (control-string (coerce control-string 'simple-string))
             (guts (expand-control-string control-string)) ; can throw
             (required nil)
             (optional nil))
        (dolist (arg *simple-args*)
          (cond ((plusp arg-count)
                 (push (car arg) required)
                 (decf arg-count))
                (t
                 (push `(,(car arg)
                         (args-exhausted ,control-string ,(cdr arg)))
                       optional))))
        (return `(named-lambda ,lambda-name
                         (stream ,@required
                                 ,@(if optional '(&optional)) ,@optional
                                 &rest args)
                   (declare (ignorable stream args))
                   ,guts
                   ,(and need-retval 'args)))))
    (let ((*orig-args-available* t)
          (*only-simple-args* nil))
      `(named-lambda ,lambda-name (stream &rest orig-args)
         (declare (ignorable stream) (muffle-conditions compiler-note))
         (let ((args orig-args))
           ,(expand-control-string control-string)
           ,(and need-retval 'args))))))

(defun args-exhausted (control-string offset)
  (format-error-at control-string offset "No more arguments"))

(defvar *format-gensym-counter*)
(defun expand-control-string (string)
  (let* ((string (etypecase string
                   (simple-string
                    string)
                   (string
                    (coerce string 'simple-string))))
         (*default-format-error-control-string* string)
         (*format-gensym-counter* 0)
         (directives (tokenize-control-string string)))
    `(block nil
       ,@(expand-directive-list directives))))

(defun expand-directive-list (directives)
  (let ((results nil)
        previous
        (remaining-directives directives))
    (loop
     (unless remaining-directives
       (return))
     (multiple-value-bind (form new-directives)
         (expand-directive (car remaining-directives)
                           (cdr remaining-directives))
       (flet ((merge-string (string)
                (cond (previous
                       ;; It would be nice if (CONCATENTE 'STRING)
                       ;; could return the "smallest" string type
                       ;; able to hold the result. Or at least if we
                       ;; had a better interface than wrapping
                       ;; it with POSSIBLY-BASE-STRINGIZE since that
                       ;; conses two new strings usually.
                       (let ((concat
                              (possibly-base-stringize
                               (concatenate 'string
                                            (string previous)
                                            (string string)))))
                         (setf previous concat)
                         (setf (car results)
                               `(write-string ,concat stream))))
                      (t
                       (setf previous string)
                       (push form results)))))
         (cond ((not form))
               ((typep form '(cons (member write-string write-char)
                                   (cons (or string character))))
                (merge-string (second form)))
               ((typep form '(cons (eql terpri)))
                (merge-string #\Newline))
               (t
                (push form results)
                (setf previous nil))))
       (setf remaining-directives new-directives)))
    (nreverse results)))

(defun expand-directive (directive more-directives)
  (etypecase directive
    (format-directive
     (let ((expander
            (aref *format-directive-expanders* (directive-code directive)))
           (*default-format-error-offset*
            (1- (directive-end directive))))
       (if (functionp expander)
           (funcall expander directive more-directives)
           (format-error "Unknown directive ~@[(character: ~A)~]"
                         (directive-char-name directive)))))
    ((simple-string 1)
     (values `(write-char ,(schar directive 0) stream)
             more-directives))
    (simple-string
     (values `(write-string ,directive stream)
             more-directives))))

(sb-xc:defmacro expander-next-arg (string offset)
  `(if args
       (pop args)
       (format-error-at ,string ,offset "No more arguments")))

(defun expand-next-arg (&optional offset)
  (if (or *orig-args-available* (not *only-simple-args*))
      `(,*expander-next-arg-macro*
        ,*default-format-error-control-string*
        ,(or offset *default-format-error-offset*))
      (let ((symbol
             (symbolicate! #.(find-package "SB-FORMAT")
                           "FORMAT-ARG" (incf *format-gensym-counter*))))
        (push (cons symbol (or offset *default-format-error-offset*))
              *simple-args*)
        symbol)))

(defmacro expand-bind-defaults (specs params &body body)
  (once-only ((params params))
    (if specs
        (collect ((expander-bindings) (runtime-bindings))
          (dolist (spec specs)
            (destructuring-bind (var default) spec
              (expander-bindings `(,var ',var))
              (runtime-bindings
               `(list ',var
                 (let* ((param-and-offset (pop ,params))
                        (offset (car param-and-offset))
                        (param (cdr param-and-offset)))
                   (case param
                     (:arg `(or ,(expand-next-arg offset) ,,default))
                     (:remaining
                      (setf *only-simple-args* nil)
                      '(length args))
                     ((nil) ,default)
                     (t param)))))))
          `(let ,(expander-bindings)
            `(let ,(list ,@(runtime-bindings))
              ,@(if ,params
                    (format-error-at
                     nil (caar ,params)
                     "Too many parameters, expected no more than ~W"
                     ,(length specs)))
              ,,@body)))
        `(progn
           (when ,params
             (format-error-at nil (caar ,params)
                              "Too many parameters, expected none"))
           ,@body))))

;;;; format directive machinery

(eval-when (:compile-toplevel :load-toplevel :execute)
  (defun directive-handler-name (char suffix)
    (package-symbolicate "SB-FORMAT"
                         (if (char= char #\Newline) "NL" (string char))
                         suffix)))

(defmacro def-complex-format-directive (char lambda-list &body body)
  ;; Assert that it isn't lowercase
  (let ((code (sb-xc:char-code char)))
    (when (<= (sb-xc:char-code #\a) code (sb-xc:char-code #\z))
      (error "Come on, use uppercase why don't you?")))
  (let ((defun-name (directive-handler-name char "-COMPILER"))
        (directive (gensym "DIRECTIVE"))
        (directives (if lambda-list (car (last lambda-list)) (gensym "DIRECTIVES"))))
    `(progn
       (defun ,defun-name (,directive ,directives)
         ,@(if lambda-list
               `((let ,(mapcar (lambda (var)
                                 `(,var
                                   (,(symbolicate "DIRECTIVE-" var) ,directive)))
                               (butlast lambda-list))
                   ,@body))
               `((declare (ignore ,directive ,directives))
                 ,@body)))
       (setf (aref *format-directive-expanders* ,(sb-xc:char-code char))
             #',defun-name)
       ',defun-name)))

(defmacro def-format-directive (char lambda-list &body body)
  (let ((directives (gensym "DIRECTIVES"))
        (declarations nil)
        (body-without-decls body))
    (loop
      (let ((form (car body-without-decls)))
        (unless (and (consp form) (eq (car form) 'declare))
          (return))
        (push (pop body-without-decls) declarations)))
    (setf declarations (reverse declarations))
    `(def-complex-format-directive ,char (,@lambda-list ,directives)
       ,@declarations
       (values (progn ,@body-without-decls)
               ,directives))))

(defun find-directive (directives kind stop-at-semi)
  (if directives
      (let ((next (car directives)))
        (if (format-directive-p next)
            (let ((char (directive-character next)))
              (if (or (char= kind char)
                      (and stop-at-semi (char= char #\;)))
                  (car directives)
                  (find-directive
                   (cdr (flet ((after (char)
                                 (member (find-directive (cdr directives)
                                                         char
                                                         nil)
                                         directives)))
                          (case char
                            (#\( (after #\)))
                            (#\< (after #\>))
                            (#\[ (after #\]))
                            (#\{ (after #\}))
                            (t directives))))
                   kind stop-at-semi)))
            (find-directive (cdr directives) kind stop-at-semi)))))

;;;; format directives for simple output

(def-format-directive #\A (colonp atsignp params)
  (if params
      (expand-bind-defaults ((mincol 0) (colinc 1) (minpad 0)
                             (padchar #\space))
                     params
        `(format-princ stream ,(expand-next-arg) ',colonp ',atsignp
                       ,mincol ,colinc ,minpad ,padchar))
      `(princ ,(if colonp
                   `(or ,(expand-next-arg) "()")
                   (expand-next-arg))
              stream)))

(def-format-directive #\S (colonp atsignp params)
  (cond (params
         (expand-bind-defaults ((mincol 0) (colinc 1) (minpad 0)
                                (padchar #\space))
                        params
           `(format-prin1 stream ,(expand-next-arg) ,colonp ,atsignp
                          ,mincol ,colinc ,minpad ,padchar)))
        (colonp
         `(let ((arg ,(expand-next-arg)))
            (if arg
                (prin1 arg stream)
                (princ "()" stream))))
        (t
         `(prin1 ,(expand-next-arg) stream))))

(def-format-directive #\C (colonp atsignp params string end)
  (expand-bind-defaults () params
    (let ((n-arg (gensym "ARG")))
      `(let ((,n-arg (the* (character :context (format ,string ,(1- end))) ,(expand-next-arg))))
         ,(cond (colonp
                 `(format-print-named-character ,n-arg stream))
                (atsignp
                 `(prin1 ,n-arg stream))
                (t
                 `(write-char ,n-arg stream)))))))

(def-format-directive #\W (colonp atsignp params)
  (expand-bind-defaults () params
    (if (or colonp atsignp)
        `(let (,@(when colonp
                   '((*print-pretty* t)))
               ,@(when atsignp
                   '((*print-level* nil)
                     (*print-length* nil))))
           (output-object ,(expand-next-arg) stream))
        `(output-object ,(expand-next-arg) stream))))

;;;; format directives for integer output

(defun expand-format-integer (base colonp atsignp params)
  (if (or colonp atsignp params)
      (expand-bind-defaults
          ((mincol 0) (padchar #\space) (commachar #\,) (commainterval 3))
          params
        `(format-print-integer stream ,(expand-next-arg) ,colonp ,atsignp
                               ,base ,mincol ,padchar ,commachar
                               ,commainterval))
      `(format-integer ,(expand-next-arg) ,base stream)))

(def-format-directive #\D (colonp atsignp params)
  (expand-format-integer 10 colonp atsignp params))

(def-format-directive #\B (colonp atsignp params)
  (expand-format-integer 2 colonp atsignp params))

(def-format-directive #\O (colonp atsignp params)
  (expand-format-integer 8 colonp atsignp params))

(def-format-directive #\X (colonp atsignp params)
  (expand-format-integer 16 colonp atsignp params))

(def-format-directive #\R (colonp atsignp params string end)
  (expand-bind-defaults
      ((base nil) (mincol 0) (padchar #\space) (commachar #\,)
       (commainterval 3))
      params

    (let ((n-arg (gensym "ARG"))
          (expanded-arg (expand-next-arg)))
      `(let ((,n-arg ,(if (car params)
                          expanded-arg
                          `(the* (integer :context (format ,string ,(1- end))) ,expanded-arg))))
         ,@(if (car params)
               `((unless (or ,base
                             (integerp ,n-arg))
                   (format-error-at ,string ,(1- end) "~S is not of type INTEGER." ,n-arg))))
         (if ,base
             (format-print-integer stream ,n-arg ,colonp ,atsignp
                                   ,base ,mincol
                                   ,padchar ,commachar ,commainterval)
             ,(if atsignp
                  (if colonp
                      `(format-print-old-roman stream ,n-arg)
                      `(format-print-roman stream ,n-arg))
                  (if colonp
                      `(format-print-ordinal stream ,n-arg)
                      `(format-print-cardinal stream ,n-arg))))))))

;;;; format directive for pluralization

(def-format-directive #\P (colonp atsignp params end)
  (expand-bind-defaults () params
    (let ((arg (cond
                ((not colonp)
                 (expand-next-arg))
                (*orig-args-available*
                 `(if (eq orig-args args)
                      (format-error-at
                       ,*default-format-error-control-string* ,(1- end)
                       "No previous argument")
                      (do ((arg-ptr orig-args (cdr arg-ptr)))
                          ((eq (cdr arg-ptr) args)
                           (car arg-ptr)))))
                (*only-simple-args*
                 (unless *simple-args*
                   (format-error "No previous argument"))
                 (caar *simple-args*))
                (t
                 (/show0 "THROWing NEED-ORIG-ARGS from tilde-P")
                 (throw 'need-orig-args nil)))))
      (if atsignp
          `(write-string (if (eql ,arg 1) "y" "ies") stream)
          `(unless (eql ,arg 1) (write-char #\s stream))))))

;;;; format directives for floating point output

(def-format-directive #\F (colonp atsignp params)
  (check-modifier "colon" colonp)
  (expand-bind-defaults ((w nil) (d nil) (k nil) (ovf nil) (pad #\space)) params
    `(format-fixed stream ,(expand-next-arg) ,w ,d ,k ,ovf ,pad ,atsignp)))

(def-format-directive #\E (colonp atsignp params)
  (check-modifier "colon" colonp)
  (expand-bind-defaults
      ((w nil) (d nil) (e nil) (k 1) (ovf nil) (pad #\space) (mark nil))
      params
    `(format-exponential stream ,(expand-next-arg) ,w ,d ,e ,k ,ovf ,pad ,mark
                         ,atsignp)))

(def-format-directive #\G (colonp atsignp params)
  (check-modifier "colon" colonp)
  (expand-bind-defaults
      ((w nil) (d nil) (e nil) (k nil) (ovf nil) (pad #\space) (mark nil))
      params
    `(format-general stream ,(expand-next-arg) ,w ,d ,e ,k ,ovf ,pad ,mark ,atsignp)))

(def-format-directive #\$ (colonp atsignp params)
  (expand-bind-defaults ((d 2) (n 1) (w 0) (pad #\space)) params
    `(format-dollars stream ,(expand-next-arg) ,d ,n ,w ,pad ,colonp
                     ,atsignp)))

;;;; format directives for line/page breaks etc.

(def-format-directive #\% (colonp atsignp params)
  (check-modifier "colon" colonp)
  (check-modifier "at-sign" atsignp)
  (cond ((not params)
         '(terpri stream))
        ((typep params '(cons (cons * (mod 65536)) null))
         `(write-string ,(make-string (cdar params) :initial-element #\Newline) stream))
        (t
         (expand-bind-defaults ((count 1)) params
           `(dotimes (i ,count)
              (terpri stream))))))

(def-format-directive #\& (colonp atsignp params)
  (check-modifier "colon" colonp)
  (check-modifier "at-sign" atsignp)
  (if params
      (expand-bind-defaults ((count 1)) params
        `(progn
           (when (plusp ,count)
             (fresh-line stream)
             (dotimes (i (1- ,count))
               (terpri stream)))))
      '(fresh-line stream)))

(def-format-directive #\| (colonp atsignp params)
  (check-modifier "colon" colonp)
  (check-modifier "at-sign" atsignp)
  (if params
      (expand-bind-defaults ((count 1)) params
        `(dotimes (i ,count)
           (write-char (code-char form-feed-char-code) stream)))
      '(write-char (code-char form-feed-char-code) stream)))

(def-format-directive #\~ (colonp atsignp params)
  (check-modifier "colon" colonp)
  (check-modifier "at-sign" atsignp)
  (if params
      (expand-bind-defaults ((count 1)) params
        `(dotimes (i ,count)
           (write-char #\~ stream)))
      '(write-char #\~ stream)))

;;; We'll only get here when the directive usage is illegal.
;;; COMBINE-DIRECTIVES would have handled a legal directive.
(def-complex-format-directive #\newline (colonp atsignp params directives)
  (check-modifier '("colon" "at-sign") (and colonp atsignp))
  (values (expand-bind-defaults () params)
          (bug "Unreachable ~S" directives)))

;;;; format directives for tabs and simple pretty printing

(def-format-directive #\T (colonp atsignp params)
  (if colonp
      (expand-bind-defaults ((n 1) (m 1)) params
        `(pprint-tab ,(if atsignp :section-relative :section)
                     ,n ,m stream))
      (if atsignp
          (expand-bind-defaults ((colrel 1) (colinc 1)) params
            `(format-relative-tab stream ,colrel ,colinc))
          (expand-bind-defaults ((colnum 1) (colinc 1)) params
            `(format-absolute-tab stream ,colnum ,colinc)))))

(def-format-directive #\_ (colonp atsignp params)
  (expand-bind-defaults () params
    `(pprint-newline ,(if colonp
                          (if atsignp
                              :mandatory
                              :fill)
                          (if atsignp
                              :miser
                              :linear))
                     stream)))

(def-format-directive #\I (colonp atsignp params)
  (check-modifier "at-sign" atsignp)
  (expand-bind-defaults ((n 0)) params
    `(pprint-indent ,(if colonp :current :block) ,n stream)))

;;;; format directive for ~*

(def-format-directive #\* (colonp atsignp params end)
  (check-modifier '("colon" "at-sign") (and colonp atsignp))
  (flet ((make-lose (index)
           `(format-error-at
             ,*default-format-error-control-string* ,(1- end)
             "Index ~W is out of bounds. It should have been between ~
              0 and ~W."
             ,index (length orig-args))))
    (if atsignp
        (expand-bind-defaults ((posn 0)) params
          (unless *orig-args-available*
            (/show0 "THROWing NEED-ORIG-ARGS from tilde-@*")
            (throw 'need-orig-args nil))
          `(if (<= 0 ,posn (length orig-args))
               (setf args (nthcdr ,posn orig-args))
               ,(make-lose posn)))
        (if colonp
            (expand-bind-defaults ((n 1)) params
              (unless *orig-args-available*
                (/show0 "THROWing NEED-ORIG-ARGS from tilde-:*")
                (throw 'need-orig-args nil))
              `(do ((cur-posn 0 (1+ cur-posn))
                    (arg-ptr orig-args (cdr arg-ptr)))
                   ((eq arg-ptr args)
                    (let ((new-posn (- cur-posn ,n)))
                      (if (<= 0 new-posn (length orig-args))
                          (setf args (nthcdr new-posn orig-args))
                          ,(make-lose 'new-posn))))))
            (if params
                (expand-bind-defaults ((n 1)) params
                  (setf *only-simple-args* nil)
                  `(dotimes (i ,n)
                     ,(expand-next-arg)))
                (expand-next-arg))))))

;;;; format directive for indirection

(def-format-directive #\? (colonp atsignp params string end)
  (check-modifier "colon" colonp)
  (expand-bind-defaults () params
    `(handler-bind
         ((format-error
           (lambda (condition)
             (declare (optimize (sb-c:store-source-form 0)))
             (error 'format-error
                    :complaint
                    "~A~%while processing indirect format string:"
                    :args (list condition)
                    :print-banner nil
                    :control-string ,string
                    :offset ,(1- end)))))
       ,(if atsignp
            (if *orig-args-available*
                `(setf args (%format stream ,(expand-next-arg) orig-args args))
                (throw 'need-orig-args nil))
            `(%format stream ,(expand-next-arg) ,(expand-next-arg))))))

;;;; format directives for capitalization

(def-complex-format-directive #\( (colonp atsignp params directives)
  (let* ((close (or (find-directive directives #\) nil)
                    (format-error "No corresponding close parenthesis")))
         (posn (position close directives))
         (before (subseq directives 0 posn))
         (after (nthcdr (1+ posn) directives)))
    (values
     (expand-bind-defaults () params
       `(let ((stream (make-case-frob-stream stream
                                             ,(if colonp
                                                  (if atsignp
                                                      :upcase
                                                      :capitalize)
                                                  (if atsignp
                                                      :capitalize-first
                                                      :downcase)))))
          ,@(expand-directive-list before)))
     after)))

(def-complex-format-directive #\) ()
  (format-error "No corresponding open parenthesis"))

;;;; format directives and support functions for conditionalization

(def-complex-format-directive #\[ (colonp atsignp params directives)
  (check-modifier '("colon" "at-sign") (and colonp atsignp))
  (multiple-value-bind (sublists last-semi-with-colon-p remaining)
      (parse-conditional-directive directives)
    (values
     (cond
       (atsignp
        (when (cdr sublists)
          (format-error "Can only specify one section"))
        (expand-bind-defaults () params
          (expand-maybe-conditional (car sublists))))
       (colonp
        (unless (= (length sublists) 2)
          (format-error "Must specify exactly two sections"))
        (expand-bind-defaults () params
          (apply #'expand-true-false-conditional sublists)))
       (t
        (expand-bind-defaults ((index nil)) params
          (setf *only-simple-args* nil)
          (let ((clauses nil)
                (case-sym (gensym))
                (case `(or ,index ,(expand-next-arg))))
            (when last-semi-with-colon-p
              (push `(t ,@(expand-directive-list (pop sublists)))
                    clauses))
            (let ((count (length sublists)))
              (dolist (sublist sublists)
                (push `(,(decf count)
                         ,@(expand-directive-list sublist))
                      clauses)))
            `(let ((,case-sym ,case))
               (unless (integerp ,case-sym)
                 (format-error-at
                  ,*default-format-error-control-string*
                  ,*default-format-error-offset*
                  "The argument to ~~[ is not an integer: ~A" ,case-sym))
               (case ,case-sym ,@clauses))))))
     remaining)))

(defun parse-conditional-directive (directives)
  (let ((sublists nil)
        (last-semi-with-colon-p nil)
        (remaining directives))
    (loop
       (let* ((close-or-semi (or (find-directive remaining #\] t)
                                 (format-error "No corresponding close bracket")))
              (posn (position close-or-semi remaining)))
         (push (subseq remaining 0 posn) sublists)
         (setf remaining (nthcdr (1+ posn) remaining))
         (when (char= (directive-character close-or-semi) #\])
           (return))
         (setf last-semi-with-colon-p
               (directive-colonp close-or-semi))))
    (values sublists last-semi-with-colon-p remaining)))

(defun expand-maybe-conditional (sublist)
  (flet ((hairy ()
           `(let ((prev-args args)
                  (arg ,(expand-next-arg)))
              (when arg
                (setf args prev-args)
                ,@(expand-directive-list sublist)))))
    (if *only-simple-args*
        (multiple-value-bind (guts new-args)
            (let ((*simple-args* *simple-args*))
              (values (expand-directive-list sublist)
                      *simple-args*))
          (cond ((and new-args (eq *simple-args* (cdr new-args)))
                 (setf *simple-args* new-args)
                 `(when ,(caar new-args)
                    ,@guts))
                (t
                 (setf *only-simple-args* nil)
                 (hairy))))
        (hairy))))

(defun expand-true-false-conditional (true false)
  (let ((arg (expand-next-arg)))
    (flet ((hairy ()
             `(if ,arg
                  (progn
                    ,@(expand-directive-list true))
                  (progn
                    ,@(expand-directive-list false)))))
      (if *only-simple-args*
          (multiple-value-bind (true-guts true-args true-simple)
              (let ((*simple-args* *simple-args*)
                    (*only-simple-args* t))
                (values (expand-directive-list true)
                        *simple-args*
                        *only-simple-args*))
            (multiple-value-bind (false-guts false-args false-simple)
                (let ((*simple-args* *simple-args*)
                      (*only-simple-args* t))
                  (values (expand-directive-list false)
                          *simple-args*
                          *only-simple-args*))
              (if (= (length true-args) (length false-args))
                  `(if ,arg
                       (progn
                         ,@true-guts)
                       ,(do ((false false-args (cdr false))
                             (true true-args (cdr true))
                             (bindings nil (cons `(,(caar false) ,(caar true))
                                                 bindings)))
                            ((eq true *simple-args*)
                             (setf *simple-args* true-args)
                             (setf *only-simple-args*
                                   (and true-simple false-simple))
                             (if bindings
                                 `(let ,bindings
                                    ,@false-guts)
                                 `(progn
                                    ,@false-guts)))))
                  (progn
                    (setf *only-simple-args* nil)
                    (hairy)))))
          (hairy)))))

(def-complex-format-directive #\; ()
  (format-error
   "~~; directive not contained within either ~~[...~~] or ~~<...~~>"))

(def-complex-format-directive #\] ()
  (format-error "No corresponding open bracket"))

;;;; format directive for up-and-out

(def-format-directive #\^ (colonp atsignp params)
  (check-modifier "at-sign" atsignp)
  (when (and colonp (not *up-up-and-out-allowed*))
    (format-error "Attempt to use ~~:^ outside a ~~:{...~~} construct"))
  `(when ,(expand-bind-defaults ((arg1 nil) (arg2 nil) (arg3 nil)) params
            `(cond (,arg3 (<= ,arg1 ,arg2 ,arg3))
                   (,arg2 (eql ,arg1 ,arg2))
                   (,arg1 (eql ,arg1 0))
                   (t ,(if colonp
                           '(null outside-args)
                           (progn
                             (setf *only-simple-args* nil)
                             '(null args))))))
     ,(if colonp
          '(return-from outside-loop nil)
          '(return))))

;;;; format directives for iteration

(def-complex-format-directive #\{ (colonp atsignp params string end directives)
  (let* ((close (or (find-directive directives #\} nil)
                    (format-error "No corresponding close brace")))
         (closed-with-colon (directive-colonp close))
         (posn (position close directives)))
    (labels
        ((compute-insides ()
           (if (zerop posn)
               (if *orig-args-available*
                   `((handler-bind
                         ((format-error
                           (lambda (condition)
                             (declare (optimize (sb-c:store-source-form 0)))
                             (format-error-at*
                              ,string ,(1- end)
                              "~A~%while processing indirect format string:"
                              (list condition)
                              :print-banner nil))))
                       (setf args
                             (%format stream inside-string orig-args args))))
                   (throw 'need-orig-args nil))
               (let ((*up-up-and-out-allowed* colonp))
                 (expand-directive-list (subseq directives 0 posn)))))
         (compute-loop (count)
           (when atsignp
             (setf *only-simple-args* nil))
           `(loop
               ,@(unless closed-with-colon
                   '((when (null args)
                       (return))))
               ,@(when count
                   `((when (and ,count (minusp (decf ,count)))
                       (return))))
               ,@(if colonp
                     (let ((*expander-next-arg-macro* 'expander-next-arg)
                           (*only-simple-args* nil)
                           (*orig-args-available* t))
                       `((let* ((orig-args ,(expand-next-arg))
                                (outside-args args)
                                (args orig-args))
                           (declare (ignorable orig-args outside-args args))
                           (block nil
                             ,@(compute-insides)))))
                     (compute-insides))
               ,@(when closed-with-colon
                   '((when (null args)
                       (return))))))
         (compute-block (count)
           (if colonp
               `(block outside-loop
                  ,(compute-loop count))
               (compute-loop count)))
         (compute-bindings (count)
           (if atsignp
               (compute-block count)
               `(let* ((orig-args ,(expand-next-arg))
                       (args orig-args))
                  (declare (ignorable orig-args args))
                  ,(let ((*expander-next-arg-macro* 'expander-next-arg)
                         (*only-simple-args* nil)
                         (*orig-args-available* t))
                     (compute-block count))))))
      (values (if params
                  (expand-bind-defaults ((count nil)) params
                    (if (zerop posn)
                        `(let ((inside-string ,(expand-next-arg)))
                           ,(compute-bindings count))
                        (compute-bindings count)))
                  (if (zerop posn)
                      `(let ((inside-string ,(expand-next-arg)))
                         ,(compute-bindings nil))
                      (compute-bindings nil)))
              (nthcdr (1+ posn) directives)))))

(def-complex-format-directive #\} ()
  (format-error "No corresponding open brace"))

;;;; format directives and support functions for justification

(defconstant-eqx !illegal-inside-justification
    '#.(mapcar (lambda (x) (directive-bits (parse-directive x 0 nil)))
               '("~:>" "~:@>"
                 "~:T" "~:@T"))
  #'equal)

;;; Reject ~W, ~_, ~I and certain other specific values of modifier+character.
(defun illegal-inside-justification-p (directive)
  (and (format-directive-p directive)
       (if (or (member (directive-bits directive) !illegal-inside-justification)
               (member (directive-character directive) '(#\W #\I #\_)))
           t
           nil)))

(def-complex-format-directive #\< (colonp atsignp params string end directives)
  (multiple-value-bind (segments first-semi close remaining)
      (parse-format-justification directives)
    (values
     (if (directive-colonp close) ; logical block vs. justification
         (multiple-value-bind (prefix per-line-p insides suffix)
             (parse-format-logical-block segments colonp first-semi
                                         close params string end)
           (expand-format-logical-block prefix per-line-p insides
                                        suffix atsignp))
         (let ((count (reduce #'+ (mapcar (lambda (x)
                                            (count-if #'illegal-inside-justification-p x))
                                          segments))))
           (when (> count 0)
             ;; ANSI specifies that "an error is signalled" in this
             ;; situation.
             (format-error*
              "~D illegal directive~:P found inside justification block"
              (list count)
              :references '((:ansi-cl :section (22 3 5 2)))))
           ;; ANSI does not explicitly say that an error should be
           ;; signalled, but the @ modifier is not explicitly allowed
           ;; for ~> either.
           (when (directive-atsignp close)
             (format-error-at*
              nil (1- (directive-end close))
              "@ modifier not allowed in close directive of ~
               justification block (i.e. ~~<...~~@>."
              '()
              :references '((:ansi-cl :section (22 3 6 2)))))
           (expand-format-justification segments colonp atsignp
                                        first-semi params)))
     remaining)))

(def-complex-format-directive #\> ()
  (format-error "No corresponding open bracket"))

(defun parse-format-logical-block
       (segments colonp first-semi close params string end)
  (when params
    (format-error-at nil (caar params)
                     "No parameters can be supplied with ~~<...~~:>."))
  (multiple-value-bind (prefix insides suffix)
      (multiple-value-bind (prefix-default suffix-default)
          (if colonp (values "(" ")") (values "" ""))
        (flet ((extract-string (list prefix-p)
                 (let ((directive (find-if #'format-directive-p list)))
                   (if directive
                       (format-error-at*
                        nil (1- (directive-end directive))
                        "Cannot include format directives inside the ~
                         ~:[suffix~;prefix~] segment of ~~<...~~:>"
                        (list prefix-p)
                        :references '((:ansi-cl :section (22 3 5 2))))
                       (apply #'concatenate 'string list)))))
        (case (length segments)
          (0 (values prefix-default nil suffix-default))
          (1 (values prefix-default (car segments) suffix-default))
          (2 (values (extract-string (car segments) t)
                     (cadr segments) suffix-default))
          (3 (values (extract-string (car segments) t)
                     (cadr segments)
                     (extract-string (caddr segments) nil)))
          (t
           (format-error "Too many segments for ~~<...~~:>")))))
    (when (directive-atsignp close)
      (setf insides
            (add-fill-style-newlines insides
                                     string
                                     (if first-semi
                                         (directive-end first-semi)
                                         end))))
    (values prefix
            (and first-semi (directive-atsignp first-semi))
            insides
            suffix)))

(defun add-fill-style-newlines (list string offset &optional last-directive)
  (cond
    (list
     (let ((directive (car list)))
       (cond
         ((simple-string-p directive)
          (let* ((non-space (position #\Space directive :test #'char/=))
                 (newlinep (and last-directive
                                (char= (directive-character last-directive)
                                       #\Newline))))
            (cond
              ((and newlinep non-space)
               (nconc
                (list (subseq directive 0 non-space))
                (add-fill-style-newlines-aux
                 (subseq directive non-space) string (+ offset non-space))
                (add-fill-style-newlines
                 (cdr list) string (+ offset (length directive)))))
              (newlinep
               (cons directive
                     (add-fill-style-newlines
                      (cdr list) string (+ offset (length directive)))))
              (t
               (nconc (add-fill-style-newlines-aux directive string offset)
                      (add-fill-style-newlines
                       (cdr list) string (+ offset (length directive))))))))
         (t
          (cons directive
                (add-fill-style-newlines
                 (cdr list) string
                 (directive-end directive) directive))))))
    (t nil)))

(defun add-fill-style-newlines-aux (literal string offset)
  (let ((end (length literal))
        (posn 0))
    (collect ((results))
      (loop
        (let ((blank (position #\space literal :start posn)))
          (when (null blank)
            (results (subseq literal posn))
            (return))
          (let ((non-blank (or (position #\space literal :start blank
                                         :test #'char/=)
                               end)))
            (results (subseq literal posn non-blank))
            (results (make-format-directive
                      string (+ offset non-blank) (+ offset non-blank)
                      nil t nil #\_ nil)) ; params,colon,atsign,char,symbol
            (setf posn non-blank))
          (when (= posn end)
            (return))))
      (results))))

(defun parse-format-justification (directives)
  (let ((first-semi nil)
        (close nil)
        (remaining directives))
    (collect ((segments))
      (loop
         (let ((close-or-semi (or (find-directive remaining #\> t)
                                  (format-error "No corresponding close bracket"))))
           (let ((posn (position close-or-semi remaining)))
             (segments (subseq remaining 0 posn))
             (setf remaining (nthcdr (1+ posn) remaining)))
           (when (char= (directive-character close-or-semi) #\>)
             (setf close close-or-semi)
             (return))
           (unless first-semi
             (setf first-semi close-or-semi))))
      (values (segments) first-semi close remaining))))

(sb-xc:defmacro expander-pprint-next-arg (string offset)
  `(progn
     (when (null args)
       (format-error-at ,string ,offset "No more arguments"))
     (pprint-pop)
     (pop args)))

(defun expand-format-logical-block (prefix per-line-p insides suffix atsignp)
  `(let ((arg ,(if atsignp 'args (expand-next-arg))))
     ,@(when atsignp
         (setf *only-simple-args* nil)
         '((setf args nil)))
     (pprint-logical-block
         (stream arg
                 ,(if per-line-p :per-line-prefix :prefix) ,prefix
                 :suffix ,suffix)
       (let ((args arg)
             ,@(unless atsignp
                 `((orig-args arg))))
         (declare (ignorable args ,@(unless atsignp '(orig-args))))
         (block nil
           ,@(let ((*expander-next-arg-macro* 'expander-pprint-next-arg)
                   (*only-simple-args* nil)
                   (*orig-args-available*
                    (if atsignp *orig-args-available* t)))
               (expand-directive-list insides)))))))

(defun expand-format-justification (segments colonp atsignp first-semi params)
  (let ((newline-segment-p
         (and first-semi
              (directive-colonp first-semi))))
    (expand-bind-defaults
        ((mincol 0) (colinc 1) (minpad 0) (padchar #\space))
        params
      `(let ((segments nil)
             ,@(when newline-segment-p
                 '((newline-segment nil)
                   (extra-space 0)
                   (line-len 72))))
         (block nil
           ,@(when newline-segment-p
               `((setf newline-segment
                       (%with-output-to-string (stream)
                         ,@(expand-directive-list (pop segments))))
                 ,(expand-bind-defaults
                      ((extra 0)
                       (line-length '(or (sb-impl::line-length stream) 72)))
                      (directive-params first-semi)
                    `(setf extra-space ,extra line-len ,line-length))))
           ,@(mapcar (lambda (segment)
                       `(push (%with-output-to-string (stream)
                                ,@(expand-directive-list segment))
                              segments))
                     segments))
         (format-justification stream
                               ,@(if newline-segment-p
                                     '(newline-segment extra-space line-len)
                                     '(nil 0 0))
                               segments ,colonp ,atsignp
                               ,mincol ,colinc ,minpad ,padchar)))))

;;;; format directive and support function for user-defined method

(def-format-directive #\/ (string start end colonp atsignp params)
  (let ((symbol (extract-user-fun-name string start end)))
    (collect ((param-names) (bindings))
      (dolist (param-and-offset params)
        (let ((param (cdr param-and-offset)))
          (let ((param-name (gensym "PARAM")))
            (param-names param-name)
            (bindings `(,param-name
                        ,(case param
                           (:arg (expand-next-arg))
                           (:remaining '(length args))
                           (t param)))))))
      `(let ,(bindings)
         (,symbol stream ,(expand-next-arg) ,colonp ,atsignp
                  ,@(param-names))))))

(defun extract-user-fun-name (string start end)
  ;; Searching backwards avoids finding the wrong slash in a funky string
  ;; such as "~'/,'//fun/" which passes #\/ twice to FUN as parameters.
  (let* ((slash (or (position #\/ string :start start :end (1- end)
                              :from-end t)
                    (format-error "Malformed ~~/ directive")))
         (name (nstring-upcase (subseq string (1+ slash) (1- end))))
         (first-colon (position #\: name))
         (second-colon (if first-colon (position #\: name :start (1+ first-colon))))
         (symbol
          (cond ((and second-colon (= second-colon (1+ first-colon)))
                 (subseq name (1+ second-colon)))
                (first-colon
                 (subseq name (1+ first-colon)))
                (t name)))
         (package
            (if (not first-colon)
                #.(find-package "COMMON-LISP-USER")
                (let ((package-name (subseq name 0 first-colon)))
                  (or (find-package package-name)
                      ;; FIXME: should be PACKAGE-ERROR? Could we just
                      ;; use FIND-UNDELETED-PACKAGE-OR-LOSE?
                      (format-error "No package named ~S" package-name))))))
    (intern symbol package)))

(defun extract-user-fun-directives (string)
  (let* ((tokens (handler-case
                     (combine-directives
                      (%tokenize-control-string string 0 (length string) nil)
                      nil)
                   (error (e)
                     (declare (ignore e))
                     (return-from extract-user-fun-directives (values nil nil)))))
         (max-len (loop for token in tokens
                        sum (if (format-directive-p token)
                                (- (directive-end token) (directive-start token))
                                (length token))))
         (new-string
          (make-array max-len :element-type 'character :fill-pointer 0))
         (symbols))
    (dolist (token tokens)
      (cond ((stringp token)
             (aver (not (find #\~ token)))
             (let ((new-start (fill-pointer new-string)))
               (incf (fill-pointer new-string) (length token))
               (replace new-string token :start1 new-start)))
            (t
             (let* ((start (directive-start token))
                    (end (directive-end token))
                    (len (- end start))
                    (new-start (fill-pointer new-string)))
               (cond ((eql (directive-character token) #\/)
                      (push (handler-case (extract-user-fun-name string start end)
                              (error (e)
                                (declare (ignore e))
                                (return-from extract-user-fun-directives
                                  (values nil nil))))
                            symbols)
                      ;; Don't copy past the first slash. Scan backwards for it
                      ;; exactly as is done in EXTRACT-USER-FUN-NAME above.
                      ;; This error can't really happen (as we've already produced
                      ;; a valid tokenization), but the error check avoids
                      ;; a warning about adding 1 to NIL.
                      (setq end (1+ (or (position #\/ string :start start :end (1- end)
                                                  :from-end t)
                                        (error "Malformed ~~/ directive"))))
                      ;; compute new length to copy, +1 is for trailing slash
                      (incf (fill-pointer new-string) (1+ (- end start)))
                      (setf (char new-string (1- (fill-pointer new-string))) #\/))
                     (t
                      (incf (fill-pointer new-string) len)))
               (replace new-string string :start1 new-start
                                          :start2 start :end2 end)))))
    (values (nreverse symbols)
            (possibly-base-stringize new-string))))

(push '("SB-FORMAT" tokens) *!removable-symbols*)
(sb-xc:defmacro tokens (string)
  (declare (string string))
  (multiple-value-bind (symbols new-string) (extract-user-fun-directives string)
    (if symbols
        (make-fmt-control-proxy new-string symbols)
        (possibly-base-stringize new-string))))

;;; compile-time checking for argument mismatch.  This code is
;;; inspired by that of Gerd Moellmann, and comes decorated with
;;; FIXMEs:
(defun %compiler-walk-format-string (string args)
  (let* ((string (coerce string 'simple-string))
         (*default-format-error-control-string* string))
    (macrolet ((incf-both (&optional (increment 1))
                 `(progn
                   (incf min ,increment)
                   (incf max ,increment)))
               (walk-complex-directive (function)
                 `(multiple-value-bind (min-inc max-inc remaining)
                   (,function directive directives args)
                   (incf min min-inc)
                   (incf max max-inc)
                   (setq directives remaining))))
      ;; FIXME: these functions take a list of arguments as well as
      ;; the directive stream.  This is to enable possibly some
      ;; limited type checking on FORMAT's arguments, as well as
      ;; simple argument count mismatch checking: when the minimum and
      ;; maximum argument counts are the same at a given point, we
      ;; know which argument is going to be used for a given
      ;; directive, and some (annotated below) require arguments of
      ;; particular types.
      (labels
          ((walk-justification (justification directives args)
             (declare (ignore args))
             (let ((*default-format-error-offset*
                    (1- (directive-end justification))))
               (multiple-value-bind (segments first-semi close remaining)
                   (parse-format-justification directives)
                 (declare (ignore segments first-semi))
                 (cond
                   ((not (directive-colonp close))
                    (values 0 0 directives))
                   ((directive-atsignp justification)
                    (values 0 call-arguments-limit directives))
                   ;; FIXME: here we could assert that the
                   ;; corresponding argument was a list.
                   (t (values 1 1 remaining))))))
           (walk-conditional (conditional directives args)
             (let ((*default-format-error-offset*
                    (1- (directive-end conditional))))
               (multiple-value-bind (sublists last-semi-with-colon-p remaining)
                   (parse-conditional-directive directives)
                 (declare (ignore last-semi-with-colon-p))
                 (let ((sub-max
                        (loop for s in sublists
                              maximize (nth-value
                                        1 (walk-directive-list s args)))))
                   (cond
                     ((directive-atsignp conditional)
                      (values 1 (max 1 sub-max) remaining))
                     ((loop for p in (directive-params conditional)
                            thereis (or (integerp (cdr p))
                                        (memq (cdr p) '(:remaining :arg))))
                      (values 0 sub-max remaining))
                     ;; FIXME: if not COLONP, then the next argument
                     ;; must be a number.
                     (t (values 1 (1+ sub-max) remaining)))))))
           (walk-iteration (iteration directives args)
             (declare (ignore args))
             (let ((*default-format-error-offset*
                    (1- (directive-end iteration))))
               (let* ((close (find-directive directives #\} nil))
                      (posn (or (position close directives)
                                (format-error "No corresponding close brace")))
                      (remaining (nthcdr (1+ posn) directives)))
                 ;; FIXME: if POSN is zero, the next argument must be
                 ;; a format control (either a function or a string).
                 (if (directive-atsignp iteration)
                     (values (if (zerop posn) 1 0)
                             call-arguments-limit
                             remaining)
                     ;; FIXME: the argument corresponding to this
                     ;; directive must be a list.
                     (let ((nreq (if (zerop posn) 2 1)))
                       (values nreq nreq remaining))))))
           (walk-directive-list (directives args)
             (let ((min 0) (max 0))
               (loop
                (let ((directive (pop directives)))
                  (when (null directive)
                    (return (values min (min max call-arguments-limit))))
                  (when (format-directive-p directive)
                    (incf-both (count :arg (directive-params directive)
                                      :key #'cdr))
                    (let ((c (directive-character directive)))
                      (cond
                        ((find c "ABCDEFGORSWX$/")
                         (incf-both))
                        ((char= c #\P)
                         (unless (directive-colonp directive)
                           (incf-both)))
                        ((or (find c "IT%&|_();>~") (char= c #\Newline)))
                        ;; FIXME: check correspondence of ~( and ~)
                        ((char= c #\<)
                         (walk-complex-directive walk-justification))
                        ((char= c #\[)
                         (walk-complex-directive walk-conditional))
                        ((char= c #\{)
                         (walk-complex-directive walk-iteration))
                        ((char= c #\?)
                         ;; FIXME: the argument corresponding to this
                         ;; directive must be a format control.
                         (cond
                           ((directive-atsignp directive)
                            (incf min)
                            (setq max call-arguments-limit))
                           (t (incf-both 2))))
                        (t (throw 'give-up-format-string-walk nil))))))))))
        (catch 'give-up-format-string-walk
          (let ((directives (tokenize-control-string string)))
            (walk-directive-list directives args)))))))

;;; Optimize common case of constant keyword arguments
;;; to WRITE and WRITE-TO-STRING
(flet
    ((expand (fn object keys)
      (do (streamvar bind ignore)
          ((or (atom keys) (atom (cdr keys)))
           (if keys ; fail
               (values nil t)
               (values
                (let* ((objvar (copy-symbol 'object))
                       (bind `((,objvar ,object) ,@(nreverse bind)))
                       (ignore (when ignore `((declare (ignore ,@ignore))))))
                  (case fn
                   (write
                    ;; When :STREAM was specified, this used to insert a call
                    ;; to (OUT-SYNONYM-OF STREAMVAR) which added junk to the
                    ;; expansion which was not likely to improve performance.
                    ;; The benefit of this transform is that it avoids runtime
                    ;; keyword parsing and binding of 16 specials vars, *not*
                    ;; that it can inline testing for T or NIL as the stream.
                    `(let ,bind ,@ignore
                       ,@(if streamvar
                            `((%write ,objvar ,streamvar))
                            `((output-object ,objvar *standard-output*)
                              ,objvar))))
                   (write-to-string
                    (if (cdr bind)
                        `(let ,bind ,@ignore (stringify-object ,objvar))
                        `(stringify-object ,object)))))
                nil)))
        (let* ((key (pop keys))
               (value (pop keys))
               (variable
                (cond ((getf '(:array *print-array*
                               :base *print-base*
                               :case *print-case*
                               :circle *print-circle*
                               :escape *print-escape*
                               :gensym *print-gensym*
                               :length *print-length*
                               :level *print-level*
                               :lines *print-lines*
                               :miser-width *print-miser-width*
                               :pprint-dispatch *print-pprint-dispatch*
                               :pretty *print-pretty*
                               :radix *print-radix*
                               :readably *print-readably*
                               :right-margin *print-right-margin*
                               :suppress-errors *suppress-print-errors*)
                             key))
                      ((and (eq key :stream) (eq fn 'write))
                       (or streamvar (setq streamvar (copy-symbol 'stream))))
                      (t
                       (return (values nil t))))))
          (when (assoc variable bind)
            ;; First key has precedence, but we still need to execute the
            ;; argument, and in the right order.
            (setf variable (gensym "IGNORE"))
            (push variable ignore))
          (push (list variable value) bind)))))

  (sb-c:define-source-transform write (object &rest keys)
    (expand 'write object keys))

  (sb-c:define-source-transform write-to-string (object &rest keys)
    (expand 'write-to-string object keys)))

#-sb-xc-host
(defun !late-format-init ()
  (setq sb-format::**tokenize-control-string-cache-vector** nil))
