* lisp/emacs-lisp/pcase.el: Add support for not to pred

(pcase--split-pred, pcase--funcall): Adjust for `not`.
(pcase--get-macroexpander): New function.
(pcase--edebug-match-macro, pcase--make-docstring)
(pcase--macroexpand): Use it.

* lisp/emacs-lisp/radix-tree.el (radix-tree-leaf): Use it!

* doc/lispref/control.texi (The @code{pcase} macro): Document it.

* lisp/emacs-lisp/ert.el (ert--explain-equal-rec): Remove redundant test.
This commit is contained in:
Stefan Monnier 2021-01-16 10:15:47 -05:00
parent df34ed8cbf
commit 0ab56a4e93
6 changed files with 56 additions and 16 deletions

View file

@ -557,8 +557,9 @@ Likewise, it makes no sense to bind keyword symbols
@item (pred @var{function}) @item (pred @var{function})
Matches if the predicate @var{function} returns non-@code{nil} Matches if the predicate @var{function} returns non-@code{nil}
when called on @var{expval}. when called on @var{expval}. The test can be negated with the syntax
the predicate @var{function} can have one of the following forms: @code{(pred (not @var{function}))}.
The predicate @var{function} can have one of the following forms:
@table @asis @table @asis
@item function name (a symbol) @item function name (a symbol)

View file

@ -326,6 +326,12 @@ the buffer cycles the whole buffer between "only top-level headings",
* Changes in Specialized Modes and Packages in Emacs 28.1 * Changes in Specialized Modes and Packages in Emacs 28.1
** pcase
+++
*** The `pred` pattern can now take the form (pred (not FUN)).
This is like (pred (lambda (x) (not (FUN x)))) but results
in better code.
+++ +++
** profiler.el ** profiler.el
The results displayed by 'profiler-report' now have the usage figures The results displayed by 'profiler-report' now have the usage figures

View file

