“Clang” CFE 内部手册

简介

本文档介绍了 Clang C 前端中一些更重要的 API 和内部设计决策。本文档的目的是既要记录一些高层信息,又要描述一些设计决策背后的原因。本手册面向希望参与 Clang 开发的人员,而不是最终用户。以下描述按库分类,不描述任何库的客户。

LLVM 支持库

LLVM libSupport 库提供了许多底层库和数据结构,包括命令行选项处理、各种容器和系统抽象层,用于文件系统访问。

Clang “基本”库

这个库确实需要一个更好的名字。“基本”库包含许多用于跟踪和操作源缓冲区、源缓冲区中的位置、诊断信息、标记、目标抽象以及关于正在编译的语言子集的信息的低级实用程序。

部分基础设施特定于 C(例如 TargetInfo 类),其他部分可用于其他非 C 语言(SourceLocationSourceManagerDiagnosticsFileManager)。如果有将来需求,我们可以考虑是否引入新库、将通用类移动到其他位置或引入其他解决方案。

我们将按依赖顺序描述这些类的角色。

诊断子系统

Clang 诊断子系统是编译器与人类通信的重要组成部分。诊断信息是在代码不正确或存在问题时生成的警告和错误。在 Clang 中,每个生成的诊断信息至少包含一个唯一 ID、一个与其关联的英文翻译、一个源位置用于“放置插入符”以及一个严重级别(例如,WARNINGERROR)。它们还可以选择包含许多诊断参数(用于填充字符串中的“%0”),以及许多与诊断相关的源范围。

在本节中,我们将给出 Clang 命令行驱动程序生成的示例,但诊断信息可以以多种方式呈现,具体取决于DiagnosticConsumer 接口的实现方式。一个典型的诊断示例是:

t.c:38:15: error: invalid operands to binary expression ('int *' and '_Complex float')
P = (P-42) + Gamma*4;
    ~~~~~~ ^ ~~~~~~~

在此示例中,您可以看到英文翻译、严重级别(错误)、源位置(插入符(”^”)和文件/行/列信息)、源范围“~~~~”、诊断参数(”int*” 和 “_Complex float”)。您必须相信我,诊断信息的背后确实有一个唯一的 ID :).

实现这一切需要多个步骤,涉及许多移动部件,本节将描述这些步骤,并讨论添加新诊断信息的最佳实践。

The Diagnostic*Kinds.td 文件

诊断信息是通过在 clang/Basic/Diagnostic*Kinds.td 文件(根据使用该文件的库的不同而有所不同)中添加条目来创建的。从该文件,tblgen 会生成诊断信息的唯一 ID、诊断信息的严重级别以及英文翻译 + 格式字符串。

目前,唯一 ID 的命名方式几乎没有一致性。有些以 err_warn_ext_ 开头,将严重级别编码到名称中。由于枚举在生成诊断信息的 C++ 代码中被引用,因此枚举名相对较短是有用的。

诊断信息的严重级别来自集合 {NOTEREMARKWARNINGEXTENSIONEXTWARNERROR}。ERROR 严重级别用于指示程序在任何情况下都是不可接受的诊断信息。当发出错误时,输入代码的 AST 可能不会完全构建。EXTENSIONEXTWARN 严重级别用于 Clang 接受的语言扩展。这意味着 Clang 完全理解并可以在 AST 中表示它们,但我们生成诊断信息以告诉用户他们的代码不可移植。区别在于前者默认情况下被忽略,而后者默认情况下发出警告。WARNING 严重级别用于在当前选择的源语言中有效但在某种程度上存在问题的构造。REMARK 严重级别提供有关编译的通用信息,这些信息不一定与任何存在问题的代码相关。NOTE 级别用于将更多信息附加到之前的诊断信息。

这些严重级别会根据各种配置选项被诊断子系统映射到更小的输出级别集合(Diagnostic::Level 枚举,{IgnoredNoteRemarkWarningErrorFatal})。Clang 在内部支持一个完全细粒度的映射机制,允许您将几乎任何诊断信息映射到您想要的输出级别。唯一不能映射的诊断信息是 NOTE,它们始终遵循之前发出的诊断信息的严重级别,以及 ERROR,它们只能映射到 Fatal(例如,不可能将错误转换为警告)。

诊断映射在许多方面都有用。例如,如果用户指定 -pedanticEXTENSION 会映射到 Warning;如果用户指定 -pedantic-errors,它将变成 Error。这用于实现诸如 -Wunused_macros-Wundef 等选项。

将诊断信息映射到 Fatal 应该只用于那些被认为非常严重的诊断信息,错误恢复将无法从这些诊断信息中合理地恢复(因此会喷出一堆错误信息)。这类别错误的一个例子是无法 #include 文件。

诊断措辞

诊断措辞至关重要,因为它是用户了解如何纠正代码的唯一途径。在措辞诊断时,请使用以下建议。

  • Clang 中的诊断不会以大写字母开头,也不会以标点符号结尾。

    • 这并不适用于像 ClangOpenMP 这样的专有名词,像 GCCARC 这样的缩略词,或者像 C23C++17 这样的语言标准。

    • 允许使用尾随问号。例如,unknown identifier %0; did you mean %1?

  • 适当地将专有名词大写,例如 ClangOpenCLGCCObjective-C 等,以及语言标准版本,例如 C11C++11

  • 措辞应简洁。如有必要,使用分号将句子片段组合在一起,而不是使用完整的句子。例如,更倾向于使用像 '%0' is deprecated; it will be removed in a future release of Clang 这样的措辞,而不是像 '%0' is deprecated. It will be removed in a future release of Clang 这样的措辞。

  • 措辞应可操作,并避免使用新手不熟悉的标准术语或语法结构。例如,更倾向于使用像 missing semicolon 这样的措辞,而不是像 syntax error(不可操作)或 expected unqualified-id(使用标准术语)这样的措辞。

  • 措辞应清楚地解释代码的错误,而不是重述代码的功能。例如,更倾向于使用像 type %0 requires a value in the range %1 to %2 这样的措辞,而不是像 %0 is invalid 这样的措辞。

  • 措辞应具有足够的上下文信息,以帮助用户在复杂的表达式中识别问题。例如,更倾向于使用像 both sides of the %0 binary operator are identical 这样的措辞,而不是像 identical operands to binary operator 这样的措辞。

  • 使用单引号表示诊断消息中命名的语法结构或命令行参数。例如,更倾向于使用像 'this' pointer cannot be null in well-defined C++ code 这样的措辞,而不是像 this pointer cannot be null in well-defined C++ code 这样的措辞。

格式字符串

诊断的格式字符串非常简单,但它有一定的功能。它采用英文字符串的形式,其中包含标记,指示诊断参数的插入位置、方式和格式。例如,以下是一些简单的格式字符串

"binary integer literals are an extension"
"format string contains '\\0' within the string body"
"more '%%' conversions than data arguments"
"invalid operands to binary expression (%0 and %1)"
"overloaded '%0' must be a %select{unary|binary|unary or binary}2 operator"
     " (has %1 parameter%s1)"

这些示例展示了格式字符串的一些重要要点。除了“%”(没有问题)之外,您可以在诊断字符串中使用任何纯 ASCII 字符,但这些是 C 字符串,因此您必须使用并了解所有 C 转义序列(如第二个示例)。如果您想在输出中生成“%”,请使用“%%”转义序列,例如第三个诊断。最后,Clang 使用“%...[digit]”序列来指定诊断参数的格式化位置和方式。

诊断参数根据 生成它们的 C++ 代码中指定的顺序进行编号,并通过 %0 .. %9 进行引用。如果您有超过 10 个诊断参数,那么您做错了什么 :)。与 printf 不同,诊断参数不需要按与指定相同的顺序出现在输出中,例如,您可以有一个格式字符串“%1 %0”,它会交换它们。百分号和数字之间的文本是格式化指令。如果没有指令,参数将直接转换为字符串并进行替换。

以下是一些编写英文格式字符串的“最佳实践”

  • 保持字符串简短。理想情况下,它应该适合 DiagnosticKinds.td 文件的 80 列限制。这避免了诊断在打印时换行,并迫使您思考您用诊断传递的重要信息。

  • 利用位置信息。用户将能够看到行和插入符号的位置,因此您无需告诉他们问题出在函数的第 4 个参数上:只需指向它即可。

  • 不要将诊断字符串大写,也不要以句号结尾。

  • 如果您需要在诊断字符串中引用某些内容,请使用单引号。

诊断不应该接受随机的英文字符串作为参数:您不应该使用“you have a problem with %0”,并将“your argument”或“your return value”之类的东西作为参数传递。这样做会阻止 Clang 诊断翻译成其他语言(因为它们将在其他语言的本地化诊断中获得随机的英文单词)。对此的例外是 C/C++ 语言关键字(例如,autoconstmutable 等)和 C/C++ 运算符 (/=)。请注意,诸如“指针”和“引用”之类的词不是关键字。另一方面,您可以包含来自用户源代码的任何内容,包括变量名、类型、标签等。“select”格式可以用于以可本地化的方式实现这类事情,请参见下文。

格式化诊断参数

诊断参数在内部是完全类型的,来自几个不同的类:整数、类型、名称和随机字符串。根据参数的类,它可以可选地以不同的方式进行格式化。这使 DiagnosticConsumer 能够了解参数的含义,而不必使用特定的表示形式(可以将此视为 Clang 的 MVC :)。

在 Clang 诊断系统中添加格式说明符非常容易,但应在添加之前进行讨论。如果您正在创建许多重复的诊断,或者对有用的格式化程序有任何想法,请在 cfe-dev 邮件列表中提出。

以下是 Clang 目前支持的不同诊断参数格式

“s” 格式

示例

"requires %0 parameter%s0"

整数

描述

这是一个用于整数的简单格式化程序,在生成英文诊断时非常有用。当整数为 1 时,它将不打印任何内容。当整数不为 1 时,它将打印为“s”。这允许某些简单的语法形式得到正确的处理,并消除了使用像 "requires %1 parameter(s)" 这样的粗俗东西的必要性。请注意,这只会处理添加一个简单的“s”字符,它不会处理将 fancy 转换为 fanciesmouse 转换为 mice 等更复杂的复数情况。您可以使用“复数”格式说明符来处理此类情况。

“select” 格式

示例

"must be a %select{unary|binary|unary or binary}0 operator"

整数

描述

此格式说明符用于将多个相关的诊断合并成一个通用的诊断,而无需将差异指定为英文字符串参数。与指定字符串不同,诊断将获得一个整数参数,并且格式字符串将选择编号选项。在本例中,“%0”值必须是 [0..2] 范围内的整数。如果它为 0,则打印“unary”,如果它为 1,则打印“binary”,如果它为 2,则打印“unary or binary”。这允许其他语言翻译根据诊断的语义替换合理的词(或整个短语),而无需以文本方式进行操作。所选字符串将进行格式化。

“plural” 格式

示例

"you have %0 %plural{1:mouse|:mice}0 connected to your computer"

整数

描述

这是一个用于复杂复数形式的格式化程序。它旨在处理即使是复数形式非常复杂的语言(如许多波罗的海语)的要求。参数由一系列表达式/形式对组成,用“:”分隔,第一个表达式求值为真的形式是修饰符的结果。

表达式可以为空,在这种情况下它始终为真。参见顶部的示例。否则,它是一系列一个或多个数值条件,用“,”分隔。如果任何条件匹配,则表达式匹配。每个数值条件可以采用三种形式之一。

  • number:如果参数与数字相同,则简单的十进制数字匹配。示例:"%plural{1:mouse|:mice}0"

  • range:如果参数在范围内,则方括号中的范围匹配。然后范围在两端都是包含的。示例:"%plural{0:none|1:one|[2,5]:some|:many}0"

  • modulo:模运算符后跟一个数字、一个等号和一个数字或范围。测试与普通数字和范围相同,但首先对参数取模运算符的结果。示例:"%plural{%100=0:even hundred|%100=[1,50]:lower half|:everything else}1"

解析器非常不宽容。语法错误,即使是空格,也会中止,与任何表达式不匹配的参数也会中止。

“ordinal” 格式

示例

"ambiguity in %ordinal0 argument"

整数

描述

这是一个格式化程序,它将参数数字表示为序数:值 1 变成 1st3 变成 3rd,等等。小于 1 的值不受支持。此格式化程序目前硬编码为使用英语序数。

“objcclass” 格式

示例

"method %objcclass0 not found"

DeclarationName

描述

这是一个简单的格式化程序,它指示 DeclarationName 对应于一个 Objective-C 类方法选择器。因此,它将选择器打印为以“+”开头。

“objcinstance” 格式

示例

"method %objcinstance0 not found"

DeclarationName

描述

这是一个简单的格式化程序,它指示 DeclarationName 对应于一个 Objective-C 实例方法选择器。因此,它将选择器打印为以“-”开头。

“q” 格式

示例

"candidate found by name lookup is %q0"

NamedDecl *

描述

此格式化程序指示应打印声明的完全限定名称,例如,“std::vector” 而不是 “vector”。

“diff” 格式

示例

"no known conversion %diff{from $ to $|from argument type to parameter type}1,2"

QualType

描述

此格式化程序采用两个 QualType 并尝试打印两个类型之间的模板差异。如果树打印关闭,则打印括号内的文本,并用格式化文本替换 $。如果树打印打开,则打印管道后的文本,并在诊断消息后打印类型树。

“sub” 格式

示例

给定类型为 TextSubstitution 的以下记录定义

def select_ovl_candidate : TextSubstitution<
  "%select{function|constructor}0%select{| template| %2}1">;

它可以用作

def note_ovl_candidate : Note<
  "candidate %sub{select_ovl_candidate}3,2,1 not viable">;

并将按以下方式编写:"candidate %select{function|constructor}3%select{| template| %1}2 not viable"

描述

此格式说明符用于避免在多个诊断中逐字重复字符串。 %sub 的参数必须命名一个 TextSubstitution tblgen 记录。替换必须指定替换使用的所有参数,并且替换中的修饰符索引相应地重新编号。替换后的文本本身必须是有效的格式字符串才能进行替换。

生成诊断

现在您已在 Diagnostic*Kinds.td 文件中创建了诊断,您需要编写检测相关条件并发出新诊断的代码。Clang 的各个组件(例如,预处理器、Sema 等)提供了一个名为“Diag”的帮助函数。它创建一个诊断并接受与之相关的参数、范围和其他信息。

例如,二元表达式错误来自类似于以下的代码

if (various things that are bad)
  Diag(Loc, diag::err_typecheck_invalid_operands)
    << lex->getType() << rex->getType()
    << lex->getSourceRange() << rex->getSourceRange();

这显示了 Diag 方法的使用:它接受一个位置(一个 SourceLocation 对象)和一个诊断枚举值(与来自 Diagnostic*Kinds.td 的名称匹配)。如果诊断采用参数,则使用 << 运算符指定它们:第一个参数变为 %0,第二个变为 %1,等等。诊断接口允许您指定多种不同类型的参数,包括 intunsigned 用于整数参数,const char*std::string 用于字符串参数,DeclarationNameconst IdentifierInfo * 用于名称,QualType 用于类型,等等。 SourceRange 也使用 << 运算符指定,但没有特定的排序要求。

如您所见,添加和生成诊断非常简单。困难的是决定您需要确切地说些什么来帮助用户,选择合适的措辞,并提供正确格式化所需的信息。好消息是,发出诊断的调用点应该完全独立于诊断的格式化方式以及它以何种语言呈现。

修复建议

在某些情况下,前端在明显地对源代码进行一些小的更改可以解决问题时会发出诊断。例如,语句末尾缺少分号或使用已弃用的语法,可以轻松地重写为更现代的形式。Clang 非常努力地发出诊断并在这些情况和其他情况下优雅地恢复。

但是,对于这些修复显而易见的情况,诊断可以添加一个提示(称为“修复建议”),描述如何更改诊断引用的代码以解决问题。例如,它可能会在语句末尾添加缺少的分号或将已弃用结构的使用重写为更令人愉快的形式。以下是来自 C++ 前端的一个这样的示例,我们将在其中警告右移运算符从 C++98 到 C++11 的含义发生了改变

test.cpp:3:7: warning: use of right-shift operator ('>>') in template argument
                       will require parentheses in C++11
A<100 >> 2> *a;
      ^
  (       )

这里,修复建议建议添加括号,并显示这些括号将被插入到源代码中的确切位置。修复建议本身以抽象的方式描述了对源代码的更改,文本诊断打印机将其渲染为插入行下方的一行“插入”。其他诊断客户端可能会选择以不同的方式渲染代码(例如,作为内联标记)甚至让用户能够自动修复问题。

