给 clang 扩展语法

由于最近所做的项目需要对 C 语言进行一些扩展,而项目中使用的编译器为 clang,因此需要对 clang 进行一些修改,以支持新的语法。本文将介绍如何给 clang 添加新的语法。

需要增加的实际是一个 pragma 的注释,用于指定一个循环的原始(即未经优化的)次数,其语法如下:

1
2
3
4
#pragma loopbound min 10 max 10 // 或者 _Pragma("loopbound min 10 max 10")
for (int i = 0; i < 10; i++) {
// do something
}

这里的 minmax 分别表示循环的最小次数和最大次数,我们要做的是提取这个信息,并将其插入到 IR 中,在后续的解析中,我们可以根据这个信息进行一些优化。

实现

Note: clang 的编译器实现是相对好阅读的,如果你对编译原理的一些基础知识有所了解,而你又对编译器的实现感兴趣,那么阅读 clang 的源码是一个不错的选择,本文不会对 clang 的源码进行详绵的解释,而是对我们感兴趣的部分进行简要的介绍。

本文 clang 版本为 14.0.6,对于不同版本的 clang,可能需要做一些调整,但是大体的思路是一样的。

预处理

这里需要一些 C 编译的基础知识,如果你对 C 编译的流程不太了解,可以先了解一下。

C 语言的编译过程大致分为四个阶段:预处理、编译、汇编、链接。其中预处理阶段主要是对源代码进行一些预处理,例如宏替换、注释删除等。这里修改的 _Pragma 实际上是在预处理阶段进行的。

在修改前,我们不妨先看一个 clang 自己的一个预处理的流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
// 源文件: clang/lib/Lex/Pragma.cpp

/// PragmaMessageHandler - Handle the microsoft and gcc \#pragma message
/// extension. The syntax is:
/// \code
/// #pragma message(string)
/// \endcode
/// OR, in GCC mode:
/// \code
/// #pragma message string
/// \endcode
/// string is a string, which is fully macro expanded, and permits string
/// concatenation, embedded escape characters, etc... See MSDN for more details.
/// Also handles \#pragma GCC warning and \#pragma GCC error which take the same
/// form as \#pragma message.
struct PragmaMessageHandler : public PragmaHandler {
private:
const PPCallbacks::PragmaMessageKind Kind;
const StringRef Namespace;

static const char* PragmaKind(PPCallbacks::PragmaMessageKind Kind,
bool PragmaNameOnly = false) {
switch (Kind) {
case PPCallbacks::PMK_Message:
return PragmaNameOnly ? "message" : "pragma message";
case PPCallbacks::PMK_Warning:
return PragmaNameOnly ? "warning" : "pragma warning";
case PPCallbacks::PMK_Error:
return PragmaNameOnly ? "error" : "pragma error";
}
llvm_unreachable("Unknown PragmaMessageKind!");
}

public:
PragmaMessageHandler(PPCallbacks::PragmaMessageKind Kind,
StringRef Namespace = StringRef())
: PragmaHandler(PragmaKind(Kind, true)), Kind(Kind),
Namespace(Namespace) {}

void HandlePragma(Preprocessor &PP, PragmaIntroducer Introducer,
Token &Tok) override {
SourceLocation MessageLoc = Tok.getLocation();
PP.Lex(Tok);
bool ExpectClosingParen = false;
switch (Tok.getKind()) {
case tok::l_paren:
// We have a MSVC style pragma message.
ExpectClosingParen = true;
// Read the string.
PP.Lex(Tok);
break;
case tok::string_literal:
// We have a GCC style pragma message, and we just read the string.
break;
default:
PP.Diag(MessageLoc, diag::err_pragma_message_malformed) << Kind;
return;
}

std::string MessageString;
if (!PP.FinishLexStringLiteral(Tok, MessageString, PragmaKind(Kind),
/*AllowMacroExpansion=*/true))
return;

if (ExpectClosingParen) {
if (Tok.isNot(tok::r_paren)) {
PP.Diag(Tok.getLocation(), diag::err_pragma_message_malformed) << Kind;
return;
}
PP.Lex(Tok); // eat the r_paren.
}

if (Tok.isNot(tok::eod)) {
PP.Diag(Tok.getLocation(), diag::err_pragma_message_malformed) << Kind;
return;
}

// Output the message.
PP.Diag(MessageLoc, (Kind == PPCallbacks::PMK_Error)
? diag::err_pragma_message
: diag::warn_pragma_message) << MessageString;

// If the pragma is lexically sound, notify any interested PPCallbacks.
if (PPCallbacks *Callbacks = PP.getPPCallbacks())
Callbacks->PragmaMessage(MessageLoc, Namespace, Kind, MessageString);
}
};

这个是用来处理这样的 pragma 指令的

1
#pragma message("Hello, World!")

其会在编译时输出 Hello, World!,这里我们可以看到 clang 是如何处理这样的 pragma 指令的。

所有的 pragma 指令都是通过 PragmaHandler 来处理的,具体的处理函数是 HandlePragma,在这个函数中,我们可以看到 clang 是如何处理这样的 pragma 指令的。

我们可以仿照这个例子,来实现我们的 #pragma loopbound

但是我们并不会在这里实际添加我们的 PragmaHandler,具体原因在后面会讲到。

