Arithmetic with unit checking at compile time.

Let us recap that annoying error needing a workaround, and why I think the mechanism behind the error is a good thing:

This bombs:

(MeV/c^2 (unittmp (* value 1.78266191e-30) kg))

because (unit …) is a macro call, and (MeV/c² (unit (* value 1.78266191e-30) kg)) is not evaluated, so the math operator (* …), which we invoke at compile time, cannot work. It passes that entire code fragment. You can walk that code fragment.

OK, let’s play with this to understand:

;; to be defined

(defmacro unit-test ...)

(defun testfun2 ()

(unit-test 1 kg))

The macro unit-test will be invoked at compile time. That means when you compile the function testfun2, actions in the macro will be carried out. We will use this to debug using the only true debug facility that exists in the world — print statements. Or in the case of Lisp, format statements.

(defmacro unit-test (value unit &rest more)

(declare (ignore more))

(format t "~%value is '~a'~%" value)

(format t "unit is '~a'~%" unit)

value)

(defun testfun2 ()

(unit-test 1 kg))

Gives you, at compile time, into the same stdout as the compiler:

value is '1'

unit is 'KG'

Before I go any further, I want to make sure you get this.

YOU HAVE THE ENTIRE LANGUAGE AT YOUR DISPOSAL AT COMPILE TIME!!!

The entire thing. You can use printf/format statements at compile time to print random data structures that float around in your half-compiled code to debug them. Seen such a facility with templates lately?

Anyway, so let’s see what happens with that evaluation thing:

(defmacro unit-test (value unit &rest more)

(declare (ignore more))

(format t "~%value is '~a'~%" value)

(format t "unit is '~a'~%" unit)

value)

(defun testfun3 ()

(unit-test (* 1 1) kg))

Output is:

; compiling (DEFUN TESTFUN3 ...)

value is '(* 1 1)'

unit is 'KG'

Oops. There you have it. (* 1 1) is passed not as an evaluated number, it is passed as a code fragment. This is awesome. I mean not always, e.g. not at Friday night when this happens in deeply nested macros and you have to debug it.

But it exposes a very powerful mechanism:

(defmacro unit-test2 (value unit &rest more)

(declare (ignore more))

(format t "~%value is '~a'~%" value)

(format t "unit is '~a'~%" unit)

(when (listp value)

(dolist (element value)

(format t "list element is '~a'~%" element)))

value)

(defun testfun3 ()

(unit-test2 (* 1 1) kg))

Output is:

value is '(* 1 1)'

unit is 'KG'

list element is '*'

list element is '1'

list element is '1'

Woah. We can walk this code. We do not only have Turing-complete code walking, we have code walking that can use THE ENTIRE LANGUAGE at compile time.

(defmacro unit-test3 (value unit &rest more)

(declare (ignore more))

(format t "~%value is '~a'~%" value)

(format t "unit is '~a'~%" unit)

(when (listp value)

(dolist (element value)

(if (and (numberp element) (= element 42))

(format t "Looks like the answer to everything~%")

(format t "list element is '~a'~%" element))))

value)

(defun testfun3 ()

(unit-test3 (* 42 1) kg))

Output:

; compiling (DEFUN TESTFUN3 ...)

value is '(* 42 1)'

unit is 'KG'

list element is '*'

Looks like the answer to everything

list element is '1'

See, we can do whatever we want.

It isn’t limited to inspecting the code. Macros are there to make new code. So let’s try this.

