[format] change naming, fix broken format sequences, add typechecking

This commit is contained in:
ben 2015-12-12 22:22:04 -05:00
parent c5ea8c032c
commit a26f7a79b4

View File

@ -1,7 +1,7 @@
#lang typed/racket/base #lang typed/racket/base
(provide (provide
format! format:
;; (-> (x:String) Any *N Void) ;; (-> (x:String) Any *N Void)
;; Takes 1 required string argument and N additional arguments, ;; Takes 1 required string argument and N additional arguments,
;; where N is the number of format sequences in the string. ;; where N is the number of format sequences in the string.
@ -11,7 +11,7 @@
;; ;;
;; If the string is not a literal, arity-checking happens at runtime. ;; If the string is not a literal, arity-checking happens at runtime.
printf! printf:
;; (-> (x:String) Any *N Void) ;; (-> (x:String) Any *N Void)
;; Similar to `format`, but displays the formatted string to `current-output-port`. ;; Similar to `format`, but displays the formatted string to `current-output-port`.
) )
@ -21,71 +21,70 @@
;; ============================================================================= ;; =============================================================================
(define-syntax format! (define-syntax format:
(syntax-parser (syntax-parser
[(f template:str arg* ...) [(f template:str arg* ...)
(let ([num-expected (count-format-escapes (syntax-e #'template))] ;; 1. Parse expected types from the template
[num-given (for/sum ([a (in-syntax #'(arg* ...))]) 1)]) (let* ([type* (template->type* (syntax-e #'template) #:src #'f)]
[num-expected (length type*)]
[num-given (for/sum ([a (in-syntax #'(arg* ...))]) 1)])
(unless (= num-expected num-given) (unless (= num-expected num-given)
(raise-arity-error (raise-arity-error
(syntax-e #'f) (syntax-e #'f)
num-expected num-expected
(for/list ([a (in-syntax #'(arg* ...))]) (syntax->datum a)))) (for/list ([a (in-syntax #'(arg* ...))]) (syntax->datum a))))
(syntax/loc #'f (format template arg* ...)))] ;; 2. If any types left obligations, use `ann` to typecheck the args
(let ([arg+*
(for/list ([a (in-syntax #'(arg* ...))]
[t (in-list type*)])
(if t (quasisyntax/loc #'f (ann #,a #,t)) a))])
(quasisyntax/loc #'f
(format template #,@arg+*))))]
[(f tmp arg* ...) [(f tmp arg* ...)
(syntax/loc #'f (format tmp arg* ...))])) (syntax/loc #'f (format tmp arg* ...))]))
(define-syntax-rule (printf! arg* ...) (define-syntax-rule (printf: arg* ...)
(display (format! arg* ...))) (display (format: arg* ...)))
;; -----------------------------------------------------------------------------
;; Count the number of format escapes in a string. ;; Count the number of format escapes in a string.
(define-for-syntax (count-format-escapes str) ;; Returns a list of optional types (to be spliced into the source code)
;; Example: If result is '(#f Integer), then
;; - Expect 2 arguments to format string
;; - First argument has no constraints, second must be an Integer
;; (: count-format-escapes (->* [String] [#:src (U #f Syntax)] (Listof (U #f Syntax))))
(define-for-syntax (template->type* str #:src [stx #f])
(define last-index (- (string-length str) 1)) (define last-index (- (string-length str) 1))
(let loop ([i 0] [acc 0]) (let loop ([i 0] [acc '()])
(cond (cond
[(>= i last-index) [(>= i last-index)
acc] (reverse acc)]
[(eq? #\~ (string-ref str i)) [(eq? #\~ (string-ref str i))
;; From fprintf docs ;; From fprintf docs
(case (string-ref str (+ i 1)) (case (string-ref str (+ i 1))
[(#\n #\% #\a #\A #\v #\V #\e #\E #\c #\C #\b #\B #\o #\O #\x #\X #\space #\tab #\newline) [(#\% #\n #\~ #\space #\tab #\newline)
(loop (+ i 2) (+ 1 acc))] ;; Need 0 arguments
(loop (+ i 2) acc)]
[(#\a #\A #\s #\S #\v #\V #\e #\E)
;; Need 1 argument, can be anything
(loop (+ i 2) (cons #f acc))]
[(#\.) [(#\.)
;; Need at most 1, can be anything
(if (and (< (+ 1 i) last-index) (if (and (< (+ 1 i) last-index)
(memq (string-ref str (+ i 2)) '(#\a #\A #\s #\S #\v #\V))) (memq (string-ref str (+ i 2)) '(#\a #\A #\s #\S #\v #\V)))
(loop (+ i 3) (+ 1 acc)) (loop (+ i 3) (cons #f acc))
(loop (+ i 3) acc))] (loop (+ i 3) acc))]
[(#\c #\C)
;; Need 1 `char?`
(loop (+ i 2) (cons (syntax/loc stx Char) acc))]
[(#\b #\B #\o #\O #\x #\X)
;; Need 1 `exact?`
(loop (+ i 2) (cons (syntax/loc stx Exact-Number) acc))]
[else [else
(loop (+ i 2) acc)])] ;; Invalid format sequence
(raise-user-error "format: unrecognized pattern string '~~~c'"
(string-ref str (+ i 1)))])]
[else [else
(loop (+ i 1) acc)]))) (loop (+ i 1) acc)])))
;; =============================================================================
(module+ test (begin-for-syntax
(require
rackunit)
;; --- count-format-escapes
(define-syntax-rule (test-escapes [str n] ...)
(begin
(check-equal? (count-format-escapes str) n) ...))
(test-escapes
["" 0]
["hello" 0]
["world" 0]
["~~ nope" 0]
["~" 0]
["\\~~" 0]
["~~~~" 0]
["hey ~.x you" 0]
;; --
["hello, ~a" 1]
["~a ~b ~c" 3]
["~\n ~%" 2]
["~a ~A ~v ~V ~e ~E ~c ~C ~b ~B ~o ~O ~x ~X" 14]
["~ " 1])
))