[分享] 对用户友好的 PEG 封装

peg的原生方法使用起来并不那么直观,我对几个主要的宏做了进一步封装,更符合一般的使用直觉。

主要定义了 define-peg-rule+, peg+peg-run+ 这三个新的宏,封装了去掉最后加号的同名宏。

主要优化了:

  1. 带参数的自定义规则,参数在内部使用时,自动使用 funcall 调用。
  2. 使用自定义的规则时,参数自动使用 peg 方法包裹生成 peg-matcher。

这样无论是参数还是自定义的规则,都可以在规则中直接使用,而不用考虑它的类型。

使用示例

例子1

(define-peg-rule any-to-pex (pex)
  (* (and (not (funcall pex)) (any))))

可以等价替换为:

(define-peg-rule+ any-to-pex (pex)
  ;; 匹配不满足 PEX 的任意字符为止
  (* (and (not pex) (any))))

例子2

(define-peg-rule any-to-eol ()
  (any-to-pex (peg (eol))))

可以等价替换为:

(define-peg-rule+ any-to-eol ()
  (any-to-pex (eol)))

一个稍微复杂的例子

(define-peg-rule+ group (pex)
  ;; 捕获 PEX 表达式分组,返回 (start string end)
  (list (region (substring pex))))

(peg-run+ (any-to-pex (and (bol) "*"))
            (group (and (+ "*") " " (any-to-pex (eol))))
            (group (any-to-pex (and (bol) "*"))))

上面定义的这个规则的含义:从当前光标位置开始匹配,分别捕获最近的org标题及其内容,如下图:

如果没有这种封装,需要写成下面的形式,多写了许多 peg 调用,增加心智负担。

(peg-run
   (peg (any-to-pex (peg (bol) "*"))
        (group (peg (and (+ "*") " " (any-to-pex (peg (eol))))))
        (group (peg (any-to-pex (peg (and (bol) "*")))))))

源码

;; -*- lexical-binding: t -*-

