1 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
3 ;;; Confidential and proprietary information of ITA Software, Inc. ;;;
5 ;;; Copyright (c) 2012 ITA Software, Inc. All rights reserved. ;;;
7 ;;; Original author: Scott McKay ;;;
9 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
11 (in-package "PROTO-IMPL")
14 ;;; .proto file parsing
18 (declaim (inline proto-whitespace-char-p))
19 (defun proto-whitespace-char-p (ch)
20 (locally (declare (optimize (speed 3) (safety 0) (debug 0)))
21 (and ch (member ch '(#\space #\tab #\return #\newline)))))
23 (declaim (inline proto-eol-char-p))
24 (defun proto-eol-char-p (ch)
25 (locally (declare (optimize (speed 3) (safety 0) (debug 0)))
26 (and ch (member ch '(#\return #\newline)))))
28 (declaim (inline proto-token-char-p))
29 (defun proto-token-char-p (ch)
30 (locally (declare (optimize (speed 3) (safety 0) (debug 0)))
31 (and ch (or (alpha-char-p ch)
33 (member ch '(#\_ #\.))))))
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)))
42 ;;--- Collect the comment so we can attach it to its associated object
43 (defun maybe-skip-comments (stream)
44 "If what appears next in the stream is a comment, skip it and any following comments,
45 then skip any following whitespace."
47 (unless (eql (peek-char nil stream nil) #\/)
50 (case (peek-char nil stream nil)
52 (skip-line-comment stream))
54 (skip-block-comment stream))
56 (error "Found a '~C' at position ~D to start a comment, but no following '~C' or '~C'"
57 #\/ (file-position stream) #\/ #\*)))))
58 (skip-whitespace stream))
60 (defun skip-line-comment (stream)
61 "Skip to the end of a line comment, that is, to the end of the line.
62 Then skip any following whitespace."
63 (loop for ch = (read-char stream nil)
64 until (or (null ch) (proto-eol-char-p ch)))
65 (skip-whitespace stream))
67 (defun skip-block-comment (stream)
68 "Skip to the end of a block comment, that is, until a '*/' is seen.
69 Then skip any following whitespace."
70 (loop for ch = (read-char stream nil)
72 (error "Premature end of file while skipping block comment"))
74 (eql (peek-char nil stream nil) #\/))
75 (read-char stream nil)
77 (skip-whitespace stream))
80 (defun expect-char (stream ch &optional within)
81 "Expect to see 'ch' as the next character in the stream; signal an error if it's not there.
82 Then skip all of the following whitespace."
84 (member (peek-char nil stream nil) ch)
85 (eql (peek-char nil stream nil) ch))
87 (error "No '~C' found~@[ within '~A'~] at position ~D"
88 ch within (file-position stream)))
89 (skip-whitespace stream))
92 (defun parse-token (stream)
93 "Parse the next token in the stream, then skip the following whitespace.
94 The returned value is the token."
95 (when (proto-token-char-p (peek-char nil stream nil))
96 (loop for ch = (read-char stream nil)
97 for ch1 = (peek-char nil stream nil)
99 until (or (null ch1) (not (proto-token-char-p ch1)))
101 (skip-whitespace stream)
102 (return (coerce token 'string))))))
104 (defun parse-parenthesized-token (stream)
105 "Parse the next token in the stream, then skip the following whitespace.
106 The token might be surrounded by parentheses.
107 The returned value is the token."
108 (let ((left (peek-char nil stream nil)))
111 (when (proto-token-char-p (peek-char nil stream nil))
112 (loop for ch = (read-char stream nil)
113 for ch1 = (peek-char nil stream nil)
114 collect ch into token
115 until (or (null ch1) (not (proto-token-char-p ch1)))
117 (skip-whitespace stream)
119 (expect-char stream #\) "option"))
120 (return (coerce token 'string)))))))
122 (defun parse-string (stream)
123 "Parse the next quoted string in the stream, then skip the following whitespace.
124 The returned value is the string, without the quotation marks."
125 (loop with ch0 = (read-char stream nil)
126 for ch = (read-char stream nil)
127 until (or (null ch) (char= ch ch0))
128 collect ch into string
130 (skip-whitespace stream)
131 (return (coerce string 'string)))))
133 (defun parse-int (stream)
134 "Parse the next token in the stream as an integer, then skip the following whitespace.
135 The returned value is the integer."
136 (when (digit-char-p (peek-char nil stream nil))
137 (loop for ch = (read-char stream nil)
138 for ch1 = (peek-char nil stream nil)
139 collect ch into token
140 until (or (null ch1) (not (digit-char-p ch1)))
142 (skip-whitespace stream)
143 (return (parse-integer (coerce token 'string)))))))
145 (defun parse-float (stream)
146 "Parse the next token in the stream as a float, then skip the following whitespace. The returned value is the float."
147 (when (let ((ch (peek-char nil stream nil)))
148 (or (digit-char-p ch) (eql ch #\-)))
149 (let ((token (parse-token stream)))
151 (skip-whitespace stream)
152 (coerce (read-from-string token) 'float)))))
155 ;;; The parser itself
157 (defun parse-protobuf-from-file (filename)
158 "Parses the named file as a .proto file, and returns the Protobufs schema."
159 (with-open-file (stream filename
161 :external-format :utf-8
162 :element-type 'character)
163 (parse-protobuf-from-stream stream
164 :name (class-name->proto (pathname-name (pathname stream)))
165 :class (pathname-name (pathname stream)))))
167 ;; The syntax for Protocol Buffers is so simple that it doesn't seem worth
168 ;; writing a sophisticated parser
169 ;; Note that we don't put the result into *all-protobufs*; do that at a higher level
170 (defun parse-protobuf-from-stream (stream &key name class)
171 "Parses a top-level .proto file from the stream 'stream'.
172 Returns the protobuf schema that describes the .proto file."
173 (let* ((protobuf (make-instance 'protobuf
176 (*protobuf* protobuf)
177 (*protobuf-package* nil))
179 (skip-whitespace stream)
180 (maybe-skip-comments stream)
181 (let ((char (peek-char nil stream nil)))
183 (return-from parse-protobuf-from-stream protobuf))
184 ((proto-token-char-p char)
185 (let ((token (parse-token stream)))
186 (cond ((string= token "syntax")
187 (parse-proto-syntax stream protobuf))
188 ((string= token "package")
189 (parse-proto-package stream protobuf))
190 ((string= token "import")
191 (parse-proto-import stream protobuf))
192 ((string= token "option")
193 (let* ((option (parse-proto-option stream protobuf))
194 (name (and option (proto-name option)))
195 (value (and option (proto-value option))))
197 (cond ((option-name= name "optimize_for")
198 (let ((value (cond ((string= value "SPEED") :speed)
199 ((string= value "CODE_SIZE") :space)
201 (setf (proto-optimize protobuf) value)))
202 ((option-name= name "lisp_package")
203 (let ((package (or (find-package value)
204 (find-package (string-upcase value)))))
205 (setf (proto-lisp-package protobuf) value)
206 (setq *protobuf-package* package)))))))
207 ((string= token "enum")
208 (parse-proto-enum stream protobuf))
209 ((string= token "extend")
210 (parse-proto-extend stream protobuf))
211 ((string= token "message")
212 (parse-proto-message stream protobuf))
213 ((string= token "service")
214 (parse-proto-service stream protobuf)))))
216 (error "Syntax error at position ~D" (file-position stream))))))))
218 (defun parse-proto-syntax (stream protobuf &optional (terminator #\;))
219 "Parse a Protobufs syntax line from 'stream'.
220 Updates the 'protobuf' object to use the syntax."
221 (let ((syntax (prog2 (expect-char stream #\= "syntax")
222 (parse-string stream)
223 (expect-char stream terminator "syntax")
224 (maybe-skip-comments stream))))
225 (setf (proto-syntax protobuf) syntax)))
227 (defun parse-proto-package (stream protobuf &optional (terminator #\;))
228 "Parse a Protobufs package line from 'stream'.
229 Updates the 'protobuf' object to use the package."
230 (check-type protobuf protobuf)
231 (let* ((package (prog1 (parse-token stream)
232 (expect-char stream terminator "package")
233 (maybe-skip-comments stream)))
234 (lisp-pkg (or (proto-lisp-package protobuf)
235 (substitute #\- #\_ package))))
236 (setf (proto-package protobuf) package)
237 (unless (proto-lisp-package protobuf)
238 (setf (proto-lisp-package protobuf) lisp-pkg))
239 (let ((package (or (find-package lisp-pkg)
240 (find-package (string-upcase lisp-pkg)))))
241 (setq *protobuf-package* package))))
243 (defun parse-proto-import (stream protobuf &optional (terminator #\;))
244 "Parse a Protobufs import line from 'stream'.
245 Updates the 'protobuf' object to use the package."
246 (check-type protobuf protobuf)
247 (let ((import (prog1 (parse-string stream)
248 (expect-char stream terminator "package")
249 (maybe-skip-comments stream))))
250 ;;---*** This needs to parse the imported file(s)
251 (setf (proto-imports protobuf) (nconc (proto-imports protobuf) (list import)))))
253 (defun parse-proto-option (stream protobuf &optional (terminator #\;))
254 "Parse a Protobufs option line from 'stream'.
255 Updates the 'protobuf' (or message, service, method) to have the option."
256 (check-type protobuf (or null base-protobuf))
257 (let* ((key (prog1 (parse-parenthesized-token stream)
258 (expect-char stream #\= "option")))
259 (val (prog1 (if (eql (peek-char nil stream nil) #\")
260 (parse-string stream)
261 (parse-token stream))
262 (expect-char stream terminator "option")
263 (maybe-skip-comments stream)))
264 (option (make-instance 'protobuf-option
268 (setf (proto-options protobuf) (nconc (proto-options protobuf) (list option)))
271 ;; If nothing to graft the option into, just return it as the value
275 (defun parse-proto-enum (stream protobuf)
276 "Parse a Protobufs 'enum' from 'stream'.
277 Updates the 'protobuf' or 'protobuf-message' object to have the enum."
278 (check-type protobuf (or protobuf protobuf-message))
279 (let* ((name (prog1 (parse-token stream)
280 (expect-char stream #\{ "enum")
281 (maybe-skip-comments stream)))
282 (enum (make-instance 'protobuf-enum
283 :class (proto->class-name name *protobuf-package*)
286 (let ((name (parse-token stream)))
288 (expect-char stream #\} "enum")
289 (maybe-skip-comments stream)
290 (setf (proto-enums protobuf) (nconc (proto-enums protobuf) (list enum)))
291 (let ((type (find-option enum "lisp_name")))
293 (setf (proto-class enum) (make-lisp-symbol type))))
294 (let ((alias (find-option enum "lisp_alias")))
296 (setf (proto-alias-for enum) (make-lisp-symbol alias))))
297 (return-from parse-proto-enum))
298 (if (string= name "option")
299 (parse-proto-option stream enum #\;)
300 (parse-proto-enum-value stream enum name))))))
302 (defun parse-proto-enum-value (stream enum name)
303 "Parse a Protobufs enum value from 'stream'.
304 Updates the 'protobuf-enum' object to have the enum value."
305 (check-type enum protobuf-enum)
306 (expect-char stream #\= "enum")
307 (let* ((idx (prog1 (parse-int stream)
308 (expect-char stream #\; "enum")
309 (maybe-skip-comments stream)))
310 (value (make-instance 'protobuf-enum-value
313 :value (proto->enum-name name *protobuf-package*))))
314 (setf (proto-values enum) (nconc (proto-values enum) (list value)))))
317 (defun parse-proto-message (stream protobuf)
318 "Parse a Protobufs 'message' from 'stream'.
319 Updates the 'protobuf' or 'protobuf-message' object to have the message."
320 (check-type protobuf (or protobuf protobuf-message))
321 (let* ((name (prog1 (parse-token stream)
322 (expect-char stream #\{ "message")
323 (maybe-skip-comments stream)))
324 (message (make-instance 'protobuf-message
325 :class (proto->class-name name *protobuf-package*)
328 (*protobuf* message))
330 (let ((token (parse-token stream)))
332 (expect-char stream #\} "message")
333 (maybe-skip-comments stream)
334 (setf (proto-messages protobuf) (nconc (proto-messages protobuf) (list message)))
335 (let ((type (find-option message "lisp_name")))
337 (setf (proto-class message) (make-lisp-symbol type))))
338 (let ((alias (find-option message "lisp_alias")))
340 (setf (proto-alias-for message) (make-lisp-symbol alias))))
341 (return-from parse-proto-message))
342 (cond ((string= token "enum")
343 (parse-proto-enum stream message))
344 ((string= token "extend")
345 (parse-proto-extend stream message))
346 ((string= token "message")
347 (parse-proto-message stream message))
348 ((member token '("required" "optional" "repeated") :test #'string=)
349 (parse-proto-field stream message token))
350 ((string= token "option")
351 (parse-proto-option stream message #\;))
352 ((string= token "extensions")
353 (parse-proto-extension stream message))
355 (error "Unrecognized token ~A at position ~D"
356 token (file-position stream))))))))
358 (defun parse-proto-extend (stream protobuf)
359 "Parse a Protobufs 'extend' from 'stream'.
360 Updates the 'protobuf' or 'protobuf-message' object to have the message."
361 (check-type protobuf (or protobuf protobuf-message))
362 (let* ((name (prog1 (parse-token stream)
363 (expect-char stream #\{ "extend")
364 (maybe-skip-comments stream)))
365 (message (find-message *protobuf* name))
366 (extends (and message
367 (make-instance 'protobuf-message
368 :class (proto->class-name name *protobuf-package*)
370 :parent (proto-parent message)
371 :conc-name (proto-conc-name message)
372 :alias-for (proto-alias-for message)
373 :enums (copy-list (proto-enums message))
374 :messages (copy-list (proto-messages message))
375 :fields (copy-list (proto-fields message))
376 :extension-p t)))) ;this message is an extension
378 (let ((token (parse-token stream)))
380 (expect-char stream #\} "extend")
381 (maybe-skip-comments stream)
382 (setf (proto-messages protobuf) (nconc (proto-messages protobuf) (list extends)))
383 (setf (proto-extenders protobuf) (nconc (proto-extenders protobuf) (list extends)))
384 (let ((type (find-option extends "lisp_name")))
386 (setf (proto-class extends) (make-lisp-symbol type))))
387 (let ((alias (find-option extends "lisp_alias")))
389 (setf (proto-alias-for extends) (make-lisp-symbol alias))))
390 (return-from parse-proto-extend))
391 (cond ((member token '("required" "optional" "repeated") :test #'string=)
392 (parse-proto-field stream extends token message))
393 ((string= token "option")
394 (parse-proto-option stream extends #\;))
396 (error "Unrecognized token ~A at position ~D"
397 token (file-position stream))))))))
399 (defun parse-proto-field (stream message required &optional extended-from)
400 "Parse a Protobufs field from 'stream'.
401 Updates the 'protobuf-message' object to have the field."
402 (check-type message protobuf-message)
403 (let* ((type (parse-token stream))
404 (name (prog1 (parse-token stream)
405 (expect-char stream #\= "message")))
406 (idx (parse-int stream))
407 (opts (prog1 (parse-proto-field-options stream)
408 (expect-char stream #\; "message")
409 (maybe-skip-comments stream)))
410 (dflt (find-option opts "default"))
411 (packed (find-option opts "packed"))
412 (ptype (if (member type '("int32" "int64" "uint32" "uint64" "sint32" "sint64"
413 "fixed32" "fixed64" "sfixed32" "sfixed64"
414 "string" "bytes" "bool" "float" "double") :test #'string=)
417 (class (if (keywordp ptype) ptype (proto->class-name type *protobuf-package*)))
418 (field (make-instance 'protobuf-field
420 :value (proto->slot-name name *protobuf-package*)
423 ;; One of :required, :optional or :repeated
424 :required (kintern required)
427 :packed (and packed (string= packed "true"))
428 :extension-p (proto-extension-p message))))
430 (assert (index-within-extensions-p idx extended-from) ()
431 "The index ~D is not in range for extending ~S"
432 idx (proto-class extended-from)))
433 (let ((slot (find-option opts "lisp_name")))
435 (setf (proto-value field) (make-lisp-symbol type))))
436 (setf (proto-fields message) (nconc (proto-fields message) (list field)))))
438 (defun parse-proto-field-options (stream)
439 "Parse any options in a Protobufs field from 'stream'.
440 Returns a list of 'protobuf-option' objects."
441 (with-collectors ((options collect-option))
443 (unless (eql (peek-char nil stream nil) #\[)
444 (return-from parse-proto-field-options options))
445 (expect-char stream #\[ "message")
446 (collect-option (parse-proto-option stream nil #\])))
449 (defun parse-proto-extension (stream message)
450 (check-type message protobuf-message)
451 (let* ((from (parse-int stream))
452 (token (parse-token stream))
453 (to (if (digit-char-p (peek-char nil stream nil))
455 (parse-token stream))))
456 (expect-char stream #\; "message")
457 (assert (string= token "to") ()
458 "Expected 'to' in 'extensions' at position ~D" (file-position stream))
459 (assert (or (integerp to) (string= to "max")) ()
460 "Extension value is not an integer or 'max' as position ~D" (file-position stream))
461 (setf (proto-extensions message)
462 (nconc (proto-extensions message)
463 (list (make-instance 'protobuf-extension
465 :to (if (integerp to) to #.(1- (ash 1 29)))))))))
468 (defun parse-proto-service (stream protobuf)
469 "Parse a Protobufs 'service' from 'stream'.
470 Updates the 'protobuf-protobuf' object to have the service."
471 (check-type protobuf protobuf)
472 (let* ((name (prog1 (parse-token stream)
473 (expect-char stream #\{ "service")
474 (maybe-skip-comments stream)))
475 (service (make-instance 'protobuf-service
476 :class (proto->class-name name *protobuf-package*)
479 (let ((token (parse-token stream)))
481 (expect-char stream #\} "service")
482 (maybe-skip-comments stream)
483 (setf (proto-services protobuf) (nconc (proto-services protobuf) (list service)))
484 (return-from parse-proto-service))
485 (cond ((string= token "option")
486 (parse-proto-option stream service #\;))
487 ((string= token "rpc")
488 (parse-proto-method stream service))
490 (error "Unrecognized token ~A at position ~D"
491 token (file-position stream))))))))
493 (defun parse-proto-method (stream service)
494 "Parse a Protobufs method from 'stream'.
495 Updates the 'protobuf-service' object to have the method."
496 (check-type service protobuf-service)
497 (let* ((name (parse-token stream))
498 (in (prog2 (expect-char stream #\( "service")
500 (expect-char stream #\) "service")))
501 (ret (parse-token stream))
502 (out (prog2 (expect-char stream #\( "service")
504 (expect-char stream #\) "service")))
505 (opts (let ((opts (parse-proto-method-options stream)))
506 (when (or (null opts) (eql (peek-char nil stream nil) #\;))
507 (expect-char stream #\; "service"))
508 (maybe-skip-comments stream)
510 (method (make-instance 'protobuf-method
511 :class (proto->class-name name *protobuf-package*)
513 :input-type (proto->class-name in *protobuf-package*)
515 :output-type (proto->class-name out *protobuf-package*)
518 (let ((name (find-option method "lisp_name")))
520 (setf (proto-function method) (make-lisp-symbol name))))
521 (assert (string= ret "returns") ()
522 "Syntax error in 'message' at position ~D" (file-position stream))
523 (setf (proto-methods service) (nconc (proto-methods service) (list method)))))
525 (defun parse-proto-method-options (stream)
526 "Parse any options in a Protobufs method from 'stream'.
527 Returns a list of 'protobuf-option' objects."
528 (when (eql (peek-char nil stream nil) #\{)
529 (expect-char stream #\{ "service")
530 (maybe-skip-comments stream)
531 (with-collectors ((options collect-option))
533 (when (eql (peek-char nil stream nil) #\})
535 (assert (string= (parse-token stream) "option") ()
536 "Syntax error in 'message' at position ~D" (file-position stream))
537 (collect-option (parse-proto-option stream nil #\;)))
538 (expect-char stream #\} "service")
539 (maybe-skip-comments stream)