错误和警告中的修复建议需要遵守以下规则

  • 由于如果向驱动程序传递 -Xclang -fixit,它们将自动应用,因此应仅在它们很可能符合用户意图时使用它们。

  • Clang 必须从错误中恢复,就好像已应用了修复建议一样。

  • 警告中的修复建议不得改变代码的含义。但是,提示可能会澄清含义是故意的,例如,当运算符的优先级不明确时,添加括号。

如果修复建议无法遵守这些规则,请将修复建议放在备注中。备注上的修复建议不会自动应用。

所有修复建议都由 FixItHint 类描述,该类的实例应使用 << 运算符附加到诊断,与突出显示的源范围和参数传递到诊断的方式相同。可以使用三种构造函数之一创建修复建议

  • FixItHint::CreateInsertion(Loc, Code)

    指定应在源位置 Loc 之前插入给定的 Code(一个字符串)。

  • FixItHint::CreateRemoval(Range)

    指定应删除给定源 Range 中的代码。

  • FixItHint::CreateReplacement(Range, Code)

    指定应删除给定源 Range 中的代码,并替换为给定的 Code 字符串。

DiagnosticConsumer 接口

一旦代码使用所有参数和其他相关信息生成了诊断,Clang 就需要知道如何处理它。如前所述,诊断机制会经过一些过滤,将严重性映射到诊断级别,然后(假设诊断未映射到“Ignore”)它会使用信息调用实现 DiagnosticConsumer 接口的对象。

可以以多种不同的方式实现此接口。例如,正常的 Clang DiagnosticConsumer(名为 TextDiagnosticPrinter)会根据各种格式化规则将参数转换为字符串,打印出文件/行/列信息和字符串,然后打印出代码行、源范围和插入符号。但是,此行为不是必需的。

另一个实现 DiagnosticConsumer 接口的是 TextDiagnosticBuffer 类,它在 Clang 处于 -verify 模式时使用。与格式化和打印诊断信息不同,此实现只是捕获并记住诊断信息,因为它们是动态的。然后 -verify 将产生的诊断列表与预期的诊断列表进行比较。如果它们不一致,它将打印出自己的输出。有关 -verify 模式的完整文档,请参见 验证诊断信息

这个接口还有许多其他可能的实现,这就是我们更倾向于诊断信息以在参数中传递丰富的结构化信息的原因。例如,HTML 输出可能希望声明名称链接到它们在源代码中的位置。另一个例子是,GUI 可能会让你点击类型定义以扩展它们。此应用程序希望通过更多关于类型的相关信息传递到 GUI,而不是一个简单的扁平字符串。该接口允许这种情况发生。

向 Clang 添加翻译

目前还不可行!诊断字符串应该用 UTF-8 编写,客户端可以在需要时翻译成相关代码页。每个翻译都会完全替换诊断信息的格式字符串。

SourceLocationSourceManager

奇怪的是,SourceLocation 类表示程序源代码中的一个位置。重要的设计要点包括

  1. sizeof(SourceLocation) 必须非常小,因为这些被嵌入到许多 AST 节点中,并且经常被传递。目前它是 32 位。

  2. SourceLocation 必须是一个简单的值对象,可以高效地复制。

  3. 我们应该能够为任何输入文件的任何字节表示源位置。这包括在标记中间、在空白处、在三元组中等等。

  4. 一个 SourceLocation 必须对当前的 #include 栈进行编码,该栈在处理位置时处于活动状态。例如,如果该位置对应于一个标记,它应该包含在标记被词法分析时处于活动状态的 #include 集。这允许我们打印诊断信息的 #include 栈。

  5. SourceLocation 必须能够描述宏扩展,同时捕获最终实例化点和原始字符数据的来源。

实际上,SourceLocationSourceManager 类一起工作,对一个位置的两个信息进行编码:它的拼写位置和它的扩展位置。对于大多数标记,它们将相同。但是,对于宏扩展(或来自 _Pragma 指令的标记),它们将描述对应于标记的字符的位置和标记使用的位置(即,宏扩展点或 _Pragma 本身的地址)。

Clang 前端本质上依赖于正确跟踪标记的位置。如果它曾经不正确,前端可能会变得混乱并死亡。造成这种情况的原因是,Clang 中 Token 的“拼写”概念取决于能够找到标记的原始输入字符。这个概念直接映射到标记的“拼写位置”。

SourceRangeCharSourceRange

Clang 通过 [first, last] 来表示大多数源范围,其中“first”和“last”分别指向各自标记的开头。例如,考虑以下语句的 SourceRange

x = foo + bar;
^first    ^last

为了从这种表示映射到基于字符的表示,需要将“last”位置调整为指向(或越过)该标记的末尾,方法是使用 Lexer::MeasureTokenLength()Lexer::getLocForEndOfToken()。对于需要字符级源范围信息的罕见情况,我们使用 CharSourceRange 类。

驱动程序库

clang 驱动程序和库的文档可以在 这里 找到。

预编译头文件

Clang 支持预编译头文件 (PCH),它使用 Clang 内部数据结构的序列化表示,并使用 LLVM 位流格式 进行编码。

前端库

前端库包含对在 Clang 库之上构建工具有用的功能,例如输出诊断信息的一些方法。

编译器调用

前端库提供的类之一是 CompilerInvocation,它包含描述当前 Clang -cc1 前端的调用信息。这些信息通常来自 Clang 驱动程序构建的命令行或来自执行自定义初始化的客户端。该数据结构被分成由编译器不同部分使用的逻辑单元,例如 PreprocessorOptionsLanguageOptionsCodeGenOptions

命令行界面

Clang -cc1 前端的命令行界面与驱动程序选项一起在 clang/Driver/Options.td 中定义。构成选项定义的信息包括它的前缀和名称(例如 -std=)、选项值的格式和位置、帮助文本、别名等。每个选项可能属于某个组,并可以标记有零个或多个标志。由 -cc1 前端接受的选项标记为 CC1Option 标志。

命令行解析

选项定义在构建的早期阶段由 -gen-opt-parser-defs tablegen 后端处理。然后,选项用于查询 llvm::opt::ArgList 实例,它是命令行参数的包装器。这在 Clang 驱动程序中完成,以根据驱动程序参数构建单个作业,并在 CompilerInvocation::CreateFromArgs 函数中完成,该函数解析 -cc1 前端参数。

命令行生成

任何从 -cc1 命令行创建的有效 CompilerInvocation 也能以确定性方式序列化回语义上等效的命令行。这使诸如隐式发现、显式构建模块等功能成为可能。

添加新的命令行选项

当添加新的命令行选项时,第一个关注的地方是声明相应选项类的头文件(例如,针对影响代码生成的命令行选项的 CodeGenOptions.h)。为选项值创建一个新的成员变量

  class CodeGenOptions : public CodeGenOptionsBase {

+   /// List of dynamic shared object files to be loaded as pass plugins.
+   std::vector<std::string> PassPlugins;

  }

接下来,在 tablegen 文件 clang/include/clang/Driver/Options.td 中声明选项的命令行界面。这是通过实例化 Option 类(在 llvm/include/llvm/Option/OptParser.td 中定义)来完成的。该实例通常通过一个辅助类来创建,该辅助类对在命令行上指定选项值的允许方式进行编码

  • Flag - 选项不接受任何值,

  • Joined - 值必须紧跟在同一个参数中的选项名称之后,

  • Separate - 值必须在下一个命令行参数中的选项名称之后,

  • JoinedOrSeparate - 值可以作为 JoinedSeparate 指定,

  • CommaJoined - 值是逗号分隔的,并且必须紧跟在同一个参数中的选项名称之后(参见 Wl, 的示例)。

辅助类接受选项的允许前缀列表(例如 "-""--""/")和选项名称

  // Options.td

+ def fpass_plugin_EQ : Joined<["-"], "fpass-plugin=">;

然后,通过混合使用其他属性

  • HelpText 包含在用户请求帮助时(例如,通过 clang --help)将在选项名称旁边打印的文本。

  • Group 指定此选项所属的选项“类别”。这被各种工具用来对选项进行分类,有时还会过滤选项。

  • Flags 可能包含与选项相关的“标签”。这些可能会影响选项的呈现方式,或者在某些情况下是否隐藏它。

  • Visibility 应该用来指定特定选项在哪些驱动程序中可用。此属性将影响工具 –help

  • Alias 表示选项是另一个选项的别名。这可以与 AliasArgs 结合使用,AliasArgs 包含隐式值。

  // Options.td

  def fpass_plugin_EQ : Joined<["-"], "fpass-plugin=">,
+   Group<f_Group>, Visibility<[ClangOption, CC1Option]>,
+   HelpText<"Load pass plugin from a dynamic shared object file.">;

如果未指定 Visibility 或包含 ClangOption,则 clang 驱动程序模式将识别新的选项。针对 clang -cc1 的选项必须使用 CC1Option 标志显式标记。指定了 CC1Option 但未指定 ClangOption 的标志只能通过 -cc1 访问。对于其他驱动程序模式(如 clang-clflang)也是如此。

接下来,解析(或制造)Clang 驱动程序中的命令行参数,并使用它们构建 -cc1 作业。

  void Clang::ConstructJob(const ArgList &Args /*...*/) const {
    ArgStringList CmdArgs;
    // ...

+   for (const Arg *A : Args.filtered(OPT_fpass_plugin_EQ)) {
+     CmdArgs.push_back(Args.MakeArgString(Twine("-fpass-plugin=") + A->getValue()));
+     A->claim();
+   }
  }

最后一步是实现 -cc1 命令行参数解析/生成,该步骤初始化/序列化存储在 CompilerInvocation 中的选项类(在本例中为 CodeGenOptions)。这可以通过在选项定义上使用编组注释来自动完成。

  // Options.td

  def fpass_plugin_EQ : Joined<["-"], "fpass-plugin=">,
    Group<f_Group>, Flags<[CC1Option]>,
    HelpText<"Load pass plugin from a dynamic shared object file.">,
+   MarshallingInfoStringVector<CodeGenOpts<"PassPlugins">>;

系统的内部机制在 编组基础设施 部分介绍,可用的注释列在 这里

如果编组基础设施不支持所需语义,请考虑简化它以适应现有模型。这使命令行更加统一,并减少了自定义的手动编写的代码数量。请记住,-cc1 命令行界面仅供 Clang 开发人员使用,这意味着它不需要镜像驱动程序界面,不需要保持向后兼容性或与 GCC 兼容。

如果选项语义无法通过编组注释编码,您可以采用手动解析/序列化命令行参数的方法。

  // CompilerInvocation.cpp

  static bool ParseCodeGenArgs(CodeGenOptions &Opts, ArgList &Args /*...*/) {
    // ...

+   Opts.PassPlugins = Args.getAllArgValues(OPT_fpass_plugin_EQ);
  }

  static void GenerateCodeGenArgs(const CodeGenOptions &Opts,
                                  SmallVectorImpl<const char *> &Args,
                                  CompilerInvocation::StringAllocator SA /*...*/) {
    // ...

+   for (const std::string &PassPlugin : Opts.PassPlugins)
+     GenerateArg(Args, OPT_fpass_plugin_EQ, PassPlugin, SA);
  }

最后,您可以在命令行上指定参数:clang -fpass-plugin=a -fpass-plugin=b,并按需要使用新的成员变量。

  void EmitAssemblyHelper::EmitAssemblyWithNewPassManager(/*...*/) {
    // ...
+   for (auto &PluginFN : CodeGenOpts.PassPlugins)
+     if (auto PassPlugin = PassPlugin::Load(PluginFN))
+        PassPlugin->registerPassBuilderCallbacks(PB);
  }

选项编组基础设施

选项编组基础设施自动将 Clang -cc1 前端命令行参数解析为 CompilerInvocation,并从 CompilerInvocation 生成它们。该系统用简单的声明性 tablegen 注释替换了大量的重复 C++ 代码,它被用于大多数 -cc1 命令行界面。本节概述了该系统。

注意:编组基础设施不适用于仅驱动程序的选项。只有 -cc1 前端的选项需要被编组到/从 CompilerInvocation 实例中。

要读取和修改 CompilerInvocation 的内容,编组系统使用关键路径,这些路径是在两个步骤中声明的。首先,通过继承自 KeyPathAndMacro 创建 CompilerInvocation 成员的 tablegen 定义。

// Options.td

class LangOpts<string field> : KeyPathAndMacro<"LangOpts->", field, "LANG_"> {}
//                   CompilerInvocation member  ^^^^^^^^^^
//                                    OPTION_WITH_MARSHALLING prefix ^^^^^

父类的第一个参数是引用 CompilerInvocation 成员的关键路径的开头。如果成员是指针类型,则该参数以 -> 结尾,如果成员是值类型,则以 . 结尾。子类接受单个参数 field,该参数作为第二个参数转发给基类。然后,可以使用子类,例如:LangOpts<"IgnoreExceptions">,构建到字段 LangOpts->IgnoreExceptions 的关键路径。传递给父类的第三个参数是一个字符串,tablegen 后端将其用作 OPTION_WITH_MARSHALLING 宏的前缀。在 Option 实例上使用关键路径作为混合,指示后端生成以下代码。

// Options.inc

#ifdef LANG_OPTION_WITH_MARSHALLING
LANG_OPTION_WITH_MARSHALLING([...], LangOpts->IgnoreExceptions, [...])
#endif // LANG_OPTION_WITH_MARSHALLING

这样的定义可以在解析和生成命令行的函数中使用。

// clang/lib/Frontend/CompilerInvoation.cpp

bool CompilerInvocation::ParseLangArgs(LangOptions *LangOpts, ArgList &Args,
                                       DiagnosticsEngine &Diags) {
  bool Success = true;

#define LANG_OPTION_WITH_MARSHALLING(                                          \
    PREFIX_TYPE, NAME, ID, KIND, GROUP, ALIAS, ALIASARGS, FLAGS, PARAM,        \
    HELPTEXT, METAVAR, VALUES, SPELLING, SHOULD_PARSE, ALWAYS_EMIT, KEYPATH,   \
    DEFAULT_VALUE, IMPLIED_CHECK, IMPLIED_VALUE, NORMALIZER, DENORMALIZER,     \
    MERGER, EXTRACTOR, TABLE_INDEX)                                            \
  PARSE_OPTION_WITH_MARSHALLING(Args, Diags, Success, ID, FLAGS, PARAM,        \
                                SHOULD_PARSE, KEYPATH, DEFAULT_VALUE,          \
                                IMPLIED_CHECK, IMPLIED_VALUE, NORMALIZER,      \
                                MERGER, TABLE_INDEX)
#include "clang/Driver/Options.inc"
#undef LANG_OPTION_WITH_MARSHALLING

  // ...

  return Success;
}

void CompilerInvocation::GenerateLangArgs(LangOptions *LangOpts,
                                          SmallVectorImpl<const char *> &Args,
                                          StringAllocator SA) {
#define LANG_OPTION_WITH_MARSHALLING(                                          \
    PREFIX_TYPE, NAME, ID, KIND, GROUP, ALIAS, ALIASARGS, FLAGS, PARAM,        \
    HELPTEXT, METAVAR, VALUES, SPELLING, SHOULD_PARSE, ALWAYS_EMIT, KEYPATH,   \
    DEFAULT_VALUE, IMPLIED_CHECK, IMPLIED_VALUE, NORMALIZER, DENORMALIZER,     \
    MERGER, EXTRACTOR, TABLE_INDEX)                                            \
  GENERATE_OPTION_WITH_MARSHALLING(                                            \
      Args, SA, KIND, FLAGS, SPELLING, ALWAYS_EMIT, KEYPATH, DEFAULT_VALUE,    \
      IMPLIED_CHECK, IMPLIED_VALUE, DENORMALIZER, EXTRACTOR, TABLE_INDEX)
#include "clang/Driver/Options.inc"
#undef LANG_OPTION_WITH_MARSHALLING

  // ...
}

PARSE_OPTION_WITH_MARSHALLINGGENERATE_OPTION_WITH_MARSHALLINGCompilerInvocation.cpp 中定义,它们实现了解析和生成命令行参数的通用算法。

选项编组注释

tablegen 后端如何知道在生成的 Options.inc 中用什么来代替 [...]?这是由下面描述的 Marshalling 实用程序指定的。它们都接受一个关键路径参数,以及解析或生成命令行参数所需的可能的其他信息。