(require 'peg)
(require 'dash)

(defun peg--rule-has-param (name)
  (when (symbolp name)
    (let ((rule-func (intern (concat "peg-rule " (symbol-name name)))))
      (and (functionp rule-func)
           (consp (help-function-arglist rule-func))))))

(defun peg--rule-tree-depth (tree)
  (cond
   ((not (listp tree)) 0)
   ((null tree) 1)
   (t (1+ (apply #'max 0 (mapcar #'peg--rule-tree-depth tree))))))

(defun peg--rule-args-add-peg-1 (pexs)
  (--tree-map-nodes
   (and (consp it)
        (peg--rule-has-param (car it))
        (if (consp (cadr it))
            (not (eq 'peg (car (cadr it))))
          t))
   (cons (car it) (--map (list 'peg it) (cdr it)))
   pexs))

(defun peg--rule-args-add-peg (pexs)
  (let ((depth (peg--rule-tree-depth pexs)))
    (dotimes (i (1- depth))
      (setq pexs (peg--rule-args-add-peg-1 pexs)))
    pexs))

(defun peg--rule-add-funcall (args pexs)
  ;; (peg--rule-add-funcall '(matcher)  '((list (region (substring matcher)))))
  (if args
      (--tree-map
       (if (member it args)
           (list 'funcall it)
         it)
       pexs)
    pexs))

(defmacro define-peg-rule+ (name args &rest pexs)
  ;; define-peg-rule 如果有参数,参数使用时用 funcall 调用;
  ;; 如果自定义的规则有参数,在所有参数加上 peg 生成 peg-matcher
  (declare (indent defun))
  (let ((new-pexs (peg--rule-add-funcall
                   args (peg--rule-args-add-peg pexs))))
    `(define-peg-rule ,name ,args
       ,@new-pexs)))

(defmacro peg+ (&rest pexs)
  `(peg ,@(peg--rule-args-add-peg pexs)))

(defmacro peg-run+ (&rest pexs)
  `(peg-run (peg+ ,@pexs)))
1 个赞

原来带参数规则是这么用的,我还以为参数只能是常量表达式 :rofl:

我写了个匹配转义字符的表达式:

;; escape
(define-peg-rule t--nl  () (or "\n" "\r\n" "\r" "\f"))
(define-peg-rule t--ws  () (or [" \t"] t--nl))
(define-peg-rule t--hex () [0-9 a-f A-F])

(define-peg-rule t--es0 ()
  "\\" (or (and (not (or t--hex t--nl)) (any))
	   (and t--hex (opt t--hex) (opt t--hex)
		(opt t--hex) (opt t--hex) (opt t--hex)
		(opt t--ws))))
(define-peg-rule t--es ()
  "\\" (or (and (not (or t--hex t--nl)) (any))
	   (and t--hex (opt t--hex) (opt t--hex)
		(opt t--hex) (opt t--hex) (opt t--hex)
		(opt (replace t--ws " ")))))

用带参数的规则的话可以简化一下:

(define-peg-rule t--es-gen (pex)
  "\\" (or (and (not (or t--hex t--nl)) (any))
	   (and t--hex (opt t--hex) (opt t--hex)
		(opt t--hex) (opt t--hex) (opt t--hex)
		(funcall pex))))

(define-peg-rule t--es0 ()
  (t--es-gen (opt (peg t--ws))))
(define-peg-rule t--es ()
  (substring (t--es-gen (peg (opt (replace t--ws " "))))))

(with-temp-buffer
  (insert "\\123\n")
  (goto-char (point-min))
  (peg-run (peg t--es)))
;; => "\\123 "

我发现我忘了在定义调用其他规则的规则时的空参数列表 () 了,难怪之前总是报错

2 个赞

我发现 peg.el 的 guard 可能比我想象的还要灵活点,写了个总结: 玩玩 peg.el

可以用 guard 实现正则那样的量词 {m,n}

(define-peg-rule Qu (rule m n)
  (guard
   (let ((cnt 0))
     (while (and (< cnt n) (funcall rule))
       (cl-incf cnt))
     (if (<= m cnt n) t nil))))
;;=>
(guard 
 (let ((cnt 0)) 
   (while (and (< cnt n) (funcall rule)) 
     (cl-incf cnt)) 
   (if (<= m cnt n) t nil)))

(with-temp-buffer
  (save-excursion (insert " "))
  (peg-run (peg (and (bob) (Qu (peg " ") 1 2) (eob))))) ;;=> t
(with-temp-buffer
  (save-excursion (insert "  "))
  (peg-run (peg (and (bob) (Qu (peg " ") 1 2) (eob))))) ;;=> t
(with-temp-buffer
  (save-excursion (insert "   "))
  (peg-run (peg (and (bob) (Qu (peg " ") 1 2) (eob))))) ;;=> nil

如果把上面的表达式的 funcall 省略掉,展开结果有点问题:

(define-peg-rule+ Qu+ (rule m n)
  (guard
   (let ((cnt 0))
     (while (and (< cnt n) (rule))
       (cl-incf cnt))
     (if (<= m cnt n) t nil))))
;;=>
(guard 
 (let ((cnt 0)) 
   (while (and (< cnt (funcall n)) ((funcall rule)))
     (cl-incf cnt)) 
   (if (<= (funcall m) cnt 
	   (funcall n)) 
       t nil)))

(由于 PEX 匹配器实际上就是函数,它的参数可能比我们想象的要更自由)

博客总结的真棒呀👍🏻。define-peg-rule+ 的宏展开确实有bug,需要更全面的考虑一下。

1 个赞

我一直以为 guard 里面的 funcall 就是普通的 elisp funcall 函数,没想到也可以用来调用规则。这样的话,情况变得更加复杂了,无法判定 参数是作为普通参数还是规则了,那么只能放弃自动包裹 funcall的宏了😂。

如果没有其他特殊的用法的话,普通参数应该只能在 guard 中能够直接使用,在其他规则里是不能直接使用的,这样的话,自动包裹 peg的宏还可以搞搞。

2 个赞

大佬,我又看了一遍你的这篇博客总结,又补充了许多有趣的细节,研究得很透彻呀,厉害 :+1::+1:,建议单独开个帖子分享出来!

另外 peg-tests.el 你在哪边找到的呀,我看发布的源码里面没有这个测试文件。

这个得去 Emacs 源代码里找,因为是测试文件所以不在 make install 的目录里:

emacs/test/lisp/progmodes/peg-tests.el at master · emacs-mirror/emacs

www,感觉用的人很少,搜索的话会找到这个帖子的 :crazy_face:

2 个赞