这里还需要注意的一个点是,PragmaHandler 的构造函数接受一个字符串参数,这个参数便是 #pragma 后面的内容,例如 #pragma message("Hello, World!") 中的 message

词法分析

实际上这里并不需要真正的修改词法分析器,只是需要在词法分析器中新增一个 Annotation Token,用于识别 #pragma loopbound

修改的文件位于 clang/include/clang/Basic/TokenKinds.def,在其中添加一个新的 Token

1
ANNOTATION(loopbound)

顺便一提,这里你能看到 clang 中的 Token 是如何定义的,对于那些需要自定义为关键字/标识符的 Token,你可以在这里进行定义,然后在后续的解析中使用。

语法分析

这里便是我们真正需要修改的地方了,我们需要在 clang 的语法分析器中添加一个新的 PragmaHandler,用于处理我们的 #pragma loopbound

为什么在这里添加我们的 PragmaHandler,因为我们的 Pragma 并不只是一个简单在预处理阶段就能够处理的 Pragma,同时我们也能看到 clang 在这里添加了很多的 PragmaHandler,例如多线程中很常见的 #pragma omp,也都是在这里添加的,这实际上便是编译原理中很常见的语义制导翻译的一个例子。

首先在 clang/include/clang/Parse/Parser.h 中添加我们的 PragmaHandler

1
2
3
4
5
6
7
8
9
class Parser : public CodeCompletionHandler {
// 省略大部分
std::unique_ptr<PragmaHandler> AttributePragmaHandler;
std::unique_ptr<PragmaHandler> MaxTokensHerePragmaHandler;
std::unique_ptr<PragmaHandler> MaxTokensTotalPragmaHandler;
// 添加我们的 PragmaHandler
std::unique_ptr<PragmaHandler> LoopBoundHandler;
// 省略大部分
}

这些 unique_ptr 会在 Parser 中的一些成员函数中被构造,我们需要在这些成员函数中添加我们的 PragmaHandler 的处理,具体在 clang/lib/Parse/ParsePragma.cpp 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
void Parser::initializePragmaHandlers() {
// ...
LoopBoundHandler = std::make_unique<PragmaLoopBoundHandler>();
PP.AddPragmaHandler(LoopBoundHandler.get());
// 省略大部分
}

void Parser::resetPragmaHandlers() {
// ...
PP.RemovePragmaHandler(LoopBoundHandler.get());
LoopBoundHandler.reset();
// 省略大部分
}

然后我们需要实现我们的 PragmaHandler,这里我们需要继承 PragmaHandler,并实现 HandlePragma 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// lib/Parse/ParsePragma.cpp
struct PragmaLoopBoundHandler : public PragmaHandler {
explicit PragmaLoopBoundHandler() : PragmaHandler("loopbound") {}
void HandlePragma(Preprocessor &PP, PragmaIntroducer Introducer,
Token &FirstToken) override;
};

void PragmaLoopBoundHandler::HandlePragma(Preprocessor &PP,
PragmaIntroducer Introducer,
Token &FirstToken) {
uint64_t lower = 0, upper = 0;

Token Tok;
bool is_max = false;

PP.Lex(Tok);

while (Tok.isNot(tok::eod)) {
if (Tok.isNot(tok::identifier)) {
PP.Diag(Tok.getLocation(), diag::warn_pragma_expected_identifier)
<< "loopbound min/max";
}
StringRef kind = Tok.getIdentifierInfo()->getName();
if (kind == "min") {
is_max = false;
} else if (kind == "max") {
is_max = true;
} else {
PP.Diag(Tok.getLocation(), diag::warn_pragma_expected_identifier)
<< "loopbound min/max";
}
PP.Lex(Tok);
// llvm::outs() << PP.getSpelling(Tok) << "\n";
if (Tok.isNot(tok::numeric_constant)) {
PP.Diag(Tok.getLocation(), diag::warn_pragma_expected_identifier)
<< "loopbound min/max";
return;
}
uint64_t value;
if (!PP.parseSimpleIntegerLiteral(Tok, value)) {
PP.Diag(Tok.getLocation(), diag::warn_pragma_expected_integer)
<< "loopbound min/max";
return;
}
if (is_max) {
upper = value;
} else {
lower = value;
}
}

if (lower > upper) {
std::swap(lower, upper);
}

// reset the token, which will be used
Tok.startToken();
Tok.setKind(tok::annot_loopbound);

// [0] is min, [1] is max
uint64_t *bound = new uint64_t[2];
bound[0] = lower;
bound[1] = upper;

Tok.setAnnotationValue((void *)bound);
Tok.setLocation(FirstToken.getLocation());
Tok.setAnnotationEndLoc(FirstToken.getLocation());
PP.EnterToken(Tok, false);
}

这里,值得注意的是,clang 会给 pragma 终止的位置添加一个 tok::eod,这样我们就可以在 HandlePragma 中通过 tok::eod 来判断 pragma 是否结束,这样我们就不需要手动去判断 pragma 是否结束了。以及最后我们将这个 Tok 的属性设置为 annot_loopbound,并让 Preprocessor 进入这个 Token,这样在后续语义制导翻译中,我们就可以通过这个 Token 来获取我们的 minmax 了。

语义制导翻译