注意:编组基础设施不适用于仅驱动程序的选项。只有 -cc1 前端的选项需要被编组到/从 CompilerInvocation 实例中。

正标志

关键路径默认设置为 false,当标志出现在命令行上时设置为 true

def fignore_exceptions : Flag<["-"], "fignore-exceptions">,
  Visibility<[ClangOption, CC1Option]>,
  MarshallingInfoFlag<LangOpts<"IgnoreExceptions">>;

负标志

关键路径默认设置为 true,当标志出现在命令行上时设置为 false

def fno_verbose_asm : Flag<["-"], "fno-verbose-asm">,
  Visibility<[ClangOption, CC1Option]>,
  MarshallingInfoNegativeFlag<CodeGenOpts<"AsmVerbose">>;

负标志和正标志

关键路径默认设置为指定的值(falsetrue 或 tablegen 文件中静态未知的某些布尔值)。然后,关键路径将设置为最后出现在命令行上的标志关联的值。

defm legacy_pass_manager : BoolOption<"f", "legacy-pass-manager",
  CodeGenOpts<"LegacyPassManager">, DefaultFalse,
  PosFlag<SetTrue, [], [], "Use the legacy pass manager in LLVM">,
  NegFlag<SetFalse, [], [], "Use the new pass manager in LLVM">,
  BothFlags<[], [ClangOption, CC1Option]>>;

对于大多数这样的标志对,-cc1 前端只接受更改默认关键路径值的标志。Clang 驱动程序负责接受两者,并转发更改标志或丢弃只会将关键路径设置为其默认值的标志。

BoolOption 的第一个参数是一个前缀,用于构造两个标志的完整名称。然后,正标志将被命名为 flegacy-pass-manager,负标志将被命名为 fno-legacy-pass-managerBoolOption 还暗示两个标志的 - 前缀。也可以使用 BoolFOption,它暗示 "f" 前缀和 Group<f_Group>。类 PosFlagNegFlag 包含关联的布尔值、传递给 FlagVisibility 类的元素数组,以及帮助文本。可选的 BothFlags 类包含 FlagVisibility 元素数组,它们对正标志和负标志以及它们的公共帮助文本后缀都是通用的。

字符串

关键路径默认设置为指定的字符串,如果省略,则为空。当选项出现在命令行上时,参数值被简单地复制。

def isysroot : JoinedOrSeparate<["-"], "isysroot">,
  Visibility<[ClangOption, CC1Option, FlangOption]>,
  MarshallingInfoString<HeaderSearchOpts<"Sysroot">, [{"/"}]>;

字符串列表

关键路径默认设置为一个空的 std::vector<std::string>。在命令行上每次出现选项时指定的 value 都被附加到 vector 中。

def frewrite_map_file : Separate<["-"], "frewrite-map-file">,
  Visibility<[ClangOption, CC1Option]>,
  MarshallingInfoStringVector<CodeGenOpts<"RewriteMapFiles">>;

整数

关键路径默认设置为指定的整数值,如果省略,则为 0。当选项出现在命令行上时,它的值被 llvm::APInt 解析,并在成功时将结果分配给关键路径。

def mstack_probe_size : Joined<["-"], "mstack-probe-size=">,
  Visibility<[ClangOption, CC1Option]>,
  MarshallingInfoInt<CodeGenOpts<"StackProbeSize">, "4096">;

枚举

关键路径默认设置为 MarshallingInfoEnum 中指定的值,该值以 NormalizedValuesScope 的内容和 :: 为前缀。这确保即使枚举位于不同的命名空间或是一个枚举类,也能正确地引用枚举 case。如果命令行上存在的值与 Values 中的任何逗号分隔的值都不匹配,则会发出错误诊断。否则,将 NormalizedValues 中相同索引处的对应元素分配给关键路径(也正确地限定范围)。逗号分隔的字符串值的数量与 NormalizedValues 中数组的元素数量必须匹配。

def mthread_model : Separate<["-"], "mthread-model">,
  Visibility<[ClangOption, CC1Option]>,
  Values<"posix,single">, NormalizedValues<["POSIX", "Single"]>,
  NormalizedValuesScope<"LangOptions::ThreadModelKind">,
  MarshallingInfoEnum<LangOpts<"ThreadModel">, "POSIX">;

还可以定义选项之间的关系。

隐含

关键路径默认设置为来自主 Marshalling 注释的默认值。然后,如果 ImpliedByAnyOf 中的任何元素都计算为 true,则关键路径值将更改为指定的值,如果缺少,则更改为 true。最后,根据主注释解析命令行。

def fms_extensions : Flag<["-"], "fms-extensions">,
  Visibility<[ClangOption, CC1Option]>,
  MarshallingInfoFlag<LangOpts<"MicrosoftExt">>,
  ImpliedByAnyOf<[fms_compatibility.KeyPath], "true">;

条件

只有当 ShouldParseIf 中的表达式计算为 true 时,才会解析选项。

def fopenmp_enable_irbuilder : Flag<["-"], "fopenmp-enable-irbuilder">,
  Visibility<[ClangOption, CC1Option]>,
  MarshallingInfoFlag<LangOpts<"OpenMPIRBuilder">>,
  ShouldParseIf<fopenmp.KeyPath>;

词法分析器和预处理器库

词法分析器库包含几个紧密相关的类,这些类参与了 C 源代码词法分析和预处理的繁琐过程。该库对外部客户端的主要接口是大型的 Preprocessor 类。它包含从翻译单元中连贯地读取标记所需的各种状态信息。

Preprocessor 对象的核心接口(一旦设置)是 Preprocessor::Lex 方法,它从预处理器流返回下一个 标记。预处理器能够读取两种类型的标记提供程序:缓冲区词法分析器(由 词法分析器 类提供)和缓冲标记流(由 标记词法分析器 类提供)。

标记类

Token 用于表示单个词法分析的标记。标记旨在由词法分析器/预处理器和解析器库使用,但不打算超出它们的范围(例如,它们不应该存在于 AST 中)。

令牌通常在解析器运行时存放在栈(或其他方便访问的位置)中,但偶尔也会被缓冲。例如,宏定义以一系列令牌存储,而 C++ 前端需要定期将令牌缓冲起来,以便进行初步解析和各种前瞻操作。因此,Token 的大小很重要。在 32 位系统上,sizeof(Token) 目前为 16 字节。

令牌有两种形式:注释令牌 和普通令牌。普通令牌是词法分析器返回的令牌,注释令牌表示语义信息,由解析器生成,替换令牌流中的普通令牌。普通令牌包含以下信息

  • 源位置 — 指示令牌开始的位置。

  • 长度 — 存储令牌在 SourceBuffer 中的存储长度。对于包含三元组和转义换行符的令牌,此长度包括这些字符,而这些字符会被编译器后面的阶段忽略。通过指向原始源缓冲区,始终可以完全准确地获取令牌的原始拼写。

  • 标识符信息 — 如果令牌采用标识符的形式,并且在对令牌进行词法分析时启用了标识符查找(例如,词法分析器不是以“原始”模式读取),则它包含指向标识符的唯一哈希值的指针。由于查找发生在关键字识别之前,即使对于像“for”这样的语言关键字,此字段也会被设置。

  • 令牌类型 — 指示词法分析器分类的令牌类型。这包括像 tok::starequal(用于“*=”运算符)、tok::ampamp 用于“&&”令牌以及关键字值(例如,tok::kw_for)用于对应于关键字的标识符。请注意,某些令牌可以以多种方式拼写。例如,C++ 支持“运算符关键字”,其中“and”与“&&”运算符完全相同。在这些情况下,类型值被设置为 tok::ampamp,这对解析器来说很好,解析器不必考虑这两种形式。对于关心使用哪种形式的东西(例如,预处理器“字符串化”运算符),拼写表示原始形式。

  • 标志 — 词法分析器/预处理器系统当前在每个令牌的基础上跟踪四个标志

    1. 行首 — 这是其输入源行上出现的第一个令牌。

    2. 前导空格 — 令牌之前或通过宏展开时令牌之前存在空格字符。此标志的定义与预处理器的字符串化要求密切相关。

    3. 禁用展开 — 此标志在预处理器内部使用,以表示宏展开禁用的标识符令牌。这将防止它们在将来被视为宏展开的候选者。

    4. 需要清理 — 如果令牌的原始拼写包含三元组或转义换行符,则设置此标志。由于这种情况很少见,许多代码可以快速路径处理不需要清理的令牌。

普通令牌的一个有趣(且有些不同寻常)的方面是,它们不包含有关词法分析值的任何语义信息。例如,如果令牌是 pp-number 令牌,我们不会表示词法分析的数字的值(这留待后面的代码部分决定)。此外,词法分析器库没有类型定义名称与变量名称的概念:两者都作为标识符返回,解析器负责决定特定标识符是类型定义还是变量(跟踪这需要范围信息以及其他信息)。解析器可以通过用“注释令牌”替换预处理器返回的令牌来进行此转换。

注释令牌

注释令牌是由解析器合成的,并注入预处理器的令牌流中(替换现有令牌),以记录解析器发现的语义信息。例如,如果发现“foo”是类型定义,则“footok::identifier 令牌将被替换为 tok::annot_typename。这很有用,原因有两个:1)这使得在 C++ 中轻松处理限定类型名(例如,“foo::bar::baz<42>::t”)作为解析器中的单个“令牌”。2)如果解析器回溯,重新解析不需要重新进行语义分析以确定令牌序列是变量、类型、模板等。

注释令牌由解析器创建,并在解析器的令牌流中重新注入(当启用回溯时)。由于它们只能存在于预处理器本身已经处理过的令牌中,因此不需要保留像“行首”这样的标志,而预处理器使用这些标志来完成它的工作。此外,注释令牌可以“覆盖”一系列预处理器令牌(例如,“a::b::c”是五个预处理器令牌)。因此,注释令牌的有效字段不同于普通令牌的字段(但它们被多路复用到普通的 Token 字段中)

  • 源位置 “位置” — 注释令牌的 SourceLocation 指示被注释令牌替换的第一个令牌。在上面的例子中,它将是“a”标识符的位置。

  • 源位置 “注释结束位置” — 此处保存被注释令牌替换的最后一个令牌的位置。在上面的例子中,它将是“c”标识符的位置。

  • void* “注释值” — 此处包含解析器从 Sema 获取的不透明对象。解析器只保存信息,以便 Sema 稍后根据注释令牌类型进行解释。

  • 令牌类型 “类型” — 指示此注释令牌的类型。有关不同有效类型的说明,请参见下文。

注释令牌目前有三种类型

  1. tok::annot_typename:此注释令牌表示已解析的类型名令牌,该令牌可能已限定。 AnnotationValue 字段包含 Sema::getTypeName() 返回的 QualType,可能附带源位置信息。

  2. tok::annot_cxxscope:此注释令牌表示 C++ 范围说明符,例如“A::B::”。这对应于语法产生式“::”和“:: [opt] nested-name-specifier”。 AnnotationValue 指针是 Sema::ActOnCXXGlobalScopeSpecifierSema::ActOnCXXNestedNameSpecifier 回调返回的 NestedNameSpecifier *

  3. tok::annot_template_id:此注释令牌表示 C++ 模板 ID,例如“foo<int, 4>”,其中“foo”是模板的名称。 AnnotationValue 指针是指向 malloc’d TemplateIdAnnotation 对象的指针。根据上下文,解析的模板 ID(命名类型)可能变为类型名注释令牌(如果我们只关心命名的类型,例如,因为它出现在类型说明符中),或者可能保持为模板 ID 令牌(如果我们想要保留更多源位置信息或产生一个新类型,例如,在类模板特化的声明中)。引用类型的模板 ID 注释令牌可以通过解析器“升级”为类型名注释令牌。

如上所述,注释令牌不是由预处理器返回的,它们是由解析器按需形成的。这意味着解析器必须意识到可能出现注释的情况,并在适当的地方形成它。这有点类似于解析器处理 C99 的翻译阶段 6 的方式:字符串连接(参见 C99 5.1.1.2)。在字符串连接的情况下,预处理器只返回不同的 tok::string_literaltok::wide_string_literal 令牌,解析器在语法指示字符串文字可以出现的地方吞噬它们。

为了做到这一点,每当解析器期望 tok::identifiertok::coloncolon 时,它应该调用 TryAnnotateTypeOrScopeTokenTryAnnotateCXXScopeToken 方法来形成注释令牌。这些方法将最大程度地形成指定的注释令牌,并在适用时用它们替换当前令牌。如果当前令牌对注释令牌无效,它将保留为标识符或“::”令牌。

Lexer

Lexer 类提供从源缓冲区中词法分析令牌并确定其含义的机制。 Lexer 的复杂之处在于它对尚未消除拼写(这是获得良好性能的必要条件)的原始缓冲区进行操作,但这通过仔细的编码以及标准性能技术(例如,注释处理代码在 X86 和 PowerPC 主机上进行了矢量化)得以解决。

词法分析器具有几个有趣的模态功能

  • 词法分析器可以以“原始”模式运行。此模式具有几个功能,可以快速词法分析文件(例如,它停止标识符查找,不特别处理预处理器令牌,以不同的方式处理 EOF 等)。例如,此模式用于在“#if 0”块中进行词法分析。

  • 词法分析器可以捕获和返回注释作为令牌。这是为了支持 -C 预处理器模式(它传递注释),并被诊断检查器用于识别预期错误注释。

  • 词法分析器可以处于 ParsingFilename 模式,这在读取 #include 指令后进行预处理时发生。此模式更改对“<”的解析,以返回“尖括号字符串”,而不是文件名中的每个内容的一堆令牌。

  • 解析预处理器指令(在“#”之后)时,将进入 ParsingPreprocessorDirective 模式。这会更改解析器,以便在新行处返回 EOD。

  • The Lexer uses a LangOptions object to determine whether trigraphs are enabled, whether C++ or ObjC keywords are recognized, etc.

In addition to these modes, the lexer keeps track of several other features that are local to a lexed buffer, which change as the buffer is lexed.

  • The Lexer uses BufferPtr to track the current character being lexed.

  • The Lexer uses IsAtStartOfLine to track whether the next lexed token will start with its “start of line” bit set.

  • The Lexer keeps track of the current active “#if” directives (which can be nested).

  • The Lexer keeps track of a MultipleIncludeOpt object, which is used to detect whether the buffer uses the standard “#ifndef XX / #define XX” idiom to prevent multiple inclusion. If a buffer does, subsequent includes can be ignored if the “XX” macro is defined.

The TokenLexer class

The TokenLexer class is a token provider that returns tokens from a list of tokens that came from somewhere else. It is typically used for two things: 1) returning tokens from a macro definition as it is being expanded, 2) returning tokens from an arbitrary buffer of tokens. The latter use is used by _Pragma and will most likely be used to handle unbounded look-ahead for the C++ parser.

The MultipleIncludeOpt class

The MultipleIncludeOpt class implements a very simple state machine that is used to detect the standard “#ifndef XX / #define XX” idiom that people typically use to prevent multiple inclusion of headers. If a buffer uses this idiom and is subsequently #include’d, the preprocessor can simply check to see whether the guarding condition is defined or not. If so, the preprocessor can completely ignore the include of the header.

The Parser Library

This library contains a recursive-descent parser that polls tokens from the preprocessor and notifies a client of the parsing progress.

Historically, the parser used to talk to an abstract Action interface that had virtual methods for parse events, for example ActOnBinOp(). When Clang grew C++ support, the parser stopped supporting general Action clients – it now always talks to the Sema library. However, the Parser still accesses AST objects only through opaque types like ExprResult and StmtResult. Only Sema looks at the AST node contents of these wrappers.

The AST Library

Design philosophy

Immutability

Clang AST nodes (types, declarations, statements, expressions, and so on) are generally designed to be immutable once created. This provides a number of key benefits.

  • Canonicalization of the “meaning” of nodes is possible as soon as the nodes are created, and is not invalidated by later addition of more information. For example, we canonicalize types, and use a canonicalized representation of expressions when determining whether two function template declarations involving dependent expressions declare the same entity.

  • AST nodes can be reused when they have the same meaning. For example, we reuse Type nodes when representing the same type (but maintain separate TypeLocs for each instance where a type is written), and we reuse non-dependent Stmt and Expr nodes across instantiations of a template.

  • Serialization and deserialization of the AST to/from AST files is simpler: we do not need to track modifications made to AST nodes imported from AST files and serialize separate “update records”.

There are unfortunately exceptions to this general approach, such as

  • The first declaration of a redeclarable entity maintains a pointer to the most recent declaration of that entity, which naturally needs to change as more declarations are parsed.

  • Name lookup tables in declaration contexts change after the namespace declaration is formed.

  • We attempt to maintain only a single declaration for an instantiation of a template, rather than having distinct declarations for an instantiation of the declaration versus the definition, so template instantiation often updates parts of existing declarations.

  • Some parts of declarations are required to be instantiated separately (this includes default arguments and exception specifications), and such instantiations update the existing declaration.

These cases tend to be fragile; mutable AST state should be avoided where possible.

As a consequence of this design principle, we typically do not provide setters for AST state. (Some are provided for short-term modifications intended to be used immediately after an AST node is created and before it’s “published” as part of the complete AST, or where language semantics require after-the-fact updates.)

Faithfulness

The AST intends to provide a representation of the program that is faithful to the original source. We intend for it to be possible to write refactoring tools using only information stored in, or easily reconstructible from, the Clang AST. This means that the AST representation should either not desugar source-level constructs to simpler forms, or – where made necessary by language semantics or a clear engineering tradeoff – should desugar minimally and wrap the result in a construct representing the original source form.

For example, CXXForRangeStmt directly represents the syntactic form of a range-based for statement, but also holds a semantic representation of the range declaration and iterator declarations. It does not contain a fully-desugared ForStmt, however.

Some AST nodes (for example, ParenExpr) represent only syntax, and others (for example, ImplicitCastExpr) represent only semantics, but most nodes will represent a combination of syntax and associated semantics. Inheritance is typically used when representing different (but related) syntaxes for nodes with the same or similar semantics.

The Type class and its subclasses

The Type class (and its subclasses) are an important part of the AST. Types are accessed through the ASTContext class, which implicitly creates and uniques them as they are needed. Types have a couple of non-obvious features: 1) they do not capture type qualifiers like const or volatile (see QualType), and 2) they implicitly capture typedef information. Once created, types are immutable (unlike decls).

Typedefs in C make semantic analysis a bit more complex than it would be without them. The issue is that we want to capture typedef information and represent it in the AST perfectly, but the semantics of operations need to “see through” typedefs. For example, consider this code

void func() {
  typedef int foo;
  foo X, *Y;
  typedef foo *bar;
  bar Z;
  *X; // error
  **Y; // error
  **Z; // error
}

The code above is illegal, and thus we expect there to be diagnostics emitted on the annotated lines. In this example, we expect to get

test.c:6:1: error: indirection requires pointer operand ('foo' invalid)
  *X; // error
  ^~
test.c:7:1: error: indirection requires pointer operand ('foo' invalid)
  **Y; // error
  ^~~
test.c:8:1: error: indirection requires pointer operand ('foo' invalid)
  **Z; // error
  ^~~

While this example is somewhat silly, it illustrates the point: we want to retain typedef information where possible, so that we can emit errors about “std::string” instead of “std::basic_string<char, std:...”. Doing this requires properly keeping typedef information (for example, the type of X is “foo”, not “int”), and requires properly propagating it through the various operators (for example, the type of *Y is “foo”, not “int”). In order to retain this information, the type of these expressions is an instance of the TypedefType class, which indicates that the type of these expressions is a typedef for “foo”.

Representing types like this is great for diagnostics, because the user-specified type is always immediately available. There are two problems with this: first, various semantic checks need to make judgements about the actual structure of a type, ignoring typedefs. Second, we need an efficient way to query whether two types are structurally identical to each other, ignoring typedefs. The solution to both of these problems is the idea of canonical types.

Canonical Types

每个 Type 类实例都包含一个规范类型指针。对于没有类型定义的简单类型(例如,“int”,“int*”,“int**”),该类型仅指向自身。对于在其结构中某处具有类型定义的类型(例如,“foo”,“foo*”,“foo**”,“bar”),规范类型指针指向其结构上等效的类型,没有任何类型定义(例如,“int”,“int*”,“int**”和“int*”)。

这种设计提供了一个恒定时间操作(取消对规范类型指针的引用),它使我们能够访问类型的结构。例如,我们可以通过取消对它们的规范类型指针的引用并执行指针比较来轻松地判断“bar”和“foo*”是否相同类型(它们都指向单个“int*”类型)。

规范类型和类型定义类型带来了一些复杂性,必须小心管理。具体来说,isa/cast/dyn_cast运算符通常不应该用于检查 AST 的代码中。例如,在对间接运算符(指针上的单目“*”)进行类型检查时,类型检查器必须验证操作数是否具有指针类型。使用“isa<PointerType>(SubExpr->getType())”来进行检查是不正确的,因为如果子表达式具有类型定义类型,此谓词将失败。

解决此问题的方法是在 Type 上使用一组辅助方法来检查它们的属性。在这种情况下,使用“SubExpr->getType()->isPointerType()”来进行检查是正确的。如果*规范类型是指针*,则此谓词将返回 true,无论何时类型在结构上都是指针类型,这都是正确的。这里唯一困难的部分是记住不要使用 isa/cast/dyn_cast运算符。

我们面临的第二个问题是如何在知道指针类型存在的情况下获得对其的访问权限。为了继续这个例子,间接运算符的结果类型是子表达式的指向类型。为了确定类型,我们需要获取最能捕获程序中类型定义信息的 PointerType 实例。如果表达式的类型确实是 PointerType,我们可以返回该类型,否则我们必须深入类型定义以查找指针类型。例如,如果子表达式具有类型“foo*”,我们可以将该类型作为结果返回。如果子表达式具有类型“bar”,我们希望返回“foo*”(注意,我们*不*希望返回“int*”)。为了提供所有这些功能,Type 有一个 getAsPointerType() 方法,它检查类型在结构上是否为 PointerType,如果是,则返回最佳类型。如果不是,则返回一个空指针。

这种结构有点神秘,但经过深思熟虑后,你就会明白:)。

QualType

QualType 类被设计为一个简单的值类,它体积小,按值传递,并且查询效率高。 QualType 的理念是它将类型限定符(constvolatilerestrict,以及语言扩展所需的某些扩展限定符)与类型本身分开存储。 QualType 在概念上是一对“Type*”和这些类型限定符的位。

通过将类型限定符作为位存储在概念对中,获取 QualType 上的限定符集非常高效(只需返回对的字段),添加类型限定符(这是一个简单的常数时间操作,它设置一个位),以及删除一个或多个类型限定符(只需返回一个 QualType,其位字段被设置为为空)。

此外,由于位存储在类型本身之外,因此我们不需要创建具有不同限定符集的类型的副本(即只有一个堆分配的“int”类型:“const int”和“volatile const int”都指向同一个堆分配的“int”类型)。这减少了用于表示位的堆大小,也意味着我们在对类型进行唯一化时不需要考虑限定符(Type 甚至不包含限定符)。

实际上,两个最常见的类型限定符(constrestrict)存储在指向 Type 对象的指针的低位中,以及一个指示扩展限定符是否存在(必须堆分配)的标志。这意味着 QualType 与指针的大小完全相同。

声明名称

DeclarationName 类表示 Clang 中声明的名称。C 语言家族中的声明可以采用多种不同的形式。大多数声明由简单的标识符命名,例如,在函数声明 f(int x) 中的“f”和“x”。在 C++ 中,声明名称还可以命名类构造函数(Classstruct Class { Class(); } 中),类析构函数(“~Class”),重载运算符名称(“operator+”),以及转换函数(“operator void const *”)。在 Objective-C 中,声明名称可以引用 Objective-C 方法的名称,这些名称包含方法名称和参数,统称为*选择器*,例如,“setWidth:height:”。由于所有这些类型的实体——变量、函数、Objective-C 方法、C++ 构造函数、析构函数和运算符——都表示为 Clang 公共 NamedDecl 类的子类,因此 DeclarationName 被设计为高效地表示任何类型的名称。

给定一个 DeclarationName NN.getNameKind() 将产生一个值,该值描述 N 存储的名称类型。有 10 种选择(所有名称都在 DeclarationName 类中)。

标识符

该名称是一个简单的标识符。使用 N.getAsIdentifierInfo() 获取相应的 IdentifierInfo*,它指向实际的标识符。

ObjCZeroArgSelectorObjCOneArgSelectorObjCMultiArgSelector

该名称是一个 Objective-C 选择器,它可以通过 N.getObjCSelector() 获取为 Selector 实例。Objective-C 的三种可能的名称种类反映了 DeclarationName 类中的优化:零参数和单参数选择器都存储为一个屏蔽的 IdentifierInfo 指针,因此需要很少的空间,因为零参数和单参数选择器比多参数选择器(使用不同的结构)更常见。

CXXConstructorName

该名称是一个 C++ 构造函数名称。使用 N.getCXXNameType() 获取此构造函数旨在构造的 类型。该类型始终是规范类型,因为给定类型的所有构造函数具有相同的名称。

CXXDestructorName

该名称是一个 C++ 析构函数名称。使用 N.getCXXNameType() 获取其析构函数正在命名的 类型。此类型始终是规范类型。

CXXConversionFunctionName

该名称是一个 C++ 转换函数。转换函数根据它们转换到的类型命名,例如,“operator void const *”。使用 N.getCXXNameType() 获取此转换函数转换到的类型。此类型始终是规范类型。

CXXOperatorName

名称是 C++ 运算符重载名称。运算符重载根据其拼写命名,例如,“operator+” 或 “operator new []”。使用 N.getCXXOverloadedOperator() 获取重载运算符(OverloadedOperatorKind 类型的返回值)。

CXXLiteralOperatorName

名称是 C++11 用户定义的字面量运算符。用户定义的字面量运算符根据它们定义的后缀命名,例如,“_foo” 用于 “operator "" _foo”。使用 N.getCXXLiteralIdentifier() 获取相应的 IdentifierInfo*,它指向标识符。

CXXUsingDirective

名称是 C++ 使用指令。使用指令实际上并不是 NamedDecls,因为它们都具有相同的名称,但它们被实现为 NamedDecls 以便将它们有效地存储在 DeclContext 中。

DeclarationName 的创建、复制和比较成本很低。在常见情况下(标识符、零参数和单参数 Objective-C 选择器),它们只需要一个指针大小的存储空间,并且对其他类型的名称使用密集的、唯一的存储空间。两个 DeclarationName 可以使用简单的按位比较来比较是否相等(==!=),可以用 <><=>= 进行排序(对普通标识符提供词法排序,但对其他类型的名称提供未指定排序),并且可以放入 LLVM DenseMapDenseSet 中。

DeclarationName 实例可以通过不同的方式创建,具体取决于实例将存储哪种类型的名称。普通标识符(IdentifierInfo 指针)和 Objective-C 选择器(Selector)可以隐式转换为 DeclarationNames。C++ 构造函数、析构函数、转换函数和重载运算符的名称可以从 DeclarationNameTable 获取,该表的实例可作为 ASTContext::DeclarationNames 获取。成员函数 getCXXConstructorNamegetCXXDestructorNamegetCXXConversionFunctionNamegetCXXOperatorName 分别返回四种类型的 C++ 特殊函数名的 DeclarationName 实例。

声明上下文

程序中的每个声明都存在于某个声明上下文中,例如翻译单元、命名空间、类或函数。Clang 中的声明上下文由 DeclContext 类表示,各种声明上下文 AST 节点(TranslationUnitDeclNamespaceDeclRecordDeclFunctionDecl 等)将从该类派生。 DeclContext 类提供了一些对每个声明上下文通用的功能

声明的以源代码为中心和以语义为中心的视图

DeclContext 提供了对存储在声明上下文中的声明的两种视图。以源代码为中心的视图准确地表示所写程序源代码,包括存在时实体的多个声明(参见 重新声明和重载 部分),而以语义为中心的视图表示程序语义。在构建 AST 时,语义分析会将这两个视图保持同步。

在该上下文中存储声明

每个声明上下文都可以包含一定数量的声明。例如,C++ 类(由 RecordDecl 表示)包含各种成员函数、字段、嵌套类型等。所有这些声明都将存储在 DeclContext 中,并且可以通过 [DeclContext::decls_begin(), DeclContext::decls_end()] 对声明进行迭代。这种机制提供了该上下文中声明的以源代码为中心的视图。

在该上下文中查找声明

DeclContext 结构为该声明上下文中的名称提供了高效的名称查找。例如,如果 N 是一个命名空间,我们可以使用 DeclContext::lookup 查找名称 N::f。查找本身基于一个延迟构建的数组(对于具有少量声明的声明上下文)或哈希表(对于具有更多声明的声明上下文)。查找操作提供了该上下文中声明的以语义为中心的视图。

声明的所有权

DeclContext 拥有在其声明上下文中声明的所有声明,并负责管理它们的内存以及它们的(反)序列化。

所有声明都存储在声明上下文之中,并且可以查询有关每个声明所在上下文的的信息。可以使用 Decl::getDeclContext 获取包含特定 DeclDeclContext。但是,请参见 词法上下文和语义上下文 部分,了解有关如何解释此上下文信息的更多信息。

重新声明和重载

在翻译单元中,实体通常会声明多次。例如,我们可能声明一个函数 “f”,然后在稍后将其重新声明为内联定义的一部分

void f(int x, int y, int z = 1);

inline void f(int x, int y, int z) { /* ...  */ }

f” 的表示形式在声明上下文的以源代码为中心的视图和以语义为中心的视图中有所不同。在以源代码为中心的视图中,所有重新声明都将存在,它们按在源代码中出现的顺序排列,这使得此视图适合希望查看源代码结构的客户端。在以语义为中心的视图中,只有最新的“f” 会被查找找到,因为它实际上替换了“f” 的第一个声明。

(注意,因为 f 可以在块级作用域中重新声明,或者在友元声明中重新声明等,所以通过名称查找找到的 f 的声明可能不是最新的。)

在以语义为中心的视图中,函数重载以显式方式表示。例如,假设函数 “g” 存在两个重载声明,例如:

void g();
void g(int);

DeclContext::lookup 操作将返回一个 DeclContext::lookup_result,其中包含一系列迭代器,这些迭代器用于迭代 “g” 的声明。对不关心实际源代码的程序执行语义分析的客户端将主要使用这种以语义为中心的视图。

词法上下文和语义上下文

每个声明都有两个可能不同的声明上下文:一个词法上下文,它对应于声明上下文的以源代码为中心的视图,以及一个语义上下文,它对应于以语义为中心的视图。词法上下文可以通过 Decl::getLexicalDeclContext 获取,而语义上下文可以通过 Decl::getDeclContext 获取,它们都返回 DeclContext 指针。对于大多数声明,这两个上下文是相同的。例如

class X {
public:
  void f(int x);
};

在这里,X::f 的语义上下文和词法上下文是与类 X 关联的 DeclContext(它本身存储为 RecordDecl AST 节点)。但是,我们现在可以定义 X::f 的内联定义

void X::f(int x = 17) { /* ...  */ }

此“f” 的定义具有不同的词法上下文和语义上下文。词法上下文对应于源代码中实际声明发生的声明上下文,例如包含 X 的翻译单元。因此,可以通过遍历翻译单元中 [decls_begin(), decls_end()] 提供的声明来找到此 X::f 的声明。

X::f 的语义上下文对应于类 X,因为此成员函数在(语义上)是 X 的成员。然后,在与 X 关联的 DeclContext 中查找名称 f 将返回 X::f 的定义(包括有关默认参数的信息)。

透明声明上下文

在 C 和 C++ 中,有几个上下文,在这些上下文中,逻辑上在另一个声明内部声明的名称实际上会从名称查找的角度来看“泄漏”到封闭作用域中。这种行为最明显的例子是枚举类型,例如:

enum Color {
  Red,
  Green,
  Blue
};

在这里,Color 是一个枚举,它是一个包含枚举器 RedGreenBlue 的声明上下文。因此,遍历包含在枚举 Color 中的声明列表将产生 RedGreenBlue。但是,在 Color 的作用域之外,可以命名枚举器 Red,而无需限定名称,例如:

Color c = Red;

C++ 中还有其他实体提供类似的行为。例如,使用花括号的链接规范

extern "C" {
  void f(int);
  void g(int);
}
// f and g are visible here

为了源代码级别的准确性,我们将链接规范和枚举类型视为声明上下文,其包含的声明(“Red”、“Green” 和 “Blue”;“f” 和 “g”)在其中声明。但是,这些声明在声明上下文的作用域之外是可见的。

