本文档描述了 Clang C 前端中做出的一些更重要的api和内部设计决策。本文档的目的是捕获一些高级信息,并描述其背后的一些设计决策。这是针对那些对Clang黑客感兴趣的人,而不是针对终端用户。下面的描述是按库分类的,并不描述库的任何客户机。
LLVM libSupport库提供了许多底层库和数据结构,包括命令行选项处理、各种容器和用于文件系统访问的系统抽象层。
这个库当然需要一个更好的名字。“basic”库包含许多底层实用程序,用于跟踪和操作源缓冲区、源缓冲区中的位置、诊断、令牌、目标抽象以及有关正在编译的语言子集的信息。
这个基础结构的一部分是特定于C的(例如TargetInfo类),其他部分可以为其他非C语言重用(SourceLocation、SourceManager、Diagnostics、FileManager)。如果将来有需求,我们可以确定是否需要引入一个新的库、将常规类迁移到其他地方或引入其他解决方案。
我们按照这些类的依赖关系来描述它们的角色。
不可能的!诊断(Diagnostic)字符串应该用 UTF-8 编写,如果需要,客户端可以转换到相关代码页。每个translation
完全替换用于诊断的格式字符串。
特别的是,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
的“拼写位置”。
Clang 使用 [first, last]
来表示大多数源范围(source ranges),其中“first”
和“last”
分别指向它们各自tokens
的开头。例如,考虑下面这句话的SourceRange
:
x = foo + bar;
^first ^last
要将这个表示映射到一个基于字符的表示,需要使用 Lexer::MeasureTokenLength()
或 Lexer::getLocForEndOfToken()
将“last”
位置调整为指向(或past)该token
的末尾。对于需要字符级源范围信息的罕见情况,我们使用 CharSourceRange
类。
这里记录了 clang Driver 和 library。
Clang 支持预编译头文件(PCH),它使用 Clang 内部数据结构的一个序列化表示,Clang 内部数据结构使用 LLVM bitstream format 来编码。
Frontend
库包含在 Clang 库之上用于构建 tools 的有用功能,例如用于输出诊断的几种方法。
Lexer
库包含几个紧密连接的类,这些类涉及到糟糕的C源代码词法分析(lexing)
和预处理(preprocessing)
过程。外部客户端到这个库的主接口是这个大的 Preprocessor
类。它包含从一个翻译单元中连贯地读取tokens
所需的各种状态片段。
Preprocessor
对象的核心接口(一旦设置好)是 Preprocessor::Lex
方法,它从 preprocessor stream 返回 next Token。preprocessor
能够从其中读取的两种类型的token providers
:一个缓冲lexer
(由 Lexer 类提供)和一个缓冲token
流(由 TokenLexer 类提供)。
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 来实现这种转换。
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”
,其中“foo”
是 template 的名称。AnnotationValue
指针是指向 malloc
的 TemplateIdAnnotation
对象的指针。根据上下文,一个解析 template-id,命名一个类型可能成为一个typename annotation token (如果所有我们关心的是命名类型,例如,因为它发生在一个类型说明符),或者也可能仍然是一个 template-id token(如果我们想要保留更多的源位置信息或产生一个新的类型,例如,声明一个类模板的专门化)。parser 可以将引用一个类型的 template-id annotation token “升级”为 typename annotation token 。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 。
Lexer
类提供了从一个源缓冲区中提取 tokens 并确定其含义的机制。Lexer是复杂的事实,Lexer对未消除拼写的原始缓冲区进行操作,这使得它变得复杂(获得良好性能是必须的),但是,通过仔细的编码和标准的性能技术可以解决这个问题(例如,注释处理代码在X86和PowerPC主机上向量化)。
lexer有几个有趣的模态特征:
“#if 0”
块内进行词法分析。-C
preprocessor 模式所必需的,该模式传递注释,并由诊断检查器使用到标识符expect-error annotations。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”宏,则可以忽略后续包含。TokenLexer
类是一个 token 提供者,它从来自其他地方的 tokens 列表中返回 tokens。它通常用于两件事:1)在宏定义展开时从宏定义返回tokens;2)从任意tokens缓冲区返回tokens。后面的用法由_Pragma
使用,很可能用于处理 C++ parser 的无界查找。
MultipleIncludeOpt
类实现了一个非常简单的小状态机,用于检测标准的“#ifndef XX / #define XX”
习惯用法,人们通常使用这个习惯用法来防止头文件的多次包含。如果一个缓冲区使用这个习惯用法,并且随后被#include 'd包含,那么预处理程序可以简单地检查是否定义了保护条件。如果是这样,预处理程序可以完全忽略头文件的包含。
这个库包含一个递归下降解析器,它从预处理程序轮询令牌,并将解析过程通知客户机。
从历史上看,解析器用于与抽象操作接口通信,该接口具有用于解析事件的虚拟方法,例如ActOnBinOp()。当Clang增加了对c++的支持时,解析器停止了对一般操作客户机的支持——现在它总是与Sema库对话。但是,解析器仍然只能通过不透明的类型(如ExprResult和StmtResult)访问AST对象。只有Sema查看这些包装器的AST节点内容。