config

Personal configuration.
git clone git://code.dwrz.net/config
Log | Files | Refs

notmuch-parser.el (7124B)


      1 ;;; notmuch-parser.el --- streaming S-expression parser  -*- lexical-binding: t -*-
      2 ;;
      3 ;; Copyright © Austin Clements
      4 ;;
      5 ;; This file is part of Notmuch.
      6 ;;
      7 ;; Notmuch is free software: you can redistribute it and/or modify it
      8 ;; under the terms of the GNU General Public License as published by
      9 ;; the Free Software Foundation, either version 3 of the License, or
     10 ;; (at your option) any later version.
     11 ;;
     12 ;; Notmuch is distributed in the hope that it will be useful, but
     13 ;; WITHOUT ANY WARRANTY; without even the implied warranty of
     14 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
     15 ;; General Public License for more details.
     16 ;;
     17 ;; You should have received a copy of the GNU General Public License
     18 ;; along with Notmuch.  If not, see <https://www.gnu.org/licenses/>.
     19 ;;
     20 ;; Authors: Austin Clements <aclements@csail.mit.edu>
     21 
     22 ;;; Code:
     23 
     24 (require 'cl-lib)
     25 (require 'pcase)
     26 (require 'subr-x)
     27 
     28 (defun notmuch-sexp-create-parser ()
     29   "Return a new streaming S-expression parser.
     30 
     31 This parser is designed to incrementally read an S-expression
     32 whose structure is known to the caller.  Like a typical
     33 S-expression parsing interface, it provides a function to read a
     34 complete S-expression from the input.  However, it extends this
     35 with an additional function that requires the next value in the
     36 input to be a list and descends into it, allowing its elements to
     37 be read one at a time or further descended into.  Both functions
     38 can return \\='retry to indicate that not enough input is available.
     39 
     40 The parser always consumes input from point in the current
     41 buffer.  Hence, the caller is allowed to delete any data before
     42 point and may resynchronize after an error by moving point."
     43   (vector 'notmuch-sexp-parser
     44 	  0     ; List depth
     45 	  nil   ; Partial parse position marker
     46 	  nil)) ; Partial parse state
     47 
     48 (defmacro notmuch-sexp--depth (sp)         `(aref ,sp 1))
     49 (defmacro notmuch-sexp--partial-pos (sp)   `(aref ,sp 2))
     50 (defmacro notmuch-sexp--partial-state (sp) `(aref ,sp 3))
     51 
     52 (defun notmuch-sexp-read (sp)
     53   "Consume and return the value at point in the current buffer.
     54 
     55 Returns \\='retry if there is insufficient input to parse a complete
     56 value (though it may still move point over whitespace).  If the
     57 parser is currently inside a list and the next token ends the
     58 list, this moves point just past the terminator and returns \\='end.
     59 Otherwise, this moves point to just past the end of the value and
     60 returns the value."
     61   (skip-chars-forward " \n\r\t")
     62   (cond ((eobp) 'retry)
     63 	((= (char-after) ?\))
     64 	 ;; We've reached the end of a list
     65 	 (if (= (notmuch-sexp--depth sp) 0)
     66 	     ;; .. but we weren't in a list.  Let read signal the
     67 	     ;; error to be consistent with all other code paths.
     68 	     (read (current-buffer))
     69 	   ;; Go up a level and return an end token
     70 	   (cl-decf (notmuch-sexp--depth sp))
     71 	   (forward-char)
     72 	   'end))
     73 	((= (char-after) ?\()
     74 	 ;; We're at the beginning of a list.  If we haven't started
     75 	 ;; a partial parse yet, attempt to read the list in its
     76 	 ;; entirety.  If this fails, or we've started a partial
     77 	 ;; parse, extend the partial parse to figure out when we
     78 	 ;; have a complete list.
     79 	 (catch 'return
     80 	   (unless (notmuch-sexp--partial-state sp)
     81 	     (let ((start (point)))
     82 	       (condition-case nil
     83 		   (throw 'return (read (current-buffer)))
     84 		 (end-of-file (goto-char start)))))
     85 	   ;; Extend the partial parse
     86 	   (let (is-complete)
     87 	     (save-excursion
     88 	       (let* ((new-state (parse-partial-sexp
     89 				  (or (notmuch-sexp--partial-pos sp) (point))
     90 				  (point-max) 0 nil
     91 				  (notmuch-sexp--partial-state sp)))
     92 		      ;; A complete value is available if we've
     93 		      ;; reached depth 0.
     94 		      (depth (car new-state)))
     95 		 (cl-assert (>= depth 0))
     96 		 (if (= depth 0)
     97 		     ;; Reset partial parse state
     98 		     (setf (notmuch-sexp--partial-state sp) nil
     99 			   (notmuch-sexp--partial-pos sp) nil
    100 			   is-complete t)
    101 		   ;; Update partial parse state
    102 		   (setf (notmuch-sexp--partial-state sp) new-state
    103 			 (notmuch-sexp--partial-pos sp) (point-marker)))))
    104 	     (if is-complete
    105 		 (read (current-buffer))
    106 	       'retry))))
    107 	(t
    108 	 ;; Attempt to read a non-compound value
    109 	 (let ((start (point)))
    110 	   (condition-case nil
    111 	       (let ((val (read (current-buffer))))
    112 		 ;; We got what looks like a complete read, but if
    113 		 ;; we reached the end of the buffer in the process,
    114 		 ;; we may not actually have all of the input we
    115 		 ;; need (unless it's a string, which is delimited).
    116 		 (if (or (stringp val) (not (eobp)))
    117 		     val
    118 		   ;; We can't be sure the input was complete
    119 		   (goto-char start)
    120 		   'retry))
    121 	     (end-of-file
    122 	      (goto-char start)
    123 	      'retry))))))
    124 
    125 (defun notmuch-sexp-begin-list (sp)
    126   "Parse the beginning of a list value and enter the list.
    127 
    128 Returns \\='retry if there is insufficient input to parse the
    129 beginning of the list.  If this is able to parse the beginning of
    130 a list, it moves point past the token that opens the list and
    131 returns t.  Later calls to `notmuch-sexp-read' will return the
    132 elements inside the list.  If the input in buffer is not the
    133 beginning of a list, throw invalid-read-syntax."
    134   (skip-chars-forward " \n\r\t")
    135   (cond ((eobp) 'retry)
    136 	((= (char-after) ?\()
    137 	 (forward-char)
    138 	 (cl-incf (notmuch-sexp--depth sp))
    139 	 t)
    140 	(t
    141 	 ;; Skip over the bad character like `read' does
    142 	 (forward-char)
    143 	 (signal 'invalid-read-syntax (list (string (char-before)))))))
    144 
    145 (defvar notmuch-sexp--parser nil
    146   "The buffer-local notmuch-sexp-parser instance.
    147 
    148 Used by `notmuch-sexp-parse-partial-list'.")
    149 
    150 (defvar notmuch-sexp--state nil
    151   "The buffer-local `notmuch-sexp-parse-partial-list' state.")
    152 
    153 (defun notmuch-sexp-parse-partial-list (result-function result-buffer)
    154   "Incrementally parse an S-expression list from the current buffer.
    155 
    156 This function consumes an S-expression list from the current
    157 buffer, applying RESULT-FUNCTION in RESULT-BUFFER to each
    158 complete value in the list.  It operates incrementally and should
    159 be called whenever the input buffer has been extended with
    160 additional data.  The caller just needs to ensure it does not
    161 move point in the input buffer."
    162   ;; Set up the initial state
    163   (unless (local-variable-p 'notmuch-sexp--parser)
    164     (setq-local notmuch-sexp--parser (notmuch-sexp-create-parser))
    165     (setq-local notmuch-sexp--state 'begin))
    166   (let (done)
    167     (while (not done)
    168       (cl-case notmuch-sexp--state
    169 	(begin
    170 	 ;; Enter the list
    171 	 (if (eq (notmuch-sexp-begin-list notmuch-sexp--parser) 'retry)
    172 	     (setq done t)
    173 	   (setq notmuch-sexp--state 'result)))
    174 	(result
    175 	 ;; Parse a result
    176 	 (let ((result (notmuch-sexp-read notmuch-sexp--parser)))
    177 	   (cl-case result
    178 	     (retry (setq done t))
    179 	     (end   (setq notmuch-sexp--state 'end))
    180 	     (t     (with-current-buffer result-buffer
    181 		      (funcall result-function result))))))
    182 	(end
    183 	 ;; Skip over trailing whitespace.
    184 	 (skip-chars-forward " \n\r\t")
    185 	 ;; Any trailing data is unexpected.
    186 	 (unless (eobp)
    187 	   (error "Trailing garbage following expression"))
    188 	 (setq done t)))))
    189   ;; Clear out what we've parsed
    190   (delete-region (point-min) (point)))
    191 
    192 (provide 'notmuch-parser)
    193 
    194 ;;; notmuch-parser.el ends here