这些语言特性(以及下面描述的几个其他特性)具有大致相同的需求集:声明在特定的词法上下文中声明,但这些声明也通过名称查找在包含声明本身的作用域中找到。此特性通过透明声明上下文实现(参见 DeclContext::isTransparentContext()),其声明在最近的封闭非透明声明上下文中可见。这意味着声明的词法上下文(例如,枚举器)将是透明的 DeclContext 本身,语义上下文也是如此,但声明将在每个外部上下文中可见,直到第一个非透明声明上下文(因为透明声明上下文可以嵌套)。

透明的 DeclContext

  • 枚举(但不是 C++11 “作用域枚举”)

    enum Color {
      Red,
      Green,
      Blue
    };
    // Red, Green, and Blue are in scope
    
  • C++ 链接规范

    extern "C" {
      void f(int);
      void g(int);
    }
    // f and g are in scope
    
  • 匿名联合体和结构体

    struct LookupTable {
      bool IsVector;
      union {
        std::vector<Item> *Vector;
        std::set<Item> *Set;
      };
    };
    
    LookupTable LT;
    LT.Vector = 0; // Okay: finds Vector inside the unnamed union
    
  • C++11 内联命名空间

    namespace mylib {
      inline namespace debug {
        class X;
      }
    }
    mylib::X *xp; // okay: mylib::X refers to mylib::debug::X
    

多重定义的声明上下文

C++ 命名空间有一个有趣的特性,即命名空间可以多次定义,每个命名空间定义提供的声明实际上会合并(从语义角度来看)。例如,以下两个代码片段在语义上是不可区分的

// Snippet #1:
namespace N {
  void f();
}
namespace N {
  void f(int);
}

// Snippet #2:
namespace N {
  void f();
  void f(int);
}

在 Clang 的表示中,声明上下文的以源代码为中心的视图实际上将在代码片段 #1 中具有两个独立的 NamespaceDecl 节点,每个节点都是一个声明上下文,包含 “f” 的单个声明。但是,通过名称查找进入命名空间 N 来查找 “f” 所提供的以语义为中心的视图将返回一个 DeclContext::lookup_result,其中包含一个范围,该范围是 “f” 声明的迭代器。

DeclContext 在内部管理多重定义的声明上下文。函数 DeclContext::getPrimaryContext 检索给定 DeclContext 实例的“主”上下文,它是负责维护用于以语义为中心的视图的查找表的 DeclContext。给定一个 DeclContext,可以通过 DeclContext::collectAllContexts 获取与该声明上下文在语义上连接的声明上下文集,按源代码顺序排列,包括该上下文(对于非命名空间上下文,将是唯一的結果)。请注意,这些函数在 DeclContext 的查找和插入方法中内部使用,因此绝大多数客户端可以忽略它们。

由于同一个实体可以在不同的模块中多次定义,因此也可能存在多个 (例如) CXXRecordDecl 的定义,所有这些定义都描述了同一个类的定义。在这种情况下,Clang 仅将其中一个“定义”视为类的定义,而其他定义则被视为包含成员声明的非定义声明。此类多重定义类的每个定义中的对应成员通过重新声明链(如果成员是 Redeclarable)或仅仅通过指向规范声明的指针来标识(如果声明不是 Redeclarable——在这种情况下,将使用 Mergeable 基类)。

错误处理

即使代码包含错误,Clang 也会生成 AST。Clang 不会为此生成和优化代码,但它在解析继续以检测输入中的更多错误时使用。基于 Clang 的工具也依赖于此类 AST,尤其是 IDE 可以从高质量的 AST 中获益,即使是对于损坏的代码。

在存在错误的情况下,clang 使用一些错误恢复策略来在 AST 中呈现损坏的代码

  • 更正错误:在 clang 对修复有信心的情况下,它会提供一个附加到错误诊断的 FixIt 并发出一个更正的 AST(反映带有应用的 FixIt 的已写入代码)。这样做的好处是可以提供更准确的后续诊断。拼写错误更正就是一个典型的例子。

  • 表示无效节点:无效节点以某种形式保留在 AST 中,例如,当声明的“声明”部分包含语义错误时,Decl 节点将被标记为无效。

  • 删除无效节点:这通常发生在我们没有优雅恢复的错误情况下。在恢复 AST 之前,不匹配参数的函数调用表达式被删除,尽管为语义分析创建了 CallExpr。

通过这些策略,clang 可以提供更好的诊断,并为 AST 消费者提供丰富的 AST,尽可能反映已写入的源代码,即使对于损坏的代码。

恢复 AST

恢复 AST 的理念是使用恢复节点,这些节点充当占位符来维护解析树的粗略结构,保留位置和子节点,但没有附加任何语言语义。

例如,请考虑以下不匹配的函数调用

int NoArg();
void test(int abc) {
  NoArg(abc); // oops, mismatched function arguments.
}

在没有恢复 AST 的情况下,无效的函数调用表达式(及其子表达式)将在 AST 中被删除

|-FunctionDecl <line:1:1, col:11> NoArg 'int ()'
`-FunctionDecl <line:2:1, line:4:1> test 'void (int)'
 |-ParmVarDecl <col:11, col:15> col:15 used abc 'int'
 `-CompoundStmt <col:20, line:4:1>

有了恢复 AST,AST 如下所示

|-FunctionDecl <line:1:1, col:11> NoArg 'int ()'
`-FunctionDecl <line:2:1, line:4:1> test 'void (int)'
  |-ParmVarDecl <col:11, col:15> used abc 'int'
  `-CompoundStmt <col:20, line:4:1>
    `-RecoveryExpr <line:3:3, col:12> 'int' contains-errors
      |-UnresolvedLookupExpr <col:3> '<overloaded function type>' lvalue (ADL) = 'NoArg'
      `-DeclRefExpr <col:9> 'int' lvalue ParmVar 'abc' 'int'

另一种选择是使用现有的 Expr,例如上面的示例中的 CallExpr。这将捕获更多调用详细信息(例如括号的位置),并允许它与有效的 CallExpr 一致地处理。但是,将我们拥有的数据塞入 CallExpr 会迫使我们削弱其不变性,例如参数计数可能不正确。这将给 AST 消费者的处理此类“不可能”情况带来巨大的负担。因此,当我们表示(而不是更正)错误时,我们会使用具有极其弱不变性的不同恢复节点类型。

RecoveryExpr 是迄今为止唯一的恢复节点。在实践中,损坏的声明需要保留更多详细的语义(当前的 Invalid 标志运行得相当好),而具有有趣内部结构的完全损坏的语句很少见(因此删除语句是可以的)。

类型和依赖关系

RecoveryExpr 是一个 Expr,因此它必须具有一个类型。在许多情况下,真正的类型直到代码被更正后才能真正知道(例如,调用一个不存在的函数)。这意味着我们无法对某些包含的构造进行适当的类型检查,例如 return 42 + unknownFunction()

为了对此进行建模,我们将 C++ 模板中的依赖关系概念泛化为表示对模板参数的依赖关系或错误如何修复的依赖关系。 RecoveryExpr unknownFunction() 具有完全未知的类型 DependentTy,这会抑制基于类型的分析,就像在模板内部一样。

在我们可以确定具体类型的某些情况下(例如,对于损坏的非重载函数调用的返回类型),RecoveryExpr 将具有此类型。这允许检查更多代码,并生成更好的 AST 和更多诊断。例如

unknownFunction().size() // .size() is a CXXDependentScopeMemberExpr
std::string(42).size() // .size() is a resolved MemberExpr

无论 RecoveryExpr 是否具有依赖类型,它始终被视为值依赖的,因为它的值直到错误被解决后才被定义好。其中一件事意味着 clang 不会在 RecoveryExpr 用作常量(例如数组大小)的地方发出更多错误,但也不会尝试对其进行评估。

ContainsErrors 位

除了模板依赖位之外,我们还添加了一个新的“ContainsErrors” 位来表达“该表达式或其内部的任何内容是否包含错误”语义,此位始终为 RecoveryExpr 设置,并传播到其他相关节点。这提供了一种快速方法来查询表达式的任何(递归)子节点是否包含错误,这通常用于改进诊断。

// C++
void recoveryExpr(int abc) {
 unknownFunction(); // type-dependent, value-dependent, contains-errors

 std::string(42).size(); // value-dependent, contains-errors,
                         // not type-dependent, as we know the type is std::string
}
// C
void recoveryExpr(int abc) {
  unknownVar + abc; // type-dependent, value-dependent, contains-errors
}

AST 导入器

ASTImporter 类将 ASTContext 的节点导入另一个 ASTContext。有关介绍,请参阅文档 AST 导入器:合并 Clang AST。请仔细阅读 导入算法的概述,这对于理解导入器的进一步实现细节至关重要。

抽象语法图

尽管名称如此,但 Clang AST 不是一棵树。它是一个具有循环的有向图。循环的一个例子是 ClassTemplateDecl 与其“模板化” CXXRecordDecl 之间的连接。模板化CXXRecordDecl 表示类模板内部的所有字段和方法,而 ClassTemplateDecl 包含与成为模板相关的信息,即模板参数等。我们可以使用 ClassTemplateDecl::getTemplatedDecl() 获取 ClassTemplateDecl模板化类(CXXRecordDecl)。我们可以从模板化类获取指向“描述”的类模板的指针:CXXRecordDecl::getDescribedTemplate()。因此,这是两个节点之间的循环:在模板化节点和描述节点之间。AST 中可能存在各种其他类型的循环,尤其是在声明的情况下。

结构等效性

导入一个 AST 节点会将该节点复制到目标 ASTContext。复制一个节点意味着我们在“目标”上下文中创建一个新节点,然后将它的属性设置为等于源节点的属性。在复制之前,我们确保源节点与目标上下文中的任何现有节点结构上等效。如果碰巧是等效的,那么我们就跳过复制。

结构等效性的非正式定义如下:两个节点结构上等效,如果它们是

  • 内置类型并引用相同的类型,例如 intint 在结构上是等效的。

  • 函数类型及其所有参数在结构上具有等效的类型。

  • 记录类型及其所有字段按定义顺序具有相同的标识符名称和结构上等效的类型。

  • 变量或函数声明,它们具有相同的标识符名称,并且它们的类型在结构上是等效的。

在 C 中,如果两种类型是兼容类型,则它们在结构上是等效的。有关兼容类型的正式定义,请参阅 C11 标准中的 6.2.7/1。但是,C++ 标准中没有关于兼容类型的定义。尽管如此,我们将结构等效性的定义扩展到模板及其实例化:除了检查前面提到的属性外,我们还需要检查等效的模板参数/参数等。

结构等效性检查可以独立于 ASTImporter 使用,并且确实独立于 ASTImporter 使用,例如 clang::Sema 类也使用它。

节点的等效性可能取决于其他节点对的等效性。因此,检查被实现为并行图遍历。我们同时遍历两个图中的节点。实际的实现类似于广度优先搜索。假设我们以节点对 <A,B> 开始遍历。每当遍历到达一对 <X,Y> 时,以下语句为真

  • A 和 X 是来自同一个 ASTContext 的节点。

  • B 和 Y 是来自同一个 ASTContext 的节点。

  • A 和 B 可能来自同一个 ASTContext,也可能不来自同一个 ASTContext。

  • 如果 A == X 且 B == Y(指针等效性),则(遍历过程中存在循环)

    • A 和 B 在结构上等效当且仅当

      • 从 <A,B> 到 <X,Y> 的路径上的所有依赖节点在结构上都是等效的。

当我们比较两个类或枚举,并且其中一个是不完整的或具有未加载的外部词法声明时,我们无法下降到比较它们包含的声明。因此,在这些情况下,如果它们具有相同的名称,则它们被认为是相等的。这就是我们比较前向声明和定义的方式。

重新声明链

早期版本的 ASTImporter 的合并机制会压缩声明,即它的目标是只有一个声明,而不是维护一个完整的重新声明链。这种早期的做法只是跳过导入函数原型,而是导入定义。为了说明这种方法的问题,让我们考虑一个空的“to”上下文和在“from”上下文中对 f 的以下 virtual 函数声明

struct B { virtual void f(); };
void B::f() {} // <-- let's import this definition

如果我们使用“压缩”方法导入定义,那么我们最终将得到一个声明,该声明实际上是一个定义,但 isVirtual() 为它返回 false。原因是定义实际上不是虚函数,而是原型属性!

因此,我们必须为定义设置虚标记(但随后我们创建了一个解析器永远不会创建的格式错误的 AST),或者我们导入函数的整个重新声明链。最新版本的 ASTImporter 使用后一种机制。我们确实会导入所有函数声明 - 无论它们是定义还是原型 - 按它们在“from”上下文中出现的顺序导入。

如果我们在“to”上下文中有一个现有的定义,那么我们不能导入另一个定义,我们将使用现有的定义。但是,我们可以导入原型:我们将新导入的原型链接到现有的定义。每当我们从第三个上下文中导入一个新的原型时,它将被添加到重新声明链的末尾。这可能会导致在某些情况下出现较长的重新声明链,例如,如果我们从包含相同头文件(其中包含原型)的多个翻译单元导入。

为了缓解自由函数长重新声明链的问题,我们可以比较原型,看看它们是否具有相同的属性,如果有,那么我们可以合并这些原型。自由函数原型压缩的实现是将来的工作。

以这种方式链接函数可确保我们从源 AST 复制所有信息。尽管如此,成员函数存在一个问题:虽然我们可以为自由函数拥有多个原型,但我们必须为成员函数只拥有一个原型。

void f(); // OK
void f(); // OK

struct X {
  void f(); // OK
  void f(); // ERROR
};
void X::f() {} // OK

因此,必须压缩成员函数的原型,我们不能简单地将新的原型附加到现有的类内原型。考虑以下上下文

// "to" context
struct X {
  void f(); // D0
};
// "from" context
struct X {
  void f(); // D1
};
void X::f() {} // D2

当我们从“from”上下文中导入 f 的原型和定义时,结果重新声明链将如下所示 D0 -> D2',其中 D2'D2 在“to”上下文中的副本。

一般来说,当我们导入声明(如枚举和类)时,我们会将新导入的声明附加到现有的重新声明链(如果存在结构等效性)。但是,我们不会像处理函数那样导入整个重新声明链。到目前为止,我们还没有发现与成员函数原型中虚标记类似的前向声明的任何重要属性。将来,这可能会改变,尽管如此。

导入过程中的遍历

节点特定的导入机制在 ASTNodeImporter::VisitNode() 函数中实现,例如 VisitFunctionDecl()。当我们导入声明时,我们首先导入调用该声明节点构造函数所需的一切。所有可以在以后设置的内容都在创建节点后设置。例如,在 FunctionDecl 的情况下,我们首先导入声明函数的声明上下文,然后创建 FunctionDecl,然后才导入函数体。这意味着 AST 节点之间存在隐式依赖关系。这些依赖关系决定了我们在“from”上下文中访问节点的顺序。与 DFS 等常规图遍历算法一样,我们使用 ASTImporter::ImportedDecls 跟踪我们已经访问了哪些节点。每当我们创建一个节点时,我们都会立即将其添加到 ImportedDecls 中。在跟踪新创建的节点之前,我们不能开始导入任何其他声明。这至关重要,否则我们将无法处理循环依赖关系。为了强制执行这一点,我们将所有 AST 节点的构造函数调用包装在 GetImportedOrCreateDecl() 中。此包装器可确保所有新创建的声明立即被标记为已导入;此外,如果声明已经被标记为已导入,那么我们只返回其在“to”上下文中的对应项。因此,直接调用声明的 ::Create() 函数会导致错误,请不要这样做!

即使使用 GetImportedOrCreateDecl(),如果以错误的方式相互导入内容,仍然存在无限导入递归的可能性。想象一下,在导入 A 期间,在我们可以为 A 创建节点之前,请求导入 B(构造函数需要对 B 的引用)。对于 B 的导入,也可能是这样(在我们可以为 B 创建节点之前,请求导入 A)。在 模板化描述的摆动 的情况下,我们格外注意打破循环依赖:我们只在创建 CXXRecordDecl 后才导入和设置描述的模板。作为最佳实践,在“to”上下文中创建节点之前,避免导入对节点 A 的构造函数不需要的其他节点。

错误处理

每个导入函数都返回 llvm::Errorllvm::Expected<T> 对象。这强制检查导入函数的返回值。如果在一次导入过程中出现错误,那么我们返回该错误。(例外:当我们导入类的成员时,我们会收集每个成员的各个错误,并将它们连接到一个 Error 对象中。)我们将这些错误缓存到声明中。在下次导入调用期间,如果有现有错误,我们只返回该错误。因此,库的客户端会收到一个 Error 对象,他们必须检查该对象。

