[번역] SIL(Swift Intermediate Language), 일단 시작해보기까지

Mar.18.2018 최광훈

APP

원문 Swift中間言語の、ひとまず入り口手前まで

이 글은 훌륭한 Swift, iOS 개발자이자 교육자인 Tomohiro Kumagai씨가 qiita에 올리신 글을 허가를 얻어 배포합니다.

*(주) 내용은 최대한 정확하게 번역하려 했지만, 문장은 초벌 번역에서 겨우 벗어난 수준입니다. 시간 날 때마다 개선할 예정입니다.

올해(2016년 글임)의 WWDC를 방문했을 때, Swift Lab에서 Sean Callanan씨가 Swift의 작은 동작을 탐색하는 방법으로 SIL을 활용하고 있는 모습을 보여주었습니다. 뭔가 있을 때 저도 그 흉내를 내어 SIL을 살펴볼 기회가 늘었습니다.

SIL은 Swift Intermediate Language의 약칭으로, Swift 중간 언어입니다.

그때부터 Swift에서 불가사의한 동작을 맞닥뜨렸을 때마다 SIL을 보고 있는데, 코드를 읽을 수 있다는 것만으로도 큰 도움이 되고 있습니다. 하지만 읽지 못하는 부분도 많아서, 좀 더 잘 읽게 되면 SIL을 통해 여러 가지를 알게 될 수 있을 것 같습니다.

그런 생각으로 이번 기사를 쓰게 되었습니다.

목표에는 도달하지 못했다.

알듯 말듯한 SIL에 대해, 어느 정도 읽을 수 있는 정도를 목표로 SIL에 대해 조사하기로 했습니다. SIL의 구문과 명령 체계 분석까지 가능했다면 좋았겠지만, 생각보다 깊은 세계였고 저의 기초가 부족한 데다가 시간도 부족하여 깊이 들어가지는 못했습니다.

어중간한 내용 같습니다만, SIL에 흥미를 불러일으키는 계기가 되기를 바랍니다.

SIL이란

SIL이란 Swift Intermediate Language의 약자로, Swift의 소스 코드를 실행 가능한 바이너리 코드로 바꿀 때, 다른 컴파일러(LLVM)가 취급하기 쉬운 중간표현(Intermediate Representation)으로 변환한 코드를 말합니다. (* LLVM IR과는 다릅니다.)

Swift의 컴파일러는 LLVM을 통해서 바이너리 코드를 생성하는데, 바이너리 코드를 생성하는 과정에서 LLVM을 위한 중간표현인 LLVM IR을 생성하게 되고, SIL은 Swift 코드와 LLVM IR과의 중간에 위치합니다.

목적

SIL의 목적은 프로그래머가 입력한 Swift 소스 코드와 LLVM IR과의 표현의 차이를 메꾸는 것입니다. 그리고 LLVM IR이 다루기 힘든 Swift 소스 코드 레벨에서의 정적 분석도 범위에 들어갑니다.

정적 단일 대입 방식

SIL은 정적 단일 배정 방식(Static single assignment form)에 가까운 언어인듯합니다.

상수만을 허용하는 방식으로 보입니다. 그렇다면, Swift의 var를 사용했을 때 let으로 표현이 바뀌는 걸까? 라고 추측했지만, 스위프트와는 별개로 SIL 언어가 그렇게 설계되어 있다고 합니다.

(Swift 빌드 순서에서) SIL의 위치를 알아본다

SIL이 Swift 소스 코드를 바이너리 코드로 빌드하는 과정 중에 어느 단계인지 알아 두면 파악하기 쉬울 것 같아, Swift 컴파일러의 처리의 흐름에 대해 조사해 보기로 했습니다.

Swift 소스 코드로 바이너리 코드를 생성할 때, 컴파일러는 다음의 과정을 따라갑니다.

구문분석(AST)의미분석모듈 임포트SIL 생성SIL 정규화SIL 최적화LLVM IR 생성 → …

