[TIL] common-lisp defmacro와 forward-declaration

🗓️ 10 Nov, 2025

의외로 단순한건데, 컴파일한 sbcl image에서 runtime에 unbound variable 컨디션을 발생시킬 수 있음.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
  (defmacro with-foo-bar (base-str (foo-var bar-var) &rest body)
    "BASE-STR에 foo, bar 문자열을 붙인 FOO-VAR, BAR-VAR 바인딩을 만들어,
  BODY을 해당 바인딩을 적용하여 실행"
    ;; NOTE let-대신 symbol-macrolet 사용해도 ok:
    `(let ((,foo-var (format nil "FOO:~a" ,base-str))
           (,bar-var (format nil "BAR:~a" ,base-str)))
       ,@body))

  ;;; 매크로 with-foo-bar 사용:
  (with-foo-bar "hello" (a b)
    (format t "~a // ~a~%" a b))
  ;; [OUTPUT] FOO:hello // BAR:hello

단순하게 코드가 커지면1, defmacro-선언보다 해당매크로 호출이 컴파일러가 볼 때에 먼저 위치할 수 있는데, 그런 경우엔 좀 어이없지만, unbound variable 런타임 컨디션이 발생.

이유를 생각해보면,

  1. 단순히 with-foo-bar-자체를 참조하지 못해서가 아니라,
  2. (with-foo-bar "hello" (a b) ...)-에서 (a b)-부분 때문.

    1. macro expansion rule이 아니라,
    2. form expression의 평가규칙을 적용하느라, 즉, a-함수를 (b)-을 인자로 호출하려고 시도.
    3. …컨디션의 에러메시지는 매크로이름이랑 상관 없이, Unbound variable: a / b-와 같이만 표시하므로 더 혼란스러워짐.

일단 추론은:

  1. sbcl 컴파일해도, 결국 실행시간에 defmacro한 것도 macro-expansion을 수행하려고 하는구나.

    • …그말인 즉, 컴파일시간에 확장해놓고 실행해도 될 부분도 그렇게 하는게 기본 컴파일된 코드구나 싶어서 당혹.
  2. …라고 납득하려고 했으나, 말이 않되는거란 생각: 그럴거면 뭐하러 eval-when 등으로 컴파일시점을 분리해놓았겠는가;;

그래서 실험:

https://github.com/ageldama/defmacro-forward-declaration--example

disassembly -bug

  • 다음과 같이, 그리고 알고 있는 것과 같이, macro-expanded한 코드를 생성한다.

    • 다른 함수를 호출할 것도 없이, 그 함수 내용으로 코드가 복사된 것을 확인.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
  ; disassembly for DEFMACRO-FORWARD-DECLARATION:MAIN
  ; Size: 294 bytes. Origin: #xB8000952BF                       ; DEFMACRO-FORWARD-DECLARATION:MAIN
  ; 2BF:       498B4510         MOV RAX, [R13+16]               ; thread.binding-stack-pointer
  ; 2C3:       488945F8         MOV [RBP-8], RAX
  ; 2C7:       488B0592FFFFFF   MOV RAX, [RIP-110]              ; 'DEFMACRO-FORWARD-DECLARATION:MAIN
  ; 2CE:       488B5009         MOV RDX, [RAX+9]
  ; 2D2:       85D2             TEST EDX, EDX
  ; 2D4:       0F8408010000     JE L0
  ; 2DA:       4883EC10         SUB RSP, 16
  ; 2DE:       B902000000       MOV ECX, 2
  ; 2E3:       48892C24         MOV [RSP], RBP
  ; 2E7:       488BEC           MOV RBP, RSP
  ; 2EA:       FF15D0E3A6FF     CALL [RIP-5839920]              ; DISASSEMBLE
  ; 2F0:       4883EC10         SUB RSP, 16
  ; 2F4:       488B156DFFFFFF   MOV RDX, [RIP-147]              ; "FOO:"
  ; 2FB:       488B3D6EFFFFFF   MOV RDI, [RIP-146]              ; "hello"
  ; 302:       B904000000       MOV ECX, 4
  ; 307:       48892C24         MOV [RSP], RBP
  ; 30B:       488BEC           MOV RBP, RSP
  ; 30E:       FF15AC80A7FF     CALL [RIP-5799764]              ; SB-KERNEL:%CONCATENATE-TO-STRING
  ; 314:       4C8BCA           MOV R9, RDX
  ; 317:       4C894DE0         MOV [RBP-32], R9
  ; 31B:       4883EC10         SUB RSP, 16
  ; 31F:       488B1552FFFFFF   MOV RDX, [RIP-174]              ; "BAR:"
  ; 326:       488B3D43FFFFFF   MOV RDI, [RIP-189]              ; "hello"
  ; 32D:       B904000000       MOV ECX, 4
  ; 332:       48892C24         MOV [RSP], RBP
  ; 336:       488BEC           MOV RBP, RSP
  ; 339:       FF158180A7FF     CALL [RIP-5799807]              ; SB-KERNEL:%CONCATENATE-TO-STRING
  ; 33F:       4C8B4DE0         MOV R9, [RBP-32]
  ; 343:       488955F0         MOV [RBP-16], RDX
  ; 347:       4D8B8578030000   MOV R8, [R13+888]               ; tls: *STANDARD-OUTPUT*
  ; 34E:       4983F8FF         CMP R8, -1
  ; 352:       4C0F440425307D0450 CMOVE R8, [#x50047D30]        ; *STANDARD-OUTPUT*
  ; 35B:       4C8945E8         MOV [RBP-24], R8
  ; 35F:       4883EC10         SUB RSP, 16
  ; 363:       498BD1           MOV RDX, R9
  ; 366:       488B7DE8         MOV RDI, [RBP-24]
  ; 36A:       B904000000       MOV ECX, 4
  ; 36F:       48892C24         MOV [RSP], RBP
  ; 373:       488BEC           MOV RBP, RSP
  ; 376:       FF158CB5A6FF     CALL [RIP-5851764]              ; WRITE-STRING
  ; 37C:       4883EC10         SUB RSP, 16
  ; 380:       488B1501FFFFFF   MOV RDX, [RIP-255]              ; " // "
  ; 387:       488B7DE8         MOV RDI, [RBP-24]
  ; 38B:       B904000000       MOV ECX, 4
  ; 390:       48892C24         MOV [RSP], RBP
  ; 394:       488BEC           MOV RBP, RSP
  ; 397:       FF156BB5A6FF     CALL [RIP-5851797]              ; WRITE-STRING
  ; 39D:       4883EC10         SUB RSP, 16
  ; 3A1:       488B55F0         MOV RDX, [RBP-16]
  ; 3A5:       488B7DE8         MOV RDI, [RBP-24]
  ; 3A9:       B904000000       MOV ECX, 4
  ; 3AE:       48892C24         MOV [RSP], RBP
  ; 3B2:       488BEC           MOV RBP, RSP
  ; 3B5:       FF154DB5A6FF     CALL [RIP-5851827]              ; WRITE-STRING
  ; 3BB:       4883EC10         SUB RSP, 16
  ; 3BF:       BA510A0000       MOV EDX, 2641
  ; 3C4:       488B7DE8         MOV RDI, [RBP-24]
  ; 3C8:       B904000000       MOV ECX, 4
  ; 3CD:       48892C24         MOV [RSP], RBP
  ; 3D1:       488BEC           MOV RBP, RSP
  ; 3D4:       FF1546B1A6FF     CALL [RIP-5852858]              ; WRITE-CHAR
  ; 3DA:       498BD4           MOV RDX, R12                    ; NIL
  ; 3DD:       C9               LEAVE
  ; 3DE:       F8               CLC
  ; 3DF:       C3               RET
  ; 3E0:       CC0F             INT3 15                         ; Invalid argument count trap
  ; 3E2: L0:   CC13             INT3 19                         ; UNDEFINED-FUN-ERROR
  ; 3E4:       00               BYTE #X00                       ; RAX(d)

disassembly +bug

  • (추론대로) A, B 심볼을 함수로서 호출하려고 애쓰다 디버거로 빠져든 상황이다.

    • 디버거에서 disassemble:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
  debugger invoked on a UNBOUND-VARIABLE @B80009505A in thread
  #<THREAD tid=37091 "main thread" RUNNING {1200030003}>:
    The variable DEFMACRO-FORWARD-DECLARATION::B is unbound.

  Type HELP for debugger help, or (SB-EXT:EXIT) to exit from SBCL.

  restarts (invokable by number or by possibly-abbreviated name):
    0: [CONTINUE   ] Retry using DEFMACRO-FORWARD-DECLARATION::B.
    1: [USE-VALUE  ] Use specified value.
    2: [STORE-VALUE] Set specified value and use it.
    3: [ABORT      ] Exit from the current thread.

  (DEFMACRO-FORWARD-DECLARATION:MAIN)
     source: (A B)
  0] (disassemble #'dmfd:main)
  ; disassembly for DEFMACRO-FORWARD-DECLARATION:MAIN
  ; Size: 323 bytes. Origin: #xB800094F1F                       ; DEFMACRO-FORWARD-DECLARATION:MAIN
  ; 4F1F:       498B4510         MOV RAX, [R13+16]              ; thread.binding-stack-pointer
  ; 4F23:       488945F8         MOV [RBP-8], RAX
  ; 4F27:       4C8B05A2FFFFFF   MOV R8, [RIP-94]               ; 'DEFMACRO-FORWARD-DECLARATION::B
  ; 4F2E:       418B50F5         MOV EDX, [R8-11]
  ; 4F32:       4A8B142A         MOV RDX, [RDX+R13]
  ; 4F36:       4883FAFF         CMP RDX, -1
  ; 4F3A:       490F445001       CMOVE RDX, [R8+1]
  ; 4F3F:       80FA09           CMP DL, 9
  ; 4F42:       0F8411010000     JE L0
  ; 4F48:       4883EC10         SUB RSP, 16
  ; 4F4C:       B902000000       MOV ECX, 2
  ; 4F51:       48892C24         MOV [RSP], RBP
  ; 4F55:       488BEC           MOV RBP, RSP
  ; 4F58:       FF153A97A7FF     CALL [RIP-5793990]             ; DEFMACRO-FORWARD-DECLARATION::A
  ; 4F5E:       480F42E3         CMOVB RSP, RBX
  ; 4F62:       488955E8         MOV [RBP-24], RDX
  ; 4F66:       4C8B056BFFFFFF   MOV R8, [RIP-149]              ; 'DEFMACRO-FORWARD-DECLARATION::A
  ; 4F6D:       418B50F5         MOV EDX, [R8-11]
  ; 4F71:       4A8B142A         MOV RDX, [RDX+R13]
  ; 4F75:       4883FAFF         CMP RDX, -1
  ; 4F79:       490F445001       CMOVE RDX, [R8+1]
  ; 4F7E:       80FA09           CMP DL, 9
  ; 4F81:       0F84D5000000     JE L1
  ; 4F87:       4C8B0542FFFFFF   MOV R8, [RIP-190]              ; 'DEFMACRO-FORWARD-DECLARATION::B
  ; 4F8E:       418B40F5         MOV EAX, [R8-11]
  ; 4F92:       4A8B0428         MOV RAX, [RAX+R13]
  ; 4F96:       4883F8FF         CMP RAX, -1
  ; 4F9A:       490F444001       CMOVE RAX, [R8+1]
  ; 4F9F:       3C09             CMP AL, 9
  ; 4FA1:       0F84B8000000     JE L2
  ; 4FA7:       488945F0         MOV [RBP-16], RAX
  ; 4FAB:       4D8B8D78030000   MOV R9, [R13+888]              ; tls: *STANDARD-OUTPUT*
  ; 4FB2:       4983F9FF         CMP R9, -1
  ; 4FB6:       4C0F440C25307D0450 CMOVE R9, [#x50047D30]       ; *STANDARD-OUTPUT*
  ; 4FBF:       4C894DE0         MOV [RBP-32], R9
  ; 4FC3:       4883EC10         SUB RSP, 16
  ; 4FC7:       488B7DE0         MOV RDI, [RBP-32]
  ; 4FCB:       B904000000       MOV ECX, 4
  ; 4FD0:       48892C24         MOV [RSP], RBP
  ; 4FD4:       488BEC           MOV RBP, RSP
  ; 4FD7:       FF153BC3A6FF     CALL [RIP-5848261]             ; PRINC
  ; 4FDD:       4883EC10         SUB RSP, 16
  ; 4FE1:       488B1500FFFFFF   MOV RDX, [RIP-256]             ; " // "
  ; 4FE8:       488B7DE0         MOV RDI, [RBP-32]
  ; 4FEC:       B904000000       MOV ECX, 4
  ; 4FF1:       48892C24         MOV [RSP], RBP
  ; 4FF5:       488BEC           MOV RBP, RSP
  ; 4FF8:       FF150AB9A6FF     CALL [RIP-5850870]             ; WRITE-STRING
  ; 4FFE:       4883EC10         SUB RSP, 16
  ; 5002:       488B55F0         MOV RDX, [RBP-16]
  ; 5006:       488B7DE0         MOV RDI, [RBP-32]
  ; 500A:       B904000000       MOV ECX, 4
  ; 500F:       48892C24         MOV [RSP], RBP
  ; 5013:       488BEC           MOV RBP, RSP
  ; 5016:       FF15FCC2A6FF     CALL [RIP-5848324]             ; PRINC
  ; 501C:       4883EC10         SUB RSP, 16
  ; 5020:       BA510A0000       MOV EDX, 2641
  ; 5025:       488B7DE0         MOV RDI, [RBP-32]
  ; 5029:       B904000000       MOV ECX, 4
  ; 502E:       48892C24         MOV [RSP], RBP
  ; 5032:       488BEC           MOV RBP, RSP
  ; 5035:       FF15E5B4A6FF     CALL [RIP-5851931]             ; WRITE-CHAR
  ; 503B:       488B15AEFEFFFF   MOV RDX, [RIP-338]             ; "hello"
  ; 5042:       488B7DE8         MOV RDI, [RBP-24]
  ; 5046:       498BF4           MOV RSI, R12                   ; NIL
  ; 5049:       B906000000       MOV ECX, 6
  ; 504E:       FF7508           PUSH QWORD PTR [RBP+8]
  ; 5051:       FF253996A7FF     JMP [RIP-5794247]              ; DEFMACRO-FORWARD-DECLARATION::WITH-FOO-BAR
  ; 5057:       CC0F             INT3 15                        ; Invalid argument count trap
  ; 5059: L0:   CC19             INT3 25                        ; UNBOUND-SYMBOL-ERROR
  ; 505B:       20               BYTE #X20                      ; R8(d)
  ; 505C: L1:   CC19             INT3 25                        ; UNBOUND-SYMBOL-ERROR
  ; 505E:       20               BYTE #X20                      ; R8(d)
  ; 505F: L2:   CC19             INT3 25                        ; UNBOUND-SYMBOL-ERROR
  ; 5061:       20               BYTE #X20                      ; R8(d)

결론

  1. sbcl이 컴파일할 때에, 해당 참조를 찾지 못하면 그냥 함수려니하고 코드를 생성해버림.
  2. asdf:make, sbcl 둘 다 나빠 ㅠ.ㅠ 경고라도 뛰워주던가…

    • (lisp-implementation-version) ; => "2.5.10"
    • (asdf:asdf-version) ; => "3.3.1"
    • …그래서 시도했지만 별 차이 없음:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      
        (setf asdf:*asdf-verbose* t
              *load-verbose* t
              *load-print* t
              *compile-verbose* t
              *compile-print* t)
      
        (declaim #+sbcl(sb-ext:unmuffle-conditions style-warning))
      
        (declaim #+sbcl(sb-ext:unmuffle-conditions compiler-note))

[EDIT] [2025-11-10 Mon]

  1. 위 disassemble 같이 만든 상황을 다시 재현하기 어려움.
  2. 흠좀무. 다행이라면 다행.
  3. 그리고 사실 (declaim (ftype ...))-으로 함수라면 전방선언(forward declaration)이 가능하겠지만, 매크로는 컴파일러가 동작하는 시점에 필요하므로 불가하다고.
  4. …결국 asdf system definition에 serial으로 의존성순서에 맞게 넣어줘야함. ㅎㅎ

Footnotes


1

최근에 작업한 코드에서 겪은 문제라.