在导入特定声明期间,可能会发生在我们识别错误之前已经创建了某些 AST 节点。在这种情况下,我们将错误信号返回给调用者,但“to”上下文将被已经创建的节点污染。理想情况下,这些节点不应该被创建,但当时我们并不知道错误,错误是后来发生的。由于 AST 是不可变的(在大多数情况下,我们无法删除现有节点),我们选择将这些节点标记为错误。

我们将与“from”上下文中的声明相关的错误缓存到 ASTImporter::ImportDeclErrors 中,并将与“to”上下文相关的错误缓存到 ASTImporterSharedState::ImportErrors 中。请注意,可能存在多个 ASTImporter 对象,它们从不同的“from”上下文导入到同一个“to”上下文;在这种情况下,它们必须共享“to”上下文相关的错误。

当出现错误时,它会通过调用堆栈传播,穿过所有依赖节点。但是,在依赖循环的情况下,这还不够,因为我们努力标记错误节点,以便客户端可以对其进行操作。在这些情况下,我们必须跟踪那些作为循环中间节点的节点的错误。

导入路径是我们在导入调用期间访问的 AST 节点的列表。如果节点 A 依赖于节点 B,那么路径包含一条 A->B 边。从导入函数的调用堆栈中,我们可以读取完全相同的路径。

现在想象一下以下 AST,其中 -> 表示导入方面的依赖关系(所有节点都是声明)。

A->B->C->D
   `->E

我们想导入 A。导入的行为类似于 DFS,因此我们将按此顺序访问节点:ABCDE。在访问期间,我们将拥有以下导入路径

A
AB
ABC
ABCD
ABC
AB
ABE
AB
A

如果在访问 E 期间出现错误,那么我们会为 E 设置错误,然后当调用堆栈缩小到 B,然后缩小到 A 时

A
AB
ABC
ABCD
ABC
AB
ABE // Error! Set an error to E
AB  // Set an error to B
A   // Set an error to A

然而,在导入过程中,我们可以成功导入 C 和 D,并且它们与 A、B 和 E 是独立的。我们不能为 C 和 D 设置错误。因此,在导入结束时,我们在 ImportDeclErrors 中为 A、B、E 设置了条目,但没有为 C、D 设置。

现在,如果导入路径中存在循环,会发生什么?让我们考虑这个 AST

A->B->C->A
   `->E

在访问过程中,我们将拥有以下导入路径,如果在访问 E 时出现错误,我们将为 E、B、A 设置错误。但是 C 怎么办?

A
AB
ABC
ABCA
ABC
AB
ABE // Error! Set an error to E
AB  // Set an error to B
A   // Set an error to A

这一次我们知道 B 和 C 都依赖于 A。这意味着我们也必须为 C 设置错误。当调用栈反转回 A 时,我们必须为所有依赖于 A 的节点设置错误(这包括 C)。但 C 不再位于导入路径上,它只是之前存在过。这种情况只有在访问过程中存在循环时才会发生。如果我们没有循环,那么通过调用栈传递错误对象的正常方式可以处理这种情况。这就是为什么我们必须在导入过程中跟踪每个访问声明的循环的原因。

查找问题

当我们从源上下文导入声明时,我们会检查在“to”上下文中是否已经存在一个结构上等效的节点,并且具有相同的名称。如果“from”节点是定义,并且找到的节点也是定义,那么我们不会创建新的节点,而是将找到的节点标记为导入节点。如果找到的定义和我们要导入的定义具有相同的名称,但它们的结构不等效,那么在 C++ 的情况下,我们将出现 ODR 违反。如果“from”节点不是定义,那么我们将它添加到找到的节点的重新声明链中。当我们将来自包含相同头文件(s)的不同翻译单元的 AST 合并时,这种行为是必不可少的。例如,我们希望只为类模板 std::vector 拥有一个定义,即使我们在几个翻译单元中包含了 <vector>

要查找结构上等效的节点,我们可以使用常规的 C/C++ 查找函数:DeclContext::noload_lookup()DeclContext::localUncachedLookup()。这些函数遵守 C/C++ 的名称隐藏规则,因此你无法在给定的声明上下文中找到某些声明。例如,未命名的声明(匿名结构体)、非第一个 friend 声明和模板特化是隐藏的。这是一个问题,因为如果我们使用常规的 C/C++ 查找,那么我们将在合并过程中创建冗余的 AST 节点!此外,拥有相同节点的两个实例会导致其他依赖于重复节点的节点出现错误的 结构不等效。由于这些原因,我们创建了一个查找类,其唯一目的是注册所有声明,以便稍后可以通过后续的导入请求查找它们。这就是 ASTImporterLookupTable 类。如果多个 ASTImporter 实例恰好导入到同一个“to”上下文中,则此查找表应在它们之间共享。这就是为什么我们只能通过 ASTImporterSharedState 类使用特定于导入器的查找的原因。

ExternalASTSource

ExternalASTSource 是与 ASTContext 类关联的抽象接口。它提供了读取存储在声明上下文中的声明的能力,无论是用于迭代还是用于名称查找。具有外部 AST 源的声明上下文可以按需加载其声明。这意味着声明列表(表示为一个链表,头是 DeclContext::FirstDecl)可能是空的。但是,像 DeclContext::lookup() 这样的成员函数可能会启动加载。

通常,外部源与预编译头文件相关联。例如,当我们从 PCH 加载一个类时,只有在我们想要在该类的上下文中查找一些东西时才会加载成员。

在 LLDB 的情况下,ExternalASTSource 接口的实现附加到与解析的表达式相关的 AST 上下文。ExternalASTSource 接口的这种实现是在 ASTImporter 类的帮助下实现的。这样,LLDB 就可以在从调试数据(例如 DWARF)合成底层 AST 时重用 Clang 的解析机制。从 ASTImporter 的角度来看,这意味着“to”和“from”上下文都可以具有具有外部词法存储的声明上下文。如果“to”AST 上下文中的 DeclContext 具有外部词法存储,那么我们必须格外注意只使用已经加载的声明!否则,我们最终会得到一个不受控制的导入过程。例如,如果我们使用常规的 DeclContext::lookup() 在“to”上下文中查找现有声明,那么 lookup() 调用本身将在我们导入声明的过程中启动新的导入!(在我们启动查找时,我们还没有注册,我们已经开始导入“from”上下文的节点。)这就是为什么我们使用 DeclContext::noload_lookup() 的原因。

类模板实例化

不同的翻译单元可能具有具有相同模板参数的类模板实例化,但具有不同的实例化 MethodDeclsFieldDecls 集。考虑以下文件

// x.h
template <typename T>
struct X {
    int a{0}; // FieldDecl with InitListExpr
    X(char) : a(3) {}     // (1)
    X(int) {}             // (2)
};

// foo.cpp
void foo() {
    // ClassTemplateSpec with ctor (1): FieldDecl without InitlistExpr
    X<char> xc('c');
}

// bar.cpp
void bar() {
    // ClassTemplateSpec with ctor (2): FieldDecl WITH InitlistExpr
    X<char> xc(1);
}

foo.cpp 中,我们使用编号为 (1) 的构造函数,它显式地将成员 a 初始化为 3,因此 InitListExpr {0} 这里没有使用,并且 AST 节点没有实例化。但是,在 bar.cpp 的情况下,我们使用编号为 (2) 的构造函数,它没有显式地初始化 a 成员,因此需要默认的 InitListExpr,因此实例化了。当我们合并 foo.cppbar.cpp 的 AST 时,我们必须为 X<char> 的类模板实例化创建一个 AST 节点,该节点具有所有必需的节点。因此,当我们找到现有的 ClassTemplateSpecializationDecl 时,我们以一种将“from”上下文中 ClassTemplateSpecializationDecl 的字段合并起来的方式,如果 InitListExpr 还不存在,就复制它。相同的合并机制应该在实例化函数的默认参数和异常规范的情况下完成。

声明的可见性

在导入具有外部可见性的全局变量时,查找将找到具有相同名称但具有静态可见性(链接)的变量。显然,我们不能将它们放在同一个重新声明链中。函数的情况也是如此。此外,如果枚举、类等在匿名命名空间中,我们也必须注意它们。因此,我们过滤查找结果,只考虑那些与我们当前导入的声明具有相同可见性的结果。

我们认为在两个匿名命名空间中的两个声明具有相同的可见性,只有当它们是从同一个 AST 上下文导入的。

处理冲突名称的策略

在导入过程中,我们查找具有相同名称的现有声明。我们根据它们的 可见性 过滤查找结果。如果任何找到的声明在结构上不等效,那么我们就会遇到名称冲突错误(C++ 中的 ODR 违反)。在这种情况下,我们将返回一个 Error,并为声明设置 Error 对象。但是,ASTImporter 的一些客户端可能需要不同的,可能不太保守,更自由的错误处理策略。

例如,静态分析客户端可能会在创建节点时受益,即使存在名称冲突。在某些项目的 CTU 分析过程中,我们发现了一些全局声明与来自其他翻译单元的声明发生冲突,但它们在其翻译单元之外没有被引用。这些声明应该在理想情况下处于未命名的命名空间中。如果我们宽容地处理这些冲突,那么 CTU 分析可以找到更多结果。请注意,能够在名称冲突处理策略之间进行选择的特性仍在开发中。

The CFG class

CFG 类旨在表示单个语句(Stmt*)的源级控制流图。通常,CFG 的实例是为函数体(通常是 CompoundStmt 的实例)构造的,但也可以实例化以表示任何继承自 Stmt 的类的控制流,这包括简单的表达式。控制流图对于在给定函数上执行 流敏感或路径敏感 的程序分析特别有用。

基本块

具体来说,CFG 的一个实例是由一系列基本块组成的集合。每个基本块都是 CFGBlock 的一个实例,它简单地包含一个有序的 Stmt* 序列(每个都引用 AST 中的语句)。块中语句的顺序表示从一个语句到下一个语句的无条件控制流。 条件控制流 使用基本块之间的边来表示。可以使用 CFGBlock::*iterator 接口遍历给定 CFGBlock 中的语句。

CFG 对象拥有它所代表的控制流图中 CFGBlock 的实例。CFG 中的每个 CFGBlock 也都有唯一的编号(可以通过 CFGBlock::getBlockID() 访问)。目前,这个数字是根据创建块的顺序确定的,但不能对 CFGBlocks 的编号方式有任何假设,除了它们是唯一的,并且从 0..N-1 编号(其中 N 是 CFG 中基本块的数量)。

入口和出口块

每个 CFG 实例都包含两个特殊的块:一个入口块(可以通过 CFG::getEntry() 访问),它没有入边;还有一个出口块(可以通过 CFG::getExit() 访问),它没有出边。这两个块都不包含任何语句,它们的作用是为一段代码(如函数体)提供一个清晰的入口和出口。这些空块的存在极大地简化了许多基于 CFG 的分析的实现。

条件控制流

条件控制流(例如由 if 语句和循环引起的控制流)表示为 CFGBlocks 之间的边。由于不同的 C 语言结构会导致控制流,因此每个 CFGBlock 还记录了一个额外的 Stmt*,它表示块的终结器。终结器只是导致控制流的语句,用于识别块之间条件控制流的性质。例如,在 if 语句的情况下,终结器引用 AST 中表示给定分支的 IfStmt 对象。

为了说明,请考虑以下代码示例

int foo(int x) {
  x = x + 1;
  if (x > 2)
    x++;
  else {
    x += 2;
    x *= 2;
  }

  return x;
}

在对这段代码片段调用解析器 + 语义分析器后,foo 函数体的 AST 由单个 Stmt* 引用。然后,我们可以通过调用一个静态类方法来构造一个 CFG 实例,它代表这个函数体的控制流图。

Stmt *FooBody = ...
std::unique_ptr<CFG> FooCFG = CFG::buildCFG(FooBody);

除了提供一个接口来遍历其 CFGBlocks 之外,CFG 类还提供了一些方法,这些方法对调试和可视化 CFG 有用。例如,方法 CFG::dump() 会将 CFG 的一个格式化版本转储到标准错误中。当使用调试器(如 gdb)时,这特别有用。例如,以下是 FooCFG->dump() 的输出

[ B5 (ENTRY) ]
   Predecessors (0):
   Successors (1): B4

[ B4 ]
   1: x = x + 1
   2: (x > 2)
   T: if [B4.2]
   Predecessors (1): B5
   Successors (2): B3 B2

[ B3 ]
   1: x++
   Predecessors (1): B4
   Successors (1): B1

[ B2 ]
   1: x += 2
   2: x *= 2
   Predecessors (1): B4
   Successors (1): B1

[ B1 ]
   1: return x;
   Predecessors (2): B2 B3
   Successors (1): B0

[ B0 (EXIT) ]
   Predecessors (1): B1
   Successors (0):

对于每个块,格式化的输出显示了每个块的前驱块数量(具有指向给定块的出边控制流的块)和后继块数量(具有控制流的块,这些块具有来自给定块的入边控制流)。我们还可以清楚地看到格式化输出开头和结尾的特殊入口和出口块。对于入口块(块 B5),前驱块的数量为 0,而对于出口块(块 B0),后继块的数量为 0。

这里最有趣的块是 B4,它的出边控制流表示由 foo 中唯一的 if 语句引起的分支。特别值得关注的是块中的第二个语句 (x > 2) 和终结器,以 if [B4.2] 打印。第二个语句表示 if 语句条件的计算,它发生在实际控制流分支之前。在 B4 的 CFGBlock 中,第二个语句的 Stmt* 引用了 AST 中 (x > 2) 的实际表达式。因此,指向 Expr 子类的指针可能出现在块中的语句列表中,而不仅仅是指向引用适当 C 语句的 Stmt 子类的指针。

块 B4 的终结器是指向 AST 中 IfStmt 对象的指针。格式化程序输出 if [B4.2] 是因为 if 语句的条件表达式在基本块中实际上有位置,因此终结器基本上是引用作为块 B4 的第二个语句的表达式(即 B4.2)。通过这种方式,控制流条件(这也包括循环和 switch 语句的条件)被提升到实际的基本块中。

Clang AST 中的常量折叠

在几个地方,常量和常量折叠对 Clang 前端非常重要。首先,一般来说,我们希望 AST 保留尽可能接近用户编写方式的源代码。这意味着如果他们写了 “5+4”,我们希望在 AST 中保留加法和两个常量,我们不想将其折叠为 “9”。这意味着常量折叠在各种方式上变成了需要处理各种情况的树遍历。

但是,在 C 和 C++ 中有一些地方需要折叠常量。例如,C 标准定义了“整型常量表达式”(i-c-e)是什么,并给出了非常精确和具体的规定。然后,语言在许多地方要求使用 i-c-e(例如,位字段的大小、case 语句的值等)。对于这些,我们必须能够折叠常量,才能进行语义检查(例如,验证位字段大小是非负的,case 语句没有重复)。我们希望 Clang 对此非常严格,诊断代码在需要使用 i-c-e 的地方没有使用 i-c-e 的情况,但除非在使用 -pedantic-errors 时接受代码。

当涉及到与实际源代码的兼容性时,情况变得更加复杂。具体来说,GCC 历史上接受了大量表达式作为 i-c-e,并且许多实际代码依赖于这种不幸的历史意外情况(包括,例如,glibc 系统头文件)。GCC 接受其“fold”优化器能够将其简化为整型常量的任何内容,这意味着它接受的内容定义会随着优化器的发展而改变。一个例子是,GCC 接受诸如 “case X-X:” 之类的东西,即使 X 是一个变量,因为它可以将其折叠为 0。

另一个问题是如何常量与我们支持的扩展进行交互,例如 __builtin_constant_p__builtin_inf__extension__ 以及许多其他扩展。C99 显然没有指定任何这些扩展的语义,并且 i-c-e 的定义不包含它们。但是,这些扩展通常用在实际代码中,我们必须有一种方法来推断它们。

最后,这不仅仅是语义分析的问题。代码生成器和其他客户端必须能够折叠常量(例如,初始化全局变量),并且必须处理比 C99 允许的范围更大的常量。此外,这些客户端可以从扩展信息中受益。例如,我们知道 “foo() || 1” 总是计算为 true,但我们不能用 true 替换表达式,因为它有副作用。

实现方法

