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