1. 구문분석

Swift 소스 코드를 파서로 처리해서, 타입 정보가 없는 추상 구문 트리(Abstract Syntax Tree, AST)를 생성합니다. 소스 코드의 문법(예약어, 타입 체크 등등이 아닌 순수한 구문 분석입니다. 예를 들면 명사 목적어 동사의 순서가 맞는지 같은.)에 맞지 않는 부분이 배제됩니다.

덧붙여 파서는 재귀 하향 파서(Recursive Descent Parser)으로 구현되어 있고, 그 코드는 lib/Parse에 있습니다.

2. 의미분석

구문분석으로 생성한 AST는 의미 분석기를 통과하며, 타입 추론 등을 수행해서, 타입 정보를 포함한 완전한 형태의 AST로 변환됩니다. 이 단계에서, 소스 코드에서 의미상 맞지 않는 부분이 배제됩니다.

추가로 이 규칙은 ocs/TypeChecker.rst에 언급되어 있으며, 코드는 lib/Sema에 있습니다.

3. 모듈 임포트

AST가 완성되면, 다음으로 Clang Importer에 의해 Clang module이 포함되어, 여기에서 Objective-C나 C api가 Swift의 규칙에 맞는 형태로 사용할 수 있습니다.

다만, 앞의 의미분석의 단계에서, 외부 모듈에 규정되어 있는 메소드 등을 토대로 해서 타입 체크를 하는 것으로 보입니다. 그러니 이 단계에서 처음으로 모듈이 관여하는 것도 아닙니다. 이 부분은 잘 이해하지 못하고 있습니다.

이 코드는 lib/ClangImporter에 있습니다.

4. SIL의 생성

여기부터, 이 글의 테마인 SIL을 생성하는 최초의 단계가 될 것 같습니다. SIL 생성기에 의해 AST로부터 Raw SIL이 생성됩니다. SIL에서는 Swift의 var 변수는, 엄격한 정적 단일 배정 방식이 아닌, 변경이 가능한 메모리 영역으로서 표현되는 것 같습니다.

덧붙여 구현은 lib/SILGen에 있는 것 같습니다.

5. SIL의 정규화

SIL을 작성했다고 완료된 것은 아니고, 계속해서 정당성의 검증을 행하고, 마지막으로 Canonical SIL이 생성되는 것 같습니다. 이에 의해, 말하자면 "변수에 값을 넣지 않은 채 사용하려고 한다"와 같은 작은 실수를 보정해서, Swift 코드로서 엄격함이 보증되는 것 같습니다.

덧붙여 구현은 lib/SILOptimizer/Mandatory에서 수행하는 것 같습니다.

6. SIL의 최적화

그리고, 정규화된 Canonical SIL을 사용해서, SIL 코드를 최적화가 이루어지는 것 같습니다. 구체적으로는 예를 들어 ARC나 가상 메소드의 호출, 제네릭 언저리의 최적화를 도모하는 것 같습니다.

덧붙여서, 이 부근의 구현은 lib/SILOptimizer/Analysis, lib/SILOptimizer/ARC, lib/SILOptimizer/LoopTransforms, lib/SILOptimizer/Transforms에서 수행하는것 같습니다.

7. LLVM IR의 생성

이렇게 해서 조정이 마무리된 Swift 코드가 IR 생성기를 통해서 LLVM IR로 변환되어, LLVM의 세계로의 다리가 놓인 것 같습니다.

덧붙여서 구현은 lib/IRGen에서 수행하는 것 같습니다.

컴파일 과정을 조사한다

또한, 이런 처리 과정은 어느 정도 단계별로 살펴볼 수 있어, Swift 컴파일러 swiftc에 다음의 옵션을 넣는 것으로 지정한 단계까지 처리한 결과를 텍스트로 확인할 수 있습니다.