在尝试了几种不同的方法后,我们最终找到了一个设计(注意,在撰写本文时,并非所有这些都已实现,请将其视为一个设计目标!)。我们的基本方法是定义一个单一的递归评估方法(Expr::Evaluate),它在 AST/ExprConstant.cpp 中实现。给定具有“标量”类型(整型、浮点型、复数型或指针型)的表达式,此方法返回以下信息

  • 表达式是否为整型常量表达式、已折叠但没有副作用的一般常量、已折叠但确实有副作用的一般常量,还是不可计算/不可折叠的值。

  • 如果表达式以任何方式可计算,则此方法返回表达式的结果的 APValue

  • 如果表达式完全不可评估,则此方法返回有关表达式问题之一的信息。这包括表达式的错误位置的 SourceLocation 和解释问题的诊断 ID。诊断应该具有 ERROR 类型。

  • 如果表达式不是整型常量表达式,则此方法返回有关表达式问题之一的信息。这包括表达式的错误位置的 SourceLocation 和解释问题的诊断 ID。诊断应该具有 EXTENSION 类型。

此信息为各种客户端提供了他们想要的灵活性,并且我们最终会为各种扩展提供一些辅助方法。例如,Sema应该有一个Sema::VerifyIntegerConstantExpression方法,该方法在表达式上调用Evaluate。如果表达式不可折叠,则会发出错误,并且它将返回true。如果表达式不是i-c-e,则会发出EXTENSION诊断信息。最后,它将返回false以指示AST 正常。

其他客户端可以使用信息以其他方式,例如,代码生成器可以使用任何可以折叠的表达式。

扩展

本节介绍了 Clang 支持的各种扩展如何与常量求值交互。

  • __extension__: 此扩展的表达式形式导致任何可评估的子表达式被接受为整型常量表达式。

  • __builtin_constant_p: 如果操作数求值为数值(即,不是指向整型类型的指针转换)或整型、枚举、浮点或复数类型,或者如果操作数求值为字符串文字第一个字符的地址(可能转换为其他类型),则此函数返回 true(作为整型常量表达式)。作为特例,如果__builtin_constant_p是条件运算符表达式(“?:”)的(可能带括号的)条件,则仅考虑条件运算符的真侧,并以完整的常量折叠进行评估。

  • __builtin_choose_expr: 条件必须是整型常量表达式,但我们接受任何常量作为“扩展的扩展”。这仅评估一个操作数,具体取决于条件的求值方式。

  • __builtin_classify_type: 此函数始终返回整型常量表达式。

  • __builtin_inf, nan, ...: 这些与浮点文字一样对待。

  • __builtin_abs, copysign, ...: 这些作为一般常量表达式进行常量折叠。

  • __builtin_strlenstrlen: 如果参数是字符串文字,则这些作为整型常量表达式进行常量折叠。

Sema 库

此库在解析期间由 解析器库 调用,以对输入进行语义分析。对于有效的程序,Sema 为解析的结构构建 AST。

代码生成库

CodeGen 以 AST 作为输入,并从中生成 LLVM IR 代码

如何更改 Clang

如何添加属性

属性是一种可以附加到程序结构的元数据形式,允许程序员将语义信息传递给编译器以供各种用途使用。例如,属性可用于更改程序结构的代码生成,或为静态分析提供额外的语义信息。本文档介绍如何向 Clang 添加自定义属性。有关现有属性的文档,请参阅 此处

属性基础

Clang 中的属性处理分为三个阶段:解析为解析的属性表示,从解析的属性转换为语义属性,然后是语义属性的处理。

属性的解析由属性可以采用的各种语法形式决定,例如 GNU、C++11 和 Microsoft 样式属性,以及属性表定义提供的其他信息。最终,属性对象的解析表示是一个ParsedAttr对象。这些解析的属性作为附加到声明符或声明说明符的解析属性列表链接在一起。属性的解析由 Clang 自动处理,除了以所谓的“自定义”关键字形式拼写的属性。在实现自定义关键字属性时,必须手动完成关键字的解析和ParsedAttr对象的创建。

最终,Sema::ProcessDeclAttributeList()将使用DeclParsedAttr被调用,此时解析的属性可以转换为语义属性。解析的属性转换为语义属性的过程取决于属性定义和属性的语义要求。但是,最终结果是语义属性对象附加到Decl对象,并且可以通过调用Decl::getAttr<T>()获得。类似地,对于语句属性,Sema::ProcessStmtAttributes()将使用Stmt和要转换为语义属性的ParsedAttr对象列表被调用。

语义属性的结构也由 Attr.td 中给出的属性定义控制。此定义用于自动生成用于实现属性的功能,例如从clang::Attr派生的类、解析器使用的信息、某些属性的自动语义检查等。

include/clang/Basic/Attr.td

向 Clang 添加新属性的第一步是将它的定义添加到 include/clang/Basic/Attr.td。此 tablegen 定义必须从Attr(tablegen,不是语义)类型或它的一个派生类型派生。大多数属性将从InheritableAttr类型派生,该类型指定属性可以由Decl的后续重新声明继承。InheritableParamAttr类似于InheritableAttr,不同的是属性是在参数而不是声明上写的。如果属性应用于语句,它应该从StmtAttr派生。如果属性旨在应用于类型而不是声明,则此类属性应该从TypeAttr派生,并且通常不会被赋予 AST 表示。(注意,本文档不涵盖类型属性的创建。)从IgnoredAttr继承的属性会被解析,但会在使用时生成被忽略的属性诊断信息,这在属性由其他供应商支持而 clang 不支持时可能有用。

定义将指定几个关键信息,例如属性的语义名称、属性支持的拼写、属性期望的参数等等。大多数Attr tablegen 类型的成员不需要在派生定义中指定定义,因为默认值就足够了。但是,每个属性都必须至少指定一个拼写列表、一个主题列表和一个文档列表。

拼写

所有属性都要求指定一个拼写列表,该列表表示属性可以拼写的方式。例如,单个语义属性可以具有关键字拼写,以及 C++11 拼写和 GNU 拼写。空拼写列表也是允许的,并且可能对隐式创建的属性有用。接受以下拼写

拼写

描述

GNU

使用 GNU 样式的__attribute__((attr))语法和位置拼写。

CXX11

使用 C++ 样式的[[attr]]语法,并带有一个可选的供应商特定命名空间。

C23

使用 C 样式的[[attr]]语法,并带有一个可选的供应商特定命名空间。

Declspec

使用 Microsoft 样式的__declspec(attr)语法拼写。

CustomKeyword

属性被拼写为关键字,并且需要自定义解析。

RegularKeyword

属性被拼写为关键字。它可以在标准[[attr]]语法可以使用的位置使用,并且与标准属性所指代的相同内容相关联。关键字的词法分析和解析由自动处理。

GCC

指定两个或三个拼写:第一个是 GNU 样式的拼写,第二个是带gnu命名空间的 C++ 样式拼写,第三个是可选的带gnu命名空间的 C 样式拼写。属性只应该为 GCC 支持的属性指定此拼写。

Clang

指定两个或三个拼写:第一个是 GNU 样式的拼写,第二个是带clang命名空间的 C++ 样式拼写,第三个是可选的带clang命名空间的 C 样式拼写。默认情况下,会提供 C 样式拼写。

Pragma

属性被拼写为#pragma,并且需要在预处理器中进行自定义处理。如果属性打算被 Clang 使用,它应该将命名空间设置为"clang"。请注意,此拼写不用于声明属性。

C++ 标准规定,“任何 [非标准属性] 如果不被实现识别,则会被忽略”([dcl.attr.grammar])。C 语言的规则类似。这使得 CXX11C23 拼写不适合影响类型系统的属性,更改代码的二进制接口的属性,或具有其他类似语义意义的属性。

RegularKeyword 提供了一种拼写此类属性的替代方法。它重用标准属性的生成规则,但将其应用于普通关键字,而不是 [[…]] 序列。不识别该关键字的编译器可能会报告某种错误。

例如,ArmStreaming 函数类型属性会影响函数的类型系统和二进制接口。因此,它不能拼写为 [[arm::streaming]],因为不理解 arm::streaming 的编译器会忽略它并错误地编译代码。ArmStreaming 而是拼写为 __arm_streaming,但它可以出现在任何假设的 [[arm::streaming]] 可以出现的地方。

主题

属性与一个或多个主题相关联。如果属性尝试附加到主题列表中不存在的主题,则会自动发出诊断。诊断是警告还是错误取决于属性的 SubjectList 的定义方式,但默认行为是发出警告。显示给用户的诊断会根据列表中的主题自动确定,但也可以在 SubjectList 中指定自定义诊断参数。为主题列表违规生成的诊断由主题列表本身自动计算或指定。如果将以前未使用的 Decl 节点添加到 SubjectList 中,则用于自动确定 utils/TableGen/ClangAttrEmitter.cpp 中诊断参数的逻辑可能需要更新。

默认情况下,SubjectList 中的所有主题必须是 DeclNodes.td 中定义的 Decl 节点,或 StmtNodes.td 中定义的语句节点。但是,可以通过创建 SubsetSubject 对象来创建更复杂的主题。每个这样的对象都有一个它所属的基本主题(必须是 Decl 或 Stmt 节点,而不是 SubsetSubject 节点),以及一些在确定属性是否属于该主题时调用的自定义代码。例如,NonBitField SubsetSubject 属于 FieldDecl,并测试给定的 FieldDecl 是否是位域。当 SubjectList 中指定 SubsetSubject 时,还必须提供自定义诊断参数。

除非 HasCustomParsing 设置为 1,否则属性主题列表的诊断检查会自动完成。

文档

所有属性都必须与其关联某种形式的文档。文档由每天运行的服务器端进程在公共 Web 服务器上进行表生成。通常,属性的文档是在 include/clang/Basic/AttrDocs.td 中以正在记录的属性命名的独立定义。

如果属性不是供公共使用,或者是一个没有可见拼写的隐式创建的属性,则文档列表可以指定 InternalOnly 对象。否则,属性应该将它的文档添加到 AttrDocs.td 中。

文档源自 Documentation tablegen 类型。所有派生类型都必须指定文档类别和实际文档本身。此外,它可以为属性指定自定义标题,尽管在可能的情况下会选择默认标题。

有四个预定义的文档类别:DocCatFunction 用于与函数式主题相关的属性,DocCatVariable 用于与变量式主题相关的属性,DocCatType 用于类型属性,DocCatStmt 用于语句属性。自定义文档类别应该用于具有类似功能的属性组。自定义类别非常适合为其下分组的属性提供概述信息。例如,消耗注释属性定义了一个自定义类别 DocCatConsumed,它在高级别解释了什么是消耗注释。

文档内容(无论是属性还是类别)都是使用 reStructuredText (RST) 语法编写的。

在编写属性的文档后,应该对其进行本地测试,以确保在服务器上生成文档时没有问题。本地测试需要重新构建 clang-tblgen。要生成属性文档,请执行以下命令

clang-tblgen -gen-attr-docs -I /path/to/clang/include /path/to/clang/include/clang/Basic/Attr.td -o /path/to/clang/docs/AttributeReference.rst

在本地测试时,不要提交对 AttributeReference.rst 的更改。该文件由服务器自动生成,对该文件所做的任何更改都会被覆盖。

参数

属性可以选择指定一个参数列表,这些参数可以传递给属性。属性参数指定属性的解析形式和语义形式。例如,如果 Args[StringArgument<"Arg1">, IntArgument<"Arg2">],那么 __attribute__((myattribute("Hello", 3))) 将是一个有效的用法;它在解析时需要两个参数,而语义属性的 Attr 子类的构造函数将需要一个字符串和一个整数参数。

所有参数都有一个名称和一个标志,该标志指定参数是否可选。参数的关联 C++ 类型由参数定义类型决定。如果现有的参数类型不足,可以创建新的类型,但这需要修改 utils/TableGen/ClangAttrEmitter.cpp 以正确支持该类型。

其他属性

Attr 定义具有其他成员,这些成员控制属性的行为。其中许多是特殊用途的,超出了本文档的范围,但有一些值得一提。

如果属性的解析形式更复杂,或者与语义形式不同,则可以将类的 HasCustomParsing 位设置为 1,并且可以更新 Parser::ParseGNUAttributeArgs() 中的解析代码以处理特殊情况。请注意,这仅适用于具有 GNU 拼写的参数——具有 __declspec 拼写的属性当前会忽略此标志,并由 Parser::ParseMicrosoftDeclSpec 处理。

请注意,将此成员设置为 1 会选择退出常见的属性语义处理,需要额外的实现工作来确保属性属于适当的主题等。

如果属性不应从模板声明传播到模板实例,则将 Clone 成员设置为 0。默认情况下,所有属性都会克隆到模板实例。

不需要 AST 节点的属性应该将 ASTNode 字段设置为 0,以避免污染 AST。请注意,任何从 TypeAttrIgnoredAttr 继承的属性都不会自动生成 AST 节点。默认情况下,所有其他属性都会生成 AST 节点。AST 节点是属性的语义表示。

LangOpts 字段指定属性所需的语言选项列表。例如,所有 CUDA 特定属性都为 LangOpts 字段指定 [CUDA],并且当 CUDA 语言选项未启用时,会发出“属性被忽略”警告诊断。由于语言选项不是表生成的节点,因此必须手动创建新的语言选项,并且应该指定 LangOptions 类使用的拼写。

可以根据属性的拼写列表为属性生成自定义访问器。例如,如果属性具有两种不同的拼写:“Foo”和“Bar”,则可以创建访问器:[Accessor<"isFoo", [GNU<"Foo">]>, Accessor<"isBar", [GNU<"Bar">]>] 这些访问器将在属性的语义形式上生成,不接受任何参数并返回一个 bool

不需要自定义语义处理的属性应该将 SemaHandler 字段设置为 0。请注意,任何从 IgnoredAttr 继承的属性都不会自动获得语义处理程序。默认情况下,所有其他属性都假定使用语义处理程序。没有语义处理程序的属性不会获得解析的属性 Kind 枚举器。

“简单”属性不需要除自动提供的以外的自定义语义处理,应该将 SimpleHandler 字段设置为 1

针对特定目标的属性可能与其他目标中的其他属性共享拼写。例如,ARM 和 MSP430 目标都具有一个拼写为 GNU<"interrupt"> 的属性,但解析和语义要求不同。为了支持此功能,继承自 TargetSpecificAttribute 的属性可以指定一个 ParseKind 字段。此字段应在所有共享拼写的参数之间具有相同的值,并对应于已解析属性的 Kind 枚举器。这允许属性共享已解析的属性类型,但具有不同的语义属性类。例如,ParsedAttr 是共享的已解析属性类型,但 ARMInterruptAttr 和 MSP430InterruptAttr 是生成的语义属性。

默认情况下,属性参数在已评估的上下文中进行解析。如果属性的参数应在未评估的上下文中进行解析(类似于对 sizeof 表达式进行解析的方式),则将 ParseArgumentsAsUnevaluated 设置为 1

如果属性的语义形式需要额外的功能,则 AdditionalMembers 字段指定要逐字复制到语义属性类对象中的代码,并具有 public 访问权限。

如果两个或多个属性不能在同一声明或语句上组合使用,则可以提供 MutualExclusions 定义来自动生成诊断代码。这将不允许属性组合,无论使用何种拼写。此外,它还会根据需要诊断同一属性列表、不同属性列表和重新声明中的组合。

样板

所有声明属性的语义处理都在 lib/Sema/SemaDeclAttr.cpp 中进行,通常从 ProcessDeclAttribute() 函数开始。如果属性的 SimpleHandler 字段设置为 1,则处理属性的函数将自动生成,这里无需做任何事情。否则,编写一个新的 handleYourAttr() 函数,并将其添加到 switch 语句中。请不要在属性的 case 中直接实现处理逻辑。

除非属性定义中另有说明,否则将自动处理已解析属性的通用语义检查。这包括诊断不属于给定 DeclStmt 的已解析属性,确保传递了正确的最小参数数量等。

如果属性添加了额外的警告,请在 include/clang/Basic/DiagnosticGroups.td 中定义一个名为属性的 SpellingDiagGroup,其中 “_” 替换为 “-“。如果只有一个诊断,则可以直接在 DiagnosticSemaKinds.td 中使用 InGroup<DiagGroup<"your-attribute">>

针对您的属性生成的任何语义诊断(包括自动生成的诊断,例如主题和参数计数)都应有相应的测试用例。

语义处理

大多数属性的实现是为了对编译器产生某种影响。例如,修改代码生成方式,或为分析过程添加额外的语义检查等。在添加了属性定义并将属性转换为语义表示后,剩下的就是实现需要使用属性的自定义逻辑。

