#lang racket
(require rackunit)
(provide parse-expr)
(define-syntax-rule (while test body ...)
(let loop ()
(when test
body ...
(loop))))
(define (ignorable-next-char? in)
(let ([next-ch (peek-char in)])
(cond
[(eof-object? next-ch)
#f]
[else
(not (member next-ch '(#\< #\> #\+ #\- #\, #\. #\[ #\])))])))
(define (parse-expr source-name in)
(while (ignorable-next-char? in) (read-char in))
(let*-values ([(line column position) (port-next-location in)]
[(next-char) (read-char in)])
(define (default-make-syntax type)
(datum->syntax #f
(list type)
(list source-name line column position 1)))
(cond
[(eof-object? next-char) eof]
[else
(case next-char
[(#\<) (default-make-syntax 'less-than)]
[(#\>) (default-make-syntax 'greater-than)]
[(#\+) (default-make-syntax 'plus)]
[(#\-) (default-make-syntax 'minus)]
[(#\,) (default-make-syntax 'comma)]
[(#\.) (default-make-syntax 'period)]
[(#\[)
(let*-values ([(elements) (parse-exprs source-name in)]
[(following-line following-column
following-position)
(port-next-location in)])
(datum->syntax #f
`(brackets ,@elements)
(list source-name
line
column
position
(- following-position
position))))]
[(#\])
eof])])))
(define (parse-exprs source-name in)
(let ([next-expr (parse-expr source-name in)])
(cond
[(eof-object? next-expr)
empty]
[else
(cons next-expr (parse-exprs source-name in))])))
(check-equal? eof (parse-expr 'test (open-input-string "")))
(check-equal? '(greater-than)
(syntax->datum (parse-expr 'test (open-input-string ">"))))
(check-equal? '(less-than)
(syntax->datum (parse-expr 'test (open-input-string "<"))))
(check-equal? '(plus)
(syntax->datum (parse-expr 'test (open-input-string "+"))))
(check-equal? '(minus)
(syntax->datum (parse-expr 'test (open-input-string "-"))))
(check-equal? '(comma)
(syntax->datum (parse-expr 'test (open-input-string ","))))
(check-equal? '(period)
(syntax->datum (parse-expr 'test (open-input-string "."))))
(check-equal? '(brackets)
(syntax->datum (parse-expr 'test (open-input-string "[]"))))
(check-equal? '(brackets (brackets))
(syntax->datum (parse-expr 'test (open-input-string "[[]]"))))
(let ([port (open-input-string ",[.,]")])
(check-equal? '(comma)
(syntax->datum (parse-expr 'test port)))
(check-equal? '(brackets (period) (comma))
(syntax->datum (parse-expr 'test port)))
(check-equal? eof
(parse-expr 'test port)))