(defmacro unit-test4 (value unit &rest more)

(declare (ignore more))

(when (listp value)

(dolist (element value)

(when (and (numberp element) (= element 42))

(return-from unit-test4 `(progn

(dotimes (i 4)

(format t "hello, world~%"))

,value)))))

value)

(defun testfun4a ()

(unit-test4 (* 41 1) kg))

(defun testfun4b ()

(unit-test4 (* 42 1) kg))

Running it:

Yes, Master? CL-USER> (testfun4a)

41

Yes, Master? CL-USER> (testfun4b)

hello, world

hello, world

hello, world

hello, world

42

Yes, Master? CL-USER>

Woah. We actually inserted new code into the function. Can we see what is going on? Sure:

Yes, Master? CL-USER> (macroexpand '(unit-test4 (* 41 1) kg))

(* 41 1)

T

Yes, Master? CL-USER> (macroexpand '(unit-test4 (* 42 1) kg))

(PROGN (DOTIMES (I 4) (FORMAT T "hello, world~%")) (* 42 1))

T

Yes, Master? CL-USER>

That is the most basic macroexpansion debugging there is. There are much more sophisticated macroexpand debugging facilities in the IDE’s, e.g. SLIME. Finely controlled evaluation until you can figure out what’s going on.

Now, how do we use this to our advantage?

Well, we can do arithmetic with unit checking at compile time.

(defmacro plus-with-units (val1 val2)

;; fancy code here

(+ val1 val2))

;; this should work

(defun testfun5a ()

(plus-with-units (unit 5 m/s) (unit 6 m/s)))

;; this should *not* work

(defun testfun5b ()

(plus-with-units (unit 5 m/s) (unit 6 m)))

;; this can be made to work later

(defun testfun5c ()

(plus-with-units (unit 5 m/s) (unit 6 km/h)))

OK, so what is the objective here?

if the units are available, they should be checked. In the first version for being equal, in the more fancy version for being compatible. Either way we want to catch errors.

we don’t want to spend an entire night implementing this.

the check should happen at compile time. The compiled code should have nothing except one compiled out number in Planck units.

(defmacro plus-with-units (val1 val2)

(let (firstunit)

(dolist (thing (list val1 val2))

(when (listp thing)

(if (not firstunit)

(setf firstunit (third thing))

(unless (equal firstunit (third thing))

;; print a clear error message. Not something people

;; need to copy into a web page to translate to human

(error "Incompatible units: ~a ~a~%"

firstunit (third thing)))))))

;; delay evaluation

`(+ ,val1 ,val2))

;; works:

(defun testfun5a ()

(plus-with-units (unit 5 m/s) (unit 6 m/s)))

;; error:

(defun testfun5b ()

(plus-with-units (unit 5 m/s) (unit 6 m)))

The error as displayed for the second test at compile-time is:

crachem.lisp:209:3:

error:

during macroexpansion of (PLUS-WITH-UNITS (UNIT 5 M/S) (UNIT 6 M)). Use

*BREAK-ON-SIGNALS* to intercept.



Incompatible units: M/S M Compilation failed.

Allright. Looks useful.

So we have the full language at our disposal, at compile-time, with printf/format and all. And we can use that do give useful error messages at compile time. Just like in real code. It is really bad when your language forces you to use a different, crippled language at compile time.

I want to wind down this post here. As you can see, the (plus-with-units …) macro is not sophisticated, it would at least have to check that the first list element is indeed “unit”. Actually, no. It should integrate with the (unit…) macro.

The way to do this further is that you change the (unit …) macro to allow the programmer to use it to learn more about the macro call. Right now you only get the converted number out of the (unit …) call. The (unit …) call knows which unit was used, but you cannot ask it to give it to you. While we are at it, the (unit …) macro could also tell us what kind of unit that is (speed, weight etc).

We use multiple-value returns for this. A function in Common Lisp can return more than one value, and unless you deliberately capture them all but the first are ignored. We also want to convert the bulk of this macro into a function, because that is easier to debug. Did I mention you can define functions and use them at compile time, from macros?

Example implementation:

;; this unit knower returns three values:

;; - the converted value

;; - the unit

;; - what kind of unit is it?

(eval-when (:compile-toplevel)

(defun unit2-helper (value unit)

(let* (whatkind

(newvalue

(case unit

(m/s (setf whatkind 'speed) (/ value 2.99792458e+8))

(Js (setf whatkind 'energy-time) (/ value 1.054571800e-34))

(m (setf whatkind 'length) (/ value 1.616229e-35))

(s (setf whatkind 'time) (/ value 5.39116e-44))

(kg (setf whatkind 'mass) (/ value 2.176470e-8))

(:none (setf whatkind 'none) value)

(MeV/c^2 (setf whatkind 'mass)

(* value (unit2-helper 1.78266191e-30 'kg)))

(t (error "unknown unit ~a" unit)))))

(values newvalue unit whatkind)))) ;; this is the dumb frontend you call from regular code

(defmacro unit2 (value unit &rest more)

(declare (ignore more))

(unit2-helper value unit))

(As you can guess, Common Lisp is hygienic. defun results are supposed to be available at run time. They are not supposed to pollute the compile-time environment with their symbols that are intended for runtime. Lisp gives us a way change that, with a “(eval-when …)” statement. That is actually required when you use a properly hygienic Lisp implementation.)

The basic macro (unit2…) behaves like (unit…) did before. That is what you use when writing ordinary code. But the new toy gives us the ability to know more about the (unit2 …) calls coming in. We can use that to improve things tremendously.

(defmacro plus-with-units2 (val1 val2)

(let (firstkind)

(dolist (thing (list val1 val2))

(when (and (listp thing) (equal (first thing) 'UNIT2))

(multiple-value-bind (newvalue unit whatkind)

(unit2-helper (second thing) (third thing))

(print whatkind)

(if (not firstkind)

(setf firstkind whatkind)

(unless (equal firstkind whatkind)

;; print a clear error message. Not something people

;; need to copy into a web page to translate to human

(error "Incompatible units: ~a ~a~%"

firstkind whatkind)))))))

;; delay evaluation until later in compilation

`(+ ,val1 ,val2))

;; this now works, the code recognizes that kg and MeV/c^2

;; are both units of the same kind - mass

(defun testfun6 ()

(plus-with-units2 (unit2 5 kg) (unit2 6 MeV/c^2)))

Make sure that everything happens at compile time:

Yes, Master? CL-USER> (disassemble 'testfun6)

; disassembly for TESTFUN6

; Size: 13 bytes. Origin: #x52E3B9B6

; B6: 488B15B3FFFFFF MOV RDX, [RIP-77]; no-arg-parsing entry point

; 2.297298e8

; BD: 488BE5 MOV RSP, RBP

; C0: F8 CLC

; C1: 5D POP RBP

; C2: C3 RET

NIL

Yes, Master? CL-USER>

Looking good.