]> asedeno.scripts.mit.edu Git - cl-protobufs.git/blob - parser.lisp
d41ef1ed19b4cd05232049820cc43d0cc5f23568
[cl-protobufs.git] / parser.lisp
1 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
2 ;;;                                                                  ;;;
3 ;;; Free Software published under an MIT-like license. See LICENSE   ;;;
4 ;;;                                                                  ;;;
5 ;;; Copyright (c) 2012 Google, Inc.  All rights reserved.            ;;;
6 ;;;                                                                  ;;;
7 ;;; Original author: Scott McKay                                     ;;;
8 ;;;                                                                  ;;;
9 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
10
11 (in-package "PROTO-IMPL")
12
13
14 ;;; .proto file parsing
15
16 ;;; Parsing utilities
17
18 (declaim (inline proto-whitespace-char-p))
19 (defun proto-whitespace-char-p (ch)
20   (declare #.$optimize-fast-unsafe)
21   (and ch (member ch '(#\space #\tab #\return #\newline))))
22
23 (declaim (inline proto-eol-char-p))
24 (defun proto-eol-char-p (ch)
25   (declare #.$optimize-fast-unsafe)   
26   (and ch (member ch '(#\return #\newline))))
27
28 (declaim (inline proto-token-char-p))
29 (defun proto-token-char-p (ch)
30   (declare #.$optimize-fast-unsafe)
31   (and ch (or (alpha-char-p ch)
32               (digit-char-p ch)
33               (member ch '(#\_ #\.)))))
34
35
36 (defun skip-whitespace (stream)
37   "Skip all the whitespace characters that are coming up in the stream."
38   (loop for ch = (peek-char nil stream nil)
39         until (or (null ch) (not (proto-whitespace-char-p ch)))
40         do (read-char stream nil)))
41
42 (defun expect-char (stream char &optional chars within)
43   "Expect to see 'char' as the next character in the stream; signal an error if it's not there.
44    Then skip all of the following whitespace.
45    The return value is the character that was eaten."
46   (let (ch)
47     (if (if (listp char)
48           (member (peek-char nil stream nil) char)
49           (eql (peek-char nil stream nil) char))
50       (setq ch (read-char stream))
51       (error "No '~C' found~@[ within '~A'~] at position ~D"
52              char within (file-position stream)))
53     (maybe-skip-chars stream chars)
54     ch))
55
56 (defun maybe-skip-chars (stream chars)
57   "Skip some optional characters in the stream,
58    then skip all of the following whitespace."
59   (skip-whitespace stream)
60   (when chars
61     (loop
62       (let ((ch (peek-char nil stream nil)))
63         (when (or (null ch) (not (member ch chars)))
64           (skip-whitespace stream)
65           (return-from maybe-skip-chars)))
66       (read-char stream))))
67
68
69 ;;--- Collect the comment so we can attach it to its associated object
70 (defun maybe-skip-comments (stream)
71   "If what appears next in the stream is a comment, skip it and any following comments,
72    then skip any following whitespace."
73   (loop
74     (let ((ch (peek-char nil stream nil)))
75       (when (or (null ch) (not (eql ch #\/)))
76         (return-from maybe-skip-comments))
77       (read-char stream)
78       (case (peek-char nil stream nil)
79         ((#\/)
80          (skip-line-comment stream))
81         ((#\*)
82          (skip-block-comment stream))
83         ((nil)
84          (skip-whitespace stream)
85          (return-from maybe-skip-comments))
86         (otherwise
87          (error "Found a '~C' at position ~D to start a comment, but no following '~C' or '~C'"
88                 #\/ (file-position stream) #\/ #\*))))))
89
90 (defun skip-line-comment (stream)
91   "Skip to the end of a line comment, that is, to the end of the line.
92    Then skip any following whitespace."
93   (loop for ch = (read-char stream nil)
94         until (or (null ch) (proto-eol-char-p ch)))
95   (skip-whitespace stream))
96
97 (defun skip-block-comment (stream)
98   "Skip to the end of a block comment, that is, until a '*/' is seen.
99    Then skip any following whitespace."
100   (loop for ch = (read-char stream nil)
101         do (cond ((null ch)
102                   (error "Premature end of file while skipping block comment"))
103                  ((and (eql ch #\*)
104                        (eql (peek-char nil stream nil) #\/))
105                   (read-char stream nil)
106                   (return))))
107   (skip-whitespace stream))
108
109
110 (defun parse-token (stream &optional additional-chars)
111   "Parse the next token in the stream, then skip the following whitespace.
112    The returned value is the token."
113   (when (let ((ch (peek-char nil stream nil)))
114           (or (proto-token-char-p ch) (member ch additional-chars)))
115     (loop for ch = (read-char stream nil)
116           for ch1 = (peek-char nil stream nil)
117           collect ch into token
118           until (or (null ch1)
119                     (and (not (proto-token-char-p ch1))
120                          (not (member ch1 additional-chars))))
121           finally (progn
122                     (skip-whitespace stream)
123                     (return (coerce token 'string))))))
124
125 (defun parse-parenthesized-token (stream)
126   "Parse the next token in the stream, then skip the following whitespace.
127    The token might be surrounded by parentheses.
128    The returned value is the token."
129   (let ((left (peek-char nil stream nil)))
130     (when (eql left #\()
131       (read-char stream))
132     (when (proto-token-char-p (peek-char nil stream nil))
133       (loop for ch = (read-char stream nil)
134             for ch1 = (peek-char nil stream nil)
135             collect ch into token
136             until (or (null ch1) (not (proto-token-char-p ch1)))
137             finally (progn
138                       (skip-whitespace stream)
139                       (when (eql left #\()
140                         (expect-char stream #\)))
141                       (return (coerce token 'string)))))))
142
143 (defun parse-token-or-string (stream)
144   (if (eql (peek-char nil stream nil) #\")
145     (values (parse-string stream) 'string)
146     (values (parse-token stream) 'symbol)))
147
148 (defun parse-string (stream)
149   "Parse the next quoted string in the stream, then skip the following whitespace.
150    The returned value is the string, without the quotation marks."
151   (loop with ch0 = (read-char stream nil)
152         for ch = (read-char stream nil)
153         until (or (null ch) (char= ch ch0))
154         when (eql ch #\\)
155           do (setq ch (unescape-char stream))
156         collect ch into string
157         finally (progn
158                   (skip-whitespace stream)
159                   (return (coerce string 'string)))))
160
161 (defun unescape-char (stream)
162   "Parse the next \"escaped\" character from the stream."
163   (let ((ch (read-char stream nil)))
164     (assert (not (null ch)) ()
165             "End of stream reached while reading escaped character")
166     (case ch
167       ((#\x)
168        ;; Two hex digits
169        (let* ((d1 (digit-char-p (read-char stream) 16))
170               (d2 (digit-char-p (read-char stream) 16)))
171          (code-char (+ (* d1 16) d2))))
172       ((#\0 #\1 #\2 #\3 #\4 #\5 #\6 #\7 #\8 #\9)
173        (if (not (digit-char-p (peek-char nil stream nil)))
174          #\null
175          ;; Three octal digits
176          (let* ((d1 (digit-char-p ch 8))
177                 (d2 (digit-char-p (read-char stream) 8))
178                 (d3 (digit-char-p (read-char stream) 8)))
179            (code-char (+ (* d1 64) (* d2 8) d3)))))
180       ((#\t) #\tab)
181       ((#\n) #\newline)
182       ((#\r) #\return)
183       ((#\f) #\page)
184       ((#\b) #\backspace)
185       ((#\a) #\bell)
186       ((#\e) #\esc)
187       (otherwise ch))))
188
189 (defun escape-char (ch)
190   "The inverse of 'unescape-char', for printing."
191   (if (and (standard-char-p ch) (graphic-char-p ch))
192     ch
193     (case ch
194       ((#\null)      "\\0")
195       ((#\tab)       "\\t")
196       ((#\newline)   "\\n")
197       ((#\return)    "\\r")
198       ((#\page)      "\\f")
199       ((#\backspace) "\\b")
200       ((#\bell)      "\\a")
201       ((#\esc)       "\\e")
202       (otherwise
203        (format nil "\\x~2,'0X" (char-code ch))))))
204
205 (defun parse-signed-int (stream)
206   "Parse the next token in the stream as an integer, then skip the following whitespace.
207    The returned value is the integer."
208   (let* ((sign (if (eql (peek-char nil stream nil) #\-)
209                  (progn (read-char stream) -1)
210                  1))
211          (int  (parse-unsigned-int stream)))
212     (* int sign)))
213
214 (defun parse-unsigned-int (stream)
215   "Parse the next token in the stream as an integer, then skip the following whitespace.
216    The returned value is the integer."
217   (when (digit-char-p (peek-char nil stream nil))
218     (loop for ch = (read-char stream nil)
219           for ch1 = (peek-char nil stream nil)
220           collect ch into token
221           until (or (null ch1) (and (not (digit-char-p ch1)) (not (eql ch #\x))))
222           finally (progn
223                     (skip-whitespace stream)
224                     (let ((token (coerce token 'string)))
225                       (if (starts-with token "0x")
226                         (let ((*read-base* 16))
227                           (return (parse-integer (subseq token 2))))
228                         (return (parse-integer token))))))))
229
230 (defun parse-float (stream)
231   "Parse the next token in the stream as a float, then skip the following whitespace.
232    The returned value is the float."
233   (let ((number (parse-number stream)))
234     (when number
235       (coerce number 'float))))
236
237 (defun parse-number (stream)
238   (when (let ((ch (peek-char nil stream nil)))
239           (or (digit-char-p ch) (member ch '(#\- #\+ #\.))))
240     (let ((token (parse-token stream '(#\- #\+ #\.))))
241       (when token
242         (skip-whitespace stream)
243         (parse-numeric-string token)))))
244
245 (defun parse-numeric-string (string)
246   (cond ((starts-with string "0x")
247          (parse-integer (subseq string 2) :radix 16))
248         ((starts-with string "-0x")
249          (- (parse-integer (subseq string 3) :radix 16)))
250         (t
251          (read-from-string string))))
252
253
254 ;;; The parser itself
255
256 (defun parse-schema-from-file (filename &key name class (conc-name ""))
257   "Parses the named file as a .proto file, and returns the Protobufs schema."
258   (with-open-file (stream filename
259                    :direction :input
260                    :external-format :utf-8
261                    :element-type 'character)
262     (let ((*compile-file-pathname* (pathname stream))
263           (*compile-file-truename* (truename stream)))
264       (parse-schema-from-stream stream
265                                   :name  (or name (class-name->proto (pathname-name (pathname stream))))
266                                   :class (or class (kintern (pathname-name (pathname stream))))
267                                   :conc-name conc-name))))
268
269 ;; The syntax for Protocol Buffers is so simple that it doesn't seem worth
270 ;; writing a sophisticated parser
271 ;; Note that we don't put the result into *all-schemas*; that's done in 'define-schema'
272 (defun parse-schema-from-stream (stream &key name class (conc-name ""))
273   "Parses a top-level .proto file from the stream 'stream'.
274    Returns the protobuf schema that describes the .proto file."
275   (let* ((schema (make-instance 'protobuf-schema
276                    :class class
277                    :name  name))
278          (*protobuf* schema)
279          (*protobuf-package* *package*)
280          (*protobuf-conc-name* conc-name))
281     (loop
282       (skip-whitespace stream)
283       (maybe-skip-comments stream)
284       (let ((char (peek-char nil stream nil)))
285         (cond ((null char)
286                (remove-options schema "lisp_package")
287                (return-from parse-schema-from-stream schema))
288               ((proto-token-char-p char)
289                (let ((token (parse-token stream)))
290                  (cond ((string= token "syntax")
291                         (parse-proto-syntax stream schema))
292                        ((string= token "package")
293                         (parse-proto-package stream schema))
294                        ((string= token "import")
295                         (parse-proto-import stream schema))
296                        ((string= token "option")
297                         (let* ((option (parse-proto-option stream schema))
298                                (name   (and option (proto-name option)))
299                                (value  (and option (proto-value option))))
300                           (when (and option (option-name= name "lisp_package"))
301                             (let ((package (or (find-proto-package value) *protobuf-package*)))
302                               (setf (proto-lisp-package schema) value)
303                               (setq *protobuf-package* package)))))
304                        ((string= token "enum")
305                         (parse-proto-enum stream schema))
306                        ((string= token "extend")
307                         (parse-proto-extend stream schema))
308                        ((string= token "message")
309                         (parse-proto-message stream schema))
310                        ((string= token "service")
311                         (parse-proto-service stream schema)))))
312               (t
313                (error "Syntax error at position ~D" (file-position stream))))))))
314
315 (defun parse-proto-syntax (stream schema &optional (terminator #\;))
316   "Parse a Protobufs syntax line from 'stream'.
317    Updates the 'protobuf-schema' object to use the syntax."
318   (let ((syntax (prog2 (expect-char stream #\= () "syntax")
319                     (parse-string stream)
320                   (expect-char stream terminator () "syntax")
321                   (maybe-skip-comments stream))))
322     (setf (proto-syntax schema) syntax)))
323
324 (defun parse-proto-package (stream schema &optional (terminator #\;))
325   "Parse a Protobufs package line from 'stream'.
326    Updates the 'protobuf-schema' object to use the package."
327   (check-type schema protobuf-schema)
328   (let* ((package  (prog1 (parse-token stream)
329                      (expect-char stream terminator () "package")
330                      (maybe-skip-comments stream)))
331          (lisp-pkg (or (proto-lisp-package schema)
332                        (substitute #\- #\_ package))))
333     (setf (proto-package schema) package)
334     (unless (proto-lisp-package schema)
335       (setf (proto-lisp-package schema) lisp-pkg))
336     (let ((package (or (find-proto-package lisp-pkg) *protobuf-package*)))
337       (setq *protobuf-package* package))))
338
339 (defun parse-proto-import (stream schema &optional (terminator #\;))
340   "Parse a Protobufs import line from 'stream'.
341    Updates the 'protobuf-schema' object to use the package."
342   (check-type schema protobuf-schema)
343   (let ((import (prog1 (parse-string stream)
344                   (expect-char stream terminator () "package")
345                   (maybe-skip-comments stream))))
346     (process-imports schema import)
347     (setf (proto-imports schema) (nconc (proto-imports schema) (list import)))))
348
349 (defun parse-proto-option (stream protobuf &optional (terminators '(#\;)))
350   "Parse a Protobufs option line from 'stream'.
351    Updates the 'protobuf-schema' (or message, service, method) to have the option."
352   (check-type protobuf (or null base-protobuf))
353   (let* (terminator
354          (key (prog1 (parse-parenthesized-token stream)
355                 (expect-char stream #\= () "option")))
356          (val (prog1 (let ((ch (peek-char nil stream nil)))
357                        (cond ((eql ch #\")
358                               (parse-string stream))
359                              ((or (digit-char-p ch) (member ch '(#\- #\+ #\.)))
360                               (parse-number stream))
361                              (t (kintern (parse-token stream)))))
362                 (setq terminator (expect-char stream terminators () "option"))
363                 (maybe-skip-comments stream)))
364          (option (make-instance 'protobuf-option
365                    :name  key
366                    :value val)))
367     (cond (protobuf
368            (setf (proto-options protobuf) (nconc (proto-options protobuf) (list option)))
369            (values option terminator))
370           (t
371            ;; If nothing to graft the option into, just return it as the value
372            (values option terminator)))))
373
374
375 (defun parse-proto-enum (stream protobuf)
376   "Parse a Protobufs 'enum' from 'stream'.
377    Updates the 'protobuf-schema' or 'protobuf-message' object to have the enum."
378   (check-type protobuf (or protobuf-schema protobuf-message))
379   (let* ((name (prog1 (parse-token stream)
380                  (expect-char stream #\{ () "enum")
381                  (maybe-skip-comments stream)))
382          (enum (make-instance 'protobuf-enum
383                  :class (proto->class-name name *protobuf-package*)
384                  :name name)))
385     (loop
386       (let ((name (parse-token stream)))
387         (when (null name)
388           (expect-char stream #\} '(#\;) "enum")
389           (maybe-skip-comments stream)
390           (setf (proto-enums protobuf) (nconc (proto-enums protobuf) (list enum)))
391           (let ((type (find-option enum "lisp_name")))
392             (when type
393               (setf (proto-class enum) (make-lisp-symbol type))))
394           (let ((alias (find-option enum "lisp_alias")))
395             (when alias
396               (setf (proto-alias-for enum) (make-lisp-symbol alias))))
397           (return-from parse-proto-enum enum))
398         (if (string= name "option")
399           (parse-proto-option stream enum)
400           (parse-proto-enum-value stream enum name))))))
401
402 (defun parse-proto-enum-value (stream enum name)
403   "Parse a Protobufs enum value from 'stream'.
404    Updates the 'protobuf-enum' object to have the enum value."
405   (check-type enum protobuf-enum)
406   (expect-char stream #\= () "enum")
407   (let* ((idx  (prog1 (parse-signed-int stream)
408                  (expect-char stream #\; () "enum")
409                  (maybe-skip-comments stream)))
410          (value (make-instance 'protobuf-enum-value
411                   :name  name
412                   :index idx
413                   :value (proto->enum-name name *protobuf-package*))))
414     (setf (proto-values enum) (nconc (proto-values enum) (list value)))
415     value))
416
417
418 (defun parse-proto-message (stream protobuf &optional name)
419   "Parse a Protobufs 'message' from 'stream'.
420    Updates the 'protobuf-schema' or 'protobuf-message' object to have the message."
421   (check-type protobuf (or protobuf-schema protobuf-message))
422   (let* ((name (prog1 (or name (parse-token stream))
423                  (expect-char stream #\{ () "message")
424                  (maybe-skip-comments stream)))
425          (class (proto->class-name name *protobuf-package*))
426          (message (make-instance 'protobuf-message
427                     :class class
428                     :name name
429                     :parent protobuf
430                     ;; Maybe force accessors for all slots
431                     :conc-name (conc-name-for-type class *protobuf-conc-name*)))
432          (*protobuf* message))
433     (loop
434       (let ((token (parse-token stream)))
435         (when (null token)
436           (expect-char stream #\} '(#\;) "message")
437           (maybe-skip-comments stream)
438           (setf (proto-messages protobuf) (nconc (proto-messages protobuf) (list message)))
439           (let ((type (find-option message "lisp_name")))
440             (when type
441               (setf (proto-class message) (make-lisp-symbol type))))
442           (let ((alias (find-option message "lisp_alias")))
443             (when alias
444               (setf (proto-alias-for message) (make-lisp-symbol alias))))
445           (return-from parse-proto-message message))
446         (cond ((string= token "enum")
447                (parse-proto-enum stream message))
448               ((string= token "extend")
449                (parse-proto-extend stream message))
450               ((string= token "message")
451                (parse-proto-message stream message))
452               ((member token '("required" "optional" "repeated") :test #'string=)
453                (parse-proto-field stream message token))
454               ((string= token "option")
455                (parse-proto-option stream message))
456               ((string= token "extensions")
457                (parse-proto-extension stream message))
458               (t
459                (error "Unrecognized token ~A at position ~D"
460                       token (file-position stream))))))))
461
462 (defun parse-proto-extend (stream protobuf)
463   "Parse a Protobufs 'extend' from 'stream'.
464    Updates the 'protobuf-schema' or 'protobuf-message' object to have the message."
465   (check-type protobuf (or protobuf-schema protobuf-message))
466   (let* ((name (prog1 (parse-token stream)
467                  (expect-char stream #\{ () "extend")
468                  (maybe-skip-comments stream)))
469          (message (find-message protobuf name))
470          (extends (and message
471                        (make-instance 'protobuf-message
472                          :class  (proto->class-name name *protobuf-package*)
473                          :name   name
474                          :parent (proto-parent message)
475                          :conc-name (proto-conc-name message)
476                          :alias-for (proto-alias-for message)
477                          :enums    (copy-list (proto-enums message))
478                          :messages (copy-list (proto-messages message))
479                          :fields   (copy-list (proto-fields message))
480                          :extensions (copy-list (proto-extensions message))
481                          :message-type :extends)))      ;this message is an extension
482          (*protobuf* extends))
483     (loop
484       (let ((token (parse-token stream)))
485         (when (null token)
486           (expect-char stream #\} '(#\;) "extend")
487           (maybe-skip-comments stream)
488           (setf (proto-messages protobuf) (nconc (proto-messages protobuf) (list extends)))
489           (setf (proto-extenders protobuf) (nconc (proto-extenders protobuf) (list extends)))
490           (let ((type (find-option extends "lisp_name")))
491             (when type
492               (setf (proto-class extends) (make-lisp-symbol type))))
493           (let ((alias (find-option extends "lisp_alias")))
494             (when alias
495               (setf (proto-alias-for extends) (make-lisp-symbol alias))))
496           (return-from parse-proto-extend extends))
497         (cond ((member token '("required" "optional" "repeated") :test #'string=)
498                (let ((field (parse-proto-field stream extends token message)))
499                  (setf (proto-extended-fields extends) (nconc (proto-extended-fields extends) (list field)))))
500               ((string= token "option")
501                (parse-proto-option stream extends))
502               (t
503                (error "Unrecognized token ~A at position ~D"
504                       token (file-position stream))))))))
505
506 (defun parse-proto-field (stream message required &optional extended-from)
507   "Parse a Protobufs field from 'stream'.
508    Updates the 'protobuf-message' object to have the field."
509   (check-type message protobuf-message)
510   (let ((type (parse-token stream)))
511     (if (string= type "group")
512       (parse-proto-group stream message required extended-from)
513       (let* ((name (prog1 (parse-token stream)
514                      (expect-char stream #\= () "message")))
515              (idx  (parse-unsigned-int stream))
516              (opts (prog1 (parse-proto-field-options stream)
517                      (expect-char stream #\; () "message")
518                      (maybe-skip-comments stream)))
519              (packed (find-option opts "packed"))
520              (ptype  (if (member type '("int32" "int64" "uint32" "uint64" "sint32" "sint64"
521                                         "fixed32" "fixed64" "sfixed32" "sfixed64"
522                                         "string" "bytes" "bool" "float" "double") :test #'string=)
523                        (kintern type)
524                        type))
525              (class  (if (keywordp ptype) ptype (proto->class-name type *protobuf-package*)))
526              (slot   (proto->slot-name name *protobuf-package*))
527              (reqd   (kintern required))
528              (field  (make-instance 'protobuf-field
529                        :name  name
530                        :type  type
531                        :class class
532                        ;; One of :required, :optional or :repeated
533                        :required reqd
534                        :index idx
535                        :value slot
536                        ;; Fields parsed from .proto files usually get an accessor
537                        :reader (let ((conc-name (proto-conc-name message)))
538                                  (and conc-name
539                                       (intern (format nil "~A~A" conc-name slot) *protobuf-package*)))
540                        :default (multiple-value-bind (default type default-p)
541                                     (find-option opts "default")
542                                   (declare (ignore type))
543                                   (if default-p
544                                     default
545                                     (if (eq reqd :repeated) $empty-list $empty-default)))
546                        :packed  (and packed (boolean-true-p packed))
547                        :message-type (proto-message-type message)
548                        :options (remove-options opts "default" "packed"))))
549         (when extended-from
550           (assert (index-within-extensions-p idx extended-from) ()
551                   "The index ~D is not in range for extending ~S"
552                   idx (proto-class extended-from)))
553         (let ((slot (find-option opts "lisp_name")))
554           (when slot
555             (setf (proto-value field) (make-lisp-symbol type))))
556         (setf (proto-fields message) (nconc (proto-fields message) (list field)))
557         field))))
558
559 (defun parse-proto-group (stream message required &optional extended-from)
560   "Parse a (deprecated) Protobufs group from 'stream'.
561    Updates the 'protobuf-message' object to have the group type and field."
562   (check-type message protobuf-message)
563   (let* ((type (prog1 (parse-token stream)
564                  (expect-char stream #\= () "message")))
565          (name (slot-name->proto (proto->slot-name type)))
566          (idx  (parse-unsigned-int stream))
567          (msg  (parse-proto-message stream message type))
568          (class (proto->class-name type *protobuf-package*))
569          (slot  (proto->slot-name name *protobuf-package*))
570          (field (make-instance 'protobuf-field
571                   :name  name
572                   :type  type
573                   :class class
574                   :required (kintern required)
575                   :index idx
576                   :value slot
577                   ;; Groups parsed from .proto files usually get an accessor
578                   :reader (let ((conc-name (proto-conc-name message)))
579                             (and conc-name
580                                  (intern (format nil "~A~A" conc-name slot) *protobuf-package*)))
581                   :message-type :group)))
582     (setf (proto-message-type msg) :group)
583     (when extended-from
584       (assert (index-within-extensions-p idx extended-from) ()
585               "The index ~D is not in range for extending ~S"
586               idx (proto-class extended-from)))
587     (setf (proto-fields message) (nconc (proto-fields message) (list field)))
588     field))
589
590 (defun parse-proto-field-options (stream)
591   "Parse any options in a Protobufs field from 'stream'.
592    Returns a list of 'protobuf-option' objects."
593   (with-collectors ((options collect-option))
594     (let ((terminator nil))
595       (loop
596         (cond ((eql (peek-char nil stream nil) #\[)
597                (expect-char stream #\[ () "message"))
598               ((eql terminator #\,))
599               (t
600                (return-from parse-proto-field-options options)))
601         (multiple-value-bind (option term)
602             (parse-proto-option stream nil '(#\] #\,))
603           (setq terminator term)
604           (collect-option option))))))
605
606 (defun parse-proto-extension (stream message)
607   (check-type message protobuf-message)
608   (let* ((from  (parse-unsigned-int stream))
609          (token (parse-token stream))
610          (to    (let ((ch (peek-char nil stream nil)))
611                   (cond ((digit-char-p (peek-char nil stream nil))
612                          (parse-unsigned-int stream))
613                         ((eql ch #\;) from)
614                         (t (parse-token stream))))))
615     (expect-char stream #\; () "message")
616     (assert (or (null token) (string= token "to")) ()
617             "Expected 'to' in 'extensions' at position ~D" (file-position stream))
618     (assert (or (integerp to) (string= to "max")) ()
619             "Extension value is not an integer or 'max' as position ~D" (file-position stream))
620     (let ((extension (make-instance 'protobuf-extension
621                        :from from
622                        :to   (if (integerp to) to #.(1- (ash 1 29))))))
623       (setf (proto-extensions message)
624             (nconc (proto-extensions message)
625                    (list extension)))
626       extension)))
627
628
629 (defun parse-proto-service (stream schema)
630   "Parse a Protobufs 'service' from 'stream'.
631    Updates the 'protobuf-schema' object to have the service."
632   (check-type schema protobuf-schema)
633   (let* ((name (prog1 (parse-token stream)
634                  (expect-char stream #\{ () "service")
635                  (maybe-skip-comments stream)))
636          (service (make-instance 'protobuf-service
637                     :class (proto->class-name name *protobuf-package*)
638                     :name name))
639          (index 0))
640     (loop
641       (let ((token (parse-token stream)))
642         (when (null token)
643           (expect-char stream #\} '(#\;) "service")
644           (maybe-skip-comments stream)
645           (setf (proto-services schema) (nconc (proto-services schema) (list service)))
646           (return-from parse-proto-service service))
647         (cond ((string= token "option")
648                (parse-proto-option stream service))
649               ((string= token "rpc")
650                (parse-proto-method stream service (iincf index)))
651               (t
652                (error "Unrecognized token ~A at position ~D"
653                       token (file-position stream))))))))
654
655 (defun parse-proto-method (stream service index)
656   "Parse a Protobufs method from 'stream'.
657    Updates the 'protobuf-service' object to have the method."
658   (check-type service protobuf-service)
659   (let* ((name (parse-token stream))
660          (in   (prog2 (expect-char stream #\( () "service")
661                    (parse-token stream)
662                  (expect-char stream #\) () "service")))
663          (ret  (parse-token stream))
664          (out  (prog2 (expect-char stream #\( () "service")
665                    (parse-token stream)
666                  (expect-char stream #\) () "service")))
667          (opts (let ((opts (parse-proto-method-options stream)))
668                  (when (or (null opts) (eql (peek-char nil stream nil) #\;))
669                    (expect-char stream #\; () "service"))
670                  (maybe-skip-comments stream)
671                  opts))
672          (stub   (proto->class-name name *protobuf-package*))
673          (method (make-instance 'protobuf-method
674                    :class stub
675                    :name  name
676                    :input-type  (proto->class-name in *protobuf-package*)
677                    :input-name  in
678                    :output-type (proto->class-name out *protobuf-package*)
679                    :output-name out
680                    :index index
681                    :options opts)))
682     (let* ((name (find-option method "lisp_name"))
683            (stub (or (and name (make-lisp-symbol name))
684                      stub)))
685       (setf (proto-class method) stub
686             (proto-client-stub method) stub
687             (proto-server-stub method) (intern (format nil "~A-~A" 'do stub) *protobuf-package*)))
688     (assert (string= ret "returns") ()
689             "Syntax error in 'message' at position ~D" (file-position stream))
690     (setf (proto-methods service) (nconc (proto-methods service) (list method)))
691     method))
692
693 (defun parse-proto-method-options (stream)
694   "Parse any options in a Protobufs method from 'stream'.
695    Returns a list of 'protobuf-option' objects."
696   (when (eql (peek-char nil stream nil) #\{)
697     (expect-char stream #\{ () "service")
698     (maybe-skip-comments stream)
699     (with-collectors ((options collect-option))
700       (loop
701         (when (eql (peek-char nil stream nil) #\})
702           (return))
703         (assert (string= (parse-token stream) "option") ()
704                 "Syntax error in 'message' at position ~D" (file-position stream))
705         (collect-option (parse-proto-option stream nil)))
706       (expect-char stream #\} '(#\;) "service")
707       (maybe-skip-comments stream)
708       options)))