Compile capturing regexes with the backtracking matcher
irregex builds a POSIX leftmost-longest DFA for a pattern when it can, and jolt
used it for everything. For a pattern with an alternation whose branches have
capturing groups, that DFA leaks a non-participating branch's group: e.g.
#"(?:([0-9])|([0-9])r([0-9]+))" on "2r11" left group 1 = "2" instead of nil, so
tools.reader (rewrite-clj's dep) misread 2r1100 as 2 and 16rFF as 16.
java.util.regex is itself a leftmost-first backtracking engine, so compile a
capturing pattern with irregex's backtracking matcher ('backtrack): its submatch
semantics match the JVM and it clears a losing branch's group. Non-capturing
patterns keep the DFA — with no groups to read, its whole-match result is all a
caller sees, and it avoids backtracking's worst case. The submatch count comes
from a first cheap compile; a capturing pattern recompiles once and caches.
This clears the last rewrite-clj parser-test failure (now 772/0/0). Corpus rows
for the alternation-group case and the radix read. make test green.
This commit is contained in:
parent
e2d842b073
commit
7c4f9bb974
2 changed files with 17 additions and 2 deletions
|
|
@ -159,10 +159,23 @@
|
||||||
;; A jolt regex value: the source string (for printing / str) + the compiled
|
;; A jolt regex value: the source string (for printing / str) + the compiled
|
||||||
;; irregex. regex? recognizes it; the printer renders #"source".
|
;; irregex. regex? recognizes it; the printer renders #"source".
|
||||||
(define-record-type regex-t (fields source irx) (nongenerative jolt-regex-v1))
|
(define-record-type regex-t (fields source irx) (nongenerative jolt-regex-v1))
|
||||||
|
;; A capturing pattern is compiled with irregex's BACKTRACKING matcher ('backtrack),
|
||||||
|
;; not its DFA. java.util.regex is itself a leftmost-first backtracking engine, so
|
||||||
|
;; this matches the JVM's submatch semantics; irregex's DFA is POSIX leftmost-longest
|
||||||
|
;; and, worse, leaks a non-participating alternation group's capture (e.g.
|
||||||
|
;; #"(?:([0-9])|([0-9])r([0-9]+))" on "2r11" left group 1 = "2"), which broke
|
||||||
|
;; tools.reader's number reader. Non-capturing patterns keep the fast DFA — with no
|
||||||
|
;; groups to read, its whole-match result is all a caller sees. The count comes from
|
||||||
|
;; a first cheap compile; a capturing pattern is recompiled once (patterns compile
|
||||||
|
;; once and cache in the regex-t).
|
||||||
(define (jolt-regex source)
|
(define (jolt-regex source)
|
||||||
(let-values (((opts pat) (regex-parse-flags source)))
|
(let-values (((opts pat) (regex-parse-flags source)))
|
||||||
(make-regex-t source
|
(let* ((p (translate-prop-classes (escape-class-shorthand-dash pat)))
|
||||||
(apply irregex (translate-prop-classes (escape-class-shorthand-dash pat)) opts))))
|
(irx (apply irregex p opts)))
|
||||||
|
(make-regex-t source
|
||||||
|
(if (> (irregex-num-submatches irx) 0)
|
||||||
|
(apply irregex p 'backtrack opts)
|
||||||
|
irx)))))
|
||||||
(define (jolt-regex? x) (regex-t? x))
|
(define (jolt-regex? x) (regex-t? x))
|
||||||
(define (jolt-re-pattern x) (if (regex-t? x) x (jolt-regex x)))
|
(define (jolt-re-pattern x) (if (regex-t? x) x (jolt-regex x)))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
[
|
[
|
||||||
{:suite "host-interop / reify ILookup" :label "(get reify k) / (:k reify) / (get reify k d) route to valAt" :expected "[1 1 :dflt]" :actual "(let [r (reify clojure.lang.ILookup (valAt [_ k] (get {:a 1} k)) (valAt [_ k d] (get {:a 1} k d)))] [(:a r) (get r :a) (get r :z :dflt)])"}
|
{:suite "host-interop / reify ILookup" :label "(get reify k) / (:k reify) / (get reify k d) route to valAt" :expected "[1 1 :dflt]" :actual "(let [r (reify clojure.lang.ILookup (valAt [_ k] (get {:a 1} k)) (valAt [_ k d] (get {:a 1} k d)))] [(:a r) (get r :a) (get r :z :dflt)])"}
|
||||||
|
{:suite "regex / alternation submatch" :label "a non-participating alternation group is nil, not a stale capture" :expected "[\"2r11\" nil \"2\" \"11\"]" :actual "(re-matches #\"(?:([0-9])|([0-9])r([0-9]+))\" \"2r11\")"}
|
||||||
|
{:suite "regex / radix read" :label "tools.reader-style radix parse: winning branch's groups, not the losing one's" :expected "[12 255]" :actual "[(read-string \"2r1100\") (read-string \"16rFF\")]"}
|
||||||
{:suite "numbers / comparison error class" :label "nil operand is NPE, non-number is CCE" :expected "[\"java.lang.NullPointerException\" \"java.lang.ClassCastException\"]" :actual "[(try (> nil 5) (catch Throwable e (.getName (class e)))) (try (> :k 5) (catch Throwable e (.getName (class e))))]"}
|
{:suite "numbers / comparison error class" :label "nil operand is NPE, non-number is CCE" :expected "[\"java.lang.NullPointerException\" \"java.lang.ClassCastException\"]" :actual "[(try (> nil 5) (catch Throwable e (.getName (class e)))) (try (> :k 5) (catch Throwable e (.getName (class e))))]"}
|
||||||
{:suite "reader / quoted #inst constructs a value" :label "a quoted #inst literal is the constructed Date, not a tagged form" :expected "\"(f #inst \\\"1939-01-01T00:00:00.000-00:00\\\")\"" :actual "(pr-str (quote (f #inst \"1939\")))"}
|
{:suite "reader / quoted #inst constructs a value" :label "a quoted #inst literal is the constructed Date, not a tagged form" :expected "\"(f #inst \\\"1939-01-01T00:00:00.000-00:00\\\")\"" :actual "(pr-str (quote (f #inst \"1939\")))"}
|
||||||
{:suite "fn / kwargs trailing map" :label "(f :k v {map}) and (f {map}) both bind & {:as m}" :expected "[{:x 1 :y 2} {:x 1 :y 2}]" :actual "[((fn [a & {:as m}] m) 1 :x 1 {:y 2}) ((fn [a & {:as m}] m) 1 {:x 1 :y 2})]"}
|
{:suite "fn / kwargs trailing map" :label "(f :k v {map}) and (f {map}) both bind & {:as m}" :expected "[{:x 1 :y 2} {:x 1 :y 2}]" :actual "[((fn [a & {:as m}] m) 1 :x 1 {:y 2}) ((fn [a & {:as m}] m) 1 {:x 1 :y 2})]"}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue