From 55bbcccf26e95bcb69a27431d72a802b7e457a75 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mattias=20Engdeg=C3=A5rd?= <mattiase@acm.org>
Date: Mon, 7 Oct 2019 19:39:08 +0200
Subject: [PATCH 3/3] Add rx `unordered-or' construct

* lisp/emacs-lisp/rx.el (rx--translate-or, rx--translate-form)
(rx--builting-forms, rx):
* test/lisp/emacs-lisp/rx-tests.el (rx-unordered-or):
* doc/lispref/searching.texi (Rx Constructs):
* etc/NEWS:
Add `unordered-or', like `or' but with unconstrained matching order.
---
 doc/lispref/searching.texi       |  8 ++++++++
 etc/NEWS                         |  5 +++++
 lisp/emacs-lisp/rx.el            | 16 +++++++---------
 test/lisp/emacs-lisp/rx-tests.el |  8 ++++++++
 4 files changed, 28 insertions(+), 9 deletions(-)

diff --git a/doc/lispref/searching.texi b/doc/lispref/searching.texi
index a6c6bf2d4a..faeedc5978 100644
--- a/doc/lispref/searching.texi
+++ b/doc/lispref/searching.texi
@@ -1084,6 +1084,13 @@ Rx Constructs
 Without arguments, the expression will not match anything at all.@*
 Corresponding string regexp: @samp{@var{A}\|@var{B}\|@dots{}}.
 
+@item @code{(unordered-or @var{rx}@dots{})}
+@cindex @code{unordered-or} in rx
+Like @code{or}, but with unspecified matching order.
+This may be more efficient when the order doesn't matter,
+in particular if all subforms are string literals.
+@xref{regexp-opt}.
+
 @item @code{unmatchable}
 @cindex @code{unmatchable} in rx
 Refuse any match.  Equivalent to @code{(or)}.
@@ -1728,6 +1735,7 @@ Regexp Functions
 any special characters.
 @end defun
 
+@anchor{regexp-opt}
 @cindex optimize regexp
 @defun regexp-opt strings &optional paren keep-order
 This function returns an efficient regular expression that will match
diff --git a/etc/NEWS b/etc/NEWS
index 96c26c6623..cf3ef8183b 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -1800,6 +1800,11 @@ These macros add new forms to the rx notation.
 *** 'anychar' is now an alias for 'anything'
 Both match any single character; 'anychar' is more descriptive.
 
++++
+*** New 'unordered-or' rx construct
+It works like 'or', but with unspecified matching order.  It may be
+faster in some cases, especially when the clauses are string literals.
+
 ** Frames
 
 +++
diff --git a/lisp/emacs-lisp/rx.el b/lisp/emacs-lisp/rx.el
index cf02df239f..0b14144698 100644
--- a/lisp/emacs-lisp/rx.el
+++ b/lisp/emacs-lisp/rx.el
@@ -125,11 +125,6 @@ rx--lookup-def
   (or (cdr (assq name rx--local-definitions))
       (get name 'rx-definition)))
 
-;; TODO: Additions to consider:
-;; - A construct like `or' but without the match order guarantee,
-;;   maybe `unordered-or'.  Useful for composition or generation of
-;;   alternatives; permits more effective use of regexp-opt.
-
 (defun rx--translate-symbol (sym)
   "Translate an rx symbol.  Return (REGEXP . PRECEDENCE)."
   (pcase sym
@@ -230,8 +225,9 @@ rx--every
     (setq list (cdr list)))
   (null list))
 
-(defun rx--translate-or (body)
+(defun rx--translate-or (body unordered)
   "Translate an or-pattern of one of more rx items.
+If UNORDERED, then matching order is unspecified.
 Return (REGEXP . PRECEDENCE)."
   ;; FIXME: Possible improvements:
   ;;
@@ -268,7 +264,7 @@ rx--translate-or
    ((null (cdr body))              ; Single item.
     (rx--translate (car body)))
    ((rx--every #'stringp body)     ; All strings.
-    (cons (list (regexp-opt body nil t))
+    (cons (list (regexp-opt body nil (not unordered)))
           t))
    (t
     (cons (append (car (rx--translate (car body)))
@@ -835,7 +831,8 @@ rx--translate-form
   (let ((body (cdr form)))
     (pcase (car form)
       ((or 'seq : 'and 'sequence) (rx--translate-seq body))
-      ((or 'or '|)              (rx--translate-or body))
+      ((or 'or '|)              (rx--translate-or body nil))
+      ((or 'unordered-or)       (rx--translate-or body t))
       ((or 'any 'in 'char)      (rx--translate-any nil body))
       ('not-char                (rx--translate-any t body))
       ('not                     (rx--translate-not nil body))
@@ -899,7 +896,7 @@ rx--translate-form
                (error "Unknown rx form `%s'" op)))))))))
 
 (defconst rx--builtin-forms
-  '(seq sequence : and or | any in char not-char not
+  '(seq sequence : and or | unordered-or any in char not-char not
     repeat = >= **
     zero-or-more 0+ *
     one-or-more 1+ +
@@ -990,6 +987,7 @@ rx
 
 (seq RX...)    Match the RXs in sequence.  Alias: :, sequence, and.
 (or RX...)     Match one of the RXs.  Alias: |.
+(unordered-or RX...) Match one of the RXs, in unspecified order.
 
 (zero-or-more RX...) Match RXs zero or more times.  Alias: 0+.
 (one-or-more RX...)  Match RXs one or more times.  Alias: 1+.
diff --git a/test/lisp/emacs-lisp/rx-tests.el b/test/lisp/emacs-lisp/rx-tests.el
index 903b191c98..bced74569f 100644
--- a/test/lisp/emacs-lisp/rx-tests.el
+++ b/test/lisp/emacs-lisp/rx-tests.el
@@ -49,6 +49,14 @@ rx-or
   (should (equal (rx (|))
                  "\\`a\\`")))
 
+(ert-deftest rx-unordered-or ()
+  (should (equal (rx (unordered-or "ab" nonl "cd"))
+                 "ab\\|.\\|cd"))
+  (should (equal (rx (unordered-or "ab" "abc" "a"))
+                 "\\(?:a\\(?:bc?\\)?\\)"))
+  (should (equal (rx (unordered-or))
+                 "\\`a\\`")))
+
 (ert-deftest rx-char-any ()
   "Test character alternatives with `]' and `-' (Bug#25123)."
   (should (equal
-- 
2.21.0 (Apple Git-122)