단계 옵션
1. 구문 분석 -dump-parse
2. 의미 분석 -dump-ast
4. SIL의 생성 -emit-silgen
5. SIL의 정규화 -emit-sil
7. LLVM IR의 생성 -emit-ir

SIL을 출력해본다

그렇다면 시험 삼아 다음의 Swift 코드의 처리 과정을 출력해보겠습니다.

code1

import Foundation

let value: UInt32 = arc4random_uniform(100)
let title = "Random" as NSString

print("(title) : (value)")

예를 들어 구문분석한 결과를 보고 싶을 때는, 이 코드를 test.swift라고 하는 이름으로 저장하고 다음과 같이 swiftc 명령어를 실행해서 확인할 수 있습니다.

swiftc -dump-parse test.swift

타입 정보가 없는 AST

먼저 -dump-parse 옵션을 추가해, 타입 정보가 없는 AST를 출력하면, 다음과 같이 됩니다.

모르는 키워드가 잔뜩 나와서 읽기가 상당히 주저됩니다만, 그래도 진득하게 살펴보면 뭔가 본 기억이 있는 단어가 여기저기 보이는 것을 알 수 있습니다. 일단 구조마다 괄호로 둘러 쌓여있고, 자식 요소는 괄호 안에 인덴트를 포함하고 있는 모양입니다.

Swift 소스 코드를 문법의 규칙에 따라 의미가 있는 부분만을 잘라 내어, 거기서부터 문법적으로 어떤 역할에 해당하는지를 예를 들어 var_decl 같은 식별자에 태그를 붙여, 그 외의 필요한듯한 정보와 함께 괄호로 묶고 있다고 생각하시면 어느 정도 읽을 수 있게 될 거라 생각합니다.

(source_file

  (import_decl 'Foundation')

  (top_level_code_decl

    (brace_stmt

      (pattern_binding_decl

        (pattern_typed

          (pattern_named 'value')

          (type_ident

            (component id='UInt32' bind=none)))

        (call_expr type='<null>' arg_labels=_:

          (unresolved_decl_ref_expr type='<null>' name=arc4random_uniform function_ref=unapplied)

          (paren_expr type='<null>'

            (integer_literal_expr type='<null>' value=100))))

))

  (var_decl "value" type='<null type>' let storage_kind=stored)

  (top_level_code_decl

    (brace_stmt

      (pattern_binding_decl

        (pattern_named 'title')

        (sequence_expr type='<null>'

          (string_literal_expr type='<null>' encoding=utf8 value="Random" builtin_initializer=NULL initializer=NULL)

          (coerce_expr type='<null>' writtenType='<null>'

            (NULL EXPRESSION))

          (coerce_expr type='<null>' writtenType='<null>'

            (NULL EXPRESSION))))

))

  (var_decl "title" type='<null type>' let storage_kind=stored)

  (top_level_code_decl

    (brace_stmt

      (call_expr type='<null>' arg_labels=_:

        (unresolved_decl_ref_expr type='<null>' name=print function_ref=unapplied)

        (paren_expr type='<null>'

          (interpolated_string_literal_expr type='<null>'

            (string_literal_expr type='<null>' encoding=utf8 value="" builtin_initializer=NULL initializer=NULL)

            (paren_expr type='<null>'

              (unresolved_decl_ref_expr type='<null>' name=title function_ref=unapplied))

            (string_literal_expr type='<null>' encoding=utf8 value=" : " builtin_initializer=NULL initializer=NULL)

            (paren_expr type='<null>'

              (unresolved_decl_ref_expr type='<null>' name=value function_ref=unapplied))

            (string_literal_expr type='<null>' encoding=utf8 value="" builtin_initializer=NULL initializer=NULL)))))))

파서가 살피는 것은 "구문이 문법적으로 올바른가" 뿐

관련해서 이 단계에서는 문법상의 오류만 검출되는 것 같습니다.

