Skip to content

Runtime type selection: The EVAL Type Specifier

j3pic edited this page Feb 17, 2022 · 2 revisions

(read-binary-type '(eval expression) stream &key (byte-order :little-endian) align element-align)

(defbinary a-structure ()
    (the-field default-value :type (eval expression)))

The EVAL Type Specifier makes it possible to defer the decision of what type a slot will be until the moment it is about to be read (or written). This makes it possible for the type to depend on the values of other slots in the structure, or on anything else you deem necessary. If this type is used as the type of a slot in a DEFBINARY struct, then the expression will be evaluated in an environment in which all the slots dealt with so far will be bound to variables with the same name.

If it's used outside of a DEFBINARY definition, then the expression will be evaluated using EVAL in the default environment used by EVAL.

The expression must evaluate to a Lisp-Binary type designator, such as (simple-array (terminated-string 1) 12).

This type will then be used to decide how to read the datum. Lisp-Binary accomplishes this by:

  1. Generating the code that would have gone into the READ-BINARY or WRITE-BINARY method at runtime instead of compile time.
  2. If in the context of a DEFBINARY form, a LET form is generated with bindings for all the slots whose values are known, and the generated code is put inside this LET form.
  3. The resulting code is passed to EVAL to perform the actual read or write.

Performance note

There is special-case logic to optimize EVAL type specifiers whose type-expression is a case form where the generated type forms don't require runtime knowledge. The following DEFBINARY type does not call EVAL at read time:

(define-enum example-type 1 () :integer :string)
(defbinary example ()
  (type :integer :type example-type)
  (value nil :type (eval (case type
                           (:integer '(unsigned-byte 32))
                           (:string '(counted-string 2))))))

However, if any of the branches of the case form requires information that is only bound at runtime, or if any other error happens while trying to expand types that may be returned by the case form, then the optimization will be skipped, and the type specifier will be processed using the three-step process described above. For example, this would not be optimized:

(define-enum example-type 1 () :integer :string :large-string)
(defbinary example ()
  (type :integer :type example-type)
  (count-length 0 :type (unsigned-byte 32))
  (value nil :type (eval (case type
                           (:integer '(unsigned-byte 32))
                           (:string '(counted-string 2))
                           (:large-string `(counted-string ,count-length))))) ;; COUNT-LENGTH is only bound at read-time; optimization skipped

An example of an error that would prevent optimization:

(define-enum example-type 1 () :integer :string)
(defbinary example ()
  (type :integer :type example-type)
  (value nil :type (eval (case type
                            (:integer (loop bloo foo boo boo)))))) ;; The LOOP macro will fail to expand,
                                                                   ;; so we'll just use EVAL. It'll fail again
                                                                   ;; at read or write time.