SIL(Swift Intermediate Language)을 통한 Swift debugging
- 이 문제는 이슈 [SR-7249], commit을 통해 해결되었고, swift 4.2 브랜치에 머지 되었습니다.
Episode III
몇일 전 그렇게 멀지않은 방이동, 경복궁 건물 6층 배민찬 개발팀에서,
제너릭을 가지는 구조체에 반환값이 있는 readonly 프로퍼티를 선언한 스위프트 개발자 A는 구조체를 확장하며 지난 선언을 잊고 프로퍼티를 중복해서 선언한 후, Xcode에 숨겨진 무서운 비밀을 만나는데…
두둥
Xcode 사용자 분들은 An internal error occured. Source editor functionality is limited. Attempting to restore...
라는 에러와 함께 코드 autocompletion동작이 동작하지 않거나 코드 하일라이트가 사라지는 등의 문제를 여러번 겪으셨을 겁니다. 이전에는 단순히 Xcode의 버그라고 생각해, 재부팅이나 clean등으로 해당 문제를 해결하려고 했습니다. 하지만 항상 Xcode의 버그만 이런 문제를 발생시키는건 아닌것 같습니다. Xcode는 소스들의 인덱싱 작업을 하며 swift빌드 단계의 특정(AST->SIL) 단계를 이용하여 코드 하일라이트나 autocompletion을 수행하고 있다는 추측을 하고 있습니다. 이 글을 쓰게 된 계기는 바로 SIL코드 생성 단계가 실패하여 발생한 문제였습니다. 이를 파악하는 도중 알게된 SIL에 대한 지식과 사용가능한 몇 가지 툴을 공유하고자 합니다.
무슨 문제였나?
코드를 작성 중 어떤 특정 변수를 실수로 중복해 선언한 상태로 중복 선언이라는 에러 메시지 없이 xcode의 데몬이 멈추고 build 시 sil 생성중 에러를 반환하는 경우를 만나게 되었습니다.
이전에 비슷한 경우를 만났던 경우 의심되는 코드를 완전히 지우고 다시 설계하고 작성해서 넘어갔었는데, 몇 번 비슷한 상황을 겪은 터라 이번에는 확실히 알고 넘어가기로 결심했습니다. 문제를 더 간단한 코드로 재현하여, 명확하게 문제를 파악해서, 제대로된 에러를 반환하고 그럼으로써 문제의 원인을 파악하여 해결책을 찾아보기로 결심했습니다.
- 중간에도 내용을 추가 해 놨습니다만, 스위프트 포럼에 문의를 해본 결과 스위프트의 버그이니 버그 리포트를 하는게 좋겠다는 답이 달렸습니다. 버그 리포트를 올렸으니 결과가 오면 해당 내용을 추가해서 글을 수정하겠습니다.
어떻게 재현할까?
구조는 좀 더 복잡했지만, 재현과 분석을 원활하게 하기 위해 원 코드를 단순화한 코드로 바꿔 진행해보겠습니다.
여기 A와 B, 두 구조체가 있습니다.
struct A
struct B
A와 B는 각각 Element라는 제너릭을 선언하고 있습니다. 이것은 어떤 값이라도 선언할 수 있도록 아무런 constraint를 선언하고 있지 않습니다.
A는 b 함수를 선언하고 반환 값은 B<Element>
입니다. A의 Element와 b 함수가 반환하는 B 클래스의 Element는 동일합니다. A.Element == B.Element
입니다.
import Foundation
struct A<Element> {
func b() -> B<Element> {
return B()
}
}
class B<Element> {
}
그런데 b함수가 인자를 받지 않는 것이 눈에 띕니다. 이것이 거슬렸던 저는 함수를 프로퍼티로 바꾸기로 결심합니다.
그러면서 한 가지 실수를 저지르게 되었죠.
import Foundation
struct A<Element> {
var b: B<Element> {
return B()
}
}
struct B<Element> {
}
extension A {
var b: B<Element> {
return B()
}
}
위의 코드는 제가 범했던 실수를 명확하게 드러내주고 있습니다.
A라는 구조체를 확장하고 b함수를 프로퍼티로 변경하는 과정에서 b는 readonly 프로퍼티이니 A의 확장에서 b를 선언하는게 좋겠다고 판단하고는 확장에서 선언을 한 후에 A 구조체 선언부에서 b 프로퍼티 부분을 제거하는 것을 잊어버렸습니다. 그리고 그 순간부터 Xcode는 지속적으로 에러와 복구를 반복해서 수행했고, 저는 Xcode가 또 땡깡을 부리는구나 판단하고는, clean, 재부팅등을 시도했습니다. 변한것은 없었고, 뭔가 문제가 있으니 빌드를 해볼까 싶어 빌드를 시도했습니다. 그런데 에러가 등장하기는 했습니다만, 소스 코드상에 특정 부분을 지적하는 에러가 아닌, 개발할 때 제일 꼴보기 싫은 에러인 Segmentation fault: 11에러를 아래와 같이 만나게 되었습니다.
swiftc ./test.swift
0 swift 0x000000010ca7236a PrintStackTraceSignalHandler(void*) + 42
1 swift 0x000000010ca717a6 SignalHandler(int) + 662
2 libsystem_platform.dylib 0x00007fff50a80f5a _sigtramp + 26
3 libsystem_platform.dylib 0x0000000000000008 _sigtramp + 2941776072
4 swift 0x0000000109cc4c9b swift::Lowering::SILGenFunction::SILGenFunction(swift::Lowering::SILGenModule&, swift::SILFunction&) + 203
5 swift 0x0000000109c36d45 swift::Lowering::SILGenModule::emitFunction(swift::FuncDecl)::$_1::operator()(swift::SILFunction) const + 261
6 swift 0x0000000109c362c9 swift::Lowering::SILGenModule::emitFunction(swift::FuncDecl*) + 761
7 swift 0x0000000109d1f176 swift::Lowering::SILGenModule::visitExtensionDecl(swift::ExtensionDecl*) + 422
8 swift 0x0000000109c3c91b swift::Lowering::SILGenModule::emitSourceFile(swift::SourceFile*, unsigned int) + 1115
9 swift 0x0000000109c3e2a9 swift::SILModule::constructSIL(swift::ModuleDecl, swift::SILOptions&, swift::FileUnit, llvm::Optional
, bool) + 841 10 swift 0x00000001093ced06 performCompile(swift::CompilerInstance&, swift::CompilerInvocation&, llvm::ArrayRef<char const>, int&, swift::FrontendObserver, swift::UnifiedStatsReporter*) + 12966
11 swift 0x00000001093ca1f4 swift::performFrontend(llvm::ArrayRef<char const>, char const, void, swift::FrontendObserver) + 7716
12 swift 0x000000010937ee78 main + 12248
13 libdyld.dylib 0x00007fff507ff115 start + 1
14 libdyld.dylib 0x000000000000000f start + 2944405243
Stack dump:
- Program arguments: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift -frontend -c -primary-file ./test.swift -target x86_64-apple-macosx10.9 -enable-objc-interop -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdk -color-diagnostics -module-name extensions -o /var/folders/1g/8g9_bvzx1g159ntm3v4v_z7c0000gn/T/extensions-d89808.o
- While emitting SIL for getter for b at ./test.swift:13:6
보통 이런 에러를 만나면 좌절했겠지만, 요즘 스위프트의 빌드 과정에 대해 공부하고 있던 차 stack dump의 1. While emitting SIL for getter for b at ./test.swift:13:6
이 눈에 들어왔습니다. 이건 스위프트 빌드 단계 중 두 번째인 Swift Intermediate Language를 생성하는 단계에서 test.swift 파일의 13번째 라인 6번째 컬럼에 있는 b를 위한 getter SIL을 생성하는 도중 에러가 발생했다는 의미입니다.
여기서 잠깐 SIL? AST? 이런 용어를 아마 처음 들으시는 분들도 계시리라 생각합니다. 이것은 스위프트의 빌드 단계를 지칭하는 이름이기도 하고 그 단계에서 생성하는 코드의 형태를 지칭하는 단어이기도 합니다. 본격적인 분석에 들어가기 전에 스위프트의 빌드는 어떻게 이루어지는지 살펴보겠습니다.
스위프트의 빌드
1. LLVM
여기서는 주제와 좀 떨어져 있으니 LLVM의 빌드 구조만 간략하게 소개합니다. 상세한 내용은 llvm.org의 내용을 참조해주세요.
Frontend
->LLVM Optimizer
->LLVM Backend
- Frontend는 C, Fotran, GO등의 언어를 LLVM이 이해할 수 있는 중간 언어로 번역하는 단계입니다.
- LLVM Optimizer에서는 바이너리로 변환하기 전에 최적화를 수행합니다.
- Backend에서는 x86, ARM등의 CPU 아키텍쳐에 최적화된 바이너리를 생성합니다.
2. 스위프트의 빌드 단계
스위프트의 코드 작성과 실행 가능한 바이너리가 만들어지기 까지는 아래와 같이 몇 단계를 거치게 됩니다.
Swift Code
-> Swift AST
-> Raw Swift IL
-> Canonical Swift IL
-> LLVM IR
-> Assembly
-> Executable
- Swift Code는 말 그대로 swift 파일입니다. 이것은 하나의 파일일 수도 여러 파일들간에 의존을 가진 덩어리일 수도 있습니다.
- Swift AST에서 AST는 Abstract Syntax Tree의 약자입니다. 이 단계에서는 스위프트의 문법 분석(예약어 검사, 구현 등을 제외한 순수한 구문 분석)을 수행합니다. IntroductionToTheClangAST를 참조하세요.
- Swift SIL에서 SIL은 Swift Intermedate Language(스위프트 중간 언어)의 약자입니다.
Raw Swift IL
과Canonical Swift IL
두 형태가 있습니다. 여기에서 스위프트, AST에서 나타나는 규칙적인 패턴과 각 문법의 구분이 흐려지고, 함수, 클로져, 변수등은 모두 동등한 구성으로 재배치 됩니다. 제가 겪은 문제는 이 단계에서 발생합니다. 여기까지가 LLVM에서의 Frontend에 속합니다. - LLVM IR에서 IR은 Intermedate Representation의 약자입니다. SIL까지는 그나마 읽을 수 있는 데이터가 많지만 여기서부터는 컴파일러를 위한 언어로 더 이상 읽기가 힘듭니다. Assembly 단계가 더 쉬울 수도 있겠네요.
SIL?
참고: swift/SIL.rst
다음 챕터부터 SIL의 코드를 보게 될텐데 그 전에 대강 SIL이 이렇구나 하는 정도로 훑어보겠습니다.
다음의 간단한 스위프트 파일을 SIL로 빌드해보겠습니다.
import Foundation
struct Person {
var n: String
}
스위프트 파일에서 SIL 파일을 생성하기 위해서는 스위프트 컴파일러인 swiftc에 -emit-silgen 혹은 -emit-sil 옵션으로 컴파일합니다. 이 옵션들은 AST로부터 SIL을 생성하게 되는데, -emit-silgen은 raw SIL을 -emit-sil은 canonical SIL을 생성하게 됩니다. swift-demangle툴은 생성한 SIL의 알아보기 힘든 이름들을 정리해서 읽기 쉽게 변경해 줍니다.
- raw SIL은 AST로부터 raw SIL을 생성합니다. 이는 최적화와 진단을 수행하지 않고, 어느 정도 오류를 내포하고 있으며, 이 코드로 네이티브 코드를 생성할 수 없습니다.
- canonical SIL은 AST로부터 canonical SIL을 생성합니다. 최적화 및 진단을 수행한 결과를 생성하고, 네이티브 코드 생성이 가능합니다.
$ swiftc -emit-silgen test.swift | xcrun swift-demangle > test.sil
Person의 n변수의 getter부분을 살펴보겠습니다.
// Person.n.getter
sil hidden [transparent] @test.Person.n.getter : Swift.String : $@convention(method) <Element> (@guaranteed Person<Element>) -> @owned String {
// %0 // users: %2, %1
bb0(%0 : $Person<Element>):
debug_value %0 : $Person<Element>, let, name "self", argno 1 // id: %1
%2 = struct_extract %0 : $Person<Element>, #Person.n // users: %3, %6
%3 = struct_extract %2 : $String, #String._core // user: %4
%4 = struct_extract %3 : $_StringCore, #_StringCore._owner // user: %5
retain_value %4 : $Optional<AnyObject> // id: %5
return %2 : $String // id: %6
} // end sil function 'test.Person.n.getter : Swift.String'
$0등의 숫자와 주석의. user: %.., id: %..등이 눈에 띕니다. 이것은 SIL에서 사용하는 레지스터를 표현하는 것이기도 하고 해당 레지스터나 선언이 어떤 레지스터와 의존관계를 이루고 있는지 알 수 있게 해주는 레이블로서의 표현이기도 합니다. 아래에서 이 소스 코드에서 사용하고 있는 SIL의 키워드에 대해 설명해보겠습니다.
-
hidden – Person 구조체를 default로 선언했기 때문에 n 프로퍼티는 같은 모듈에서만 사용할 수 있다는 뜻입니다. 스위프트의 코드 스코프를 알려주는 키워드입니다.
-
[transparent] – inline 함수라는 의미입니다.
-
@test.Person.n.getter : Swift.String : $@convention(method)
(@guaranteed Person ) -> @owned String @test.Person.n.getter
에서 test는 모듈, 즉 파일의 이름입니다.- Swift.String은
@test.Person.n.getter
가 반환하는 타입이 Swift의 String임을 알려줍니다. - @convention(method)는 스위프트의 메소드의 구현임을 알려줍니다. Objective-C인 경우 @convention(objc_method)로 표현합니다.
- @guaranteed는 직접 매개 변수라고 번역해보겠습니다. 함수는 항상 숨은 self를 첫번째 인수로 가지는데 Person
타입을 복사없이 직접 사용하겠다는 의미라고 생각됩니다. - @owned는 아직은 잘 모르겠습니다. 아마도 객체를 소유하고 있음을 명시해주는 부분이 아닐까 합니다. retain등과 관련이 있을 것이라 생각합니다.
-
// %0 // users: %2, %1
- %0 레지스터를 선언하고 있고 %0은 %2, %1에서 사용하고 있습니다.
-
bb0(%0 : $Person
): - bb0: 함수의 구현시 항상 bb0으로 시작하는데 SIL언어의 default인듯 합니다.
- %0에 Person
를 지정합니다.
-
debug_value %0 : $Person
, let, name "self", argno 1 // id: %1 - %1를 레이블로 지정합니다.
- debug_value: %0의 유형을 let으로 변경하고 Person
를 self의 이름으로 할당한다는 내용을 명시합니다. 스위프트에서 함수나 클로져에서 사용하는 self가 바로 이렇게 만들어집니다.
-
%2 = struct_extract %0 : $Person
, #Person.n // users: %3, %6 - Person의 n 필드를 %2에 복사 없이 할당하고, %2는 %3, %6에서 사용합니다.
-
%3 = struct_extract %2 : $String, #String._core // user: %4
- String의 private 필드인 _core를 %3에 할당하고, %3은. %4에서 사용합니다.
-
%4 = struct_extract %3 : $_StringCore, #_StringCore._owner // user: %5
- String._core의 타입이 StringCore입니다.
- %3: StringCore의 _owner(객체의 소유권을 관리하는 private 필드라고 생각합니다.)를 %4에 할당하고 이는 %5에서 사용합니다.
-
retain_value %4 : $Optional
// id: %5 - %4에 할당된 소유권 객체의 참조(소유권을 가짐)합니다.
- %5는 이 코드 라인의 레이블입니다.
-
return %2 : $String // id: %6
- %2의 값을 반환합니다.
-
} // end sil function ‘test.Person.n.getter : Swift.String’
- 이렇게 Swift.String 타입을 가지는 값을 반환하는 test.Person.n.getter sil 함수의 선언이 종료됩니다.
대충 보면 도저히 읽을 수 없을 것 같은 코드들을 SIL.rst를 참조하며 읽어보았습니다.
이렇게 SIL의 극히 일부분을 살펴봤습니다만, 문서를 참조하며 한땀한땀 차분히 읽다보면, 다른 부분을 읽을 때도 의미를 잘 파악하실 수 있으리라 생각합니다.
자 드디어 다음 단락부터 문제의 분석에 들어갑니다.
분석 및 재현
우선 분석에 앞서 더 간단한 코드를 통해 invalidate redeclaration 에러를 발생시킬 수 있을지 알아보겠습니다.
A의 b를 프로퍼티가 아닌 초반에 만들었던 함수로 만들어 확장 시 해당 함수를 재선언 해보겠습니다.
struct A<Element> {
func b() -> B<Element> {
return B()
}
}
struct B<Element> {
}
extension A {
func b() -> B<Element> {
return B()
}
}
$ swiftc ./test.swift
./test.swift:11:7: error: invalid redeclaration of 'b()'
func b() -> B<Element> {
^
./test.swift:2:7: note: 'b()' previously declared here
func b() -> B<Element> {
제대로 에러를 발생합니다.
흠, 혹시 제너릭 선언과 프로퍼티 선언 간의 복합적인 문제일까요? 그래서 몇 가지 조건을 실험해보기로 했습니다.
- 공통조건 b함수 혹은 프로터피를 A를 확장하며 중복 선언
-
실험 1 A와 B 둘 다 제너릭이 없고 b가 프로퍼티인 경우
- 결과 invalid redeclaration 에러 발생
-
실험 2 A와 B 둘 다 제너릭이 없고 b가 함수인 경우
- 결과 invalid redeclaration 에러 발생
-
실험 3 A만 제너릭을 선언하고 b가 프로퍼티인 경우
- 결과 sil terminated
-
실험 4 A만 제너릭을 선언하고 b가 함수인 경우
- 결과 invalid redeclaration 에러 발생
-
실험 5 B만 제너릭을 선언하고 b가 프로퍼티인 경우
- 이 경우 b의 B타입에 명확한 제너릭 타입을 지정해야 합니다. Any도 관계없습니다.
- 결과 invalid redeclaration 에러 발생
-
실험 6 B만 제너릭을 선언하고 b가 함수인 경우
- 이 경우 함수에서 generic을 선언해 줄 수 있기 때문에, 명확한 제너릭 타입을 지정하지 않아도 됩니다.
func b<Element>() -> B<Element>
- 결과 invalid redeclaration 에러 발생
-
실험 7 A와 B 둘 다 제너릭이 있고 b가 프로퍼티인 경우
- 이 경우 A와 B의 Element는 동일하게 취급됨
- *결과** sil terminated
-
실험 8 A와 B 둘 다 제너릭이 있고, 제너릭의 타입이 동일하며 b가 함수인 경우
A.Element == B.Element
- 결과 invalid redeclaration 에러 발생
-
실험 9 A와 B 둘 다 제너릭이 있고, 제너릭의 타입이 다르며 b가 함수인 경우
-
A.Element != B.Element
-
struct A
{ func b () -> B { -
결과 invalid redeclaration 에러 발생
-
-
실험 10 A와 B 둘 다 제너릭이거나 A만 제너릭이며, 확장 시 where 절을 통해 Element의 타입을 명시할 경우
-
extension A where A: Collection {
-
결과 빌드가 정상적으로 실행
-
다만 제너릭 타입을 Any로 명시하게 되면 아무런 명시가 없는 경우 Any로 명시된 것과 같기 때문에 실험 3, 7과 동일한 결과를 보게 됩니다.
결과를 보면 sil단계에서 에러를 발생하는 경우는 다음과 같습니다.
- A만 제너릭이며 b는 프로퍼티
- A와 B 둘 다 제너릭이며 b는 프로퍼티
( 이런 경우를 복잡한 코드에서 만나게 되는 경우 정말 손도 발도 쓰기 힘들다는 게 가장 문제입니다.*)
결국 b가 어떤 타입이냐는 중요하지 않습니다. A가 제너릭을 가질 것, 제너릭이 Any타입(암묵적이든 명시적이든) 일 것, 확장 시 이미 선언한 프로퍼티를 중복해서 선언할 것으로 문제를 발생시키는 경우를 좁힐 수 있습니다.
자 swift의 빌드 구조가 과연 어떻게 되기에 이런 문제가 발생하는지 한 번 알아보겠습니다.
먼저 빌드를 성공시킬 수 있는 정도로 코드를 단순화해서 sil 코드를 만들어 보겠습니다. class는 현재 단계에서 볼 필요가 없는 코드를 많이 생성하기 때문에 좀 더 간단한 결과를 볼 수 있는 struct로 바꿨습니다.
struct A<Element> {
var value: [Element]
}
extension A {
}
$ swiftc -emit-silgen test.swift > test.silgen
sil_stage raw
import Builtin
import Swift
import SwiftShims
// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
%2 = integer_literal $Builtin.Int32, 0 // user: %3
%3 = struct $Int32 (%2 : $Builtin.Int32) // user: %4
return %3 : $Int32 // id: %4
} // end sil function 'main'
// A.value.getter
sil hidden [transparent] @_T04test1AV5valueSayxGfg : $@convention(method) <Element> (@guaranteed A<Element>) -> @owned Array<Element> {
// %0 // users: %2, %1
bb0(%0 : $A<Element>):
debug_value %0 : $A<Element>, let, name "self", argno 1 // id: %1
%2 = struct_extract %0 : $A<Element>, #A.value // user: %3
%3 = copy_value %2 : $Array<Element> // user: %4
return %3 : $Array<Element> // id: %4
} // end sil function '_T04test1AV5valueSayxGfg'
// A.value.setter
sil hidden [transparent] @_T04test1AV5valueSayxGfs : $@convention(method) <Element> (@owned Array<Element>, @inout A<Element>) -> () {
// %0 // users: %11, %10, %4, %2
// %1 // users: %6, %3
bb0(%0 : $Array<Element>, %1 : $*A<Element>):
debug_value %0 : $Array<Element>, let, name "value", argno 1 // id: %2
debug_value_addr %1 : $*A<Element>, var, name "self", argno 2 // id: %3
%4 = begin_borrow %0 : $Array<Element> // users: %10, %5
%5 = copy_value %4 : $Array<Element> // user: %8
%6 = begin_access [modify] [unknown] %1 : $*A<Element> // users: %9, %7
%7 = struct_element_addr %6 : $*A<Element>, #A.value // user: %8
assign %5 to %7 : $*Array<Element> // id: %8
end_access %6 : $*A<Element> // id: %9
end_borrow %4 from %0 : $Array<Element>, $Array<Element> // id: %10
destroy_value %0 : $Array<Element> // id: %11
%12 = tuple () // user: %13
return %12 : $() // id: %13
} // end sil function '_T04test1AV5valueSayxGfs'
// A.value.materializeForSet
sil hidden [transparent] @_T04test1AV5valueSayxGfm : $@convention(method) <Element> (Builtin.RawPointer, @inout Builtin.UnsafeValueBuffer, @inout A<Element>) -> (Builtin.RawPointer, Optional<Builtin.RawPointer>) {
// %2 // user: %3
bb0(%0 : $Builtin.RawPointer, %1 : $*Builtin.UnsafeValueBuffer, %2 : $*A<Element>):
%3 = struct_element_addr %2 : $*A<Element>, #A.value // user: %4
%4 = address_to_pointer %3 : $*Array<Element> to $Builtin.RawPointer // user: %6
%5 = enum $Optional<Builtin.RawPointer>, #Optional.none!enumelt // user: %6
%6 = tuple (%4 : $Builtin.RawPointer, %5 : $Optional<Builtin.RawPointer>) // user: %7
return %6 : $(Builtin.RawPointer, Optional<Builtin.RawPointer>) // id: %7
} // end sil function '_T04test1AV5valueSayxGfm'
// A.init(value:)
sil hidden @_T04test1AVACyxGSayxG5value_tcfC : $@convention(method) <Element> (@owned Array<Element>, @thin A<Element>.Type) -> @owned A<Element> {
// %0 // user: %2
bb0(%0 : $Array<Element>, %1 : $@thin A<Element>.Type):
%2 = struct $A<Element> (%0 : $Array<Element>) // user: %3
return %2 : $A<Element> // id: %3
} // end sil function '_T04test1AVACyxGSayxG5value_tcfC'
우리가 class, struct 등으로 묶어서 그리고 있는 구조가 sil에서는 해체되고 각 선언들만이 남게 됩니다. 이것을 mangled format이라고 합니다. 다만 네이밍에 AVACyx…등의 알아보기 힘든 형태로 되어있어 툴을 사용해 좀 더 알아보기 쉽게 바꿔보겠습니다.
$ cat ./test.silgen | xcrun swift-demangle
sil_stage raw
import Builtin
import Swift
import SwiftShims
// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
%2 = integer_literal $Builtin.Int32, 0 // user: %3
%3 = struct $Int32 (%2 : $Builtin.Int32) // user: %4
return %3 : $Int32 // id: %4
} // end sil function 'main'
// A.value.getter
sil hidden [transparent] @test.A.value.getter : [A] : $@convention(method) <Element> (@guaranteed A<Element>) -> @owned Array<Element> {
// %0 // users: %2, %1
bb0(%0 : $A<Element>):
debug_value %0 : $A<Element>, let, name "self", argno 1 // id: %1
%2 = struct_extract %0 : $A<Element>, #A.value // user: %3
%3 = copy_value %2 : $Array<Element> // user: %4
return %3 : $Array<Element> // id: %4
} // end sil function 'test.A.value.getter : [A]'
// A.value.setter
sil hidden [transparent] @test.A.value.setter : [A] : $@convention(method) <Element> (@owned Array<Element>, @inout A<Element>) -> () {
// %0 // users: %11, %10, %4, %2
// %1 // users: %6, %3
bb0(%0 : $Array<Element>, %1 : $*A<Element>):
debug_value %0 : $Array<Element>, let, name "value", argno 1 // id: %2
debug_value_addr %1 : $*A<Element>, var, name "self", argno 2 // id: %3
%4 = begin_borrow %0 : $Array<Element> // users: %10, %5
%5 = copy_value %4 : $Array<Element> // user: %8
%6 = begin_access [modify] [unknown] %1 : $*A<Element> // users: %9, %7
%7 = struct_element_addr %6 : $*A<Element>, #A.value // user: %8
assign %5 to %7 : $*Array<Element> // id: %8
end_access %6 : $*A<Element> // id: %9
end_borrow %4 from %0 : $Array<Element>, $Array<Element> // id: %10
destroy_value %0 : $Array<Element> // id: %11
%12 = tuple () // user: %13
return %12 : $() // id: %13
} // end sil function 'test.A.value.setter : [A]'
// A.value.materializeForSet
sil hidden [transparent] @test.A.value.materializeForSet : [A] : $@convention(method) <Element> (Builtin.RawPointer, @inout Builtin.UnsafeValueBuffer, @inout A<Element>) -> (Builtin.RawPointer, Optional<Builtin.RawPointer>) {
// %2 // user: %3
bb0(%0 : $Builtin.RawPointer, %1 : $*Builtin.UnsafeValueBuffer, %2 : $*A<Element>):
%3 = struct_element_addr %2 : $*A<Element>, #A.value // user: %4
%4 = address_to_pointer %3 : $*Array<Element> to $Builtin.RawPointer // user: %6
%5 = enum $Optional<Builtin.RawPointer>, #Optional.none!enumelt // user: %6
%6 = tuple (%4 : $Builtin.RawPointer, %5 : $Optional<Builtin.RawPointer>) // user: %7
return %6 : $(Builtin.RawPointer, Optional<Builtin.RawPointer>) // id: %7
} // end sil function 'test.A.value.materializeForSet : [A]'
// A.init(value:)
sil hidden @test.A.init(value: [A]) -> test.A<A> : $@convention(method) <Element> (@owned Array<Element>, @thin A<Element>.Type) -> @owned A<Element> {
// %0 // user: %2
bb0(%0 : $Array<Element>, %1 : $@thin A<Element>.Type):
%2 = struct $A<Element> (%0 : $Array<Element>) // user: %3
return %2 : $A<Element> // id: %3
} // end sil function 'test.A.init(value: [A]) -> test.A<A>'
이름들이 이제 알아보기 쉬운 형태로 변환되었습니다. 주석으로 각 선언이 어떤 역할을 수행하는지도 알아보기 쉽게 되어 있습니다.
이번에는 다음과 같이 확장의 Element타입을 명시적으로 Int로 선언해보겠습니다.
struct A<Element> {
var value: [Element]
}
extension A where Element == Int {
}
여기까지는 sil 코드가 이전과 별 차이는 없습니다.
그렇다면 A.Element == Int
일 경우에만 A의 확장 내용을 사용할 수 있도록 where절로 Element 제너릭을 Int형으로 명시적으로 선언하고 value1 프로퍼티를 추가해보겠습니다.
struct A<Element> {
var value: [Element]
}
extension A where Element == Int {
var value1
}
이번에는 SIL 코드에 value1의 getter 선언이 추가되었습니다.
code 1
// A<A>.value1.getter
sil hidden @(extension in test):test.A<A where A == Swift.Int>.value1.getter : Swift.Int : $@convention(method) (@guaranteed A<Int>) -> Int {
// %0 // user: %1
bb0(%0 : $A<Int>):
debug_value %0 : $A<Int>, let, name "self", argno 1 // id: %1
// function_ref Int.init(_builtinIntegerLiteral:)
%2 = function_ref @Swift.Int.init(_builtinIntegerLiteral: Builtin.Int2048) -> Swift.Int : $@convention(method) (Builtin.Int2048, @thin Int.Type) -> Int // user: %5
%3 = metatype $@thin Int.Type // user: %5
%4 = integer_literal $Builtin.Int2048, -1 // user: %5
%5 = apply %2(%4, %3) : $@convention(method) (Builtin.Int2048, @thin Int.Type) -> Int // user: %6
return %5 : $Int // id: %6
} // end sil function '(extension in test):test.A<A where A == Swift.Int>.value1.getter : Swift.Int'
where constraint를 빼면 어떨까요?
code 2
// A.value1.getter
sil hidden @test.A.value1.getter : Swift.Int : $@convention(method) <Element> (@guaranteed A<Element>) -> Int {
// %0 // user: %1
bb0(%0 : $A<Element>):
debug_value %0 : $A<Element>, let, name "self", argno 1 // id: %1
// function_ref Int.init(_builtinIntegerLiteral:)
%2 = function_ref @Swift.Int.init(_builtinIntegerLiteral: Builtin.Int2048) -> Swift.Int : $@convention(method) (Builtin.Int2048, @thin Int.Type) -> Int // user: %5
%3 = metatype $@thin Int.Type // user: %5
%4 = integer_literal $Builtin.Int2048, -1 // user: %5
%5 = apply %2(%4, %3) : $@convention(method) (Builtin.Int2048, @thin Int.Type) -> Int // user: %6
return %5 : $Int // id: %6
} // end sil function 'test.A.value1.getter : Swift.Int'
code 1에서는 value1.getter가 A의 확장 함수임을 명시하고 있지만, code 2의 경우에는 해당 부분이 빠져있습니다.
문제가 있던 코드 상태로 다시 돌아가 보겠습니다.
struct A<Element> {
var value: [Element]
}
extension A {
var value
}
여기서 sil코드를 출력해서 보고싶지만, 당연하게도 에러가 발생하는 상황으로 인해 더 방법이 없습니다. 그렇다면, 좀 더 자세하게 sil 코드가 생성되는 과정을 디버깅할 수 있는 옵션을 추가해서 출력을 해 보겠습니다.
$ swiftc -emit-silgen -Xfrontend -debug-constraints ./test.swift | xcrun swift-demangle
debug constrainted output
---Constraint solving for the expression at <invalid range>---
(overload set choice binding $T0 := A<Element>)
(overload set choice binding $T1 := [$T2])
---Initial constraints for the given expression---
(member_ref_expr implicit type='[Element]' decl=test.(file).A.value@./test.swift:2:6 direct_to_storage
(declref_expr implicit type='A<Element>' decl=test.(file).A.<anonymous>.self@./test.swift:2:6 function_ref=unapplied))
Score: 0 0 0 0 0 0 0 0 0 0 0 0 0
Contextual Type: [Element]
Type Variables:
#0 = $T0 [lvalue allowed] [inout allowed] as A<Element>
#1 = $T1 [lvalue allowed] [inout allowed] as [Element]
#2 = $T2 as Element
Active Constraints:
Inactive Constraints:
Resolved overloads:
selected overload set choice A<Element>.value: $T1 == [$T2]
selected overload set choice self: $T0 == A<Element>
Opened types:
locator@0x7fe6a20bd890 [MemberRef@<invalid loc> -> member] opens τ_0_0 -> $T2
(found solution 0 0 0 0 0 0 0 0 0 0 0 0 0)
---Solution---
Fixed score: 0 0 0 0 0 0 0 0 0 0 0 0 0
Type variables:
$T1 as [Element]
$T0 as A<Element>
$T2 as Element
Overload choices:
locator@0x7fe6a20bd800 [DeclRef@<invalid loc>] with test.(file).A.<anonymous>.self@./test.swift:2:6 as self: A<Element>
locator@0x7fe6a20bd890 [MemberRef@<invalid loc> -> member] with test.(file).A.value@./test.swift:2:6 as A<Element>.value: [$T2]
Constraint restrictions:
A<Element> to A<Element> is [deep equality]
Disjunction choices:
Opened types:
locator@0x7fe6a20bd890 [MemberRef@<invalid loc> -> member] opens τ_0_0 -> $T2
---Type-checked expression---
(member_ref_expr implicit type='[Element]' decl=test.(file).A.value@./test.swift:2:6 [with Element] direct_to_storage
(declref_expr implicit type='A<Element>' decl=test.(file).A.<anonymous>.self@./test.swift:2:6 function_ref=unapplied))
---Constraint solving for the expression at <invalid range>---
(overload set choice binding $T0 := @lvalue A<Element>)
(overload set choice binding $T1 := @lvalue [$T2])
(overload set choice binding $T3 := [Element])
---Initial constraints for the given expression---
(assign_expr
(member_ref_expr implicit type='@lvalue [Element]' decl=test.(file).A.value@./test.swift:2:6 direct_to_storage
(declref_expr implicit type='@lvalue A<Element>' decl=test.(file).A.<anonymous>.self@./test.swift:2:6 function_ref=unapplied))
(declref_expr implicit type='[Element]' decl=test.(file).A.<anonymous>.value@./test.swift:2:6 function_ref=unapplied))
Score: 0 0 0 0 0 0 0 0 0 0 0 0 0
Type Variables:
#0 = $T0 [lvalue allowed] [inout allowed] as @lvalue A<Element>
#1 = $T1 [lvalue allowed] [inout allowed] as @lvalue [Element]
#2 = $T2 as Element
#3 = $T3 [lvalue allowed] [inout allowed] as [Element]
Active Constraints:
Inactive Constraints:
Resolved overloads:
selected overload set choice value: $T3 == [Element]
selected overload set choice @lvalue A<Element>.value: $T1 == @lvalue [$T2]
selected overload set choice self: $T0 == @lvalue A<Element>
Opened types:
locator@0x7fe6a1800890 [MemberRef@<invalid loc> -> member] opens τ_0_0 -> $T2
(found solution 0 0 0 0 0 0 0 0 0 0 0 0 0)
---Solution---
Fixed score: 0 0 0 0 0 0 0 0 0 0 0 0 0
Type variables:
$T0 as @lvalue A<Element>
$T1 as @lvalue [Element]
$T2 as Element
$T3 as [Element]
Overload choices:
locator@0x7fe6a1800800 [DeclRef@<invalid loc>] with test.(file).A.<anonymous>.self@./test.swift:2:6 as self: @lvalue A<Element>
locator@0x7fe6a1800890 [MemberRef@<invalid loc> -> member] with test.(file).A.value@./test.swift:2:6 as @lvalue A<Element>.value: @lvalue [$T2]
locator@0x7fe6a1800ac8 [DeclRef@<invalid loc>] with test.(file).A.<anonymous>.value@./test.swift:2:6 as value: [Element]
Constraint restrictions:
A<Element> to A<Element> is [deep equality]
Disjunction choices:
Opened types:
locator@0x7fe6a1800890 [MemberRef@<invalid loc> -> member] opens τ_0_0 -> $T2
---Type-checked expression---
(assign_expr
(member_ref_expr implicit type='@lvalue [Element]' accessKind=write decl=test.(file).A.value@./test.swift:2:6 [with Element] direct_to_storage
(declref_expr implicit type='@lvalue A<Element>' accessKind=readwrite decl=test.(file).A.<anonymous>.self@./test.swift:2:6 function_ref=unapplied))
(declref_expr implicit type='[Element]' decl=test.(file).A.<anonymous>.value@./test.swift:2:6 function_ref=unapplied))
---Constraint solving for the expression at [./test.swift:6:32 - line:6:33]---
---Initial constraints for the given expression---
(array_expr type='[Element]' location=./test.swift:6:32 range=[./test.swift:6:32 - line:6:33])
Score: 0 0 0 0 0 0 0 0 0 0 0 0 0
Contextual Type: [Element]
Type Variables:
Active Constraints:
Inactive Constraints:
(found solution 0 0 0 0 0 0 0 0 0 0 0 0 0)
---Solution---
Fixed score: 0 0 0 0 0 0 0 0 0 0 0 0 0
Type variables:
Overload choices:
Constraint restrictions:
Disjunction choices:
Conformances:
At locator@0x7fe6a084ae00 [Array@./test.swift:6:32]
(specialized_conformance type=[Element] protocol=ExpressibleByArrayLiteral
Element
(normal_conformance type=Array<Element> protocol=ExpressibleByArrayLiteral lazy))
(found solution 0 0 0 0 0 0 0 0 0 0 0 0 0)
---Type-checked expression---
(array_expr type='[Element]' location=./test.swift:6:32 range=[./test.swift:6:32 - line:6:33])
0 swift 0x00000001046bb36a PrintStackTraceSignalHandler(void*) + 42
1 swift 0x00000001046ba7a6 SignalHandler(int) + 662
2 libsystem_platform.dylib 0x00007fff5b3b2f5a _sigtramp + 26
3 libsystem_platform.dylib 0x0000000000000008 _sigtramp + 2764361928
4 swift 0x000000010190dc9b swift::Lowering::SILGenFunction::SILGenFunction(swift::Lowering::SILGenModule&, swift::SILFunction&) + 203
5 swift 0x000000010187fd45 swift::Lowering::SILGenModule::emitFunction(swift::FuncDecl*)::$_1::operator()(swift::SILFunction*) const + 261
6 swift 0x000000010187f2c9 swift::Lowering::SILGenModule::emitFunction(swift::FuncDecl*) + 761
7 swift 0x0000000101968176 swift::Lowering::SILGenModule::visitExtensionDecl(swift::ExtensionDecl*) + 422
8 swift 0x000000010188591b swift::Lowering::SILGenModule::emitSourceFile(swift::SourceFile*, unsigned int) + 1115
9 swift 0x00000001018872a9 swift::SILModule::constructSIL(swift::ModuleDecl*, swift::SILOptions&, swift::FileUnit*, llvm::Optional<unsigned int>, bool) + 841
10 swift 0x0000000101017d06 performCompile(swift::CompilerInstance&, swift::CompilerInvocation&, llvm::ArrayRef<char const*>, int&, swift::FrontendObserver*, swift::UnifiedStatsReporter*) + 12966
11 swift 0x00000001010131f4 swift::performFrontend(llvm::ArrayRef<char const*>, char const*, void*, swift::FrontendObserver*) + 7716
12 swift 0x0000000100fc7e78 main + 12248
13 libdyld.dylib 0x00007fff5b131115 start + 1
14 libdyld.dylib 0x0000000000000010 start + 2766991100
Stack dump:
0. Program arguments: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift -frontend -emit-silgen -primary-file ./test.swift -target x86_64-apple-macosx10.9 -enable-objc-interop -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdk -debug-constraints -color-diagnostics -module-name test -o -
1. While emitting SIL for getter for value at ./test.swift:6:6
fish: Process 78833, 'swiftc' 'swiftc -emit-silgen -Xfrontend…' terminated by signal SIGSEGV (Address boundary error)
사실 여기서 뭘 자세히 조사해야 문제를 알 수 있을지 잘 모르겠습니다. 일단 해본 것으로 만족하고 다른 방법을 모색해보겠습니다.
그렇다면, 아래와 같이 A
struct A<Element> {
var s
}
extension A where Element == Int {
var s
}
에서 생성한 SIL 코드에서 var s 끼리 비교해보면 아래와 같습니다.
where Element == Int
// A.s.getter
sil hidden @test.A.s.getter : Swift.String : $@convention(method) (A) -> @owned String {
// %0 // user: %1
bb0(%0 : $A):
debug_value %0 : $A, let, name "self", argno 1 // id: %1
%2 = string_literal utf8 "s" // user: %7
%3 = integer_literal $Builtin.Word, 1 // user: %7
%4 = integer_literal $Builtin.Int1, -1 // user: %7
%5 = metatype $@thin String.Type // user: %7
// function_ref String.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:)
%6 = function_ref @Swift.String.init(_builtinStringLiteral: Builtin.RawPointer, utf8CodeUnitCount: Builtin.Word, isASCII: Builtin.Int1) -> Swift.String : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // user: %7
%7 = apply %6(%2, %3, %4, %5) : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // user: %8
return %7 : $String // id: %8
} // end sil function 'test.A.s.getter : Swift.String'
// A.s.getter
sil hidden @(extension in test):test.A.s.getter : Swift.String : $@convention(method) (A) -> @owned String {
// %0 // user: %1
bb0(%0 : $A):
debug_value %0 : $A, let, name "self", argno 1 // id: %1
%2 = string_literal utf8 "s1" // user: %7
%3 = integer_literal $Builtin.Word, 2 // user: %7
%4 = integer_literal $Builtin.Int1, -1 // user: %7
%5 = metatype $@thin String.Type // user: %7
// function_ref String.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:)
%6 = function_ref @Swift.String.init(_builtinStringLiteral: Builtin.RawPointer, utf8CodeUnitCount: Builtin.Word, isASCII: Builtin.Int1) -> Swift.String : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // user: %7
%7 = apply %6(%2, %3, %4, %5) : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // user: %8
return %7 : $String // id: %8
} // end sil function '(extension in test):test.A.s.getter : Swift.String'
where이 있는 경우 메소드 선언부에서 @(extension in test)
가 이 getter는 test에서 A의 확장 함수라는 것을 알려주는 것으로 보입니다. 이 경우 struct A의 s와는 구현은 같지만, 완전히 다른 함수로 선언됨을 알 수 있습니다. 이런 연유로 중복선언처럼 보이지만, 문제 없이 빌드가 됩니다.
여기까지 와서 실험할 때 제가 빠트린 경우의 수가 생각이 나서 두 가지를 추가해봤습니다.
- extension A where Element: Any -> sil terminated
- extension A where Element == Any -> 성공!
- extension A 에서 value의 배열 타입을 var value: [Int]… 처럼 명시 -> 성공!
여기까지는 결국 Element의 타입이 어디에도 명시되지 않았기 때문에, 문제가 발생했다고 볼 수 있습니다. 그런데 이건 처음의 구조와는 좀 다르기 때문에, 새로 알게 된 옵션을 이용하여 아래와 같이 코드를 다시 구성해 결과를 살펴봤습니다.
struct A<Element> {
var b: B
}
struct B {
}
extension A {
var b
}
이 경우 아래와 같이 에러를 표시합니다.
Disjunction choices:
---Type-checked expression---
(call_expr type='B' location=./test.swift:12:20 range=[./test.swift:12:20 - line:12:22] arg_labels=
(constructor_ref_call_expr type='() -> B' location=./test.swift:12:20 range=[./test.swift:12:20 - line:12:20]
(declref_expr implicit type='(B.Type) -> () -> B' location=./test.swift:12:20 range=[./test.swift:12:20 - line:12:20] decl=test.(file).B.init()@./test.swift:7:8 function_ref=single)
(type_expr type='B.Type' location=./test.swift:12:20 range=[./test.swift:12:20 - line:12:20] typerepr='B'))
(tuple_expr type='()' location=./test.swift:12:21 range=[./test.swift:12:21 - line:12:22]))
0 swift 0x000000010d4e036a PrintStackTraceSignalHandler(void*) + 42
1 ...
...
1. While emitting SIL for getter for b at test.swift:12:6
A 확장에서 var b: B 변수의 getter에 대한 SIL코드를 생성하는 도중 에러가 났다는 사실은 확실합니다.
중반부에서 A를 확장하며 Element타입을 명시하면 SIL 코드 생성에서 에러가 발생하지 않는다는 것을 실험에서 알 수가 있었습니다. 이제 같은 상황에 Element타입을 Int로 명시해보겠습니다. 그리고 extension A.b의 getter를 생성하는 부분을 살펴보겠습니다.
// A<A>.b.getter
sil hidden @(extension in test):test.A<A where A == Swift.Int>.b.getter : test.B : $@convention(method) (A<Int>) -> B {
// %0 // user: %1
bb0(%0 : $A<Int>):
debug_value %0 : $A<Int>, let, name "self", argno 1 // id: %1
// function_ref B.init()
%2 = function_ref @test.B.init() -> test.B : $@convention(method) (@thin B.Type) -> B // user: %4
%3 = metatype $@thin B.Type // user: %4
%4 = apply %2(%3) : $@convention(method) (@thin B.Type) -> B // user: %5
return %4 : $B // id: %5
} // end sil function '(extension in test):test.A<A where A == Swift.Int>.b.getter : test.B'
// A.b.getter에서 제너릭이 A로 선언되어있는데 이 부분은 스위프트코드에서 무슨 이름으로 선언하던지 제너릭 선언 순서대로 A, B, …로 바뀌게 됩니다.
제너릭이 두 개인 경우
A<Element, Value>로 선언해도 SIL에서는
// A<A, B>.b.getter 로 바뀌게 됩니다.
getter구문이 생성된 것을 보면 , 이 코드를 SIL코드로 변환하는 부분에서 문제가 생긴다는 추정이 가능합니다.
자 여기까지 오느라 힘드셨죠. 고지가 얼마 안남았습니다(물론 저는 남이 이런말을 하면 믿지 않습니다). 그래도 우리 조금만 더 힘내서 하나만 더 테스트해보겠습니다.
이번에는 protocol P를 만들고 변수 b에 대한 선언을 명시해서 A가 P를 상속하도록 해보겠습니다. 코드는 sil terminated 되는 그 코드입니다.
protocol P {
var b
}
struct A<Element>: P {
let b: B
}
struct B
extension A {
var b
}
$ swiftc test.swift
test.swift:7:8: error: type 'A<Element>' does not conform to protocol 'P'
struct A<Element>: P {
^
test.swift:4:6: note: multiple matching properties named 'b' with type 'B'
var b
^
test.swift:8:6: note: candidate exactly matches
let b: B
^
test.swift:14:6: note: candidate exactly matches
var b
^
어라 좀 다르네요? 첫 번째는 그렇다 치고 두 번째에 타입 B인 b 변수가 하나 이상 있다는 에러를 볼 수 있습니다. 여기까지 SIL문법을 제대로 모르지만, 제 작은 회색 뇌세포를 총동원해서 추론해보면, 구조에 대한 선언 없이 확장을 통해 A 타입에 어떤 프로퍼티를 추가하는 경우, 그리고 A의 제너릭 타입을 명시하지 않는 경우 SIL이 getter 구문을 생성할 때 참고할 만한 어떤 자료가 전혀 없기 때문에, sil 생성시 terminated가 발생하게 된다고 생각할 수 있습니다. 그리고 protocol로 먼저 구조를 만들게 되면 SIL 구문 체크시 참고할 데이터가 있기 때문에, 좀 더 명확한 에러를 반환해주는 것 같습니다. 혹은 확장시 A의 제너릭 타입(Swift에서 제공하거나 혹은 개발자 명확하게 선언한 어떤 타입인 경우)을 명시해 주는 경우에는 참조 가능한 구조가 생성되어 별도의 SIL함수 제공되니 빌드가 정상적으로 이루어진다고 추론해보겠습니다. 하지만 추론은 추론이고 더구나 근거가 매우 미약합니다.
저는 명확한 답을 얻어보고자 스위프트 사용자 포럼에 문의를 해놓은 상태입니다. 좋은 답이 오게되면 이 글의 해당 부분을 개선할 수 있을거 같습니다.
추가 답이 달렸습니다. 스위프트의 버그가 틀림없으니 버그 리포트를 하라는군요. ㅎㅎ 신나게 버그 리포트를 달고오겠습니다.
지금까지 SIL코드와 툴, 여러가지 변수를 통해 문제를 파악해보려 했습니다.
그러면 어떻게 하면 이런 문제를 미연에 방지 할 수 있을까요?
결국은 실수에서 벌어진 일입니다. 스위프트는 Protocol Oriented Programming 추구한다는 것이 힌트가 될 수 있겠죠.
객체 혹은 타입을 작성하기 전에 protocol로 구조를 먼저 만들고 해당 protocol을 상속하는 객체 혹은, 타입을 만듭시다. 이것은 여러 장점이 있겠지만, 이 글에서 발생한 문제에 대해서도 에러가 명확해진다는 큰 장점이 있습니다. Xcode도 뭔지 모를 에러를 만들지 않겠지요.
그 외에 예상하기 힘든 문제는 또 없을까?
초반에 스위프트의 확장을 사용할 때, 저는 단순하게 상속의 오버라이딩과 비슷할 것이라 단정하고 코드를 구현하다 버그를 만들어낸 적이 있습니다. 확장은 객체의 상속과는 사실상 아니 당연하지만 매우 다릅니다.
예를 들어 아래와 같은 코드를 보겠습니다. 이 코드에서 저는 A를 확장하며 A.Element == Int
인 상황에서는 프로퍼티 s1이 "extension A" 문자열을 반환하리라 기대했습니다.
code a
import Foundation
protocol P {
var s
}
struct A<Element>: P {
let s: String
var s1
}
extension A where Element == Int {
var s
}
let a1 = A<String>.init(s: "A1").s1
let a2 = A<Int>.init(s: "A2").s1
print(a1)
print(a2)
하지만 기대를 배신하고
A1
A2
를 출력합니다.
A를 확장하며 또 A.Element == Int인 경우 "extension A"를 반환하도록 했음에도 A2를 출력합니다.
이번에는 코드를 한 줄 추가해보겠습니다.
code b
import Foundation
protocol P {
var s
}
struct A<Element>: P {
let s: String
var s1
}
extension A where Element == Int {
var s
var s1 // 명시적으로 A<Int>일 때 s1이 A<Int>.s를 반환하도록 합니다.
}
let a1 = A<String>.init(s: "A1").s1
let a2 = A<Int>.init(s: "A2").s1
print(a1)
print(a2)
A1
extension A
의 출력 결과를 볼 수 있습니다.
이런 경우를 스위프트 코드만으로는 애매모호한 부분이 있지만, SIL코드를 살펴보게 되면 이유가 명확해집니다. 여기에서는 swift-demangle을 쓰게 되면 실제 좌표가 사라지기 때문에, 이번에는 좀 읽기는 어렵겠지만, 가리키는 곳이 명확하게 드러나게 SIL코드를 생성하겠습니다.
$ swiftc -emit-gen test.swift
sil a
let a2 = A<Int>.init(s: "A2").s1
=>
// function_ref A.s1.getter
%28 = function_ref @_T04test1AV2s1SSfg : $@convention(method) <τ_0_0> (@guaranteed A<τ_0_0>) -> @owned String // user: %29
%29 = apply %28<Int>(%27) : $@convention(method) <τ_0_0> (@guaranteed A<τ_0_0>) -> @owned String // user: %31
...
...
extension A where Element == Int {
var s
=>
// A<A>.s.getter
sil hidden @_T04test1AVAASiRszlE1sSSfg : $@convention(method) (@guaranteed A<Int>) -> @owned String {
// %0 // user: %1
bb0(%0 : $A<Int>):
debug_value %0 : $A<Int>, let, name "self", argno 1 // id: %1
%2 = string_literal utf8 "extension A" // user: %7
위의 구조를 찬찬히 보면 확장한 A에서 반환하는 s는 _T04test1AVAASiRszlE1sSSfg가 됩니다만 print부분에서는 사용하고 있지 않습니다. _T04test1AV2s1SSfg주소는 원 선언의 s의 getter 주소입니다.
sil b**
let a2 = A<Int>.init(s: "A2").s1
print(a2)
=>
// function_ref A<A>.s1.getter
%28 = function_ref @_T04test1AVAASiRszlE2s1SSfg : $@convention(method) (@guaranteed A<Int>) -> @owned String // user: %29
%29 = apply %28(%27) : $@convention(method) (@guaranteed A<Int>) -> @owned String // user: %31
...
...
extension A where Element == Int {
var s
var s1
=>
// A<A>.s1.getter
sil hidden @_T04test1AVAASiRszlE2s1SSfg : $@convention(method) (@guaranteed A<Int>) -> @owned String {
// %0 // users: %3, %1
bb0(%0 : $A<Int>):
debug_value %0 : $A<Int>, let, name "self", argno 1 // id: %1
// function_ref A<A>.s.getter
%2 = function_ref @_T04test1AVAASiRszlE1sSSfg : $@convention(method) (@guaranteed A<Int>) -> @owned String // user: %3
%3 = apply %2(%0) : $@convention(method) (@guaranteed A<Int>) -> @owned String // user: %4
return %3 : $String // id: %4
} // end sil function '_T04test1AVAASiRszlE2s1SSfg'
이번에는 명확하게 a2를 print 할 때, A
SIL코드를 보지 않았다면, 이런 문제는 머리속에 계속 물음표로만 남을 수 밖에 없었을 것입니다.
이렇듯 스위프트를 다루며 명확하지 않은 에러나 현상을 만날 때, SIL에 어느 정도 익숙해져 있다면, 상당한 도움이 될 수 있습니다.
WWDC가면 이런 거 잘 다루는 애플 개발자를 만날 수 있다는 전설이 있습니다. WWDC 2018 가고싶다.
여기까지 길고 재미없는 글을 읽어주신 분들에게 감사를 표하며 행복과 행운이 있기를 바랍니다. 앞에서도 이야기했지만, 지적과 지식은 언제나 환영합니다.
참조
- [번역] Swift Intermedate Language, 우선 시작해보기까지
- Debuggin the Swift 2.2 Compiler
- When your Swift code breaks the compiler
- Swift High Performance
그리고, 가지가지 오타와 문법 오류, 교정을 봐주신 종립님 고맙습니다.