DEV Community

vindarel
vindarel

Posted on

Compile-time exhaustiveness checking in Common Lisp with Serapeum

Serapeum is an excellent CL library, with lots of utilities. You should check it out. It provides a case-like macro, to use on enums, that warns you at compile-time if you handle all the states of that enum.

Example with ecase-of from its README. First we define the enum:

(deftype switch-state ()
  '(member :on :off :stuck :broken))
Enter fullscreen mode Exit fullscreen mode

We write a function that does something depending on the state of a switch object. We use ecase-of against this enum type to not miss any state.

Below we only check two states, so we'll get a warning (at compile-time, with a C-c C-c on Slime: the feedback is… instantaneous). Imagine that (state switch) is a function call that gets the current state of the "switch" variable passed to the function:

(defun flick (switch)
  (ecase-of switch-state (state switch)
    (:on (switch-off switch))
    (:off (switch-on switch))))
Enter fullscreen mode Exit fullscreen mode

=> the warning:

; caught WARNING:
;   Non-exhaustive match: (MEMBER :ON :OFF) is a proper subtype of SWITCH-STATE.
;   There are missing types: ((EQL :STUCK) (EQL :BROKEN))
; in: DEFUN FLICK
;     (CL-USER::STATE CL-USER::SWITCH)
Enter fullscreen mode Exit fullscreen mode

And a nice message too.

The syntax is the following:

(ecase-of switch-state state 
  (:state (do-something))
  (:state (do-something)))
Enter fullscreen mode Exit fullscreen mode

where switch-state is the enum-type defined above, state is the current value we are testing.

Another example: you think you fixed the previous example with the following version, but it has a bug. Serapeum catches it:

(defun flick (switch)
  (ecase-of switch-state (state switch)
    (:no (switch-off switch)) ;; <---- typo!
    (:off (switch-on switch))
    ((:stuck :broken) (error "Sorry, can't flick ~a" switch))))
Enter fullscreen mode Exit fullscreen mode

=>

; caught WARNING:
;   (MEMBER :NO) is not a subtype of SWITCH-STATE
Enter fullscreen mode Exit fullscreen mode

We see another possible syntax:

(ecase-of  
  ((:stuck :broken) (do-something)))
Enter fullscreen mode Exit fullscreen mode

we can group and handle states together.

Union-types

And we can do the same with union-types, using etypecase-of:

(defun negative-integer? (n)
  (etypecase-of t n
    ((not integer) nil)
    ((integer * -1) t)
    ((integer 1 *) nil)))
Enter fullscreen mode Exit fullscreen mode

=> Warning

; caught WARNING:
;   Can't check exhaustiveness: cannot determine if (OR (NOT INTEGER)
;                                                       (INTEGER * -1)
;                                                       (INTEGER 1 *)) is the same as T
Enter fullscreen mode Exit fullscreen mode

and indeed, we are handling ]∞, -1] U [1, ∞[

(defun negative-integer? (n)
  (etypecase-of t n
    ((not integer) nil)
    ((integer * -1) t)
    ((integer 1 *) nil)
    ((integer 0) nil)))
=> No warning: handle 0.
Enter fullscreen mode Exit fullscreen mode

This all happens at compile time, when we define functions, and the feedback is immediate, thanks to the live Lisp image.

Go play with it!

(ql:quickload "serapeum")
Enter fullscreen mode Exit fullscreen mode

Top comments (0)