관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swift] 10. WWDC2023 정리 - (2) Swift Macro의 expansion (Macro 구현 방법, 올바른 Macro 에러 작성 방법, SwiftSyntax, SwiftSyntaxMacros, SwiftSyntaxBuilder) 본문

WWDC 정리/WWDC 2023 정리

[iOS - swift] 10. WWDC2023 정리 - (2) Swift Macro의 expansion (Macro 구현 방법, 올바른 Macro 에러 작성 방법, SwiftSyntax, SwiftSyntaxMacros, SwiftSyntaxBuilder)

jake-kim 2023. 6. 16. 02:00

(1). Swift Macro의 expansion (Macro의 목적, Macro 모델, Macro Role 이해하기, @freestanding, @attached)

(2). Swift Macro의 expansion (Macro 구현 방법, 올바른 Macro 에러 작성 방법, #externalMacro, SwiftSyntax,

SwiftSyntaxMacros, SwiftSyntaxBuilder)

(3). Swift Macro의 expansion (Syntax를 이용하여 매크로 구현방법, literal interpolation, TokenSyntax, ExprSyntax, MacroExpansionContext, 이름 충돌)

매크로 구현부의 위치

  • 매크로 구현부는 별도의 모듈인, 컴파일러 플러그인 모듈에 속함
  • 매크로는 보통 외부 매크로를 사용하는 형태
    • #externalMacro 키워드를 사용하여 외부에 있는 매크로를 사용
    • externalMacro는 컴파일러 플러그인에 의해 구현되는 매크로를 의미

  • Swift compilercompiler plugin에게 매크로에 대한 선언 값을 보내고, 플러그인에서 매크로를 찾아 매크로의 expansion부분을 Swift compiler에게 넘겨주는 형태

  • #externalMacro는 compiler plugin에 관한 관계를 정의하는것
    • module: 플러그인의 이름
    • type: 플러그인 내부 유형의 이름

  • 즉 #externalMacro를 사용하면 Swift compiler가 플러그인에게 StringifyMacro라는 유형에 대해 expansion(확장)을 요청하는 것
  • #externalMacro를 선언하면, 다른 API와 함께 일반 라이브러리에 들어가지만 매크로 구현은 별도의 컴파일러 플러그인 모듈에 들어감

매크로 구현 방법

예제로 사용할 매크로) DictinoaryStorageMacro

SwiftSyntax 임포트

  • 매크로를 구현하는 곳에서 SwiftSyntax 라는 라이브러리를 import
    • SwiftSyntax: 소스 코드를 구문 분석, 검사, 조작 및 생성하는데 도움이 되는 swift 프로젝트에서 유지 관리하는 패키지

  • SwiftSyntax는 Syntax trees를 생성
  • 아래 Person 구조는 "StructDeclSyntax"라는 유형의 인스턴스로 표현하며 각 가지에서는 소스코드에 해당하는 특정 표현식들을 분류한것
    • attributes
    • modifiers
    • structKeyword
    • identifer
    • memberBlock

  • 이러한 tree구조의 구문 노드를 "token"이라고 지칭하고 이름, 키워드, 구두점과 같은 소스 파일의 특정 텍스트를 나타내며 텍스트와 공백 및 주석과 같은 사소한 정보도 포함
  • 단, "token"이 아닌 노드들이 예외적으로 존재
    • attribtues 속성의 AttributeListSyntax
    • AttributeListSyntax 노드
    • memberBlock 속성의 MemberDeclBlockSyntax
  • memberBlock속성의 MemberDeclBlockSyntax 노드에는 자체 속성에 자식 노드가 있는 형태
    • 여는 중괄호 "{" 토큰
    • 멤버 MemberDeclListSyntax 토큰
    • 닫는 중괄호 "}" 토큰

  • 내부적으로  노드의 내용을 계속 탐색하면 결국 각 속성에 대한 노드를 찾아가는 원리
  • SwiftSyntax 관련 내용은 아래 세션 참고
    • Write Swift Macro 세션에서 소스 코드의 특정 부분이 구문 트리로 표현되는 방법을 파악하기 위한 실용적인 방법이 존재
    • SwiftSyntax 패키지 문서

SwiftSyntaxMacros 임포트

  • 매크로를 구현하는 곳에서 SwiftSyntaxMacros 라는 라이브러리를 import
    • 매크로 작성에 필요한 프로토콜과 유형을 제공하는 라이브러리

SwiftSyntaxBuilder 임포트

  • 매크로를 구현하는 곳에서 SwiftSyntaxBuilder를 import
    • 새로 생성된 코드를 나타내는 구문 트리를 구성하기 위해서 편리한 API를 제공

MemberMacro 프로토콜 준수

  • MemberMacro를 준수하면 매크로가 제공하는 각 역할에 대해 필수로 구현해야되는 요소가 생성

  • 이전 포스팅 글에서 알아봤던 매크로의 7가지 role은 각 해당 프로토콜이 있으며 구현은 매크로가 제공하는 각 열할에 대한 프로토콜을 준수하여 구현

매크로의 7가지의 role

  • DictionaryStorage 매크로에는 이러한 역할 중 4가지가 있으므로 DictionaryStoarageMacro 유형은 네가지 프로토콜을 준수해야함