문법의 오류중에는 예를 들어 private와 public을 동시에 지정했다던가 하는 것도 포함합니다만, 이건 분명 의미를 분석했다기보다는, 무엇의 뒤에 무엇이 와야 하는지에 대한 규칙에 맞지 않는 것이 검출된다는 느낌입니다.

함수는 아직 "알지 못한다"

이때, 함수를 호출하는 곳에서는 (unresolved_decl_ref_expr type=’‘ name=arc4random_uniform specialized=no)라고 표기하고 있습니다.

세세한 의미까지는 모르겠습니다만, 이 단계에서는 아직 미해결(unresolved)인 상태, 함수명은 있지만, 애초에 존재하는지도 포함하여 "알지 못한다"라는 것을 알 수 있습니다. 실제로 아예 적당한 함수명을 지정해도, 이 단계에서는 에러를 검출하지 않습니다.

타입 정보는, 어디까지나 "식별자" 정도로 취급

이 단계에서 출력에 타입 정보가 등장한다고 해도, 어디까지나 기호에 지나지 않는 모양입니다.

예를 들어, 변수 선언에서 타입을 지정한 경우는 그 타입 정보가 (type_ident (component id=’UInt32′ bind=none)) 라고 나타납니다만, 이것은 어디까지나 "타입 식별자가 있어야 하는 장소에 UInt32라고 쓰여 있다" 정도에 지나지 않습니다. 게다가, 타입 추론을 돕는 타입 명시라고 한다면, 여기에서는 as NSString 입니다만, 이 단계에서는 불필요한 데다, 애초에 정보로서 포함하지 않는 것 같습니다.

타입 정보가 있는 AST

계속해서 -dump-ast 옵션을 지정해서, 타입 정보까지 포함해서 완성하는 AST까지 진행해보겠습니다.

이 단계에서 갑자기 코드가 많아져서, 저도 모르게 보는 것을 그만두고 싶어집니다만, 구조상으로는 앞의 -dump-parse의 경우와 크게 달라지지는 않았기 때문에, 그렇게 생각하며 기분을 차분하게 한다면 계속 살펴볼 수 있을 것입니다.

어째서 여기까지 많아진 이유는, 바로 전의 단계에서 <null type>으로 되어 있던 type의 위치에 구체적인 타입 정보가 포함되었기 때문에, 소스 코드의 해당 지점에 관련한 정보가 추가된 것이 길어진 요인입니다. 이에 의해, 얼핏 봐서는 알아보기 힘들게 되었지만, 반대로 어디가 어떻게 해석되었는지가 확실히 쉬워졌습니다.

(source_file

  (import_decl 'Foundation')

  (top_level_code_decl

    (brace_stmt

      (pattern_binding_decl

        (pattern_typed type='UInt32'

          (pattern_named type='UInt32' 'value')

          (type_ident

            (component id='UInt32' bind=Swift.(file).UInt32)))

        (call_expr type='UInt32' location=test.swift:3:21 range=[test.swift:3:21 - line:3:43] nothrow arg_labels=_:

          (declref_expr type='(UInt32) -> UInt32' location=test.swift:3:21 range=[test.swift:3:21 - line:3:21] decl=Darwin.(file).arc4random_uniform function_ref=single)

          (paren_expr type='(UInt32)' location=test.swift:3:40 range=[test.swift:3:39 - line:3:43]

            (call_expr implicit type='UInt32' location=test.swift:3:40 range=[test.swift:3:40 - line:3:40] nothrowarg_labels=_builtinIntegerLiteral:

              (constructor_ref_call_expr implicit type='(_MaxBuiltinIntegerType) -> UInt32' location=test.swift:3:40range=[test.swift:3:40 - line:3:40] nothrow

                (declref_expr implicit type='(UInt32.Type) -> (MaxBuiltinIntegerType) -> UInt32' location=test.swift:3:40range=[test.swift:3:40 - line:3:40] **decl=Swift.(file).UInt32.init(builtinIntegerLiteral:)** function_ref=single)

                (type_expr implicit type='UInt32.Type' location=test.swift:3:40 range=[test.swift:3:40 - line:3:40]typerepr='UInt32'))

              (tuple_expr implicit type='(builtinIntegerLiteral: Int2048)' location=test.swift:3:40 range=[test.swift:3:40 - line:3:40] names=builtinIntegerLiteral

                (integer_literal_expr type='Int2048' location=test.swift:3:40 range=[test.swift:3:40 - line:3:40]value=100))))))

))

  (var_decl "value" type='UInt32' interface type='UInt32' access=internal let storage_kind=stored)

  (top_level_code_decl

    (brace_stmt

      (pattern_binding_decl

        (pattern_named type='NSString' 'title')

        (coerce_expr type='NSString' location=test.swift:4:22 range=[test.swift:4:13 - line:4:25] writtenType='NSString'

          (string_literal_expr type='NSString' location=test.swift:4:13 range=[test.swift:4:13 - line:4:13] encoding=utf8 value="Random" builtin_initializer=Swift.(file).StaticString.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:) initializer=Foundation.(file).NSString.init(stringLiteral:))))

))

  (var_decl "title" type='NSString' interface type='NSString' access=internal let storage_kind=stored)

  (top_level_code_decl

    (brace_stmt

      (call_expr type='()' location=test.swift:6:1 range=[test.swift:6:1 - line:6:28] nothrow arg_labels=_:

        (declref_expr type='(Any..., String, String) -> ()' location=test.swift:6:1 range=[test.swift:6:1 - line:6:1]decl=Swift.(file).print(_:separator:terminator:) function_ref=single)

        (tuple_shuffle_expr implicit type='(Any..., separator: String, terminator: String)' location=test.swift:6:7 range=[test.swift:6:6 - line:6:28] source_is_scalar elements=[-2, -1, -1] variadic_sources=[0] default_args_owner=Swift.(file).print(_:separator:terminator:)

          (paren_expr type='Any' location=test.swift:6:7 range=[test.swift:6:6 - line:6:28]

            (erasure_expr implicit type='Any' location=test.swift:6:7 range=[test.swift:6:7 - line:6:7]

              (interpolated_string_literal_expr type='String' location=test.swift:6:7 range=[test.swift:6:7 - line:6:7]

                (string_literal_expr type='String' location=test.swift:6:7 range=[test.swift:6:7 - line:6:7] encoding=utf8 value="" builtin_initializer=Swift.(file).String.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:) initializer=NULL)

                (paren_expr type='(NSString)' location=test.swift:6:10 range=[test.swift:6:9 - line:6:15]

                  (declref_expr type='NSString' location=test.swift:6:10 range=[test.swift:6:10 - line:6:10] decl=test.(file).title@test.swift:4:5 direct_to_storage function_ref=unapplied))

                (string_literal_expr type='String' location=test.swift:6:16 range=[test.swift:6:16 - line:6:16] encoding=utf8 value=" : " builtin_initializer=Swift.(file).String.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:) initializer=NULL)

                (paren_expr type='(UInt32)' location=test.swift:6:21 range=[test.swift:6:20 - line:6:26]

                  (declref_expr type='UInt32' location=test.swift:6:21 range=[test.swift:6:21 - line:6:21] decl=test.(file).value@test.swift:3:5 direct_to_storage function_ref=unapplied))

                (string_literal_expr type='String' location=test.swift:6:27 range=[test.swift:6:27 - line:6:27] encoding=utf8 value="" builtin_initializer=Swift.(file).String.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:) initializer=NULL)))))))))

타입 정보는, 완성

이 단계에서, 작성하지 않는 타입이 추론되어서, 앞의 단계에서는 <null type>으로 지정되어 있던 type이 모두 타입이 명확하게 정해져 있습니다. 함수나 메소드등에 대해서도, 그것이 어떤 타입의 인수를 받는지 어떤 타입을 반환하는지를 고려해서 타입 정보가 지정되어 있습니다. 아울러서, 그 함수가 어떤 namespace에 속해 있는지에 대해서도 decl에서 확인이 가능합니다

함수나 타입의 정보가 조사되기 때문에, 함수가 정의되어 있는지, 대입이나 인수에 넘기는 등에서 제대로 적절한 타입을 사용하고 있는지 등이 체크됩니다. 여기에 부정합이 발생할 때는 에러로 통지됩니다.

소스 코드 상의 등장 위치도 알 수 있다

덧붙여서 이 단계에서는, AST의 어느 부분이, 어느 소스 파일의 어디에 해당하는지나, 함수 등이 어떤 namespace에 정의되어있는지 등의 정보가 location과 range로 나타나고 있습니다.

그 정보가 너무 길어서 코드의 양이 지나치게 길어진 것처럼 보이지만, 오히려 파일의 이름과 행 번호로 목적하는 것을 찾을 수 있기 때문에 찾기 쉽게 되어 있습니다. 물론, 코드 양이 많은 만큼, 전체 구조를 파악한다는 것에서는 가독성이 떨어지기 때문에, 구조만 보고 싶다면 -dump-parse를 사용하는 것이 좋을지도 모릅니다.

상수값의 변경을 검출

이 단계에서 상수 let에 값을 2회 대입하는 문제도 검출합니다.

타입 체크가 거기까지 판단에 관여하는 것도 신기하지만, "값을 넣을 수 있는 상황" 같은 인식하는 방법은 하고 있는 것 같은 느낌이어서, 혹시 그에 관계되어 완전한 AST를 만들 수 없었다고 하는 결론에 도달하지도 모릅니다.

Raw SIL

다음으로 SIL로의 변환을 살펴보겠습니다. 여기부터 착 하고 인상이 변하는 건, 드디어 Swift Intermediate Language의 세계에 들어갔기 때문에, 지금까지의 Swift 소스 코드와는 다른 관점에서 코드가 만들어집니다. 여기부터, 어쩐지 읽을 수가 없게 되었습니다.

코드의 양도 2435줄(역주: -emit-silgen 옵션으로 raw sil을 생성해보니 191줄이 나오니 뭔지 하는 생각이 듭니다.)로 크게 늘어, 여기에 모든 코드를 발췌하는 것은 어렵기 때문에, 중간을 모두 생략해서 기록하고 있습니다.

sil_stage raw

import Builtin
import Swift
import SwiftShims

// value
sil_global hidden [let] @_Tv4test5valueVs6UInt32 : $UInt32

// title
sil_global hidden [let] @_Tv4test5titleCSo8NSString : $NSString

sil_scope 1 

// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
// %0                                             // user: %3
// %1                                             // user: %3
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
 // function_ref _stdlib_didEnterMain(argc : Int32, argv : UnsafeMutablePointer<UnsafeMutablePointer<Int8>?>) -> ()
 %2 = function_ref @_TFs20_stdlib_didEnterMainFT4argcVs5Int324argvGSpGSqGSpVs4Int8____T_ : $@convention(thin) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> (), scope 1 // user: %3
 %3 = apply %2(%0, %1) : $@convention(thin) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> (), scope 1
 alloc_global @_Tv4test5valueVs6UInt32, loc "/tmp/test.swift":8:5, scope 1 // id: %4

 :

 dealloc_stack %57 : $*NSString, loc "/tmp/test.swift":11:28, scope 1 // id: %99
 %100 = integer_literal $Builtin.Int32, 0, scope 1 // user: %101
 %101 = struct $Int32 (%100 : $Builtin.Int32), scope 1 // user: %102
 return %101 : $Int32, scope 1                   // id: %102
}

:

// arc4random_uniform
sil [clang arc4random_uniform] @arc4random_uniform : $@convention(c) (UInt32) -> UInt32

// UInt32.init(_builtinIntegerLiteral : Builtin.Int2048) -> UInt32
sil [transparent] [fragile] @_TFVs6UInt32CfT22_builtinIntegerLiteralBi2048__S_ : $@convention(method) (Builtin.Int2048, @thin UInt32.Type) -> UInt32

:

어찌 됐건, 먼저의 구문이 바뀐 느낌입니다만, Swift 언어와 Swift Intermediate Language는 다른 언어이기 때문에, 새로운 언어를 하나 더 기억하는 기분으로 바라보는 편이 대하기 쉬울 것 같습니다. 무엇보다 양측 모두가 Swift의 세계를 이루고 있다는 것에는 변함이 없기에, Swift에 대한 지식이 깊어진다면 조금씩 더 나아질 것 같습니다.

다만, 뭔가 (머리가) 잘 돌아가지 않으니 이걸 조금이라도 더 읽을 수 있게 하는 것이 저 자신의 이번 목표입니다.

읽어낼 수 있는 정보

여기서는 단순한 Swift의 타입 정보만이 아닌 그것을 어떤 방식으로 메모리에 배치하는 지나, 메모리 관리의 조작이라던가 하는 세밀한 부분도 코드에 표현하고 있는 인상입니다.

또, 이 Swift Intermediate Language의 단계에서도, 원래의 소스 코드의 어느 부분이 작성된 부분인지가 loc에 포함되어 있기 때문에, 원래의 코드의 어느 부분에 대응하고 있는지 알 수 있을 것 같습니다. 다만, 상당히 코드가 자세하게 기재되어 있는 것인지, Swift의 엄밀한 동작을 알고 있지 않으면 소스 코드의 장소가 기록되어 있다고 해도, "어째서 거기에 그 intermediate code가 있을까?" 같은 느낌이 될 거 같습니다.

거꾸로 말하면, 자세한 동작을 읽어 해석할 수 없을 때 SIL에 의지하면 보이지 않던 작은 동작을 알 수 있을 것 같습니다.

이 단계까지 도달해서 등장하는 에러는 뭘까

그런데, 생각하지 않았습니다만, 타입 체크를 완료한 AST로부터 Raw SIL을 만드는 단계에서 새로 발생한 에러는 뭐가 있을까요. 확실히 무언가 있었던 기분이 듭니다만, 잊어버렸습니다.

구체적으로 어째서 에러가 되었는지 안다면, 무엇을 하고 있는지를 구체적으로 상상하기 쉬울 터이니, 혹시 뭔가 알고 계신 분이 있다면 알려주시기 바랍니다.

Canonical SIL

하나 더, 정규화가 완료된 SIL이 있습니다. 비록 2종류가 있다고 해도 "어느 쪽도 SIL 언어이고, 정규화로 그다지 변하지 않겠지"라고 생각하고 있었습니다만, 자 하고 살펴보면, 코드의 양이 4188줄까지 늘어난 모양입니다.

무엇보다 스코프 같은 것이 형성되어 있거나, 가상 테이블 같은 것이 정의되어 있거나, 상상 이상으로 늘어나 있습니다. 무엇보다, 임포트한 기능에 관련된 정보도 포함된 것 같이 보여서, 생각한 것보다 많은 것을 하고 있는 것 같습니다.

덧붙여서 이 코드가 Raw SIL인지 Canonical SIL 인지는, 가장 최초의 줄에 있는 sil_stage를 확인하면 알 수 있습니다.

sil_stage canonical

import Builtin
import Swift
import SwiftShims

// value
sil_global hidden [let] @_Tv4test5valueVs6UInt32 : $UInt32

// title
sil_global hidden [let] @_Tv4test5titleCSo8NSString : $NSString

// static CommandLine._argc
sil_global [fragile] @_TZvOs11CommandLine5_argcVs5Int32 : $Int32

:

// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
// %0                                             // user: %3
// %1                                             // user: %9
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
 %2 = global_addr @_TZvOs11CommandLine5_argcVs5Int32 : $*Int32, scope 1 // user: %3
 store %0 to %2 : $*Int32, scope 1               // id: %3

 :

 dealloc_stack %64 : $*NSString, loc "/tmp/test.swift":11:28, scope 1 // id: %106
 %107 = integer_literal $Builtin.Int32, 0, scope 1 // user: %108
 %108 = struct $Int32 (%107 : $Builtin.Int32), scope 1 // user: %109
 return %108 : $Int32, scope 1                   // id: %109
}

:

// _ContiguousArrayBuffer.init(_ContiguousArrayStorageBase) -> _ContiguousArrayBuffer<A>
sil hidden_external [fragile] @_TFVs22_ContiguousArrayBufferCfCs27_ContiguousArrayStorageBaseGS_x_ : $@convention(method) <Element> (@owned _ContiguousArrayStorageBase, @thin _ContiguousArrayBuffer<Element>.Type) -> @owned _ContiguousArrayBuffer<Element>

sil_scope 31 

// specialized UnsafeMutablePointer.initialize(to : A, count : Int) -> ()
sil shared_external [fragile] @_TTSgq5Vs10_ArrayBody___TFSp10initializefT2tox5countSi_T_ : $@convention(method) (_ArrayBody, Int, UnsafeMutablePointer<_ArrayBody>) -> () {
// %0                                             // users: %118, %3
// %1                                             // users: %44, %9, %4
// %2                                             // users: %113, %5
bb0(%0 : $_ArrayBody, %1 : $Int, %2 : $UnsafeMutablePointer<_ArrayBody>):
 debug_value %0 : $_ArrayBody, scope 31         // id: %3
 debug_value %1 : $Int, scope 31                 // id: %4
 debug_value %2 : $UnsafeMutablePointer<_ArrayBody>, scope 31 // id: %5
 %6 = integer_literal $Builtin.Int8, 2, scope 31 // users: %86, %72, %58, %37, %30, %21, %17
 br bb1, scope 31                               // id: %7

:

sil_vtable _SwiftNativeNSArray {
 #_SwiftNativeNSArray.deinit!deallocator: _TFCs19_SwiftNativeNSArrayD // _SwiftNativeNSArray.__deallocating_deinit
}

sil_witness_table _SwiftNativeNSArray: AnyObject module Swift

sil_witness_table <Value, Element> _HeapBufferStorage<Value, Element>: AnyObject module Swift

무엇을 하고 있는지 이해를 하지 못했다.

이 단계에서 일어나고 있는 일에 대해서는 Guaranteed Optimization and Diagnostic Passes에 있습니다만, 지식이 부족해 무엇을 하고 있는지까지는 이해할 수 없었습니다.

어쨌거나, 함수의 인라인화 같은 것도 이 단계에서 일어나는 것 같으니, 원래의 Raw SIL에 나름의 다양한 가공이 일어나고 있는 인상입니다.

초기화하지 않는 변수나 상수를 검출

그 외에도, 이 단계에서 변수 let이나 var를 초기화하지 않은 채로 사용한 경우를 에러로 검출하는 것 같습니다.

리터럴 값의 오버 플로우

예를 들어 128을 초과하는 정수 리터럴을 Int8로 캐스팅할 때에 오버 플로우를 검출하는 것도 이 단계로 들어가면서부터 입니다.

여기서, 시간이 다 되었다.

이것으로 일단 막연하지만 SIL의 입구 부근까지 살펴볼 수 있었기에, 가능하면 이 대로 SIL 문법구조나 명령 셋 같은 것도 살펴보고 싶었지만, 안타깝게도 시간이 부족했습니다.

명령 셋도 꽤 수가 많은 것 같아서, 일단 SIL을 정말 알아야 할 필요가 있어야 진행이 수월할 거 같습니다. 그렇게 천천히 알아가보도록 하겠습니다.