(in-package "SB!FORMAT")
\f
-(define-condition format-error (error)
+(define-condition format-error (error reference-condition)
((complaint :reader format-error-complaint :initarg :complaint)
(args :reader format-error-args :initarg :args :initform nil)
(control-string :reader format-error-control-string
:initform *default-format-error-control-string*)
(offset :reader format-error-offset :initarg :offset
:initform *default-format-error-offset*)
+ (second-relative :reader format-error-second-relative
+ :initarg :second-relative :initform nil)
(print-banner :reader format-error-print-banner :initarg :print-banner
:initform t))
- (:report %print-format-error))
+ (:report %print-format-error)
+ (:default-initargs :references nil))
(defun %print-format-error (condition stream)
(format stream
- "~:[~;error in format: ~]~
- ~?~@[~% ~A~% ~V@T^~]"
+ "~:[~*~;error in ~S: ~]~?~@[~% ~A~% ~V@T^~@[~V@T^~]~]"
(format-error-print-banner condition)
+ 'format
(format-error-complaint condition)
(format-error-args condition)
(format-error-control-string condition)
- (format-error-offset condition)))
+ (format-error-offset condition)
+ (format-error-second-relative condition)))
\f
(def!struct format-directive
(string (missing-arg) :type simple-string)
(start (missing-arg) :type (and unsigned-byte fixnum))
(end (missing-arg) :type (and unsigned-byte fixnum))
- (character (missing-arg) :type base-char)
+ (character (missing-arg) :type character)
(colonp nil :type (member t nil))
(atsignp nil :type (member t nil))
(params nil :type list))
(declare (simple-string string))
(let ((index 0)
(end (length string))
- (result nil))
+ (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)))
(when (> next-directive index)
(push (subseq string index next-directive) result))
(when (= next-directive end)
(return))
- (let ((directive (parse-directive string next-directive)))
+ (let* ((directive (parse-directive string next-directive))
+ (char (format-directive-character directive)))
+ ;; this processing is required by CLHS 22.3.5.2
+ (cond
+ ((char= char #\<) (push directive block))
+ ((and block (char= char #\;) (format-directive-colonp directive))
+ (setf semicolon directive))
+ ((char= char #\>)
+ (aver block)
+ (cond
+ ((format-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 (format-directive-colonp directive)
+ (not pprint))
+ (setf pprint directive))))))
(push directive result)
(setf index (format-directive-end directive)))))
+ (when (and pprint justification-semicolon)
+ (let ((pprint-offset (1- (format-directive-end pprint)))
+ (justification-offset
+ (1- (format-directive-end justification-semicolon))))
+ (error 'format-error
+ :complaint "misuse of justification and pprint directives"
+ :control-string string
+ :offset (min pprint-offset justification-offset)
+ :second-relative (- (max pprint-offset justification-offset)
+ (min pprint-offset justification-offset)
+ 1)
+ :references (list '(:ansi-cl :section (22 3 5 2))))))
(nreverse result)))
(defun parse-directive (string start)
(flet ((get-char ()
(if (= posn end)
(error 'format-error
- :complaint "String ended before directive was found."
+ :complaint "string ended before directive was found"
:control-string string
:offset start)
(schar string posn)))
(error 'format-error
:complaint "parameters found after #\\: or #\\@ modifier"
:control-string string
- :offset posn))))
+ :offset posn
+ :references (list '(:ansi-cl :section (22 3)))))))
(loop
(let ((char (get-char)))
(cond ((or (char<= #\0 char #\9) (char= char #\+) (char= char #\-))
(error 'format-error
:complaint "too many colons supplied"
:control-string string
- :offset posn)
+ :offset posn
+ :references (list '(:ansi-cl :section (22 3))))
(setf colonp t)))
((char= char #\@)
(if atsignp
(error 'format-error
:complaint "too many #\\@ characters supplied"
:control-string string
- :offset posn)
+ :offset posn
+ :references (list '(:ansi-cl :section (22 3))))
(setf atsignp t)))
(t
- (when (char= (schar string (1- posn)) #\,)
+ (when (and (char= (schar string (1- posn)) #\,)
+ (or (< posn 2)
+ (char/= (schar string (- posn 2)) #\')))
(check-ordering)
(push (cons (1- posn) nil) params))
(return))))
(etypecase directive
(format-directive
(let ((expander
- (aref *format-directive-expanders*
- (char-code (format-directive-character directive))))
+ (let ((char (format-directive-character directive)))
+ (typecase char
+ (base-char
+ (aref *format-directive-expanders* (char-code char)))
+ (character nil))))
(*default-format-error-offset*
(1- (format-directive-end directive))))
(declare (type (or null function) expander))
(expand-format-integer 16 colonp atsignp params))
(def-format-directive #\R (colonp atsignp params)
- (if params
- (expand-bind-defaults
- ((base 10) (mincol 0) (padchar #\space) (commachar #\,)
- (commainterval 3))
- params
- `(format-print-integer stream ,(expand-next-arg) ,colonp ,atsignp
- ,base ,mincol
- ,padchar ,commachar ,commainterval))
- (if atsignp
- (if colonp
- `(format-print-old-roman stream ,(expand-next-arg))
- `(format-print-roman stream ,(expand-next-arg)))
- (if colonp
- `(format-print-ordinal stream ,(expand-next-arg))
- `(format-print-cardinal stream ,(expand-next-arg))))))
+ (expand-bind-defaults
+ ((base nil) (mincol 0) (padchar #\space) (commachar #\,)
+ (commainterval 3))
+ params
+ (let ((n-arg (gensym)))
+ `(let ((,n-arg ,(expand-next-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))))))))
\f
;;;; format directive for pluralization
(setf args (nthcdr ,posn orig-args))
(error 'format-error
:complaint "Index ~W out of bounds. Should have been ~
- between 0 and ~W."
+ between 0 and ~W."
:args (list ,posn (length orig-args))
:offset ,(1- end)))))
(if colonp
(error 'format-error
:complaint
"Index ~W is out of bounds; should have been ~
- between 0 and ~W."
+ between 0 and ~W."
:args (list new-posn (length orig-args))
:offset ,(1- end)))))))
(if params
(error 'format-error
:complaint
"must specify exactly two sections"))
- (expand-bind-defaults ((index (expand-next-arg))) params
+ (expand-bind-defaults ((index nil)) params
(setf *only-simple-args* nil)
- (let ((clauses nil))
+ (let ((clauses nil)
+ (case `(or ,index ,(expand-next-arg))))
(when last-semi-with-colon-p
(push `(t ,@(expand-directive-list (pop sublists)))
clauses))
(push `(,(decf count)
,@(expand-directive-list sublist))
clauses)))
- `(case ,index ,@clauses)))))
+ `(case ,case ,@clauses)))))
remaining)))
(defun parse-conditional-directive (directives)
(let ((*simple-args* *simple-args*))
(values (expand-directive-list sublist)
*simple-args*))
- (cond ((eq *simple-args* (cdr new-args))
+ (cond ((and new-args (eq *simple-args* (cdr new-args)))
(setf *simple-args* new-args)
`(when ,(caar new-args)
,@guts))
(when (and colonp (not *up-up-and-out-allowed*))
(error 'format-error
:complaint "attempt to use ~~:^ outside a ~~:{...~~} construct"))
- `(when ,(case (length params)
- (0 (if colonp
- '(null outside-args)
- (progn
- (setf *only-simple-args* nil)
- '(null args))))
- (1 (expand-bind-defaults ((count 0)) params
- `(zerop ,count)))
- (2 (expand-bind-defaults ((arg1 0) (arg2 0)) params
- `(= ,arg1 ,arg2)))
- (t (expand-bind-defaults ((arg1 0) (arg2 0) (arg3 0)) params
- `(<= ,arg1 ,arg2 ,arg3))))
+ `(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))))
(throw 'need-orig-args nil))
(let ((*up-up-and-out-allowed* colonp))
(expand-directive-list (subseq directives 0 posn)))))
- (compute-loop-aux (count)
+ (compute-loop (count)
(when atsignp
(setf *only-simple-args* nil))
`(loop
,@(when closed-with-colon
'((when (null args)
(return))))))
- (compute-loop ()
- (if params
- (expand-bind-defaults ((count nil)) params
- (compute-loop-aux count))
- (compute-loop-aux nil)))
- (compute-block ()
+ (compute-block (count)
(if colonp
`(block outside-loop
- ,(compute-loop))
- (compute-loop)))
- (compute-bindings ()
+ ,(compute-loop count))
+ (compute-loop count)))
+ (compute-bindings (count)
(if atsignp
- (compute-block)
- `(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))))))
- (values (if (zerop posn)
- `(let ((inside-string ,(expand-next-arg)))
- ,(compute-bindings))
- (compute-bindings))
+ (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 #\} ()
;; situation.
(error 'format-error
:complaint "~D illegal directive~:P found inside justification block"
- :args (list count)))
+ :args (list count)
+ :references (list '(:ansi-cl :section (22 3 5 2)))))
(expand-format-justification segments colonp atsignp
- first-semi params)))
+ first-semi params)))
remaining)))
(def-complex-format-directive #\> ()
:offset (caar params)))
(multiple-value-bind (prefix insides suffix)
(multiple-value-bind (prefix-default suffix-default)
- (if colonp (values "(" ")") (values nil ""))
+ (if colonp (values "(" ")") (values "" ""))
(flet ((extract-string (list prefix-p)
(let ((directive (find-if #'format-directive-p list)))
(if directive
(error 'format-error
:complaint
- "cannot include format directives inside the ~
- ~:[suffix~;prefix~] segment of ~~<...~~:>"
+ "cannot include format directives inside the ~
+ ~:[suffix~;prefix~] segment of ~~<...~~:>"
:args (list prefix-p)
- :offset (1- (format-directive-end directive)))
+ :offset (1- (format-directive-end directive))
+ :references
+ (list '(:ansi-cl :section (22 3 5 2))))
(apply #'concatenate 'string list)))))
(case (length segments)
(0 (values prefix-default nil suffix-default))
insides
suffix)))
-(defun add-fill-style-newlines (list string offset)
- (if list
- (let ((directive (car list)))
- (if (simple-string-p directive)
- (nconc (add-fill-style-newlines-aux directive string offset)
- (add-fill-style-newlines (cdr list)
- string
- (+ offset (length directive))))
- (cons directive
- (add-fill-style-newlines (cdr list)
- string
- (format-directive-end directive)))))
- nil))
+(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=
+ (format-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
+ (format-directive-end directive) directive))))))
+ (t nil)))
(defun add-fill-style-newlines-aux (literal string offset)
(let ((end (length literal))
(subseq name (1+ first-colon)))
(t name))
package))))
+
+;;; 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)
+ (declare (type simple-string string))
+ (let ((*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- (format-directive-end justification))))
+ (multiple-value-bind (segments first-semi close remaining)
+ (parse-format-justification directives)
+ (declare (ignore segments first-semi))
+ (cond
+ ((not (format-directive-colonp close))
+ (values 0 0 directives))
+ ((format-directive-atsignp justification)
+ (values 0 sb!xc: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- (format-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
+ ((format-directive-atsignp conditional)
+ (values 1 (max 1 sub-max) remaining))
+ ((loop for p in (format-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- (format-directive-end iteration))))
+ (let* ((close (find-directive directives #\} nil))
+ (posn (or (position close directives)
+ (error 'format-error
+ :complaint "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 (format-directive-atsignp iteration)
+ (values (if (zerop posn) 1 0)
+ sb!xc: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 sb!xc:call-arguments-limit))))
+ (when (format-directive-p directive)
+ (incf-both (count :arg (format-directive-params directive)
+ :key #'cdr))
+ (let ((c (format-directive-character directive)))
+ (cond
+ ((find c "ABCDEFGORSWX$/")
+ (incf-both))
+ ((char= c #\P)
+ (unless (format-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
+ ((format-directive-atsignp directive)
+ (incf min)
+ (setq max sb!xc: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)))))))