(예제에서는 편의상 MemberMacro에 대해서만 준수하도록 구현)

  • MemberMacro는 expansion(of:providingMemberOf:in:) 타입 메소드를 구현이 필요
    • 해당 메소드의 의미: 매크로가 사용될 때 Swift compiler가 구성원 역할을 확장하기 위해 호출하는 것
    • 주의할점) expansion은 instance method가 아닌 static method이며, 이 이유는 Swift가 실제로 DictionaryStorageMacro유형의 인스턴스를 생성하지 않도록 하기 위함
    • 즉, DictionaryStoargeMacro는 단순히 method의 컨테이너의 역할만 수행

  • 위와같은 expansion methods는 모두 소스 코드에 정의한 SwiftSyntax 노드를 반환
    • MemberMacro는 타입에 멤버로 추가할 declaration의 리스트들을 확장하여, expansion method는 [DeclSyntax]를 반환

  • 본문 내부를 보면 배열이 생성되는 것을 확인이 가능
    • 본문 중 "var dictionary" 같은 경우는 단순히 문자열 리터럴이 아니고, Swift는 실제로 이를 소스 코드의 조각으로 취급하고 Swift parser에서 DeclSyntax 노드로 전환하도록 요청
    • (이것은 SwiftSyntaxBuilder가 제공해주는 일종의 편리함)

  • macro의 다른 세 가지 role에 대한 프로토콜을 extension으로 준수하게하여 DictionaryStorage 매크로의 작업 구현을 완성

올바른 Macro 사용

  • 위에서 만든 DictionaryStorageMacro를 enum타입에서 사용한다면?
  • 컴파일 에러가 발생하지만, 에러만 보았을때 어떻게 수정해야한다는 의미를 담고 있지 않아서 명확하지가 않은 상태

  • 이전 포스팅 글에서 알아보았듯이 Swift macro의 목표 중 하나인 매크로가 입력에서 실수를 감지하고 사용자에게 지정 오류를 내보낼 수 있도록 하는 것이므로 다른 방법이 존재
    • ex) @DictionaryStorage는 구조체에만 적용할 수 있도록 명확한 오류 메시지를 생성하도록 매크로 구현부를 수정
    • macro 구현에서 보았던 메소드인 expansion의 파라미터를 변경하면 해결

expansion 메소드의 파라미터 개념

  • Member macro의 경우 3가지의 파라미터가 존재
    • AttributeSyntax
    • DeclGroupSyntax
    • MacroExpansionContext
  • AttribteSytnax 파라미터
    • 개발자가 매크로를 사용하기 위해 작성한 실제 DictionaryStorage 속성

  • DeclGroupSyntax 파라미터
    • struct, enum, class, actor, protocol에 관한 노드가 모두 준수하는 프로토콜 형태

  • MacroExpansionContext 파라미터
    • 일종의 Context 개체로, 매크로 구현이 Swift compiler와 통신하려고 할 때 사용

  • 살펴본 3가지의 파라미터 타입을 사용하여 사용자에게 특정 오류 메시지를 던져주기가 가능

사용자에게 명확한 오류 보내기

  • 먼저 declaration 매개변수의 유형을 확인
    • struct같은 경우는 StructDeclSyntax
    • enum같은 경우는 EnumDeclSyntax

수정후)

  • macro 구현부에서 StructDeclSyntax로 타입 체크가 가능
    • declaration.is()메소드를 사용
    • 지금은 guard문 안에서 빈 배열을 리턴하고 있기 때문에 사용자에게 "struct만 사용해"라는 메시지를 던져주지는 않음

  • throw를 사용하여 에러를 던지도록 수정
    • 아직 부족한 점: 현재는 타입만을 trhow로 던지고 있으며, output을 제어할 수 없는 상태

  • 오류를 방출하는 더 좋은 형태?
    • Diagnostic(진단) 이라는 유형의 인스턴스를 생성하는것
    • DIagnostic은 일종의 컴파일러 용어이며, 부러진 다리의 엑스레이를 보는 의사가 골절을 진단하듯이 부러진 코드의 구문 트리를 보는 컴파일러나 매크로는 오류나 경고를 나타내는것

  • Diagnostic은 두 가지 정보가 필요
    • node: 컴파일러는 어디서 잘못되었는지 위치는 알고 있으므로, 어떤 속성에서 에러가 발생헀는지 알려주어야 하는데 지금은DictionaryStorage를 속성을 쓰고 있었고 이 정보는 attribute 속성을 넘기기만하면 해결
    • message: 컴파일러에서 생성하려는 실제 메세지
  • MyLibDiagnostic은 enum으로 따로 정의한 것이며, DiagnosticMessage 프로토콜을 따르는 형태
    • 가장 중요한 부분은 severity이며 여기서는 이 진단이 error인지 warning인지 결정이 가능

  • 이러한 내용을 정의하여 context.diagnose()에 넘기면 swift compiler에게 알려주어 에러 메시지를 띄우기 가능

  • fix-it 기능도 제공 가능

(다음 포스팅 글에서 Building syntax trees 계속)

 

* 참고

https://swiftpackageindex.com/apple/swift-syntax/508.0.1/documentation/swiftsyntax

https://developer.apple.com/videos/play/wwdc2023/10166/

https://developer.apple.com/videos/play/wwdc2023/10167/

Comments