Static type checking in the programmable programming language (Lisp)

(defun meh (p1)
(declare (fixnum p1))
(+ p1 3))
(defun meh2 (p1)
(declare (fixnum p1))
(+ p1 "8"))
2 compiler notes:typecheck-demo.lisp:7:3:                                                        
note: deleting unreachable code
warning:
Constant "8" conflicts with its asserted type NUMBER.
See also:
SBCL Manual, Handling of Types [:node]
Compilation failed.
(defun meh3a (p1)
(+ p1 3))
(declaim (ftype (function (fixnum) t) meh3a))
(defun meh3b ()
(meh3a "moo"))
==>
2 compiler notes:
typecheck-demo.lisp:13:3:
note: deleting unreachable code
warning:
Constant "moo" conflicts with its asserted type FIXNUM.
See also:
SBCL Manual, Handling of Types [:node]
Compilation failed.
(declaim (ftype (function (string) t) meh4a))
(defun meh4a (p1)
(+ p1 3))
(defun meh4b ()
(meh4a "moo"))
2 compiler notes:
typecheck-demo.lisp:18:3:
note: deleting unreachable code
warning:
Derived type of P1 is
(VALUES STRING &OPTIONAL),
conflicting with its asserted type
NUMBER.
See also:
SBCL Manual, Handling of Types [:node]
Compilation failed.
;; note that this is the first function failing
;; the second one compiles fine.
(defunt meh5c ((int p1) (int p2))
(+ p1 p2))
(meh5c 1 2) ; ==> 3
(defmacro defunt (name (&rest args) &body body)
"defun with optional type declarations"
`(progn
(declaim (ftype
(function
,(let (declares)
(dolist (arg args)
(push
(if (listp arg)
(if (equalp (string (first arg)) "int")
'fixnum
(first arg))
t)
declares))
declares)
t) ,name))
(defun ,name
,(loop for arg in args
collect
(if (listp arg)
(second arg)
arg))
,@body)))
;; simple use with no type declaration
(defunt meh5a (p1 p2)
(+ p1 p2))
;; one declared type, not the other
(defunt meh5b ((int p1) p2)
(+ p1 p2))
;; make sure this works
(defun meh5btest (p1)
(+ p1 "8"))
(defunt meh5c ((int p1) (int p2))
(+ p1 p2))
typecheck-demo.lisp:81:3:                                                       
note: deleting unreachable code
warning:
Constant "8" conflicts with its asserted type NUMBER.
See also:
SBCL Manual, Handling of Types [:node]
Compilation failed.
(defmacro defunt (name (&rest args) &body body)
"defun with optional type declarations"
;; backtick enters "code echoing", do not
;; evalluate when macro is run
`(progn
;; this macro emits two definitions to
;; the compiler:
;; 1 - the declaim type declarations
;; 2 - the defun
(declaim (ftype
(function
;; comma inside backtick means
;; "execute when macro is run"
,(let (declares)
(dolist (arg args)
(push
(if (listp arg)
;; also translate for C people, but case
;; independent
(if (equalp (string (first arg)) "int")
'fixnum
(first arg))
t)
declares))
;; return this list, which is integrated
;; into the code. You see that is why we
;; have so many parenthesis. Because what
;; is a passive data list here turns into
;; code, without any reformatting.
;; Read below for a step-by-step explanation
;; of what is going on
declares)
t) ,name))
(defun ,name
;; use the loop macro here for the same purpose we
;; manually collected the args with dolist and
;; push above
,(loop for arg in args
collect
(if (listp arg)
(second arg)
arg))
,@body)))
;; the result of the above macro is then fed into the compiler.
;; So we use three levels of evaluation time here:
;; 1 - when the macro is run
;; 2 - the output of the macro call, which is fed into the compiler
;; 3 - run time, when you actually call the resulting function
  • a single macro call can issue several new statements. We use this to emit both the declare and the defun, from a single user-issued definition
  • inside the macro you can control which code runs at macro call time and what gets emitted to the compiler. Keep in mind that you have the full language at your disposal at both times. One reason why these macros can be a bit hard to read is that you explicitly need to change between the different evaluation times, and the code has the same syntax. A C preprocessor macro or a C++ template use different languages at compile and run time, so it is a bit more clear what is evaluated when. Of course you cannot use your usual library at compile time like you can in Lisp
  • unless you do say otherwise the code inside the macro runs at macro expansion time. The return value is what is fed into the compiler. The return value much be a list, a list of Lisp language statements. That is why in Lisp you have to use list syntax for code. Got that? It is important. You construct this nested-parenthesis thing that is a list at compile time, and you feed it into the compiler, so the list becomes code.
  • a tick (‘) or a backtick (`) leaves things as lists and does not evaluate it at macro call time. That is how you return a list (which is data, not evaluated) from the macro (which is then fed into the compiler).
  • A comma (,) can be used inside a backticked (`) block to switch evaluation time back to macro call time. Your comma block also returns lists, and they become integrated into the backticked block.
  • The ,@ construct removes one list nesting, so it turns ((foo bar)) into (foo bar). You often need that when you return collections of items from the macro’s own variables or comma block. This is getting into the depths of macros and some obscure syntax, but it is not particularly hard to get right given the debug tools.
  • get an editor that has auto-indentation for Lisp code. It also needs to show you matching open parenthesis when you type a closing parenthesis. No Lisp programmer positions those parenthesis by hand. This is important. Trying to count these things by hand is maddening.
  • have some books ready. Paul Grapham’s “On Lisp” is a free book that explains Lisp macros really well. Going along with this blog post alone is probably tough