@ -487,7 +487,7 @@ Errors during evaluation are caught and handled like nil."
Returns nil if they are." Returns nil if they are."
(if (not (eq (type-of a) (type-of b))) (if (not (eq (type-of a) (type-of b)))
`(different-types ,a ,b) `(different-types ,a ,b)
(pcase-exhaustive a (pcase a
((pred consp) ((pred consp)
(let ((a-length (proper-list-p a)) (let ((a-length (proper-list-p a))
(b-length (proper-list-p b))) (b-length (proper-list-p b)))
@ -538,7 +538,7 @@ Returns nil if they are."
for xi = (ert--explain-equal-rec ai bi) for xi = (ert--explain-equal-rec ai bi)
do (when xi (cl-return `(array-elt ,i ,xi))) do (when xi (cl-return `(array-elt ,i ,xi)))
finally (cl-assert (equal a b) t)))) finally (cl-assert (equal a b) t))))
((pred atom) (_
(if (not (equal a b)) (if (not (equal a b))
(if (and (symbolp a) (symbolp b) (string= a b)) (if (and (symbolp a) (symbolp b) (string= a b))
`(different-symbols-with-the-same-name ,a ,b) `(different-symbols-with-the-same-name ,a ,b)

View file

@ -39,10 +39,10 @@
;; - along these lines, provide patterns to match CL structs. ;; - along these lines, provide patterns to match CL structs.
;; - provide something like (setq VAR) so a var can be set rather than ;; - provide something like (setq VAR) so a var can be set rather than
;; let-bound. ;; let-bound.
;; - provide a way to fallthrough to subsequent cases (not sure what I meant by ;; - provide a way to fallthrough to subsequent cases
;; this :-() ;; (e.g. Like Racket's (=> ID).
;; - try and be more clever to reduce the size of the decision tree, and ;; - try and be more clever to reduce the size of the decision tree, and
;; to reduce the number of leaves that need to be turned into function: ;; to reduce the number of leaves that need to be turned into functions:
;; - first, do the tests shared by all remaining branches (it will have ;; - first, do the tests shared by all remaining branches (it will have
;; to be performed anyway, so better do it first so it's shared). ;; to be performed anyway, so better do it first so it's shared).
;; - then choose the test that discriminates more (?). ;; - then choose the test that discriminates more (?).
@ -97,11 +97,15 @@
(declare-function get-edebug-spec "edebug" (symbol)) (declare-function get-edebug-spec "edebug" (symbol))
(declare-function edebug-match "edebug" (cursor specs)) (declare-function edebug-match "edebug" (cursor specs))
(defun pcase--get-macroexpander (s)
"Return the macroexpander for pcase pattern head S, or nil"
(get s 'pcase-macroexpander))
(defun pcase--edebug-match-macro (cursor) (defun pcase--edebug-match-macro (cursor)
(let (specs) (let (specs)
(mapatoms (mapatoms
(lambda (s) (lambda (s)
(let ((m (get s 'pcase-macroexpander))) (let ((m (pcase--get-macroexpander s)))
(when (and m (get-edebug-spec m)) (when (and m (get-edebug-spec m))
(push (cons (symbol-name s) (get-edebug-spec m)) (push (cons (symbol-name s) (get-edebug-spec m))
specs))))) specs)))))
@ -128,6 +132,7 @@ PATTERN matches. PATTERN can take one of the forms:
If a SYMBOL is used twice in the same pattern If a SYMBOL is used twice in the same pattern
the second occurrence becomes an `eq'uality test. the second occurrence becomes an `eq'uality test.
(pred FUN) matches if FUN called on EXPVAL returns non-nil. (pred FUN) matches if FUN called on EXPVAL returns non-nil.
(pred (not FUN)) matches if FUN called on EXPVAL returns nil.
(app FUN PAT) matches if FUN called on EXPVAL matches PAT. (app FUN PAT) matches if FUN called on EXPVAL matches PAT.
(guard BOOLEXP) matches if BOOLEXP evaluates to non-nil. (guard BOOLEXP) matches if BOOLEXP evaluates to non-nil.
(let PAT EXPR) matches if EXPR matches PAT. (let PAT EXPR) matches if EXPR matches PAT.
@ -193,7 +198,7 @@ Emacs Lisp manual for more information and examples."
(let (more) (let (more)
;; Collect all the extensions. ;; Collect all the extensions.
(mapatoms (lambda (symbol) (mapatoms (lambda (symbol)
(let ((me (get symbol 'pcase-macroexpander))) (let ((me (pcase--get-macroexpander symbol)))
(when me (when me
(push (cons symbol me) (push (cons symbol me)
more))))) more)))))
@ -424,7 +429,7 @@ of the elements of LIST is performed as if by `pcase-let'.
((eq head 'let) `(let ,(pcase--macroexpand (cadr pat)) ,@(cddr pat))) ((eq head 'let) `(let ,(pcase--macroexpand (cadr pat)) ,@(cddr pat)))
((eq head 'app) `(app ,(nth 1 pat) ,(pcase--macroexpand (nth 2 pat)))) ((eq head 'app) `(app ,(nth 1 pat) ,(pcase--macroexpand (nth 2 pat))))
(t (t
(let* ((expander (get head 'pcase-macroexpander)) (let* ((expander (pcase--get-macroexpander head))
(npat (if expander (apply expander (cdr pat))))) (npat (if expander (apply expander (cdr pat)))))
(if (null npat) (if (null npat)
(error (if expander (error (if expander
@ -658,6 +663,14 @@ MATCH is the pattern that needs to be matched, of the form:
'(:pcase--succeed . nil)))) '(:pcase--succeed . nil))))
(defun pcase--split-pred (vars upat pat) (defun pcase--split-pred (vars upat pat)
"Indicate the overlap or mutual-exclusion between UPAT and PAT.
More specifically retuns a pair (A . B) where A indicates whether PAT
can match when UPAT has matched, and B does the same for the case
where UPAT failed to match.
A and B can be one of:
- nil if we don't know
- `:pcase--fail' if UPAT match's result implies that PAT can't match
- `:pcase--succeed' if UPAT match's result implies that PAT matches"
(let (test) (let (test)
(cond (cond
((and (equal upat pat) ((and (equal upat pat)
@ -670,6 +683,19 @@ MATCH is the pattern that needs to be matched, of the form:
;; and catch at least the easy cases such as (bug#14773). ;; and catch at least the easy cases such as (bug#14773).
(not (macroexp--fgrep (mapcar #'car vars) (cadr upat))))) (not (macroexp--fgrep (mapcar #'car vars) (cadr upat)))))
'(:pcase--succeed . :pcase--fail)) '(:pcase--succeed . :pcase--fail))
;; In case UPAT is of the form (pred (not PRED))
((and (eq 'pred (car upat)) (eq 'not (car-safe (cadr upat))))
(let* ((test (cadr (cadr upat)))
(res (pcase--split-pred vars `(pred ,test) pat)))
(cons (cdr res) (car res))))
;; In case PAT is of the form (pred (not PRED))
((and (eq 'pred (car-safe pat)) (eq 'not (car-safe (cadr pat))))
(let* ((test (cadr (cadr pat)))
(res (pcase--split-pred vars upat `(pred ,test)))
(reverse (lambda (x) (cond ((eq x :pcase--succeed) :pcase--fail)
((eq x :pcase--fail) :pcase--succeed)))))
(cons (funcall reverse (car res))
(funcall reverse (cdr res)))))
((and (eq 'pred (car upat)) ((and (eq 'pred (car upat))
(let ((otherpred (let ((otherpred
(cond ((eq 'pred (car-safe pat)) (cadr pat)) (cond ((eq 'pred (car-safe pat)) (cadr pat))
@ -728,8 +754,10 @@ MATCH is the pattern that needs to be matched, of the form:
(defun pcase--funcall (fun arg vars) (defun pcase--funcall (fun arg vars)
"Build a function call to FUN with arg ARG." "Build a function call to FUN with arg ARG."
(if (symbolp fun) (cond
`(,fun ,arg) ((symbolp fun) `(,fun ,arg))
((eq 'not (car-safe fun)) `(not ,(pcase--funcall (cadr fun) arg vars)))
(t
(let* (;; `env' is an upper bound on the bindings we need. (let* (;; `env' is an upper bound on the bindings we need.
(env (mapcar (lambda (x) (list (car x) (cdr x))) (env (mapcar (lambda (x) (list (car x) (cdr x)))
(macroexp--fgrep vars fun))) (macroexp--fgrep vars fun)))
@ -747,7 +775,7 @@ MATCH is the pattern that needs to be matched, of the form:
;; Let's not replace `vars' in `fun' since it's ;; Let's not replace `vars' in `fun' since it's
;; too difficult to do it right, instead just ;; too difficult to do it right, instead just
;; let-bind `vars' around `fun'. ;; let-bind `vars' around `fun'.
`(let* ,env ,call))))) `(let* ,env ,call))))))
(defun pcase--eval (exp vars) (defun pcase--eval (exp vars)
"Build an expression that will evaluate EXP." "Build an expression that will evaluate EXP."

View file

@ -198,9 +198,10 @@ If not found, return nil."
(pcase-defmacro radix-tree-leaf (vpat) (pcase-defmacro radix-tree-leaf (vpat)
"Pattern which matches a radix-tree leaf. "Pattern which matches a radix-tree leaf.
The pattern VPAT is matched against the leaf's carried value." The pattern VPAT is matched against the leaf's carried value."
;; FIXME: We'd like to use a negative pattern (not consp), but pcase ;; We used to use `(pred atom)', but `pcase' doesn't understand that
;; doesn't support it. Using `atom' works but generates sub-optimal code. ;; `atom' is equivalent to the negation of `consp' and hence generates
`(or `(t . ,,vpat) (and (pred atom) ,vpat)))) ;; suboptimal code.
`(or `(t . ,,vpat) (and (pred (not consp)) ,vpat))))
(defun radix-tree-iter-subtrees (tree fun) (defun radix-tree-iter-subtrees (tree fun)
"Apply FUN to every immediate subtree of radix TREE. "Apply FUN to every immediate subtree of radix TREE.

View file

@ -32,6 +32,10 @@
(should (equal (pcase '(2 . 3) ;bug#18554 (should (equal (pcase '(2 . 3) ;bug#18554
(`(,hd . ,(and (pred atom) tl)) (list hd tl)) (`(,hd . ,(and (pred atom) tl)) (list hd tl))
((pred consp) nil)) ((pred consp) nil))
'(2 3)))
(should (equal (pcase '(2 . 3)
(`(,hd . ,(and (pred (not consp)) tl)) (list hd tl))
((pred consp) nil))
'(2 3)))) '(2 3))))
(pcase-defmacro pcase-tests-plus (pat n) (pcase-defmacro pcase-tests-plus (pat n)