scsh-0.6/scsh/rx/posixstr.scm

633 lines
22 KiB
Scheme

;;; Regexp-ADT -> Posix-string translator.
;;; Olin Shivers January 1997, May 1998.
;;; - If the regexp value contains nul character constants, or character sets
;;; that contain the nul character, they will show up in the Posix string
;;; we produce. Spencer's C regexp engine can handle regexp strings that
;;; contain nul bytes, but this might blow up other implementations -- that
;;; is, the nul byte might prematurely terminate the C string passed to the
;;; regexp engine.
;;;
;;; - The code is ASCII-specific in only one place: the expression for
;;; a regexp that matches nothing is the 6-char pattern "[^\000-\177]",
;;; which assumes a 7-bit character code. Note that the static simplifier
;;; can remove *all* occurences of this "empty regexp" except for the
;;; un-simplifiable case of a single, top-level empty regexp, e.g.
;;; (rx (in))
;;; We can handle this one special case specially, so we shouldn't *ever*
;;; have to produce this ASCII-specific pattern.
;;; Exports: regexp->posix-string
;;; Todo: A dumb, simple char-set renderer.
;;; These functions translate static regular expressions into Posix regexp
;;; strings. They generally return four values:
;;; - string (regexp)
;;;
;;; - syntax level: 0 parenthesized exp, 1 piece, 2 branch, 3 top
;;; ("piece", "branch" and "top" are Spencer's terms):
;;; + A parenthesized exp is syntactically equivalent to a piece.
;;; (But it's useful to know when an exp is parenthesized for
;;; eliminating redundant submatch-generated parens.)
;;; + A piece is something that would bind to a following *
;;; ("a" but not "aa").
;;; + A branch is a sequence of pieces -- something that would bind to a |
;;; ("ab*d" but not "ab*|d"). That is, a branch is not allowed to contain
;;; top-level |'s.
;;; + Top is for a sequence of branches -- "a|b*c|d".
;;;
;;; - paren count in the returned string.
;;;
;;; [This is a newer description; is it correct?]
;;; - A vector mapping submatches (vector index 0 is submatch 1)
;;; to the paren for that submatch (the first paren is paren #1).
;;;
;;; [This is my original description.]
;;; - Vector of parens numbers used for submatching. The first paren is
;;; numbered 1. #F means a dead submatch -- one we can tell statically
;;; will never match anything.
;;; Non-R4RS imports:
;;; ? = COND
;;; Multiple-value return: VALUES RECEIVE CALL-WITH-VALUES
;;; SORT-LIST
;;; Useful little utility -- pad vector V with
;;; PRE initial and POST following #f's.
(define (pad-vector pre post v)
(if (= pre post 0) v
(let* ((vlen (vector-length v))
(alen (+ pre post vlen))
(ans (make-vector alen #f)))
(do ((from (- vlen 1) (- from 1))
(to (+ pre vlen -1) (- to 1)))
((< from 0))
(vector-set! ans to (vector-ref v from)))
ans)))
(define (n-falses n) (make-vector n #f))
;;; There's no representation for regexps that never match anything (e.g.,
;;; (|)) in strict Posix notation. When we get one of these, we treat it
;;; specially, producing [#f #f #f #f].
;;;
;;; We can always detect these empty regexps, because they always simplify
;;; to one of these two values:
;;; - (make-re-char-set char-set:empty)
;;; - (dsm m n (make-re-char-set char-set:empty))
(define (simple-empty-re? re)
(or (and (re-char-set? re)
(char-set-empty? (re-char-set:cset re)))
(and (re-dsm? re)
(simple-empty-re? (re-dsm:body re)))))
;;; Top-level
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (regexp->posix-string re)
;; We *must* simplify, to guarantee correct translation.
(let ((re (simplify-regexp re)))
(if (simple-empty-re? re) (values #f #f #f #f)
(translate-regexp re))))
(define (translate-regexp re)
(cond
((re-string? re) (translate-string (re-string:chars re)))
((re-repeat? re) (translate-repeat re))
((re-choice? re) (translate-choice re))
((re-seq? re) (translate-seq re))
((re-char-set? re) (translate-char-set (re-char-set:cset re)))
((re-submatch? re) (translate-submatch re))
((re-bos? re) (values "^" 1 0 '#()))
((re-eos? re) (values "$" 1 0 '#()))
((re-bol? re) (error "Beginning-of-line regexp not supported in this implementation."))
((re-eol? re) (error "End-of-line regexp not supported in this implementation."))
((re-dsm? re) (let ((pre-dsm (re-dsm:pre-dsm re))
(body (re-dsm:body re)))
(translate-dsm body pre-dsm
(- (re-dsm:tsm re)
(+ pre-dsm (re-tsm body))))))
(else (error "Illegal regular expression" re))))
;;; Translate reloc-elt ELT = (N . RE) from a sequence or choice
;;; into a Posix string.
;;; - Relocate the submatch indices by PREV-PCOUNT.
;;; (That is, assume rendering preceding elts used PREV-PCOUNT parens.)
;;; - Assume preceding elements allocated PREV-SMCOUNT submatches
;;; (we may have to pad our returned submatches string with some
;;; initial #F's to account for dead submatches PREV-SMCOUNT through N.)
;;; - If SUB-LEV3? is true, the result string is guaranteed to be < level 3.
;;; This is used by the & and | translators.
;;; - Returns the usual 4 values plus the final submatch count including
;;; this regexp.
(define (translate-elt elt prev-pcount prev-smcount sub-lev3?)
(let ((offset (car elt))
(re (cdr elt)))
(receive (s level pcount submatches) (translate-regexp re)
;; Relocate submatch indices by OFFSET and force level <3, if needed:
(receive (s level pcount submatches)
(if (and sub-lev3? (= level 3))
(values (string-append "(" s ")")
0
(+ pcount 1)
(mapv (lambda (sm) (and sm (+ prev-pcount 1 sm)))
submatches))
(values s level pcount
(mapv (lambda (sm) (and sm (+ prev-pcount sm)))
submatches)))
;; Tack onto submatches as many initial #F's as needed to bump
;; the previous submatches count from PREV-SMCOUNT to OFFSET.
(values s level pcount
(pad-vector (- offset prev-smcount) 0 submatches)
(+ offset (re-tsm re)))))))
;;; Force the string to be level < 3 by parenthesizing it if necessary.
(define (paren-if-necessary s lev pcount submatches)
(if (< lev 3)
(values s lev pcount submatches)
(values (string-append "(" s ")")
0
(+ pcount 1)
(mapv (lambda (sm) (and sm (+ 1 sm)))
submatches))))
;;; (: re1 ... ren)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (translate-seq re)
(let ((elts (re-seq:elts re))
(tsm (re-seq:tsm re)))
(let recur ((elts elts) (prev-pcount 0) (prev-smcount 0))
;; Render a sequence tail ELTS, assuming the previous elements translated
;; to a string with PREV-PCOUNT parens, and allocated PREV-SMCOUNT
;; submatches.
(if (pair? elts)
(let* ((elt (car elts))
(elts (cdr elts)))
(receive (s1 level1 pcount1 submatches1)
(translate-regexp elt)
(receive (s1 level1 pcount1 submatches1)
(paren-if-necessary s1 level1 pcount1 submatches1)
(receive (s level pcount submatches)
(recur elts
(+ pcount1 prev-pcount)
(+ prev-smcount (re-tsm elt)))
(values (string-append s1 s)
2
(+ pcount1 pcount)
(vector-append (mapv (lambda (p) (and p (+ p prev-pcount)))
submatches1)
submatches))))))
(values "" 2 0 '#()))))) ; Empty seq
;;; (| re1 ... ren)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (translate-choice re)
(let ((elts (re-choice:elts re))
(tsm (re-choice:tsm re)))
(if (pair? elts)
(let recur ((elts elts) (prev-pcount 0) (prev-smcount 0))
;; ELTS is a non-empty choice tail. Render it, assuming the
;; previous elements translated to a string with PREV-PCOUNT parens,
;; and allocated PREV-SMCOUNT submatches.
(let ((elt (car elts)) (tail (cdr elts)))
(receive (s1 level1 pcount1 submatches1) (translate-regexp elt)
(let ((submatches1 (mapv (lambda (sm) (and sm (+ sm prev-pcount)))
submatches1)))
(if (pair? tail)
(receive (s level pcount submatches)
(recur tail
(+ pcount1 prev-pcount)
(+ prev-smcount (re-tsm elt)))
(values (string-append s1 "|" s) 3
(+ pcount1 pcount)
(vector-append submatches1 submatches)))
(values s1 level1 pcount1 submatches1))))))
(values "[^\000-\377]" 1 0 (n-falses tsm))))) ; Empty choice.
;;; Repeated cases: * + ? and {n,m} ranges.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (translate-repeat re)
(let ((from (re-repeat:from re))
(to (re-repeat:to re))
(body (re-repeat:body re))
(tsm (re-repeat:tsm re)))
(cond
((and to (> from to)) ; Unsatisfiable
(values "[^\000-\377]" 1 0 (n-falses tsm)))
((and to (= from to 1)) (translate-seq body)) ; RE{1,1} => RE
((and to (= to 0)) ; RE{0,0} => ""
(values "" 2 0 (n-falses tsm)))
(else ; General case
(receive (s level pcount submatches) (translate-regexp body)
(receive (s level pcount submatches) ; Coerce S to level <2.
(if (> level 1)
(values (string-append "(" s ")")
0
(+ pcount 1)
(mapv (lambda (i) (and i (+ i 1))) submatches))
(values s level pcount submatches))
(values (if to
(cond ((and (= from 0) (= to 1)) (string-append s "?"))
((= from to)
(string-append s "{" (number->string to) "}"))
(else
(string-append s "{" (number->string from)
"," (number->string to) "}")))
(cond ((= from 0) (string-append s "*"))
((= from 1) (string-append s "+"))
(else (string-append s "{" (number->string from) ",}"))))
1 pcount submatches)))))))
;;; Submatch
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (translate-submatch re)
(let ((body (re-submatch:body re))
(pre-dsm (re-submatch:pre-dsm re)))
;; Translate the body, along with any leading or trailing dead submatches.
(receive (s level pcount submatches)
(translate-dsm body
pre-dsm
(- (re-submatch:tsm re)
(+ 1 pre-dsm (re-tsm body))))
;; If the whole expression isn't already wrapped in a paren, wrap it.
;; This outer paren becomes the new submatch -- add to submatches list.
(if (= level 0)
(values s 0 pcount (vector-append '#(1) submatches))
(values (string-append "(" s ")")
0
(+ pcount 1)
(mapv! (lambda (i) (and i (+ i 1))) ; Excuse me.
(vector-append '#(0) submatches)))))))
;;; Translating DSM
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Translate the body, and paste enough #F's before and after the submatches
;;; list to account for extra dead submatches.
(define (translate-dsm body pre-dsm post-dsm)
(receive (s level pcount submatches) (translate-regexp body)
(values s level pcount (pad-vector pre-dsm post-dsm submatches))))
;;; Constant regexps
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Convert a string into a regexp pattern that matches that string exactly --
;;; quote the special chars with backslashes.
(define translate-string
(let ((specials (string->char-set "{}[.*?()|\\$^+")))
(lambda (s)
(let ((len (string-length s)))
(if (zero? len)
(values "()" 0 1 '#()) ; Special case ""
(let* ((len2 (string-fold (lambda (c len) ; Length of answer str
(+ len (if (char-set-contains? specials c) 2 1)))
0 s))
(s2 (make-string len2))) ; Answer string
;; Copy the chars over to S2.
(string-fold (lambda (c i)
;; Write char C at index I, return the next index.
(let ((i (cond ((char-set-contains? specials c)
(string-set! s2 i #\\)
(+ i 1))
(else i))))
(string-set! s2 i c)
(+ i 1)))
0 s)
(values s2 (if (= len 1) 1 2)
0 '#())))))))
;;; Translating char-sets to [...] strings
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; This is the nastiest code in the system. We make an effort to return
;;; succinct encodings of the char-sets, in the event these encodings are
;;; being shown to humans.
;;; - A singleton set is rendered as that char.
;;; - A full set is rendered as "."
;;; - An empty set is rendered as [^\000-\177].
;;; - Otherwise, render it both as a [...] and as a [^...] spec, and
;;; take whichever is shortest.
;;; Take a char set, and return the standard
;;; [regexp-string, level, pcount, submatches]
;;; quadruple.
;;;
(define *nul* (ascii->char 0))
(define (translate-char-set cset)
(if (char-set-full? cset)
(values "." 1 0 '#()) ; Full set
(let* ((cset (char-set-delete cset *nul*))
(nchars (char-set-size cset))
(->bracket-string (lambda (cset in?)
(receive (loose ranges) (char-set->in-pair cset)
(hack-bracket-spec loose ranges in?)))))
(cond
((= 0 nchars) (values "[^\000-\177]" 1 0 '#())) ; Empty set
((= 1 nchars) ; Singleton set
(translate-string (string (car (char-set->list cset)))))
;; General case. Try both [...] and [^...].
(else (let ((s- (->bracket-string cset #t))
(s+ (->bracket-string
(char-set-delete (char-set-complement cset) *nul*)
#f)))
(values (if (< (string-length s-) (string-length s+))
s- s+)
1 0 '#())))))))
;;; Commentary
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Hacking special chars in character-class strings:
;;; ] - ^ ]...^-
;;; ] - ]...-
;;; ] ^ ]...^
;;; ] ]...
;;; - ^ ...^- (or doubleton screw-case)
;;; - ...-
;;; ^ ...^ (or singleton screw-case)
;;;
;;; Two screw cases:
;;; "^-" must be converted to "-^" for IN.
;;; "^" must be converted to non-class "^" for IN.
;;; Rendering a general char-set into a correct Posix [...] bracket expression
;;; is a complete mess.
;;;
;;; The rules on bracket expressions:
;;; - ] terminates the exp unless it is the first char
;;; (after an optional leading ^).
;;; - .*[\ are not special in bracket expressions.
;;; - However, [. [= and [: *are* special, so you can't follow an
;;; open bracket by one of .=: -- argh. See below.
;;; - ^ isn't special unless it's the first char.
;;; - - is special unless it's first (after an optional ^), last,
;;; or as the ending char in a range (e.g., a--).
;;; This means:
;;; - You must ensure that ] doesn't begin or terminate a range.
;;; - You must ensure that .=: don't follow [
;;; + This can happen in the loose char list;
;;; + This can happen in the range list -- consider the pair of
;;; ranges "x-[.-%" Handle this by prohibiting [ as a range-terminator.
;;; + It can happen at the loose/range boundary: %[:-?
;;; First, run-length encode the set into loose and range-pairs.
;;; If the set is a singleton set, then punt the whole [...] effort,
;;; and do it as a simple char.
;;; Repeat until stable:
;;; - Sort the ranges in this order:
;;; 1. other ranges;
;;; 2. ranges that begin with ^ (not priority)
;;; 3. ranges that begin with .=: (priority)
;;; 4. ranges that end with [ (priority)
;;; This eliminates [. [= [: problems in the ranges, and
;;; minimises the chances of the problem at the loose/range boundary.
;;; and problems with initial ^ chars.
;;; - Sort the loose chars so that ] is first, then -, then .=:, then [,
;;; then others, then ^. This eliminates [. [= [: problems in the loose
;;; chars, and minimises the chances of the problem at the loose/range
;;; boundary.
;;; - Shrink ranges by moving an opening or closing range char into the
;;; loose-char set:
;;; + If ] opens or closes a range, shrink it out.
;;; + If any range opens with -, shrink it out.
;;; + If the first range opens with .=:, and the last loose char is [,
;;; shrink it out.
;;; + If there are no loose chars, the first range begins with ^, and
;;; we're doing an IN range, shrink out the ^.
;;; + Shrinking a range down to <3 chars means move it's elts into the
;;; loose char set.
;;; - If both [ and - are in the loose char set,
;;; pull - out as special end-hypen.
;;; Finally, we have to hack things so that ^ doesn't begin an IN sequence.
;;; - If it's a NOT-IN sequence, no worries.
;;; - If ^ is the opening loose char, then it's the only loose char.
;;; If there are ranges, move it to the end of the string.
;;; If there are no ranges, then just punt the char-class and convert
;;; it to a singleton ^. In fact, do this up-front, for any singleton
;;; set.
;;;
;;; If the special end-hyphen flag is set, add - to the end of the string.
;;; This general approach -- starting out with maximal ranges, and then
;;; shrinking them to avoid other syntax violations -- has the advantage
;;; of not relying on the details of the ASCII encodings.
;;; Ordering ranges:
;;; 1. other ranges (ordered by start char)
;;; 2. ranges that begin with ^ (not priority)
;;; 3. ranges that begin with .=:
;;; 4. ranges that end with [ (priority over #2 & #3)
(define (range< r1 r2)
(let ((r1-start (car r1)) (r1-end (cdr r1))
(r2-start (car r2)) (r2-end (cdr r2)))
(or (char=? r2-end #\[) ; Range ending with [ comes last.
(and (not (char=? r1-end #\[))
;; Range begin with one of .=: comes next-to-last
(or (char=? r2-start #\.) (char=? r2-start #\=) (char=? r2-start #\:)
(and (not (char=? r1-start #\.))
(not (char=? r1-start #\=))
(not (char=? r1-start #\:))
;; Range beginning with ^ comes before that.
(or (char=? r1-start #\^)
(and (not (char=? r2-start #\^))
;; Other ranges are ordered by start char.
(< (char->ascii r1-start)
(char->ascii r2-start))))))))))
;;; Order loose chars:
;;; ] is first,
;;; - is next,
;;; .=: are next,
;;; [ is next,
;;; then others (ordered by ascii val)
;;; ^ is last.
(define (loose<= c1 c2)
(or (char=? c1 #\]) ; ] is first,
(and (not (char=? c2 #\]))
(or (char=? c1 #\-) ; - is next,
(and (not (char=? c2 #\-))
;; .=: are next,
(or (char=? c1 #\.) (char=? c1 #\=) (char=? c1 #\:)
(and (not (char=? c2 #\.))
(not (char=? c2 #\=))
(not (char=? c2 #\:))
(or (char=? c1 #\[) ; [ is next,
(and (not (char=? c2 #\[))
(or (char=? c2 #\^) ; ^ is last,
(and (not (char=? c1 #\^))
;; other chars by ASCII.
(<= (char->ascii c1)
(char->ascii c2)))))))))))))
;;; Returns (1) a list of 0-3 loose chars, (2) a list of 0 or 1 ranges.
(define (shrink-range-start r)
(let ((start (char->ascii (car r)))
(end (char->ascii (cdr r))))
(shrink-range-finish-up start (+ start 1) end)))
(define (shrink-range-end r)
(let ((start (char->ascii (car r)))
(end (char->ascii (cdr r))))
(shrink-range-finish-up end start (- end 1))))
(define (shrink-range-finish-up c start end)
(cond
((> start end) (values (list (ascii->char c)) '())) ; Empty range
((= start end) ; Collapse singleton range.
(values (list (ascii->char c) (ascii->char start))
'()))
((= (+ start 1) end) ; Collapse doubleton range.
(values (list (ascii->char c) (ascii->char start) (ascii->char end))
'()))
(else (values (list (ascii->char c))
(list (cons (ascii->char start) (ascii->char end)))))))
;;; We assume the bracket-spec is not a singleton, not empty, and not complete.
;;; (These cases get rendered as the letter, [^\000-\177], and ".",
;;; respectively.) We assume the loose chars and the ranges are all disjoint.
(define (hack-bracket-spec loose ranges in?)
(let lp ((loose0 loose) (ranges0 ranges) (end-hyphen? #f))
;; Repeat until stable:
(let ((loose (sort-list loose0 loose<=)) ; Sort loose chars and ranges.
(ranges (sort-list ranges0 range<)))
;; If ] opens or closes a range, shrink it out.
;; If - opens a range, shrink it out.
(receive (loose ranges)
(let recur ((ranges ranges))
(if (pair? ranges)
(let* ((range (car ranges))
(start (car range))
(end (cdr range))
(ranges (cdr ranges)))
(receive (new-loose new-ranges) (recur ranges)
(receive (new-loose0 new-ranges0)
(cond ((char=? #\] start)
(shrink-range-start range))
((char=? #\] end)
(shrink-range-end range))
((char=? #\- start)
(shrink-range-start range))
(else (values '() (list range))))
(values (append new-loose0 new-loose)
(append new-ranges0 new-ranges)))))
(values loose '())))
(let ((loose (sort-list loose loose<=)) ; Sort loose chars and ranges.
(ranges (sort-list ranges range<)))
(cond
((or (not (equal? loose0 loose)) ; Loop if anything changed.
(not (equal? ranges0 ranges)))
(lp loose ranges end-hyphen?))
;; If the first range opens with .=:, and the last loose char is [,
;; shrink it out & loop.
((and (pair? ranges)
(memv (caar ranges) '(#\. #\= #\:))
(pair? loose)
(char=? #\[ (car (reverse loose))))
(receive (new-loose new-ranges)
(shrink-range-start (car ranges))
(lp (append new-loose loose) (append new-ranges (cdr ranges)) end-hyphen?)))
;; If there are no loose chars, the first range begins with ^, and
;; we're doing an IN range, shrink out the ^.
((and in? (null? loose) (pair? ranges) (char=? #\^ (caar ranges)))
(receive (new-loose new-ranges) (shrink-range-start (car ranges))
(lp (append new-loose loose) (append new-ranges ranges) end-hyphen?)))
;; If both [ and - are in the loose char set,
;; pull - out as special end-hypen.
((and (pair? loose)
(pair? (cdr loose))
(char=? (car loose) #\[)
(char=? (car loose) #\-))
(lp (cons (car loose) (cddr loose)) ranges #t))
;; No change! Build the answer...
(else (string-append (if in? "[" "[^")
(list->string loose)
(apply string-append
(map (lambda (r) (string (car r) #\- (cdr r)))
ranges))
"]"))))))))