Appearance
Macro checkers
Type-checking macros is substantially different than type-checking functions. While a function type models a type transformation, a macro checker must instead model control-flow.
For this, we introduce "checker expressions". A checker expression is built out of building blocks, each representing a common control-flow pattern.
$expand
Many simple macros can get away with just using the $expand checker. This tells the type-checker to expand the macro, and then type-check the resulting expression, instead of trying to type-check the original macro invocation.
emacs-lisp
(defmacro my-increment (place)
(declare (et ($expand)))
`(setf ,place (1+ ,place)))$body
Many macros take a fixed number of positional arguments (zero or more), and the remaining arguments are expressions to be evaluated. The $body checker can be used in this case:
emacs-lisp
(defmacro save-excursion (&rest body)
(declare (et ($body)))
...)
(defmacro with-current-buffer (buf &rest body)
(declare (et ($body Buffer)))
...)Some utility checkers
The following can be used as building blocks for defining more complex checkers:
($at PATH...)traverse the current expression usingPATH, and type-check that sub-expression.($tail PATH...)traverse the current expression usingPATH, and type-check that sub-expression and all remaining sub-expressions of its parent. Return the last type.($tail-tuple PATH...)like$tail, but return a tuple type containing the types of all of the tail entries.($exp EXPR)type-check the expressionEXPR.($exps EXPRS)type-check the expressionsEXPRS, returning the type of the last one.($type TYPE-SPEC)do nothing and return the typeTYPE-SPEC.($nil)do nothing and return the typeNil.($progn CHECKERS...)perform the checks specified byCHECKERS, and return the last one.($prog1 CHECKERS...)perform the checks specified byCHECKERS, and return the first one.($ignore CHECKERS...)perform the checks specified byCHECKERS, and returnNil.($with-rec REC-CHECKER CHECKER)perform the check specified byREC-CHECKER, and set the current type-recommendation to its return type. Then, perform the checks specified byCHECKER.($expect EXPECTED-CHECKER CHECKER)similar to$with-rec, but throw an error if the type returned byCHECKERis not a subtype of the expected type.
These checkers all implement fairly linear control-flow. There are numerous other checkers for checking non-linear control flow, such as $branches, $loop, $if, and many others. This documentation needs to be expanded to contain in-depth explanations of each of these checkers.
Pattern matching
The $pcase checker can be used to match the macro's arguments, and route to different checkers based on the matched branch. The following is the real checker for let*:
emacs-lisp
($pcase
`(() . ,_body) ($tail 2)
`((,first . ,rest) . ,body)
($exp `(let (,first) (let* ,rest ,@body))))Here, ($tail 2) type-checks the expression's tail, starting at index 2. For let*, this is the body. ($exp (let (,first) ...)) type-checks a let expression containing just the first binding, whose body recursively references let*. This will recurse until each binding of the original let* has been processed individually within its own let.
Alternatively, the $arglist checker destructures the arguments like a function arglist.
emacs-lisp
(defmacro my-increment (place)
(declare
(et ($arglist (p) ($exp `(setf ,p (1+ ,p))))))
...)This checker tells the type-checker that when it encounters (my-increment PLACE), it should instead type-check the expression (setf PLACE (1+ PLACE)). It is often easier to describe how to check a macro in terms of an equivalent expression rather than trying to describe its functionality from the ground up.