可以使用 hasAttr<T>() 查询 clang::Decl 对象以查看是否存在属性。要获取指向属性的语义表示的指针,可以使用 getAttr<T>

可以使用 getAttrs() 调用 clang::AttributedStmt 对象来查询是否存在属性,并循环遍历属性列表。

如何添加表达式或语句

表达式和语句是编译器中最基本的概念之一,因为它们与 AST、语义分析和 IR 生成的许多不同部分交互。因此,在 Clang 中添加新的表达式或语句类型需要谨慎。以下列表详细说明了在 Clang 中引入表达式或语句的各个地方,以及一些遵循的模式,以确保新的表达式或语句在所有 C 语言中都能正常工作。我们将重点关注表达式,但语句类似。

  1. 在解析器中引入解析操作。递归下降解析大多不言自明,但有一些值得注意的地方

    • 尽可能保留源位置信息!您以后将需要它来生成出色的诊断信息并支持 Clang 的各种将源代码映射到 AST 的功能。

    • 为所有“错误”解析情况编写测试,以确保您的恢复良好。如果您有匹配的定界符(例如,括号、方括号等),请使用 Parser::BalancedDelimiterTracker 来在出现问题时给出良好的诊断信息。

  2. Sema 中引入语义分析操作。语义分析应始终包含两个函数:一个 ActOnXXX 函数,该函数将直接从解析器调用,以及一个 BuildXXX 函数,该函数执行实际的语义分析,并将(最终!)构建 AST 节点。对于 ActOnXXX 函数来说,通常很少执行(通常只是从解析器的表示到 Sema 表示的相同内容的微小转换),但分离仍然很重要:例如,C++ 模板实例化应始终调用 BuildXXX 变体。在深入探讨 AST 的构建之前,关于语义分析的一些注意事项

    • 您的表达式可能包含一些类型和一些子表达式。确保完全检查这些类型以及这些子表达式的类型是否符合您的预期。在必要的地方添加隐式转换,以确保所有类型都完全按照您的预期对齐。编写广泛的测试以检查您是否获得了有关错误的良好诊断信息,以及您是否可以将各种形式的子表达式与您的表达式一起使用。

    • 在对类型或子表达式进行类型检查时,请确保首先检查类型是否“依赖”(Type::isDependentType())或子表达式是否类型依赖(Expr::isTypeDependent())。如果这些返回值中的任何一个返回 true,那么您就在模板内部,现在无法进行太多类型检查。这很正常,您的 AST 节点(当您到达那里时)将不得不处理这种情况。此时,您可以编写使用模板中表达式的测试,但不要尝试实例化模板。

    • 对于每个子表达式,请确保调用 Sema::CheckPlaceholderExpr() 来处理行为不佳的“奇怪”表达式作为子表达式。然后,确定您是否需要执行左值到右值转换(Sema::DefaultLvalueConversions)或通常的单目转换(Sema::UsualUnaryConversions),用于子表达式生成要使用的值的那些地方。

    • 您的 BuildXXX 函数可能会在此处返回 ExprError(),因为您还没有 AST。这完全没问题,不应该影响您的测试。

  3. 为您的新表达式引入一个 AST 节点。这从在 include/Basic/StmtNodes.td 中声明节点以及在相应的 include/AST/Expr*.h 头文件中为您的表达式创建一个新类开始。最好查看类似表达式的类以获得想法,还有一些需要注意的地方

    • 如果您需要分配内存,请使用 ASTContext 分配器来分配内存。永远不要使用原始 mallocnew,也不要在 AST 节点中保存任何资源,因为 AST 节点的析构函数永远不会被调用。

    • 确保 getSourceRange() 涵盖了表达式的确切源范围。这是诊断和 IDE 支持所需的。

    • 确保 children() 访问所有子表达式。这对许多功能(例如,IDE 支持、C++ 可变参数模板)很重要。如果您有子类型,则还需要在 RecursiveASTVisitor 中访问这些子类型。

    • 为您的表达式添加打印支持(StmtPrinter.cpp)。

    • 为您的 AST 节点添加分析支持(StmtProfile.cpp),注意表达式的实例的区分(非源位置)特征。省略此步骤会导致与模板声明匹配相关的难以诊断的错误。

    • 为您的 AST 节点添加序列化支持(ASTReaderStmt.cppASTWriterStmt.cpp)。

  4. 教语义分析构建您的 AST 节点。此时,您可以将 Sema::BuildXXX 函数连接起来,以实际创建您的 AST。在此处检查一些事项

    • 如果您的表达式可以构造一个新的 C++ 类或返回一个新的 Objective-C 对象,请确保更新并调用 Sema::MaybeBindToTemporary 来处理您刚创建的 AST 节点,以确保对象被正确销毁。测试此方法的一个简单方法是返回一个带有私有析构函数的 C++ 类:语义分析应在尝试调用析构函数时在此处标记错误。

    • 使用 clang -cc1 -ast-print 打印生成的 AST,以确保您捕获了有关 AST 写入方式的所有重要信息。

    • 检查使用 clang -cc1 -ast-dump 生成的 AST,以验证所有生成的 AST 中的类型是否按预期排列。请记住,AST 的客户端永远不应该“思考”才能理解发生了什么。例如,所有隐式转换都应该在 AST 中显式显示。

    • 编写测试,使用您的表达式作为其他知名表达式的子表达式。您可以使用您的表达式作为参数调用函数吗?您可以使用三元运算符吗?

  5. 教授代码生成以创建您的 AST 节点的 IR。此步骤是第一个(也是唯一一个)需要 LLVM IR 知识的步骤。需要注意以下几点

    • 代码生成分为标量/聚合/复数和左值/右值路径,具体取决于您的表达式产生什么样的结果。在某些情况下,这需要对代码进行一些细致的分解,以避免重复。

    • CodeGenFunction 包含函数 ConvertTypeConvertTypeForMem,它们将 Clang 的类型(clang::Type*clang::QualType)转换为 LLVM 类型。对于值使用前者,对于内存位置使用后者:使用 C++ 的“bool”类型进行测试以检查这一点。如果您发现必须使用 LLVM 位转换来使表达式的子表达式具有表达式期望的类型,请停止!修复语义分析和 AST,这样您就不需要这些位转换。

    • CodeGenFunction 类有一些辅助函数,可以使某些操作变得容易,例如生成代码以产生左值或右值,或使用给定值初始化内存位置。优先使用这些函数而不是直接编写加载和存储,因为这些函数会为您处理一些棘手的细节(例如,用于异常)。

    • 如果您的表达式在发生异常时需要一些特殊行为,请查看 CodeGenFunction 中的 push*Cleanup 函数以引入清理操作。您不必直接处理异常处理。

    • 在 IR 生成中,测试非常重要。使用 clang -cc1 -emit-llvmFileCheck 来验证您是否生成了正确的 IR。

  6. 教授模板实例化如何处理您的 AST 节点,这需要一些相当简单的代码

    • 确保表达式的构造函数正确计算类型依赖性标志(即表达式产生的类型可以在一次实例化到下一次实例化之间发生变化)、值依赖性标志(即表达式产生的常数值可以在一次实例化到下一次实例化之间发生变化)、实例化依赖性标志(即模板参数出现在表达式的任何地方),以及表达式是否包含参数包(用于可变参数模板)。通常,计算这些标志只是意味着组合来自各种类型和子表达式的结果。

    • Sema 中的 TreeTransform 类模板添加 TransformXXXRebuildXXX 函数。 TransformXXX 应该(递归地)转换表达式中的所有子表达式和类型,使用 getDerived().TransformYYY。如果所有子表达式和类型都成功转换,它将调用 RebuildXXX 函数,该函数反过来将调用 getSema().BuildXXX 来执行语义分析并构建您的表达式。

    • 要测试模板实例化,请使用您编写的那些测试来确保您正在使用类型相关的表达式和相关类型进行类型检查(来自步骤 #2),并使用各种类型实例化这些模板,其中一些类型检查通过,一些类型检查失败,并测试每种情况下的错误消息。

  7. 有一些“额外”的东西可以使其他功能工作得更好。处理这些额外的东西值得付出努力,以使您的表达式完全集成到 Clang 中

    • SemaCodeComplete.cpp 中添加对表达式的代码完成支持。

    • 如果您的表达式中包含类型,或者除了子表达式之外还有任何“有趣”的功能,请扩展 libclang 的 CursorVisitor 以为您的表达式提供适当的访问,从而启用各种 IDE 功能,例如语法高亮、交叉引用等等。 c-index-test 辅助程序可用于测试这些功能。

测试

对 Clang 的所有功能更改都应提供测试覆盖范围,以证明行为的变化。

验证诊断

Clang -cc1 支持 -verify 命令行选项,作为验证诊断行为的一种方式。此选项将在测试文件内使用特殊注释来验证预期的诊断是否出现在正确的源位置。如果所有预期的诊断与 Clang 的实际输出匹配,则调用将正常返回。如果预期输出和实际输出之间存在差异,Clang 将发出有关哪些预期诊断未被发现或哪些意外诊断被发现的详细信息等等。一个完整的示例是

如果测试运行并且预期错误在预期行上发出,则诊断验证器将通过。但是,如果预期错误未出现或出现在与预期不同的位置,或者出现了其他诊断,则诊断验证器将失败并发出有关失败原因的信息。

-verify 命令可以选择接受一个或多个以逗号分隔的验证前缀列表,这些前缀可用于制作这些特殊注释。每个前缀必须以字母开头,并且只能包含字母数字字符、连字符和下划线。 -verify 本身等效于 -verify=expected,这意味着特殊注释将以 expected 开头。使用不同的前缀使在同一测试文件中具有导致不同诊断行为的不同 RUN: 行变得更容易。例如

// RUN: %clang_cc1 -verify=foo,bar %s

int A = B; // foo-error {{use of undeclared identifier 'B'}}
int C = D; // bar-error {{use of undeclared identifier 'D'}}
int E = F; // expected-error {{use of undeclared identifier 'F'}}

验证器将识别 foo-errorbar-error 作为特殊注释,但不会识别 expected-error,因为 -verify 行不包含它作为前缀。因此,此测试将无法通过验证,因为在 E 的声明中将出现意外的诊断。

多次出现会累积前缀。例如, -verify -verify=foo,bar -verify=baz 等效于 -verify=expected,foo,bar,baz

指定诊断

指示一行期望出现错误或警告很容易。在包含诊断的行上放置注释,使用 expected-{error,warning,remark,note} 来标记它是预期的错误、警告、备注或注释(分别),并将预期的文本放在 {{}} 标记之间。不必包含完整的文本,只需包含足以确保发出了正确的诊断的文本即可。(注意:除非有充分理由使用截断的文本,否则应在测试用例中包含完整的文本。)

有关匹配行为的完整描述,包括更复杂的匹配场景,请参见下面的 匹配

以下是以最常用方式指定预期诊断的示例

int A = B; // expected-error {{use of undeclared identifier 'B'}}

您可以在一行上放置任意多个诊断。为了使代码更易读,您可以使用斜杠换行符将诊断隔开。

或者,可以通过将 @<line> 附加到 expected-<type> 来指定诊断应出现的行,例如

#warning some text
// expected-warning@10 {{some text}}

行号可以是绝对的(如上),也可以相对于当前行,方法是在数字前面加上 +-

如果诊断是在单独的文件中生成的,例如在共享头文件中,则可能需要声明诊断将出现的文件,而不是将 expected-* 指令放在实际文件中本身。可以使用以下语法实现此目的

// expected-error@path/include.h:15 {{error message}}

路径可以是绝对路径或相对路径,并将使用与 #include 指令相同的搜索路径。外部文件中的行号可以用 * 替换,这意味着任何行号都将匹配(在包含的文件是系统头文件的情况下很有用,其中实际行号可能会发生变化,并且并不重要)。

作为指定固定行号的替代方法,可以改用 #<marker> 形式的标记来指示诊断的位置。标记通过将它们包含在注释中来指定,然后通过将标记附加到带有 @#<marker> 的诊断来引用,例如

#warning some text  // #1
// ... other code ...
// expected-warning@#1 {{some text}}

在指令中使用的标记名称在编译中必须是唯一的。

上面的简单语法允许每个规范精确匹配一个诊断。您可以使用扩展语法来自定义此操作。扩展语法是 expected-<type> <n> {{diag text}},其中 <type>errorwarningremarknote 之一,而 <n> 是一个正整数。这允许诊断出现指定的次数。例如

void f(); // expected-note 2 {{previous declaration is here}}

如果诊断预计出现最少次数,则可以通过在数字后面附加一个 + 来指定。例如

void f(); // expected-note 0+ {{previous declaration is here}}
void g(); // expected-note 1+ {{previous declaration is here}}

在第一个示例中,诊断变得可选,即如果它出现,它将被吞噬,但如果它不出现,它将不会生成错误。在第二个示例中,诊断必须至少出现一次。作为一种简写,“一次或多次”可以通过简单的 + 来指定。例如

void g(); // expected-note + {{previous declaration is here}}

也可以通过 <n>-<m> 来指定范围。例如

void f(); // expected-note 0-1 {{previous declaration is here}}

在此示例中,诊断最多只能出现一次,即使出现也是如此。

匹配模式

默认的匹配模式是简单的字符串匹配,它会查找在注释中第一个 `{{` 和 `}}` 对之间出现的预期文本。该字符串按原样解释,只有一个例外:序列 `\n` 将被转换为一个换行符。这种模式在文本作为发射消息中任何位置的子字符串出现时,会匹配发射的诊断信息。

为了能够匹配包含 `}}` 或 `{{` 的目标字符串,字符串模式解析器接受超过两个花括号的开启定界符,例如 `{{{`。然后它会寻找同样“宽度”的关闭定界符(例如 `}}}`)。例如:

// expected-note {{{evaluates to '{{2, 3, 4}} == {0, 3, 4}'}}}

目的是允许定界符比内容中最长的 `{` 或 `}` 花括号序列更宽,因此如果您的预期文本包含 `{{{`(三个花括号),它可以用 `{{{{`(四个花括号)来定界,依此类推。

正则表达式匹配模式可以通过在诊断类型后追加 `-re` 并将用双花括号 `{{` 和 `}}` 包裹的正则表达式包含在指令中来选择,例如:

expected-error-re {{format specifies type 'wchar_t **' (aka '{{.+}}')}}

匹配错误的示例:“variable has incomplete type ‘struct s’”。

// expected-error {{variable has incomplete type 'struct s'}}
// expected-error {{variable has incomplete type}}
// expected-error {{{variable has incomplete type}}}
// expected-error {{{{variable has incomplete type}}}}

// expected-error-re {{variable has type 'struct {{.}}'}}
// expected-error-re {{variable has type 'struct {{.*}}'}}
// expected-error-re {{variable has type 'struct {{(.*)}}'}}
// expected-error-re {{variable has type 'struct{{[[:space:]](.*)}}'}}

特性测试宏

Clang 实现了几种方法来测试是否支持某个特性。其中一些特性测试是标准化的,例如 `__has_cpp_attribute` 或 `__cpp_lambdas`,而另一些则是 Clang 扩展,例如 `__has_builtin`。所有这些特性测试的共同点是,它们是用来告诉用户我们认为某个特性已经完成的工具。但是,完整性是一个难以定义的属性,因为特性可能仍然存在一些遗留的 bug,可能只在某些目标上有效等等。我们在决定是否公开特性测试宏(或特性测试的特定结果值)时,会使用以下标准:

  • 是否存在已知问题,我们拒绝了应该被接受的有效代码?

  • 是否存在已知问题,我们接受了应该被拒绝的无效代码?

  • 是否存在已知的崩溃、断言失败或编译错误?

  • 是否存在特定相关目标上的已知问题?

如果对上述任何问题的答案是“是”,那么特性测试宏要么不应该被定义,要么应该有非常充分的理由解释为什么这些问题不应该阻止定义它。请注意,如果需要,可以根据目标的差异来定义特性测试宏。

如果有疑问,保守的做法比激进的做法更好。如果我们不声称支持该特性,但它确实有一些有用的功能,用户仍然可以使用它,并向我们提供关于哪些功能缺失的有用反馈。但是,如果我们声称支持一个存在重大 bug 的特性,那么特性测试宏的大部分用处就会消失,因为用户必须测试正在使用的编译器版本才能获得更准确的答案。

特性测试宏报告的状态应始终反映在相应特性的语言支持页面(如果有的话,请参见 C++C)。该页面可以向用户提供更细致的信息,例如声称部分支持某个特性,并指定关于哪些工作尚未完成的详细信息。