设计文档 —— Clang CFE 内部手册

Clang CFE 内部手册


本文为译文,点击 此处查看原文。

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类表示程序源代码中的一个位置。重要的设计要点包括:

  1. sizeof(SourceLocation)必须非常小,因为它们被嵌入到许多 AST 节点中,并且经常被传递。目前是32位。
  2. SourceLocation必须是一个可以高效复制的简单值对象。
  3. 我们应该能够表示任何输入文件的任何字节的一个源位置。这包括tokens的中间、whitespacetrigraphs等。
  4. SourceLocation必须编码当前#include堆栈,在处理此位置时该堆栈是活动的。例如,如果此位置对应于一个token,那么它应该包含token被释放时的#include活动集。这允许我们打印用于诊断的#include堆栈。
  5. SourceLocation必须能够描述宏扩展,同时捕获最终实例化点和原始字符数据的源。

在实践中,SourceLocationSourceManager类一起对一个位置的两部分信息进行编码:拼写位置(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 Tokenpreprocessor能够从其中读取的两种类型的token providers:一个缓冲lexer(由 Lexer 类提供)和一个缓冲token流(由 TokenLexer 类提供)。

7.1 Token 类

Token 类用于表示单个已词法分析的 token。Tokens 用于lexer/preprocessparser库,但不打算超出它们(例如,它们不应该存在于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:
  1. StartOfLine —— 这是在其输入源代码行上生成的第一个 token。
  2. LeadingSpace —— 在 token 之前 immediately/transitively 有一个空格字符,因为它是通过一个宏展开的。此 flag 的定义由 preprocessor 的严格需求非常紧密地定义。
  3. DisableExpand —— 此 flag 用于 preprocessor 的内部,以表示禁用了宏扩展的标识符 tokens。这就使他们无法被认为是未来宏扩展的候选者。
  4. NeedsCleaning —— 如果 flag 的原始拼写包含一个 trigraph 或转义换行,则设置此 flag。由于这是不常见的,许多代码片段可以在不需要清理的 tokens 上快速通过(fast-path)。

normal tokens 的一个有趣(而且有些不寻常)之处在于,它们不包含关于已词法分析值的任何语义信息。例如,如果 token 是一个pp-number token,那么我们就不表示被词法分析的数字的值(这留给以后的代码片段来决定)。此外,lexer 库没有 typedef namesvariable 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::ActOnCXXGlobalScopeSpecifierSema::ActOnCXXNestedNameSpecifier回调返回的NestedNameSpecifier *
  • tok::annot_template_id:这个 annotation token 表示一个 C++ template-id,比如“foo<int, 4>”,其中“foo”是 template 的名称。AnnotationValue 指针是指向 mallocTemplateIdAnnotation 对象的指针。根据上下文,一个解析 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_literaltok::wide_string_literal tokens,语法指示字符串文字可能出现的地方,parser就会吃掉其中的一个序列。

为此,每当 parser 需要tok::identifiertok::coloncolon时,它应该调用TryAnnotateTypeOrScopeTokenTryAnnotateCXXScopeToken方法来形成 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节点内容。

9. AST 库

9.1 设计理念
9.1.1 不变性
9.1.2 诚实
9.2 Type 类及其子类
9.2.1 Canonical Types
9.3 QualType 类
9.4 Declaration names
9.5 Declaration contexts
9.5.1 重声明和重载(Redeclarations and Overloads)
9.5.2 词法和语义上下文(Lexical and Semantic Contexts)
9.5.3 透明声明上下文(Transparent Declaration Contexts)
9.5.4 多重定义声明上下文(Multiply-Defined Declarations Contexts)
9.6 CFG 类
9.6.1 基本块
9.6.2 入口和出口块
9.6.3 条件控制流
9.7 Clang AST 中的常量折叠
9.7.1 实现方法
9.7.2 扩展

10. Sema 库

11. CodeGen 库

12. 如何改变 Clang

12.1 如何添加属性
12.1.1 属性基础
12.1.2 include/clang/Basic/Attr.td
12.1.2.1 拼写
12.1.2.2 主题
12.1.2.3 文档
12.1.2.4 参数
12.1.2.5 其他属性
12.1.3 引用(Boilerplate)
12.1.4 语义处理
12.2 如何添加表达式和 statement
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值