编译期间 C 中何时需要 space(或括号)?
When space (or parentheses) are required in C during compilation?
我正在学习编译的工作原理,我的最终目标是编写一个迷你 C 编译器。我还处于这个项目的开始阶段。当我在扫描器和解析器部分工作以构建 AST 时,我意识到 space 是(或圆括号)在 i+ +4
、i+(+4)
、[=13= 这样的表达式中是必需的],或i-(-4)
。否则,在 i--4
表达式(例如)中,--
被解释为一元运算符 --
并引发错误。我完全理解原因。这不是问题。
问题是下面的,之前,我天真地认为 spaces 在 C 中并不是那么重要,如果只是为了代码的可读性。但是现在,我想知道是否还有其他类似上述这些的例子?
大多数语言中的词法分析器都基于贪婪的正则表达式——一个标记是尽可能长的。
如果 ++ 可以解释为 ++
运算符(从左到右),则它不会被解释为两个加号。如果 inta
可以解释为标识符,则不会将其解释为 int
后跟 a
,等等
i+ +4
需要空格、括号或介于 +
和 +
之间的东西,否则词法分析器将贪婪地从左到右使用它,如 ++
.
我不得不修复一些旧代码并进行更改
#define ALT_7 (0xfe+OFFSET)
到
#define ALT_7 (0xfe +OFFSET)
原因是 0xfe+OFFSET
是一个 预处理数字 标记,而不是人们可能天真地认为的三个标记。旧编译器将其解析为三个,但新编译器将其解析为一个无效数字常量,因此失败。
预处理器方面的内容可能更多,但更晦涩(作为 C/C++ 预处理的整个主题)。
C 中何时需要 space 的规则并未明确指定,而是 C 解析方式的结果。这方面的规则相当复杂,因为它们涉及多个分析阶段和各种情况的一些例外情况。如果您正在编写 C 编译器,则需要使用 C 标准作为参考。
C 2018 5.1.1.2 指定翻译阶段(重新措辞和总结,不是精确引用):
物理源文件多字节字符映射到源字符集。三字母序列被单字符表示取代。
合并以反斜杠继续的行。
源文件由字符转换为预处理标记和white-space字符——每个可以作为预处理标记的字符序列都转换为预处理标记,并且每个注释变成一个 space.
执行预处理(执行指令和扩展宏)。
字符常量和字符串文字中的源字符被转换为执行字符集的成员。
连接相邻的字符串文字。
白色-space字符被丢弃。 “每个预处理令牌都被转换成一个令牌。生成的标记在句法和语义上进行分析,并作为翻译单元进行翻译。” (引用的文字是我们认为的 C 编译的主要部分!)
程序链接成为可执行文件。
首先,C 源代码中需要 spaces 的地方由阶段 3 控制,即预处理标记的形成。这在 C 2018 6.4 中指定。第 1 段给出了预处理标记的语法(更多内容见下文),第 4 段告诉我们:
If the input stream has been parsed into preprocessing tokens up to a given character, the next preprocessing token is the longest sequence of characters that could constitute a preprocessing token. There is one exception to this rule: header name preprocessing tokens are recognized only within #include
preprocessing directives and in implementation-defined locations within #pragma
directives. In such contexts, a sequence of characters that could be either a header name or a string literal is recognized as the former.
第 1 段告诉我们预处理标记是 header-name、identifier、pp-number 之一、字符常量、字符串文字、标点符号,或非- white-space 不属于前面各项的字符。
然后 6.4 中的进一步子条款告诉我们这些标记是什么样的。
第 3 阶段针对需要 space 的地方引入了两个规则,它们本质上是:
- 如果根据上述规则将源代码解析为一个预处理标记,您需要两个,那么您必须在您希望第一个标记结束的地方插入一个space。
- 如果使用
/
和 *
而不是 /*
来引入评论,请在它们之间添加一个 space。
阶段 4 引入了另一个规则。因为 6.10.3 3 说“在类对象宏的定义中标识符和替换列表之间应该有白色 space”,所以你需要一个 space 来区分类函数宏:
#define foo(x) (3*(x)) // Macro that acts on argument x.
#define foo (x) // Macro that expands to `(x)`.
很多情况下确实需要空格:
宏定义:
#define MACRO (a) // defines a simple macro, it expands to (a)
#define MACRO(a) // defines a function-like macro with a single parameter a
注释语法陷阱:
a/*b // starts a comment */
a/ *b // a divided by the value pointed to by b
预处理数字文字:
0x2e+1 <> 0x2e +1
三字母的类似问题:
"??/??/????" <> "??" "/??" "/????" // "??/??/????" is parsed as "\????"
令牌分离:
a+ +b <> a++b // a++b would be a syntax error
a- -b <> a--b // a--b would be a syntax error
a& &b <> a&&b // but &b is unlikely to be a valid operand for &
C++嵌套模板问题:
template a<b<c> >
C 编译器有几个部分,问题是:您要实现哪个部分?
C 预处理器实际上为白色生成一个标记space 并使用它来确定事物。如果您要实现组合 preprocessor/compiler,您可能只想进行一次标记化,然后在将标记流交给编译器之前丢弃白色 space 标记。
C 本身似乎主要关注 spaces、制表符和换行符作为标记结束的指示符。
除此之外,它还有一个或两个字符运算符的概念,并且似乎在贪婪地匹配它们。也就是说,- -
会变成 MINUS_TOKEN, MINUS_TOKEN
,而 --
无论在哪里,总是会变成 DECREMENT
。
也就是说,您的 i--4
示例给出了解析器错误,因为在后缀递减运算符之后有一个无关的 4
。
所以证明运算符是贪婪匹配的。写 i - -4
OTOH 是可行的,因为贪婪匹配将 space 视为第一个 -
标记的结尾,并开始一个新的标记,然后产生第二个负号。
总而言之,C 本身忽略了标记化阶段之外的 whitespace,而预处理器则不会。
我正在学习编译的工作原理,我的最终目标是编写一个迷你 C 编译器。我还处于这个项目的开始阶段。当我在扫描器和解析器部分工作以构建 AST 时,我意识到 space 是(或圆括号)在 i+ +4
、i+(+4)
、[=13= 这样的表达式中是必需的],或i-(-4)
。否则,在 i--4
表达式(例如)中,--
被解释为一元运算符 --
并引发错误。我完全理解原因。这不是问题。
问题是下面的,之前,我天真地认为 spaces 在 C 中并不是那么重要,如果只是为了代码的可读性。但是现在,我想知道是否还有其他类似上述这些的例子?
大多数语言中的词法分析器都基于贪婪的正则表达式——一个标记是尽可能长的。
如果 ++ 可以解释为 ++
运算符(从左到右),则它不会被解释为两个加号。如果 inta
可以解释为标识符,则不会将其解释为 int
后跟 a
,等等
i+ +4
需要空格、括号或介于 +
和 +
之间的东西,否则词法分析器将贪婪地从左到右使用它,如 ++
.
我不得不修复一些旧代码并进行更改
#define ALT_7 (0xfe+OFFSET)
到
#define ALT_7 (0xfe +OFFSET)
原因是 0xfe+OFFSET
是一个 预处理数字 标记,而不是人们可能天真地认为的三个标记。旧编译器将其解析为三个,但新编译器将其解析为一个无效数字常量,因此失败。
预处理器方面的内容可能更多,但更晦涩(作为 C/C++ 预处理的整个主题)。
C 中何时需要 space 的规则并未明确指定,而是 C 解析方式的结果。这方面的规则相当复杂,因为它们涉及多个分析阶段和各种情况的一些例外情况。如果您正在编写 C 编译器,则需要使用 C 标准作为参考。
C 2018 5.1.1.2 指定翻译阶段(重新措辞和总结,不是精确引用):
物理源文件多字节字符映射到源字符集。三字母序列被单字符表示取代。
合并以反斜杠继续的行。
源文件由字符转换为预处理标记和white-space字符——每个可以作为预处理标记的字符序列都转换为预处理标记,并且每个注释变成一个 space.
执行预处理(执行指令和扩展宏)。
字符常量和字符串文字中的源字符被转换为执行字符集的成员。
连接相邻的字符串文字。
白色-space字符被丢弃。 “每个预处理令牌都被转换成一个令牌。生成的标记在句法和语义上进行分析,并作为翻译单元进行翻译。” (引用的文字是我们认为的 C 编译的主要部分!)
程序链接成为可执行文件。
首先,C 源代码中需要 spaces 的地方由阶段 3 控制,即预处理标记的形成。这在 C 2018 6.4 中指定。第 1 段给出了预处理标记的语法(更多内容见下文),第 4 段告诉我们:
If the input stream has been parsed into preprocessing tokens up to a given character, the next preprocessing token is the longest sequence of characters that could constitute a preprocessing token. There is one exception to this rule: header name preprocessing tokens are recognized only within
#include
preprocessing directives and in implementation-defined locations within#pragma
directives. In such contexts, a sequence of characters that could be either a header name or a string literal is recognized as the former.
第 1 段告诉我们预处理标记是 header-name、identifier、pp-number 之一、字符常量、字符串文字、标点符号,或非- white-space 不属于前面各项的字符。
然后 6.4 中的进一步子条款告诉我们这些标记是什么样的。
第 3 阶段针对需要 space 的地方引入了两个规则,它们本质上是:
- 如果根据上述规则将源代码解析为一个预处理标记,您需要两个,那么您必须在您希望第一个标记结束的地方插入一个space。
- 如果使用
/
和*
而不是/*
来引入评论,请在它们之间添加一个 space。
阶段 4 引入了另一个规则。因为 6.10.3 3 说“在类对象宏的定义中标识符和替换列表之间应该有白色 space”,所以你需要一个 space 来区分类函数宏:
#define foo(x) (3*(x)) // Macro that acts on argument x.
#define foo (x) // Macro that expands to `(x)`.
很多情况下确实需要空格:
宏定义:
#define MACRO (a) // defines a simple macro, it expands to (a) #define MACRO(a) // defines a function-like macro with a single parameter a
注释语法陷阱:
a/*b // starts a comment */ a/ *b // a divided by the value pointed to by b
预处理数字文字:
0x2e+1 <> 0x2e +1
三字母的类似问题:
"??/??/????" <> "??" "/??" "/????" // "??/??/????" is parsed as "\????"
令牌分离:
a+ +b <> a++b // a++b would be a syntax error a- -b <> a--b // a--b would be a syntax error a& &b <> a&&b // but &b is unlikely to be a valid operand for &
C++嵌套模板问题:
template a<b<c> >
C 编译器有几个部分,问题是:您要实现哪个部分?
C 预处理器实际上为白色生成一个标记space 并使用它来确定事物。如果您要实现组合 preprocessor/compiler,您可能只想进行一次标记化,然后在将标记流交给编译器之前丢弃白色 space 标记。
C 本身似乎主要关注 spaces、制表符和换行符作为标记结束的指示符。
除此之外,它还有一个或两个字符运算符的概念,并且似乎在贪婪地匹配它们。也就是说,- -
会变成 MINUS_TOKEN, MINUS_TOKEN
,而 --
无论在哪里,总是会变成 DECREMENT
。
也就是说,您的 i--4
示例给出了解析器错误,因为在后缀递减运算符之后有一个无关的 4
。
所以证明运算符是贪婪匹配的。写 i - -4
OTOH 是可行的,因为贪婪匹配将 space 视为第一个 -
标记的结尾,并开始一个新的标记,然后产生第二个负号。
总而言之,C 本身忽略了标记化阶段之外的 whitespace,而预处理器则不会。