
chevron_right
Macroexpandtime branching
Michał "phoe" Herda · Sunday, 9 January, 2022  23:31 · 3 minutes
Let's consider the following function:
(defun makeadder (x hugep)
(lambda (y) (+ x y (if hugep 1000 0))))
The result of calling (MAKEADDER 10)
closes over HUGEP
and makes a runtime check for its value.
CLUSER> (disassemble (makeadder 10 nil))
; disassembly for (LAMBDA (Y) :IN MAKEADDER)
; Size: 65 bytes. Origin: #x53730938 ; (LAMBDA (Y) :IN MAKEADDER)
; 38: 488975F8 MOV [RBP8], RSI
; 3C: 488BD3 MOV RDX, RBX
; 3F: E8EC012DFF CALL #x52A00B30 ; GENERIC+
; 44: 488B75F8 MOV RSI, [RBP8]
; 48: 4881FE17011050 CMP RSI, #x50100117 ; NIL
; 4F: BFD0070000 MOV EDI, 2000
; 54: B800000000 MOV EAX, 0
; 59: 480F44F8 CMOVEQ RDI, RAX
; 5D: E8CE012DFF CALL #x52A00B30 ; GENERIC+
; 62: 488BE5 MOV RSP, RBP
; 65: F8 CLC
; 66: 5D POP RBP
; 67: C3 RET
; 68: CC10 INT3 16 ; Invalid argument count trap
; 6A: 6A20 PUSH 32
; 6C: E8FFFA2CFF CALL #x52A00470 ; ALLOCTRAMP
; 71: 5B POP RBX
; 72: E958FFFFFF JMP #x537308CF
; 77: CC10 INT3 16 ; Invalid argument count trap
NIL
It would be better for performance if the test was only made once, in MAKEADDER
, rather than on every call of the adder closure. MAKEADDER
could then return one of two functions depending on whether the check succeeds.
(defun makeadder (x hugep)
(if hugep
(lambda (y) (+ x y 1000))
(lambda (y) (+ x y 0))))
A brief look at the disassembly of this fixed version shows us that we're right:
CLUSER> (disassemble (makeadder 10 nil))
; disassembly for (LAMBDA (Y) :IN MAKEADDER)
; Size: 21 bytes. Origin: #x53730BC7 ; (LAMBDA (Y) :IN MAKEADDER)
; C7: 488BD1 MOV RDX, RCX
; CA: E861FF2CFF CALL #x52A00B30 ; GENERIC+
; CF: 31FF XOR EDI, EDI
; D1: E85AFF2CFF CALL #x52A00B30 ; GENERIC+
; D6: 488BE5 MOV RSP, RBP
; D9: F8 CLC
; DA: 5D POP RBP
; DB: C3 RET
NIL
Still, with more flags than one, this style of writing code is likely to become unwieldy. For three flags, we would need to write something like this for the runtime version:
(defun makeadder (x hugep enormousp humongousp)
(lambda (y) (+ x y
(if hugep 1000 0)
(if enormousp 2000 0)
(if humongousp 3000 0))))
But it would look like this for the macroexpandtime version:
(defun makeadder (x hugep enormousp humongousp)
(if hugep
(if enormousp
(if humongousp
(lambda (y) (+ x y 1000 2000 3000))
(lambda (y) (+ x y 1000 2000 0)))
(if humongousp
(lambda (y) (+ x y 1000 0 3000))
(lambda (y) (+ x y 1000 0 0))))
(if enormousp
(if humongousp
(lambda (y) (+ x y 0 2000 3000))
(lambda (y) (+ x y 0 2000 0)))
(if humongousp
(lambda (y) (+ x y 0 0 3000))
(lambda (y) (+ x y 0 0 0))))))
The total number of combinations for n
boolean flags is 2^n
, making it hard to write and maintain code with so many branches. This is where WITHMACROEXPANDTIMEBRANCHING
comes into play. Using it, we can write our code in a way that looks similar to the runtimecheck version:
(defun makeadder (x hugep enormousp humongousp)
(withmacroexpandtimebranching (hugep enormousp humongousp)
(lambda (y) (+ x y
(macroexpandtimeif hugep 1000 0)
(macroexpandtimeif enormousp 2000 0)
(macroexpandtimeif humongousp 3000 0)))))
This code gives us the clarity of runtimechecked version and the performance of a macroexpandtimechecked version. A total of eight versions of the body (and therefore, eight possible LAMBDA
forms) are generated. At runtime, only one of them is selected, based on the boolean values of the three flags we provided.
Three conditional operators are provided  MACROEXPANDTIMEIF
, MACROEXPANDTIMEWHEN
, and MACROEXPANDTIMEUNLESS
, mimicking the syntax of, respectively, IF
, WHEN
, and UNLESS
.
It is possible to use the variable *MACROEXPANDTIMEBRANCHBYPASS*
for bypassing macroexpandtime branching; this is useful e.g. when trying to read the macroexpansions or when debugging. If that variable is set to true, the behavior of the macroexpander is modified:
WITHMACROEXPANDTIMEBRANCHING
expands into aPROGN
form,MACROEXPANDTIMEIF
expands into anIF
form,MACROEXPANDTIMEWHEN
expands into aWHEN
form,MACROEXPANDTIMEUNLESS
expands into anUNLESS
form.
Trying to use MACROEXPANDTIMEIF
, MACROEXPANDTIMEWHEN
, or MACROEXPANDTIMEUNLESS
outside the lexical environment established by WITHMACROEXPANDTIMEBRANCHES
will signal a PROGRAMERROR
.
Trying to use a branch name MACROEXPANDTIMEIF
, MACROEXPANDTIMEWHEN
, or MACROEXPANDTIMEUNLESS
that wasn't declared in WITHMACROEXPANDTIMEBRANCHES
will signal a PROGRAMERROR
.
Grab the code from GitHub.