Debugging

(defmacro defunt (name (&rest args) &body body)
"defun with optional type declarations"
`(progn
(declaim (ftype
(function
,(let (declares)
(dolist (arg args)
(push
(if (listp arg)
;; <== HACK HERE, with typo
(if (equalp (string (first arg)) "intt")
'fixnum
(first arg))
t)
declares))
declares)
t) ,name))
...)
(macroexpand '(defunt meh5 ((int p1) p2) (+ p1 p2)))
==>
(PROGN
(DECLAIM (FTYPE (FUNCTION (T INT) T) MEH5))
(DEFUN MEH5 (P1 P2) (+ P1 P2)))
(defmacro defunt (name (&rest args) &body body)
"defun with optional type declarations"
`(progn
(declaim (ftype
(function
,(let (declares)
(dolist (arg args)
(push
(if (listp arg)
;; <== HACK HERE, with typo
(if (equalp (string (print (first arg))) "intt")
'fixnum
(first arg))
t)
declares))
declares)
t) ,name))
...)
(format t "~%debug args: ~a ~a" arg (type-of arg))
(if (listp arg)
;; <== HACK HERE, with typo
;;(if (equalp (string (print (first arg))) "intt")
(macroexpand '(defunt meh5 ((int p1) p2) (+ p1 p2)))
==>
debug args: (INT P1) CONS
debug args: P2 SYMBOL
;; expansion follows
(defvar *expandlog* nil)
(setf *expandlog*
(open "expand.log" :direction :output
:if-does-not-exist :create
:if-exists :append))
(defmacro defunt (name (&rest args) &body body)
"defun with optional type declarations"
`(progn
(declaim (ftype
(function
,(let (declares)
(dolist (arg args)
(format *expandlog*
"~%debug args: ~a ~a"
arg (type-of arg))
(push
(if (listp arg)
(defvar *expandlog* nil)
(defmacro defunt (name (&rest args) &body body)
;; as above)
;; use macro without logfile printing
(defunt2 meh6a (p1 p2)
(+ p1 p2))
;; use macro with logfile printing
(setf *expandlog*
(open "expand.log" :direction :output
:if-does-not-exist :create
:if-exists :append))
(defunt2 meh6b ((int p1) p2)
(+ p1 p2)))
(close *expandlog*)
;; important. (format nil ...) works fine, print nothing
;; (format closedfd ...) does not work
(setf *expandlog* nil)
debug args: (INT P1) CONS
debug args: P2 SYMBOL
(unwind-protect
(progn
;; protected body
(setf *expandlog*
(open "expand.log" :direction :output
:if-does-not-exist :create
:if-exists :append))
(defunt2 meh6b ((int p1) p2)
(+ p1 p2)))
(progn
;; these statements will be executed no matter what.
;; If the protected body has a nonlocal exit (throws
;; an exception through here, or if you interrupt
;; it with Control-C then these statements are still
;; executed like in a "finally" clause
(close *expandlog*)
(setf *expandlog* nil)

A quick look at performance

(declaim (ftype (function (fixnum fixnum) fixnum) moo1)
(inline moo1))
(defun moo1 (p1 p2)
(+ p1 p2))
(defun caller1 ()
(let ((n 42)
(new (+ (moo1 1
;; disable compiler optimization
(the fixnum
(parse-integer "2"))))))
(declare (fixnum n new))
(if (= new 45)
(print 'yes)
(print 'no))))
Yes, Master? CL-USER> (disassemble 'caller1)

; disassembly for CALLER1
; Size: 78 bytes. Origin: #x52E3FF56
; 56: 4883EC10 SUB RSP, 16 ; no-arg-parsing entry point
; 5A: 488B158FFFFFFF MOV RDX, [RIP-113] ; "2"
; 61: B902000000 MOV ECX, 2
; 66: 48892C24 MOV [RSP], RBP
; 6A: 488BEC MOV RBP, RSP
; 6D: E8065BE6FF CALL #x52CA5A78 ; #<FDEFN PARSE-INTEGER>
; 72: 4883C202 ADD RDX, 2
; 76: 4883FA5A CMP RDX, 90
; 7A: 7514 JNE L0
; 7C: 488B157DFFFFFF MOV RDX, [RIP-131] ; 'YES
; 83: B902000000 MOV ECX, 2
; 88: FF7508 PUSH QWORD PTR [RBP+8]
; 8B: E94831EBFF JMP #x52CF30D8 ; #<FDEFN PRINT>
; 90: L0: 488B1579FFFFFF MOV RDX, [RIP-135] ; 'NO
; 97: B902000000 MOV ECX, 2
; 9C: FF7508 PUSH QWORD PTR [RBP+8]
; 9F: E93431EBFF JMP #x52CF30D8 ; #<FDEFN PRINT>
NIL
Yes, Master? CL-USER>
struct foo {
int bar;
int baz;
} array[1024];

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Martin Cracauer

Martin Cracauer

Writing about programmer productivity, teambuilding and enabling a wide variety of different engineering personalities. CTO at www.thirdlaw.tech #lisp #freebsd