388 lines
14 KiB
Scheme
388 lines
14 KiB
Scheme
;;; Olin Shivers, June 1998
|
|
;;; Copyright (c) 1998 by the Scheme Underground.
|
|
|
|
;;; One export: (simplify-regexp re) -> re
|
|
|
|
;;; Regexp simplifier
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;;; (| (in c1 ...) (in c2 ...) re ...) => (| (in c1 ... c2 ...) re ...)
|
|
;;; (| (not-in c1 ...) (not-in c2 ...)) => (| (not-in [intersect (c1 ...)
|
|
;;; (c2 ...)])
|
|
;;; A run of BOS's or a run of EOS's in a sequence may be elided.
|
|
;;; Nested exponents can be collapsed (*, +, ?) -- multiply the "from's"
|
|
;;; together; multiply the "to's" together.
|
|
;;; Exponent range [1,1] simplifies, as does [0,0].
|
|
;;; Uniquify branches
|
|
;;; Adjacent literals in a sequence can be collapsed
|
|
;;; A singleton-char char class can be collapsed into a constant
|
|
;;; Nested choices can be collapsed
|
|
;;; Nested sequences can be collapsed
|
|
;;; An empty sequence (:) can be turned into an empty-string match "".
|
|
;;; Singleton choices and sequences can be reduced to their body.
|
|
;;;
|
|
;;; The simplifier is carefully written so that it won't blow up
|
|
;;; when applied to a dynamic regexp -- that is,
|
|
;;; - a chunk of Scheme code that produces a regexp instead of
|
|
;;; an actual regexp value;
|
|
;;; - a repeat regexp whose FROM or TO fields are chunks of Scheme code
|
|
;;; rather than integers;
|
|
;;; - a char-set regexp whose CSET field is a chunk of Scheme code rather
|
|
;;; than an actual char-set value.
|
|
;;; This is useful because the RX macro can build such a regexp as part
|
|
;;; of its expansion process.
|
|
|
|
(define (simplify-regexp re)
|
|
(receive (simp-re pre-dsm) (simp-re re)
|
|
(re-dsm simp-re pre-dsm (- (re-tsm re) (+ (re-tsm simp-re) pre-dsm)))))
|
|
|
|
(define (simp-re re)
|
|
(? ((re-string? re) (values re 0))
|
|
((re-seq? re) (simp-seq re))
|
|
((re-choice? re) (simp-choice re))
|
|
|
|
;; Singleton char-sets reduce to the character.
|
|
;; Bear in mind the cset field might be Scheme code instead
|
|
;; of an actual char set if the regexp is dynamic.
|
|
((re-char-set? re)
|
|
(values (let ((cs (re-char-set:cset re)))
|
|
(if (and (char-set? cs)
|
|
(= 1 (char-set-size cs)))
|
|
(make-re-string (string (car (char-set->list cs))))
|
|
re))
|
|
0))
|
|
|
|
((re-repeat? re) (simp-repeat re))
|
|
|
|
((re-submatch? re) (simp-submatch re))
|
|
((re-dsm? re) (simp-dsm re))
|
|
|
|
(else (values re 0))))
|
|
|
|
|
|
|
|
;;; If the body of a submatch is the empty re, reduce it to the empty re.
|
|
|
|
(define (simp-submatch re)
|
|
(let ((tsm (re-submatch:tsm re))
|
|
(pre-dsm (re-submatch:pre-dsm re)))
|
|
(receive (body1 pre-dsm1) (simp-re (re-submatch:body re))
|
|
(if (re-empty? body1)
|
|
(values re-empty tsm)
|
|
(values (%make-re-submatch body1 (+ pre-dsm pre-dsm1) tsm)
|
|
0)))))
|
|
|
|
;;; - Flatten nested DSM's.
|
|
;;; - Return pre-dsm field and body field as the two return values.
|
|
|
|
(define (simp-dsm re)
|
|
(receive (body pre-dsm1) (simp-re (re-dsm:body re))
|
|
(values body (+ (re-dsm:pre-dsm re) pre-dsm1))))
|
|
|
|
|
|
|
|
;;; Simplifying sequences
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;;; - Collapse nested sequences and DSM's.
|
|
;;; - Merge adjacent strings, identical adjacent anchors (bos, eos, etc.).
|
|
;;; - Bubble DSM's forwards past elts that don't contain live submatches.
|
|
;;; (Going past live submatches would switch the submatch indexes around,
|
|
;;; which would be an error). This helps to coalesce DSMs and if we bring
|
|
;;; them all the way to the front, we can pop them off and make them a
|
|
;;; pre-dsm for the entire seq record.
|
|
;;; - If an elt is the re-empty, reduce the whole re to the empty re.
|
|
;;; - Reduce singleton and empty seq.
|
|
|
|
(define (simp-seq re)
|
|
(let ((tsm (re-seq:tsm re))
|
|
(elts (map simplify-regexp (re-seq:elts re))))
|
|
(if (pair? elts)
|
|
|
|
(call-with-current-continuation
|
|
(lambda (abort)
|
|
(receive (pre-dsm head tail) (simp-seq1 elts abort tsm)
|
|
(values (if (pair? tail)
|
|
(%make-re-seq (cons head tail) (- tsm pre-dsm))
|
|
head) ; Singleton seq
|
|
pre-dsm))))
|
|
|
|
(values re-trivial 0)))) ; Empty seq
|
|
|
|
|
|
;;; Simplify the non-empty sequence ELTS.
|
|
;;; - Return the result split out into three values:
|
|
;;; [head-elt-pre-dsm, head-elt, tail].
|
|
;;; - If any elt is the empty (impossible) re, abort by calling
|
|
;;; (abort elt tsm). TSM is otherwise unused.
|
|
|
|
(define (simp-seq1 elts abort tsm)
|
|
(let recur ((elt (car elts)) (elts (cdr elts)))
|
|
(receive (elt pre-dsm) (open-dsm elt)
|
|
(? ((re-seq? elt) ; Flatten nested seqs.
|
|
(let ((sub-elts (re-seq:elts elt)))
|
|
(recur (re-dsm (car sub-elts) pre-dsm 0)
|
|
(append (cdr sub-elts) elts))))
|
|
|
|
((re-empty? elt) (abort elt tsm)) ; Bomb out on the empty
|
|
; (impossible) re.
|
|
((pair? elts)
|
|
(receive (next-pre-dsm next tail) ; Simplify the tail,
|
|
(recur (car elts) (cdr elts)) ; then think about
|
|
; the head:
|
|
;; This guy is called when we couldn't find any other
|
|
;; simplification. If ELT contains live submatches, then
|
|
;; there really is nothing to be done at this step -- just
|
|
;; assemble the pieces together and return them. If ELT
|
|
;; *doesn't* contain any live submatches, do the same, but
|
|
;; bubble its following next-pre-dsm submatches forwards.
|
|
(define (no-simp)
|
|
(if (has-live-submatches? elt)
|
|
(values pre-dsm elt (cons (re-dsm next next-pre-dsm 0) tail))
|
|
(values (+ pre-dsm next-pre-dsm) elt (cons next tail))))
|
|
|
|
;; Coalesces two adjacent bol's, two adjacent eol's, etc.
|
|
(define (coalesce-anchor anchor?)
|
|
(if (and (anchor? elt) (anchor? next))
|
|
(values (+ pre-dsm next-pre-dsm) elt tail)
|
|
(no-simp)))
|
|
|
|
(? ((re-trivial? elt) ; Drop trivial re's.
|
|
(values (+ pre-dsm next-pre-dsm) next tail))
|
|
|
|
;; Coalesce adjacent strings
|
|
((re-string? elt)
|
|
(if (re-string? next)
|
|
(values (+ pre-dsm next-pre-dsm)
|
|
(make-re-string (string-append (re-string:chars elt)
|
|
(re-string:chars next)))
|
|
tail)
|
|
(no-simp)))
|
|
|
|
;; Coalesce adjacent bol/eol/bos/eos's.
|
|
((re-bol? elt) (coalesce-anchor re-bol?))
|
|
((re-eol? elt) (coalesce-anchor re-eol?))
|
|
((re-bos? elt) (coalesce-anchor re-bos?))
|
|
((re-eos? elt) (coalesce-anchor re-eos?))
|
|
(else (no-simp)))))
|
|
|
|
(else (values pre-dsm elt '()))))))
|
|
|
|
|
|
|
|
;;; Simplifying choices
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;;; - Collapse nested choices and DSM's.
|
|
;;; - Delete re-empty's.
|
|
;;; - Merge sets; merge identical anchors (bos, eos, etc.).
|
|
;;; But you can't merge across an element that contains a live submatch,
|
|
;;; see below.
|
|
;;; - A singleton string "c" is included into the char-set merge as a
|
|
;;; singleton set.
|
|
;;; - Bubble DSM's forwards past elts that don't contain live submatches.
|
|
;;; (Going past live submatches would switch the submatch indexes around,
|
|
;;; which would be an error). This helps to coalesce DSMs and if we bring
|
|
;;; them all the way to the front, we can pop them off and make them a
|
|
;;; pre-dsm for the entire seq record.
|
|
;;; - Reduce singleton and empty choice.
|
|
;;;
|
|
;;; You have to be careful simplifying choices -- you can't merge two sets
|
|
;;; that appear on different sides of an element containing a live submatch.
|
|
;;; The problem is that the assignment of submatches breaks ties left-to-right.
|
|
;;; So these aren't the same:
|
|
;;; (| (submatch "x") any) (| any (submatch "x"))
|
|
;;; The first assigns the submatch, the second doesn't -- the ANY gets credit.
|
|
;;; We want to collapse multiple char-sets, bos's, and eos's, but we have
|
|
;;; to deal with this issue. So
|
|
;;; - When we coalesce anchors, we retain the *leftmost* one.
|
|
;;; - We coalesce sets that appear between live-submatch boundaries.
|
|
;;; When we do this, we subtract from the set any char that was in
|
|
;;; an earlier coalesced char-set. If this gets us down to the empty set,
|
|
;;; we drop it. If it gets us down to a singleton set, we convert it into
|
|
;;; a singleton string.
|
|
;;; Whew. I had to think about this one.
|
|
|
|
(define (simp-choice re)
|
|
(let ((tsm (re-choice:tsm re)))
|
|
|
|
(receive (pre-dsm cset bos? eos? bol? eol? tail)
|
|
(simp-choice1 (map simplify-regexp (re-choice:elts re)))
|
|
|
|
(let ((tail (assemble-boundary-tail char-set:empty cset
|
|
bos? eos? bol? eol?
|
|
#f #f #f #f
|
|
tail)))
|
|
(values (if (pair? tail)
|
|
(if (pair? (cdr tail))
|
|
(%make-re-choice tail (- tsm pre-dsm))
|
|
(car tail)) ; Singleton choice
|
|
re-empty) ; Empty choice
|
|
pre-dsm)))))
|
|
|
|
|
|
|
|
;;; Given the return values from simp-choice1, this tacks all
|
|
;;; the various pieces (CSET, BOS?, EOS?, etc.) onto the front of
|
|
;;; TAIL. However, elements are not added onto TAIL that are already
|
|
;;; described by PREV-CSET, PREV-BOS?, etc. -- they will be added onto
|
|
;;; some earlier bit of the final result.
|
|
|
|
(define (assemble-boundary-tail prev-cset cset
|
|
bos? eos? bol? eol?
|
|
prev-bos? prev-eos?
|
|
prev-bol? prev-eol?
|
|
tail)
|
|
(let* ((cset (char-set-difference cset prev-cset))
|
|
(numchars (char-set-size cset))
|
|
(tail (if (and eos? (not prev-eos?)) (cons re-eos tail) tail))
|
|
(tail (if (and eol? (not prev-eol?)) (cons re-eol tail) tail))
|
|
(tail (if (and bol? (not prev-bol?)) (cons re-bol tail) tail))
|
|
(tail (if (and bos? (not prev-bos?)) (cons re-bos tail) tail))
|
|
(tail (? ((zero? numchars) tail) ; Drop empty char set.
|
|
((= 1 numchars) ; {c} => "c"
|
|
(cons (make-re-string (string (car (char-set->list cset))))
|
|
tail))
|
|
(else (cons (make-re-char-set cset) tail)))))
|
|
tail))
|
|
|
|
|
|
;;; Simplify the non-empty list of choices ELTS.
|
|
;;; Return the result split out into the values
|
|
;;; [pre-dsm, cset, bos?, eos?, bol?, eol?, tail]
|
|
|
|
(define (simp-choice1 elts)
|
|
(let recur ((elts elts)
|
|
|
|
(prev-cset char-set:empty) ; Chars we've already seen.
|
|
|
|
(prev-bos? #f) (prev-eos? #f) ; These flags say if we've
|
|
(prev-bol? #f) (prev-eol? #f)) ; already seen one of these anchors.
|
|
|
|
|
|
(if (pair? elts)
|
|
(let ((elt (car elts))
|
|
(elts (cdr elts)))
|
|
(receive (elt pre-dsm) (open-dsm elt)
|
|
(if (re-choice? elt)
|
|
|
|
;; Flatten nested choices.
|
|
(let ((sub-elts (re-seq:elts elt)))
|
|
(receive (tail-pre-dsm cset bos? eos? bol? eol? tail)
|
|
(recur (append sub-elts elts)
|
|
prev-cset
|
|
prev-bos? prev-eos?
|
|
prev-bol? prev-eol?)
|
|
(values (+ pre-dsm tail-pre-dsm)
|
|
cset bos? eos? bol? eol? tail)))
|
|
|
|
;; Simplify the tail, then think about the head.
|
|
(receive (tail-pre-dsm cset bos? eos? bol? eol? tail)
|
|
(recur elts
|
|
(? ((and (re-string? elt)
|
|
(= 1 (string-length (re-string:chars elt))))
|
|
(char-set-union prev-cset
|
|
(string->char-set (re-string:chars elt))))
|
|
|
|
;; The cset might be a Scheme exp.
|
|
((and (re-char-set? elt)
|
|
(char-set? (re-char-set:cset elt)))
|
|
(char-set-union prev-cset
|
|
(re-char-set:cset elt)))
|
|
|
|
(else prev-cset))
|
|
(or prev-bos? (re-bos? elt))
|
|
(or prev-eos? (re-eos? elt))
|
|
(or prev-bol? (re-bol? elt))
|
|
(or prev-eol? (re-eol? elt)))
|
|
|
|
;; This guy is called when we couldn't find any other
|
|
;; simplification. If ELT contains live submatches, then we
|
|
;; are at a merge boundary, and have to take all the
|
|
;; TAIL-PRE-DSM, CSET, BOS?, EOS?, ... stuff we've collected
|
|
;; and tack them onto TAIL as elements, then put ELT on
|
|
;; front. Otherwise, we can commute TAIL-PRE-DSM, CSET,
|
|
;; BOS?, etc. with ELT, since it contains no live
|
|
;; submatches, so just tack ELT onto TAIL.
|
|
|
|
(define (no-simp)
|
|
(if (has-live-submatches? elt)
|
|
(let ((tail (assemble-boundary-tail prev-cset cset
|
|
bos? eos?
|
|
bol? eol?
|
|
prev-bos? prev-eos?
|
|
prev-bol? prev-eol?
|
|
tail)))
|
|
(values pre-dsm char-set:empty #f #f #f #f
|
|
(if (pair? tail)
|
|
;; Tack tail-pre-dsm onto
|
|
;; TAIL's first elt.
|
|
(cons elt
|
|
(cons (re-dsm (car tail)
|
|
tail-pre-dsm 0)
|
|
(cdr tail)))
|
|
|
|
;; Squirrel case: TAIL is empty, so use
|
|
;; TAIL-PRE-DSM as ELT's post-dsm.
|
|
(list (re-dsm elt 0 tail-pre-dsm)))))
|
|
|
|
;; ELT has no live submatches, so we can commute all
|
|
;; the recursion state forwards past it.
|
|
(values (+ pre-dsm tail-pre-dsm)
|
|
cset bos? eos? bol? eol?
|
|
(cons elt tail))))
|
|
|
|
(? ((and (re-char-set? elt)
|
|
(char-set? (re-char-set:cset elt))) ; Might be Scheme code
|
|
(values (+ pre-dsm tail-pre-dsm)
|
|
(char-set-union cset (re-char-set:cset elt))
|
|
bos? eos? bol? eol? tail))
|
|
|
|
;; Treat a singleton string "c" as a singleton set {c}.
|
|
((and (re-string? elt) (= 1 (string-length (re-string:chars elt))))
|
|
(values (+ pre-dsm tail-pre-dsm)
|
|
(char-set-union cset (string->char-set (re-string:chars elt)))
|
|
bos? eos? bol? eol? tail))
|
|
|
|
;; Coalesce bol/eol/bos/eos's.
|
|
((re-bos? elt) (values (+ pre-dsm tail-pre-dsm) cset
|
|
#t eos? bol? eol? tail))
|
|
((re-eos? elt) (values (+ pre-dsm tail-pre-dsm) cset
|
|
bos? #t bol? eol? tail))
|
|
((re-bol? elt) (values (+ pre-dsm tail-pre-dsm) cset
|
|
bos? eos? #t eol? tail))
|
|
((re-eol? elt) (values (+ pre-dsm tail-pre-dsm) cset
|
|
bos? eos? bol? #t tail))
|
|
|
|
(else (no-simp)))))))
|
|
|
|
(values 0 char-set:empty #f #f #f #f '()))))
|
|
|
|
|
|
|
|
(define (simp-repeat re)
|
|
(let ((from (re-repeat:from re))
|
|
(to (re-repeat:to re))
|
|
(body (re-repeat:body re)))
|
|
(receive (simp-body pre-dsm) (simp-re body) ; Simplify body.
|
|
;; The fancy reductions are all handled by REDUCE-REPEAT.
|
|
(reduce-repeat from to simp-body pre-dsm))))
|
|
|
|
|
|
|
|
;;; Does RE contain a live submatch?
|
|
;;; If RE is dynamic, we can't tell, so we err conservatively,
|
|
;;; which means we say "yes."
|
|
|
|
(define (has-live-submatches? re)
|
|
(or (re-submatch? re)
|
|
(? ((re-seq? re) (every has-live-submatches? (re-seq:elts re)))
|
|
((re-choice? re) (every has-live-submatches? (re-choice:elts re)))
|
|
((re-repeat? re) (has-live-submatches? (re-repeat:body re)))
|
|
((re-dsm? re) (has-live-submatches? (re-dsm:body re)))
|
|
|
|
;; If it's not one of these things, then this isn't a regexp -- it's
|
|
;; a chunk of Scheme code producing a regexp, and we conservatively
|
|
;; return #T -- the expression *might* produce a regexp containing
|
|
;; a live submatch:
|
|
(else (not (or (re-char-set? re) (re-string? re)
|
|
(re-bos? re) (re-eos? re)
|
|
(re-bol? re) (re-eol? re)))))))
|