Commit fdb812b7 authored by 张栋澈's avatar 张栋澈 🤡

use more formal phrasing

parent ac469991
...@@ -143,8 +143,8 @@ int words = 0; ...@@ -143,8 +143,8 @@ int words = 0;
%% %%
/* 注意这里的%%开头*/ /* 注意这里的%%开头*/
/* %%开头和%%结尾之间的内容就是使用flex进行解析的部分 */ /* %%开头和%%结尾之间的内容就是使用flex进行解析的部分 */
/* 你可以按照这种方式在这个部分写注释,注意注释最开头的空格,这是必须的 */ /* 你可以按照这种方式在这个部分写注释,注意注释最开头的空格,这是必须的 */
/* 你可以在这里使用你熟悉的正则表达式来编写模式, 你可以用C代码来指定模式匹配时对应的动作 */ /* 你可以在这里使用你熟悉的正则表达式来编写模式,你可以用C代码来指定模式匹配时对应的动作 */
/* 在%%和%%之间,你应该按照如下的方式写模式和动作 */ /* 在%%和%%之间,你应该按照如下的方式写模式和动作 */
/* 模式 动作 */ /* 模式 动作 */
/* 其中模式就是正则表达式,动作为模式匹配执行成功后执行相应的动作,这里的动作就是相应的代码 */ /* 其中模式就是正则表达式,动作为模式匹配执行成功后执行相应的动作,这里的动作就是相应的代码 */
...@@ -189,7 +189,7 @@ look, I find 2 words of 10 chars ...@@ -189,7 +189,7 @@ look, I find 2 words of 10 chars
至此,你已经成功使用Flex完成了一个简单的分析器! 至此,你已经成功使用Flex完成了一个简单的分析器!
为了对实验有较好的体验,我建议你好好阅读以下两个关于flex文档: 为了对实验有较好的体验,我建议你好好阅读以下两个关于flex文档:
* [Flex matching](./Flex-matching.md) * [Flex matching](./Flex-matching.md)
* [Flex regular expressions](./Flex-regular-expressions.md) * [Flex regular expressions](./Flex-regular-expressions.md)
...@@ -238,7 +238,7 @@ int yylex(void) ...@@ -238,7 +238,7 @@ int yylex(void)
case EOF: return YYEOF; case EOF: return YYEOF;
case 'R': return REIMU; case 'R': return REIMU;
default: return YYUNDEF; // 报告 token 未定义,迫使 bison 报错。 default: return YYUNDEF; // 报告 token 未定义,迫使 bison 报错。
// 由于 bison 不同版本有不同的定义。如果这里 YYUNDEF 未定义,请尝试 YYUNDEFTOK 或使用一个随意的整数,如 114514 或 19260817 // 由于 bison 不同版本有不同的定义。如果这里 YYUNDEF 未定义,请尝试 YYUNDEFTOK 或使用一个随意的整数。
} }
} }
...@@ -256,13 +256,13 @@ int main(void) ...@@ -256,13 +256,13 @@ int main(void)
另外有一些值得注意的点: 另外有一些值得注意的点:
1. Bison 传统上将 token 用大写单词表示,将 symbol 用小写字母表示。 1. Bison 传统上将 token 用大写单词表示,将 symbol 用小写字母表示。
2. Bison 能且只能生成解析器源代码(一个 `.c` 文件),并且入口是 `yyparse`,所以为了让程序能跑起来,你需要手动提供 `main` 函数(但不一定要在 `.y` 文件中——你懂“链接”是什么,对吧?) 2. Bison 能且只能生成解析器源代码(一个 `.c` 文件),并且入口是 `yyparse`,所以为了让程序能跑起来,你需要手动提供 `main` 函数。
3. Bison 不能检测你的 action code 是否正确——它只能检测文法的部分错误,其他代码都是原样粘贴到 `.c` 文件中。 3. Bison 不能检测你的 action code 是否正确——它只能检测文法的部分错误,其他代码都是原样粘贴到 `.c` 文件中。
4. Bison 需要你提供一个 `yylex` 来获取下一个 token。 4. Bison 需要你提供一个 `yylex` 来获取下一个 token。
5. Bison 需要你提供一个 `yyerror` 来提供合适的报错机制。 5. Bison 需要你提供一个 `yyerror` 来提供合适的报错机制。
顺便提一嘴,上面这个 `.y` 是可以工作的——尽管它只能接受两个字符串。把上面这段代码保存为 `reimu.y`,执行如下命令来构建这个程序: 另外,上面这个 `.y` 是可以工作的——尽管它只能接受两个字符串。把上面这段代码保存为 `reimu.y`,执行如下命令来构建这个程序:
```shell ```shell
$ bison reimu.y $ bison reimu.y
...@@ -283,9 +283,9 @@ syntax error <-- 发现了错误 ...@@ -283,9 +283,9 @@ syntax error <-- 发现了错误
于是我们验证了上述代码的确识别了该文法定义的语言 `{ "", "R" }` 于是我们验证了上述代码的确识别了该文法定义的语言 `{ "", "R" }`
### Bison 和 Flex 联动 ### Bison 与 Flex 配合
聪明的你应该发现了,我们这里手写了一个 `yylex` 函数作为词法分析器。而在上文中我们正好使用 flex 自动生成了一个词法分析器。如何让这两者协同工作呢?特别是,我们需要在这两者之间共享 token 定义和一些数据,难道要手动维护吗?哈哈,当然不用!下面我们用一个四则运算计算器来简单介绍如何让 bison 和 flex 协同工作——重点是如何维护解析器状态、`YYSTYPE` 和头文件的生成。 我们这里手写了一个 `yylex` 函数作为词法分析器。而在上文中我们正好使用 flex 自动生成了一个词法分析器。如何让这两者协同工作呢?特别是,我们需要在这两者之间共享 token 定义和一些数据,而不用我们手动维护。下面我们用一个四则运算计算器来简单介绍如何让 bison 和 flex 协同工作——重点是如何维护解析器状态、`YYSTYPE` 和头文件的生成。
首先,我们必须明白,整个工作流程中,bison 是占据主导地位的,而 flex 仅仅是一个辅助工具,仅用来生成 `yylex` 函数。因此,最好先写 `.y` 文件。 首先,我们必须明白,整个工作流程中,bison 是占据主导地位的,而 flex 仅仅是一个辅助工具,仅用来生成 `yylex` 函数。因此,最好先写 `.y` 文件。
...@@ -343,7 +343,7 @@ term ...@@ -343,7 +343,7 @@ term
{ {
switch ($2) { switch ($2) {
case '*': $$ = $1 * $3; break; case '*': $$ = $1 * $3; break;
case '/': $$ = $1 / $3; break; // 想想看,这里会出什么问题? case '/': $$ = $1 / $3; break; // 这里会出什么问题?
} }
} }
...@@ -416,7 +416,7 @@ $ ./calc ...@@ -416,7 +416,7 @@ $ ./calc
= 3.000000 = 3.000000
``` ```
如果你复制粘贴了上述程序,可能会觉得很神奇,并且有些地方看不懂。下面就详细讲解上面新出现的各种构造。 下面详细讲解上面新出现的各种构造。
* `YYSTYPE`: 在 bison 解析过程中,每个 symbol 最终都对应到一个语义值上。或者说,在 parse tree 上,每个节点都对应一个语义值,这个值的类型是 `YYSTYPE``YYSTYPE` 的具体内容是由 `%union` 构造指出的。上面的例子中, * `YYSTYPE`: 在 bison 解析过程中,每个 symbol 最终都对应到一个语义值上。或者说,在 parse tree 上,每个节点都对应一个语义值,这个值的类型是 `YYSTYPE``YYSTYPE` 的具体内容是由 `%union` 构造指出的。上面的例子中,
...@@ -436,7 +436,7 @@ $ ./calc ...@@ -436,7 +436,7 @@ $ ./calc
} YYSTYPE; } YYSTYPE;
``` ```
为什么使用 `union` 呢?因为不同节点可能需要不同类型的语义值。比如,上面的例子中,我们希望 `ADDOP` 的值是 `char` 类型,而 `NUMBER` 应该是 `double` 类型的。 使用 `union`因为不同节点可能需要不同类型的语义值。比如,上面的例子中,我们希望 `ADDOP` 的值是 `char` 类型,而 `NUMBER` 应该是 `double` 类型的。
* `$$``$1`, `$2`, `$3`, ...:现在我们来看如何从已有的值推出当前节点归约后应有的值。以加法为例: * `$$``$1`, `$2`, `$3`, ...:现在我们来看如何从已有的值推出当前节点归约后应有的值。以加法为例:
...@@ -452,7 +452,7 @@ $ ./calc ...@@ -452,7 +452,7 @@ $ ./calc
其实很好理解。当前节点使用 `$$` 代表,而已解析的节点则是从左到右依次编号,称作 `$1`, `$2`, `$3`... 其实很好理解。当前节点使用 `$$` 代表,而已解析的节点则是从左到右依次编号,称作 `$1`, `$2`, `$3`...
* `%type <>``%token <>`:注意,我们上面可没有写 `$1.num` 或者 `$2.op` 哦!那么 bison 是怎么知道应该用 `union` 的哪部分值的呢?其秘诀就在文件一开始的 `%type``%token` 上。 * `%type <>``%token <>`:注意,我们上面没有写 `$1.num` 或者 `$2.op` ,那么 bison 是怎么知道应该用 `union` 的哪部分值的呢?其秘诀就在文件一开始的 `%type``%token` 上:
例如,`term` 应该使用 `num` 部分,那么我们就写 例如,`term` 应该使用 `num` 部分,那么我们就写
...@@ -464,10 +464,10 @@ $ ./calc ...@@ -464,10 +464,10 @@ $ ./calc
`%token<>` 见下一条。 `%token<>` 见下一条。
* `%token`:当我们用 `%token` 声明一个 token 时,这个 token 就会导出到 `.h` 中,可以在 C 代码中直接使用(注意 token 名千万不要和别的东西冲突!),供 flex 使用。`%token <op> ADDOP` 与之类似,但顺便也将 `ADDOP` 传递给 `%type`,这样一行代码相当于两行代码,岂不是很赚 * `%token`:当我们用 `%token` 声明一个 token 时,这个 token 就会导出到 `.h` 中,可以在 C 代码中直接使用(注意 token 名千万不要和别的东西冲突!),供 flex 使用。`%token <op> ADDOP` 与之类似,但顺便也将 `ADDOP` 传递给 `%type`
* `yylval`:这时候我们可以打开 `.h` 文件,看看里面有什么。除了 token 定义,最末尾还有一个 `extern YYSTYPE yylval;` 。这个变量我们上面已经使用了,通过这个变量,我们就可以在 lexer **里面**设置某个 token 的值。 * `yylval`:这时候我们可以打开 `.h` 文件,看看里面有什么。除了 token 定义,最末尾还有一个 `extern YYSTYPE yylval;` 。这个变量我们上面已经使用了,通过这个变量,我们就可以在 lexer **里面**设置某个 token 的值。
呼……说了这么多,现在回头看看上面的代码,应该可以完全看懂了吧!这时候你可能才意识到为什么 flex 生成的分析器入口是 `yylex`,因为这个函数就是 bison 专门让程序员自己填的,作为一种扩展机制。另外,bison(或者说 yacc)生成的变量和函数名通常都带有 `yy` 前缀,希望在这里说还不太晚…… 以上就是 Flex 与 Bison 协同工作的讲解。你可能注意到 flex 生成的分析器入口是 `yylex`,因为这个函数就是 bison 专门让程序员自己补充的,作为一种扩展机制。另外,bison生成的变量和函数名通常都带有 `yy` 前缀,为了兼容 yacc 代码。
最后还得提一下,尽管上面所讲已经足够应付很大一部分解析需求了,但是 bison 还有一些高级功能,比如自动处理运算符的优先级和结合性(于是我们就不需要手动把 `expr` 拆成 `factor`, `term` 了)。这部分功能,就留给同学们自己去探索吧! 作为拓展,尽管上述内容已经足够应付很大一部分解析需求了,但是 bison 还有一些高级功能,比如自动处理运算符的优先级和结合性(无需手动把 `expr` 拆成 `factor`, `term` 了)。这部分功能留给同学们自己去探索。
...@@ -77,7 +77,7 @@ factor("2x") = (Some(Expr.Mul(Expr.Const(2), Expr.Val("x"))), "") ...@@ -77,7 +77,7 @@ factor("2x") = (Some(Expr.Mul(Expr.Const(2), Expr.Val("x"))), "")
不难看出函数组合成大函数的过程,就是我们把小解析器组合成大解析器的过程,并且可以很自然地把自己想要的逻辑嵌入进去。更有趣的是,编译器是完全知道每个函数的类型的。 不难看出函数组合成大函数的过程,就是我们把小解析器组合成大解析器的过程,并且可以很自然地把自己想要的逻辑嵌入进去。更有趣的是,编译器是完全知道每个函数的类型的。
由此可见,解析器组合子是一种编程技巧而不是一种解析技术(解析技术是隐含在组合子的实现里的),使用这种技巧可以让代码模块化程度更高,并且在类型较强的语言中可以在编译时就捕获错误。此外,尽管代码是完全手写的,但代码却可以和使用解析器生成器一样干净整洁。感兴趣的同学请务必在自己喜欢的高级语言中尝试一番,或者亲自动手写一套组合子。 由此可见,解析器组合子是一种编程技巧而不是一种解析技术(解析技术是隐含在组合子的实现里的),使用这种技巧可以让代码模块化程度更高,并且在类型系统较强的语言中可以在编译时就捕获错误。此外,尽管代码是完全手写的,但代码却可以和使用解析器生成器一样干净整洁。感兴趣的同学请务必在自己喜欢的高级语言中尝试一番,或者亲自动手写一套组合子。
## 解析器表达式文法 ## 解析器表达式文法
...@@ -106,7 +106,7 @@ match something: ...@@ -106,7 +106,7 @@ match something:
- [JEP 286: Local-Variable Type Inference](https://openjdk.java.net/jeps/286) - [JEP 286: Local-Variable Type Inference](https://openjdk.java.net/jeps/286)
所谓的软关键字,就是与硬关键字相对(废话),其含义与上下文有关(这并不意味着上下文相关文法)。引入软关键字的目的主要是避免原先合法的代码在新版中出错。 软关键字,与硬关键字相对,其含义与上下文有关(这并不意味着上下文相关文法)。引入软关键字的目的主要是避免原先合法的代码在新版中出错。
一个硬关键字在词法分析阶段会被直接分析成关键字。例如, `if` 被分析成一个叫做 `IF` 的 token,这和 `[` 被分析成 `LBRACKET` 没有什么区别。而一个像 `var` 这样的软关键字,则会首先分析成某种 `IDENT(var)``VAR` 的复合状态,这里不妨就叫做 `VAR`,然后在语法分析阶段再进行分析。大致思想如下: 一个硬关键字在词法分析阶段会被直接分析成关键字。例如, `if` 被分析成一个叫做 `IF` 的 token,这和 `[` 被分析成 `LBRACKET` 没有什么区别。而一个像 `var` 这样的软关键字,则会首先分析成某种 `IDENT(var)``VAR` 的复合状态,这里不妨就叫做 `VAR`,然后在语法分析阶段再进行分析。大致思想如下:
...@@ -141,7 +141,7 @@ var var = 1; // 旧版报错 ...@@ -141,7 +141,7 @@ var var = 1; // 旧版报错
聪明的同学可能会提出这样的问题:我们随便写文法,然后让机器自动检查这个文法是否是二义的,并转换成一种非常高效的文法表示,最后自动生成代码,这是可以办到的吗? 聪明的同学可能会提出这样的问题:我们随便写文法,然后让机器自动检查这个文法是否是二义的,并转换成一种非常高效的文法表示,最后自动生成代码,这是可以办到的吗?
很遗憾,答案是否定的。要理解背后的原理,需要进一步学习相关理论才可以(而且证明也有点繁杂)。在这里,我们(用非常不严谨的语言)列出与上下文无关文法相关的一些结论。注:“不可判定” 的意思是 “不可能写出来这样一个程序”,是不是听起来非常中二 :-p 很遗憾,答案是否定的,该问题是一个[不可判定问题](https://en.wikipedia.org/wiki/Undecidable_problem)。要理解背后的原理,需要进一步学习相关理论(可计算性理论)才可以(而且证明也有点繁杂)。在这里,我们(用非常不严谨的语言)列出与上下文无关文法相关的一些结论。注:“不可判定” 的意思是 “不可能构建这样一个算法,使得其对一个 Decision Problem 总是回答正确的是或否”
1. 上下文无关文法的二义性是不可判定的。 1. 上下文无关文法的二义性是不可判定的。
2. 检查文法是否接受任何字符串是不可判定的。 2. 检查文法是否接受任何字符串是不可判定的。
...@@ -150,15 +150,13 @@ var var = 1; // 旧版报错 ...@@ -150,15 +150,13 @@ var var = 1; // 旧版报错
另外,尽管有上面 2 这样的结论,但是 “检查文法是否什么字符串都不接受” 却是可判定的,将 CFG 转换成 Chomsky normal form 就可以轻松办到。 另外,尽管有上面 2 这样的结论,但是 “检查文法是否什么字符串都不接受” 却是可判定的,将 CFG 转换成 Chomsky normal form 就可以轻松办到。
(聪明的同学可能会开始思考对正则语言来说上面这些问题的结论是怎样的……) 思考:对正则语言来说,上面这些问题的结论是怎样的?
下面介绍一个著名的问题 [Post correspondence problem](https://en.wikipedia.org/wiki/Post_correspondence_problem),来说明有时候人类的直觉是很不靠谱的。 下面介绍一个著名的问题 [Post correspondence problem](https://en.wikipedia.org/wiki/Post_correspondence_problem),来说明有时候人类的直觉是很不靠谱的。
给定相同长度的两个字符串列表 a[1], a[2], a[3], ..., a[n] 和 b[1], b[2], b[3], ..., b[n],回答:是否存在一列下标 i[1], i[2], ..., i[k],使得 a[i[1]] a[i[2]] ... a[i[k]] = b[i[1]] b[i[2]] ... b[i[k]]? 给定相同长度的两个字符串列表 a[1], a[2], a[3], ..., a[n] 和 b[1], b[2], b[3], ..., b[n],回答:是否存在一列下标 i[1], i[2], ..., i[k],使得 a[i[1]] a[i[2]] ... a[i[k]] = b[i[1]] b[i[2]] ... b[i[k]]?
帮助大家有一个感性认识,下面复读一下 Wikipedia 上的例子: 考虑如下的例子
| a₁ | a₂ | a₃ | | a₁ | a₂ | a₃ |
| ----- | ----- | ----- | | ----- | ----- | ----- |
...@@ -172,7 +170,7 @@ var var = 1; // 旧版报错 ...@@ -172,7 +170,7 @@ var var = 1; // 旧版报错
尽管一时半会可能想不到高效的做法,但是直觉告诉我们,似乎可以去暴力枚举,然后一一比较…… 尽管一时半会可能想不到高效的做法,但是直觉告诉我们,似乎可以去暴力枚举,然后一一比较……
**然而**,这个问题是不可能机械求解的!不可能写出一个程序来判定这个问题。PCP不可判定是怎么回事呢?PCP相信大家都很熟悉,但是PCP不可判定是怎么回事呢,下面就让小编带大家一起了解吧。PCP不可判定,其实就是停机问题不可判定,大家可能会很惊讶PCP怎么会不可判定呢?但事实就是这样,小编也感到非常惊讶 **然而**,这个问题是不可能机械求解的!不可能写出一个程序来判定这个问题。事实上,不可判定问题无处不在,[莱斯定理](https://en.wikipedia.org/wiki/Rice%27s_theorem)告诉我们,任何non-trivial程序的属性都是不可判定的
## 在线解析 ## 在线解析
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment