Clang CFE 内部手册
- 1. 介绍
- 2. LLVM 支持库(LLVM Support Library)
- 3. Clang 基本库(Clang “Basic” Library)
- 4. Driver 库
- 5. 预编译的头文件
- 6. Frontend 库
- 7. Lexer 和 Preprocessor 库
- 8. Parser 库
- 9. AST 库
- 10. Sema 库
- 11. CodeGen 库
- 12. 如何改变 Clang
本文为译文,点击 此处查看原文。
1. 介绍
本文档描述了 Clang C 前端中做出的一些更重要的api和内部设计决策。本文档的目的是捕获一些高级信息,并描述其背后的一些设计决策。这是针对那些对Clang黑客感兴趣的人,而不是针对终端用户。下面的描述是按库分类的,并不描述库的任何客户机。
2. LLVM 支持库(LLVM Support Library)
LLVM libSupport库提供了许多底层库和数据结构,包括命令行选项处理、各种容器和用于文件系统访问的系统抽象层。
3. Clang 基本库(Clang “Basic” Library)
这个库当然需要一个更好的名字。“basic”库包含许多底层实用程序,用于跟踪和操作源缓冲区、源缓冲区中的位置、诊断、令牌、目标抽象以及有关正在编译的语言子集的信息。
这个基础结构的一部分是特定于C的(例如TargetInfo类),其他部分可以为其他非C语言重用(SourceLocation、SourceManager、Diagnostics、FileManager)。如果将来有需求,我们可以确定是否需要引入一个新的库、将常规类迁移到其他地方或引入其他解决方案。
我们按照这些类的依赖关系来描述它们的角色。
3.1 诊断子系统(The Diagnostics Subsystem)
3.1.1 Diagnostic*Kinds.td 文件
3.1.2 格式化字符串(The Format String)
3.1.3 格式化一个诊断参数(Formatting a Diagnostic Argument)
3.1.4 生成诊断信息(Producing the Diagnostic)
3.1.5 Fix-It 提示(Fix-It Hints)
3.1.6 DiagnosticClient 接口
3.1.7 向 Clang 添加转换(Adding Translations to Clang)
不可能的!诊断(Diagnostic)字符串应该用 UTF-8 编写,如果需要,客户端可以转换到相关代码页。每个translation
完全替换用于诊断的格式字符串。
3.2 SourceLocation 和 SourceManager 类
特别的是,SourceLocation
类表示程序源代码中的一个位置。重要的设计要点包括:
sizeof(SourceLocation)
必须非常小,因为它们被嵌入到许多 AST 节点中,并且经常被传递。目前是32位。SourceLocation
必须是一个可以高效复制的简单值对象。- 我们应该能够表示任何输入文件的任何字节的一个源位置。这包括
tokens
的中间、whitespace
、trigraphs
等。 SourceLocation
必须编码当前#include
堆栈,在处理此位置时该堆栈是活动的。例如,如果此位置对应于一个token
,那么它应该包含token
被释放时的#include
活动集。这允许我们打印用于诊断的#include
堆栈。SourceLocation
必须能够描述宏扩展,同时捕获最终实例化点和原始字符数据的源。
在实践中,SourceLocation
与SourceManager
类一起对一个位置的两部分信息进行编码:拼写位置(spelling location)
和展开位置(expansion location)
。对于大多数tokens
,这些都是相同的。然而,对于一个宏扩展(或来自一个_Pragma
指示的tokens
),这些将描述与token
对应的字符的位置和使用token
的位置(即,宏展开点或_Pragma
本身的位置)。
Clang 前端本质上依赖于正在正确跟踪的一个token
的位置。如果它曾经是不正确的,前端可能会混淆和死亡。原因是 Clang 中一个Token
的“spelling”
概念依赖于能够找到token
的原始输入字符。这个概念直接映射到token
的“拼写位置”。
3.3 SourceRange 和 CharSourceRange
Clang 使用 [first, last]
来表示大多数源范围(source ranges),其中“first”
和“last”
分别指向它们各自tokens
的开头。例如,考虑下面这句话的SourceRange
:
x = foo + bar;
^first ^last
要将这个表示映射到一个基于字符的表示,需要使用 Lexer::MeasureTokenLength()
或 Lexer::getLocForEndOfToken()
将“last”
位置调整为指向(或past)该token
的末尾。对于需要字符级源范围信息的罕见情况,我们使用 CharSourceRange
类。
4. Driver 库
这里记录了 clang Driver 和 library。
5. 预编译的头文件
Clang 支持预编译头文件(PCH),它使用 Clang 内部数据结构的一个序列化表示,Clang 内部数据结构使用 LLVM bitstream format 来编码。
6. Frontend 库
Frontend
库包含在 Clang 库之上用于构建 tools 的有用功能,例如用于输出诊断的几种方法。
7. Lexer 和 Preprocessor 库
Lexer
库包含几个紧密连接的类,这些类涉及到糟糕的C源代码词法分析(lexing)
和预处理(preprocessing)
过程。外部客户端到这个库的主接口是这个大的 Preprocessor
类。它包含从一个翻译单元中连贯地读取tokens
所需的各种状态片段。
Preprocessor
对象的核心接口(一旦设置好)是 Preprocessor::Lex
方法,它从 preprocessor stream 返回 next Token。preprocessor
能够从其中读取的两种类型的token providers
:一个缓冲lexer
(由 Lexer 类提供)和一个缓冲token
流(由 TokenLexer 类提供)。
7.1 Token 类
Token
类用于表示单个已词法分析的 token。Tokens 用于lexer/preprocess
和parser
库,但不打算超出它们(例如,它们不应该存在于ASTs中)。
在parser
运行时,Tokens 通常位于堆栈上(或其他一些可以有效访问的位置),但偶尔也会得到缓冲。例如,宏定义(macro definitions)被存储为一系列 tokens,C++ 前端需要周期性地缓冲 tokens,以便进行试探性解析和各种前瞻性操作。因此,Token
的大小很重要。在32位系统上,sizeof(Token)
当前是 16
字节。
Tokens 有两种形式:annotation tokens 和 normal tokens。normal tokens 是 lexer
返回的,annotation tokens
表示语义信息,由 parser
生成,替换 token 流中的 normal tokens。normal tokens 包含以下信息:
SourceLocation
—— 这表示 token 开始的位置。length
—— 它将 token 的长度存储在SourceBuffer
中。对于包含它们的 tokens,这个长度包括 trigraphs 和转义换行,编译器的后续阶段将忽略这些换行。通过指向原始源文件缓冲区,始终可以完全准确地获得一个 token 的原始拼写。IdentifierInfo
—— 如果一个 token 采用一个标识符的形式,并且在 token 被词法分析后启用了标识符查找(例如,lexer 没有以“原始(raw)”模式读取),那么它包含一个指向标识符的唯一 hash 值的指针。因为查找发生在关键字标识之前,所以这个字段甚至为像“for”
这样的语言关键字设置。TokenKind
—— 表示按lexer
分类的 token 类型。这包括tok::starequal
(用于“*=”
操作符)、tok::ampamp
(用于“&&”
token)和关键字值(用于对应于关键字的标识符;例如,tok::kw_for
)。注意,有些 tokens 可以有多种拼写方式。例如,C++ 支持“操作符关键字”,其中像“and”
的操作符与像“&&”
的操作符完全一样。在这些情况下,kind
值被设置为tok::ampamp
,这对 parser 很好,它不必同时考虑两种形式。对于关心使用哪种形式的内容(例如,preprocessor“stringize”
操作符),拼写(spelling)指示原始形式。Flags
—— 目前lexer/preprocessor
系统在一个per-token basis
上追踪四个 flags:
StartOfLine
—— 这是在其输入源代码行上生成的第一个 token。LeadingSpace
—— 在 token 之前 immediately/transitively 有一个空格字符,因为它是通过一个宏展开的。此 flag 的定义由 preprocessor 的严格需求非常紧密地定义。DisableExpand
—— 此 flag 用于 preprocessor 的内部,以表示禁用了宏扩展的标识符 tokens。这就使他们无法被认为是未来宏扩展的候选者。NeedsCleaning
—— 如果 flag 的原始拼写包含一个 trigraph 或转义换行,则设置此 flag。由于这是不常见的,许多代码片段可以在不需要清理的 tokens 上快速通过(fast-path)。
normal tokens 的一个有趣(而且有些不寻常)之处在于,它们不包含关于已词法分析值的任何语义信息。例如,如果 token 是一个pp-number
token,那么我们就不表示被词法分析的数字的值(这留给以后的代码片段来决定)。此外,lexer 库没有 typedef names
与 variable names
的概念:两者都作为标识符返回,parser 将决定一个特定标识符是一个 typedef
还是一个 variable
(跟踪这一点需要范围信息和其他信息)。parser 器可以通过用“Annotation Tokens”
替换 preprocessor 返回的 tokens 来实现这种转换。
7.2 Annotation Tokens
Annotation tokens
是由 parser 合成并注入 preprocessor 的 token 流(替换现有的 tokens)以记录 parser 发现的语义信息的 tokens。例如,如果“foo”
被发现是一个typedef
,那么 “foo”
tok::identifier
token 将被一个 tok::annot_typename
替换。这样做有几个原因:1)这使得在 C++ 中作为 parser 中的单个“token”来处理限定类型名(例如,“foo::bar::baz<42>::t”
)很容易。2)如果 parser 回溯,则重新解析不需要重新进行语义分析来确定一个 token 序列是否为变量、类型、模板等。
Annotation tokens
由 parser 创建,并重新注入 parser 的 token 流(启用回溯时)。因为它们只能存在于 preprocessor 所使用的 token 中,所以它不需要保留 preprocessor 用来执行其工作的诸如“start of line”
之类的 flags。此外,一个 annotation token 可以“覆盖”一系列 preprocessor tokens (例如,“a::b::c”
是五个 preprocessor tokens )。因此,一个 annotation token 的有效字段与一个 normal token 的字段不同(但是它们被多路复用到 normal Token 字段中):
SourceLocation “Location”
—— annotation token 的SourceLocation
表示 annotation token 替换的第一个 token。在上面的例子中,它是“a”
标识符的位置。SourceLocation “AnnotationEndLoc”
—— 它保存最后一个 token 被 annotation token 替换的位置。在上面的例子中,它是“c”
标识符的位置。void* “AnnotationValue”
—— 它包含一个 parser 从Sema
获取的不透明对象。parser 只保留用于 Sema 的信息,稍后根据 annotation token kind 来解释。TokenKind “Kind”
—— 这表示这是 Annotation token的 kind。请参阅下面的不同有效 kinds。
Annotation tokens
目前有三种 kinds:
tok::annot_typename
:这个 annotation token 表示一个已解析的typename token
,它可能是限定的。AnnotationValue
字段包含Sema::getTypeName()
返回的QualType
,可能还附加了源位置信息。tok::annot_cxxscope
:这个 annotation token 表示一个 C++ 范围说明符,比如“A::B::”
。这对应于语法结果“::”
和“:: [opt] nested-name-specifier”
。AnnotationValue
指针是一个由Sema::ActOnCXXGlobalScopeSpecifier
和Sema::ActOnCXXNestedNameSpecifier
回调返回的NestedNameSpecifier *
。tok::annot_template_id
:这个 annotation token 表示一个 C++ template-id,比如“foo<int, 4>”
,其中“foo”
是 template 的名称。AnnotationValue
指针是指向malloc
的TemplateIdAnnotation
对象的指针。根据上下文,一个解析 template-id,命名一个类型可能成为一个typename annotation token (如果所有我们关心的是命名类型,例如,因为它发生在一个类型说明符),或者也可能仍然是一个 template-id token(如果我们想要保留更多的源位置信息或产生一个新的类型,例如,声明一个类模板的专门化)。parser 可以将引用一个类型的 template-id annotation token “升级”为 typename annotation token 。
如上所述, annotation tokens 不是由 preprocessor 返回的,而是由 parser 根据需要生成的。这意味着 parser 必须知道可能出现一个 annotation 的情况,并在适当的地方形成annotation。这有点类似于 parser 处理 C99 的Translation Phase 6:字符串连接(String Concatenation)(参见C99 5.1.1.2)。在字符串连接的情况下,preprocessor 只返回不同的tok::string_literal
和tok::wide_string_literal
tokens,语法指示字符串文字可能出现的地方,parser就会吃掉其中的一个序列。
为此,每当 parser 需要tok::identifier
或tok::coloncolon
时,它应该调用TryAnnotateTypeOrScopeToken
或TryAnnotateCXXScopeToken
方法来形成 annotation tokens 。这些方法将最大限度地形成指定的 annotation token ,并使用它们替换当前 token (如果适用的话)。如果当前 token 对 一个annotation token 无效,它将保留标识符或“::”
token 。
7.3 Lexer 类
Lexer
类提供了从一个源缓冲区中提取 tokens 并确定其含义的机制。Lexer是复杂的事实,Lexer对未消除拼写的原始缓冲区进行操作,这使得它变得复杂(获得良好性能是必须的),但是,通过仔细的编码和标准的性能技术可以解决这个问题(例如,注释处理代码在X86和PowerPC主机上向量化)。
lexer有几个有趣的模态特征:
- lexer 可以在“原始”模式下运行。这种模式有几个特性可以快速对文件进行词法分析(例如,它停止标识符查找,不专门处理 preprocessor tokens,以不同的方式处理EOF,等等)。例如,此模式用于在
“#if 0”
块内进行词法分析。 - lexer 可以捕获注释并将其作为 tokens 返回。这是支持
-C
preprocessor 模式所必需的,该模式传递注释,并由诊断检查器使用到标识符expect-error annotations。 - lexer可以处于
ParsingFilename
模式,这是在读取#include
指令后进行预处理时发生的。这种模式改变了对“<”
的解析,以返回一个“有角度的字符串(angled string)”,而不是文件名中每个东西的一堆 tokens。 - 当解析预处理指示(在
“#”
之后)时,将进入ParsingPreprocessorDirective
模式。这将更改解析器,使其在换行时返回EOD。 Lexer
使用一个LangOptions
对象来知道是否启用了trigraphs,是否识别 C++ 或 ObjC 关键字,等等。
除了这些模式之外,lexer还跟踪了一些其他的特性,这些特性是词法分析后缓冲区的本地特性,这些特性会随着缓冲区的词法分析而变化:
Lexer
使用BufferPtr
跟踪当前被释放的字符。Lexer
使用IsAtStartOfLine
跟踪下一个词法分析后 token 是否将以其“start of line”位集开始。Lexer
跟踪当前活动的“#if”
指令(可以嵌套)。Lexer
跟踪MultipleIncludeOpt
对象,该对象用于检测缓冲区是否使用标准的“#ifndef XX / #define XX”
习惯用法来防止多个包含。如果缓冲区这样做,则如果定义了“XX”宏,则可以忽略后续包含。
7.4 TokenLexer 类
TokenLexer
类是一个 token 提供者,它从来自其他地方的 tokens 列表中返回 tokens。它通常用于两件事:1)在宏定义展开时从宏定义返回tokens;2)从任意tokens缓冲区返回tokens。后面的用法由_Pragma
使用,很可能用于处理 C++ parser 的无界查找。
7.5 MultipleIncludeOpt 类
MultipleIncludeOpt
类实现了一个非常简单的小状态机,用于检测标准的“#ifndef XX / #define XX”
习惯用法,人们通常使用这个习惯用法来防止头文件的多次包含。如果一个缓冲区使用这个习惯用法,并且随后被#include 'd包含,那么预处理程序可以简单地检查是否定义了保护条件。如果是这样,预处理程序可以完全忽略头文件的包含。
8. Parser 库
这个库包含一个递归下降解析器,它从预处理程序轮询令牌,并将解析过程通知客户机。
从历史上看,解析器用于与抽象操作接口通信,该接口具有用于解析事件的虚拟方法,例如ActOnBinOp()。当Clang增加了对c++的支持时,解析器停止了对一般操作客户机的支持——现在它总是与Sema库对话。但是,解析器仍然只能通过不透明的类型(如ExprResult和StmtResult)访问AST对象。只有Sema查看这些包装器的AST节点内容。