<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>ANTLR on Lorn</title><link>https://july-h5kf3.github.io/tags/antlr/</link><description>Recent content in ANTLR on Lorn</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><lastBuildDate>Sat, 03 Oct 2026 15:32:40 +0800</lastBuildDate><atom:link href="https://july-h5kf3.github.io/tags/antlr/index.xml" rel="self" type="application/rss+xml"/><item><title>[从零开始的编译原理][理论] Chapter 2：语法分析</title><link>https://july-h5kf3.github.io/p/compiler-ch2-syntax-analysis/</link><pubDate>Sat, 03 Oct 2026 15:32:40 +0800</pubDate><guid>https://july-h5kf3.github.io/p/compiler-ch2-syntax-analysis/</guid><description>&lt;p&gt;这一节我们主要学习的是语法分析。&lt;/p&gt;&#10;&lt;h2 id="语法分析"&gt;语法分析&#10;&lt;/h2&gt;&lt;p&gt;在进行完词法分析后，我们得到了一组Token序列，而语法分析则用来分析这些token之间的结构关系。&lt;/p&gt;&#10;&lt;p&gt;例如我们有一个表达式:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;a = b + 3 * 4;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;经过词法分析后，我们可以得到:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ID(a) = ID(b) + NUM(3) * NUM(4);&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;那么它应该被解析为:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;b + (3 * 4)&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;对应这个AST:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1&#10;&lt;/span&gt;&lt;span class="lnt"&gt;2&#10;&lt;/span&gt;&lt;span class="lnt"&gt;3&#10;&lt;/span&gt;&lt;span class="lnt"&gt;4&#10;&lt;/span&gt;&lt;span class="lnt"&gt;5&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; +&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; / \&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; b *&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; / \&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 3 4&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;语法分析器同样有三种实现方法：&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;语法分析器生成器&lt;/li&gt;&#10;&lt;li&gt;手写语法分析器&lt;/li&gt;&#10;&lt;li&gt;自动化语法分析器&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;我们按照同样的顺序，介绍如何用语法分析器生成器来进行语法分析，以及自动化语法分析器背后的基本原理。&lt;/p&gt;&#10;&lt;h2 id="语法分析器生成器"&gt;语法分析器生成器&#10;&lt;/h2&gt;&lt;p&gt;在本节中，我们继续用antlr来设计一个类C 语言的grammer: &lt;code&gt;Cymbol.g4&lt;/code&gt;( &lt;a class="link" href="https://github.com/remenska/Grammars/blob/master/book-examples/examples/Cymbol.g4" target="_blank" rel="noopener"&#10; &gt;github&lt;/a&gt;实现参考，我们下面的可能有细微的不同),这一次，我们重点关心其中的语法部分。&lt;/p&gt;&#10;&lt;p&gt;在生成语法分析器的同时，我们还会利用这个分析器去抽取函数调用图.&lt;/p&gt;&#10;&lt;h3 id="cymbol-的语法规则实现"&gt;Cymbol 的语法规则实现&#10;&lt;/h3&gt;&lt;p&gt;接下来我们先来尝试用antlr来描述&lt;code&gt;Cymbol.g4&lt;/code&gt;这个语言的语法结构，我们采用从上到下的描述顺序。&lt;/p&gt;&#10;&lt;p&gt;首先，整个程序，我们可以认为是有若干个变量声明和函数声明构成的，因此我们可以写出第一条语法规则:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;prog : (varDecl | functionDecl)* EOF ;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;那么接下来我们就需要去写变量声明和函数声明的语法:&lt;/p&gt;&#10;&lt;p&gt;变量声明写作:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1&#10;&lt;/span&gt;&lt;span class="lnt"&gt;2&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;varDecl : type ID (&amp;#39;=&amp;#39; expr)? &amp;#39;;&amp;#39; ;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;type : &amp;#39;int&amp;#39; | &amp;#39;double&amp;#39; | &amp;#39;void&amp;#39; ;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;其中括号中的内容是变量初始值声明，这是一个可选的，因此我们括号后面用？描述&lt;/p&gt;&#10;&lt;p&gt;函数声明则写作:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1&#10;&lt;/span&gt;&lt;span class="lnt"&gt;2&#10;&lt;/span&gt;&lt;span class="lnt"&gt;3&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;functionDecl : type ID &amp;#39;(&amp;#39; formalParameters ? &amp;#39;)&amp;#39; block ;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;formalParameters : formalParameter (&amp;#39;,&amp;#39; formalParameter)* ;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;formalParameter : type ID ;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;其中，&lt;code&gt;formalParameters&lt;/code&gt;还可以写作递归形式:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1&#10;&lt;/span&gt;&lt;span class="lnt"&gt;2&#10;&lt;/span&gt;&lt;span class="lnt"&gt;3&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;formalParameters : formalParameters &amp;#39;,&amp;#39; formalParameter&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | formalParameter&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;接下来我们看看函数体&lt;code&gt;block&lt;/code&gt;的语法规则:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1&#10;&lt;/span&gt;&lt;span class="lnt"&gt;2&#10;&lt;/span&gt;&lt;span class="lnt"&gt;3&#10;&lt;/span&gt;&lt;span class="lnt"&gt;4&#10;&lt;/span&gt;&lt;span class="lnt"&gt;5&#10;&lt;/span&gt;&lt;span class="lnt"&gt;6&#10;&lt;/span&gt;&lt;span class="lnt"&gt;7&#10;&lt;/span&gt;&lt;span class="lnt"&gt;8&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;block : &amp;#39;{&amp;#39; stat* &amp;#39;}&amp;#39; ;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;stat : block&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | varDecl&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | &amp;#39;if&amp;#39; expr &amp;#39;then&amp;#39; stat (&amp;#39;else&amp;#39; stat)?&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | &amp;#39;return&amp;#39; expr? &amp;#39;;&amp;#39;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | expr &amp;#39;=&amp;#39; expr &amp;#39;;&amp;#39;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | expr &amp;#39;;&amp;#39;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;其中 stat定义的第一行中调用了block，而block又调用了stat，这是一种互递归。&lt;/p&gt;&#10;&lt;p&gt;接下来我们重点看看一个&lt;code&gt;expr&lt;/code&gt;也就是表达式的语法规则应该是怎样的:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1&#10;&lt;/span&gt;&lt;span class="lnt"&gt; 2&#10;&lt;/span&gt;&lt;span class="lnt"&gt; 3&#10;&lt;/span&gt;&lt;span class="lnt"&gt; 4&#10;&lt;/span&gt;&lt;span class="lnt"&gt; 5&#10;&lt;/span&gt;&lt;span class="lnt"&gt; 6&#10;&lt;/span&gt;&lt;span class="lnt"&gt; 7&#10;&lt;/span&gt;&lt;span class="lnt"&gt; 8&#10;&lt;/span&gt;&lt;span class="lnt"&gt; 9&#10;&lt;/span&gt;&lt;span class="lnt"&gt;10&#10;&lt;/span&gt;&lt;span class="lnt"&gt;11&#10;&lt;/span&gt;&lt;span class="lnt"&gt;12&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;expr : ID &amp;#39;(&amp;#39; exprList ? &amp;#39;)&amp;#39; # 表达函数调用&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | expr &amp;#39;[&amp;#39; expr &amp;#39;]&amp;#39; # 表达下标&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | &amp;#39;-&amp;#39; expr&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | &amp;#39;!&amp;#39; expr&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | expr &amp;#39;^&amp;#39; expr&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | expr (&amp;#39;*&amp;#39; | &amp;#39;/&amp;#39;) expr&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | expr (&amp;#39;+&amp;#39; | &amp;#39;-&amp;#39;) expr&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | expr (&amp;#39;==&amp;#39; | &amp;#39;!=&amp;#39;) expr&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | &amp;#39;(&amp;#39; expr &amp;#39;)&amp;#39;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | ID&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | INT;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;exprList : expr (&amp;#39;,&amp;#39; expr)* ;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;h3 id="一些问题"&gt;一些问题&#10;&lt;/h3&gt;&lt;p&gt;在上一小节中，我们初步完成了Cymbol语言的语法规则定义，但是上述语法规则会存在一定的问题:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;if a then if b then c else d&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;我们现在用上面的语法规则去解析这个语句，发现有两种解析方法:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1&#10;&lt;/span&gt;&lt;span class="lnt"&gt;2&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;if a then [if b then c] else d&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;if a then [if b then c else d]&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;这就是语言的二义性，在设计语法分析器时，我们应该消除这种二义性。&lt;/p&gt;&#10;&lt;p&gt;这种二义性叫做Dangling Else二义性&lt;/p&gt;&#10;&lt;p&gt;一种消除的方式是改写上述匹配规则:&lt;/p&gt;&#10;&lt;p&gt;简单来说，我们规定&lt;code&gt;else&lt;/code&gt;总是和最近的，尚未匹配&lt;code&gt;else&lt;/code&gt;的&lt;code&gt;if&lt;/code&gt;匹配。&lt;/p&gt;&#10;&lt;p&gt;为了体现这种规则，我们把语句分为两类: &lt;code&gt;matched&lt;/code&gt;和&lt;code&gt;open&lt;/code&gt;,那么此时文法可以写作:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1&#10;&lt;/span&gt;&lt;span class="lnt"&gt;2&#10;&lt;/span&gt;&lt;span class="lnt"&gt;3&#10;&lt;/span&gt;&lt;span class="lnt"&gt;4&#10;&lt;/span&gt;&lt;span class="lnt"&gt;5&#10;&lt;/span&gt;&lt;span class="lnt"&gt;6&#10;&lt;/span&gt;&lt;span class="lnt"&gt;7&#10;&lt;/span&gt;&lt;span class="lnt"&gt;8&#10;&lt;/span&gt;&lt;span class="lnt"&gt;9&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;stat : matched_stat | open_stat;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;matched_stat : &amp;#39;if&amp;#39; expr &amp;#39;then&amp;#39; matched_stat &amp;#39;else&amp;#39; matched_stat&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | expr&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;open_stat : &amp;#39;if&amp;#39; expr &amp;#39;then&amp;#39; stat&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | &amp;#39;if&amp;#39; expr &amp;#39;then&amp;#39; matched_stat &amp;#39;else&amp;#39; open_stat&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;在这种文法下，我们上面的例子，就只会匹配第二种解析方法了。&lt;/p&gt;&#10;&lt;p&gt;而在antlr这样的语法分析器生成器中，则巧妙的用最前优先匹配原则解决了这个问题。&lt;/p&gt;&#10;&lt;p&gt;除了悬空的Else带来的二义性外，运算符的结合性也有可能带来二义性:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1&#10;&lt;/span&gt;&lt;span class="lnt"&gt;2&#10;&lt;/span&gt;&lt;span class="lnt"&gt;3&#10;&lt;/span&gt;&lt;span class="lnt"&gt;4&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;expr : expr &amp;#39;*&amp;#39; expr&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | expr &amp;#39;-&amp;#39; expr&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | DIGIT&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;例如&lt;code&gt;1-2-3&lt;/code&gt;,此时可能是&lt;code&gt;(1-2)-3&lt;/code&gt;也可以被识别为&lt;code&gt;1-(2-3)&lt;/code&gt;这实际上是一个左结合，右结合的问题，由于大部分的运算符都是左递归的，因此像antlr这样的语法分析器生成器都是默认左结合的，那自然就会带来一个问题，那右递归的运算符怎么办？&lt;br&gt;&#10;一般来说，右递归的运算符大多都是前缀运算符或后缀运算符，这种运算符只有一种匹配规则，因此不太需要考虑二义性，而真正需要考虑二义性的就是像&lt;code&gt;^&lt;/code&gt;这样的右递归运算符。&lt;/p&gt;&#10;&lt;p&gt;对于这一类需要右结合的运算符，在antlr中，我们可以手动指定结合的方式:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1&#10;&lt;/span&gt;&lt;span class="lnt"&gt;2&#10;&lt;/span&gt;&lt;span class="lnt"&gt;3&#10;&lt;/span&gt;&lt;span class="lnt"&gt;4&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;expr : &amp;#39;!&amp;#39; expr&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | &amp;lt;assoc = right&amp;gt; expr &amp;#39;^&amp;#39; expr&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | DIGIT&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;其他语法分析器生成器也存在类似的手动指定左/右结合的功能。&lt;/p&gt;&#10;&lt;h3 id="利用语法分析器得到函数调用图"&gt;利用语法分析器得到函数调用图&#10;&lt;/h3&gt;&lt;p&gt;通过语法分析器，我们最终可以得到一个AST，而antlr提供了一个接口:&lt;code&gt;ParseTreeWalker&lt;/code&gt;,它可以以DFS的方式自动遍历整个AST，并且我们可以利用&lt;code&gt;Listener&lt;/code&gt;来负责监听进入，退出节点的事件。&lt;/p&gt;&#10;&lt;p&gt;假设我们要得到函数调用图，那么一个必要的事情就是得到每个函数的名称。那利用&lt;code&gt;Listener&lt;/code&gt;就很好实现这一点了，我们只需要每次监听到事件:&lt;code&gt;functionDecl Enter&lt;/code&gt;就记录下节点的函数名称就能获取所有的函数名称了。&lt;/p&gt;&#10;&lt;p&gt;得到了所有函数的名称之后，我们还需要得到函数之间的调用关系，根据我们的语法规则，我们可以知道，函数调用语句会被语法规则:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1&#10;&lt;/span&gt;&lt;span class="lnt"&gt; 2&#10;&lt;/span&gt;&lt;span class="lnt"&gt; 3&#10;&lt;/span&gt;&lt;span class="lnt"&gt; 4&#10;&lt;/span&gt;&lt;span class="lnt"&gt; 5&#10;&lt;/span&gt;&lt;span class="lnt"&gt; 6&#10;&lt;/span&gt;&lt;span class="lnt"&gt; 7&#10;&lt;/span&gt;&lt;span class="lnt"&gt; 8&#10;&lt;/span&gt;&lt;span class="lnt"&gt; 9&#10;&lt;/span&gt;&lt;span class="lnt"&gt;10&#10;&lt;/span&gt;&lt;span class="lnt"&gt;11&#10;&lt;/span&gt;&lt;span class="lnt"&gt;12&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;expr : ID &amp;#39;(&amp;#39; exprList ? &amp;#39;)&amp;#39;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | expr &amp;#39;[&amp;#39; expr &amp;#39;]&amp;#39;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | &amp;#39;-&amp;#39; expr&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | &amp;#39;!&amp;#39; expr&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | expr &amp;#39;^&amp;#39; expr&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | expr (&amp;#39;*&amp;#39; | &amp;#39;/&amp;#39;) expr&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | expr (&amp;#39;+&amp;#39; | &amp;#39;-&amp;#39;) expr&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | expr (&amp;#39;==&amp;#39; | &amp;#39;!=&amp;#39;) expr&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | &amp;#39;(&amp;#39; expr &amp;#39;)&amp;#39;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | ID&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | INT;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;exprList : expr (&amp;#39;,&amp;#39; expr)* ;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;识别，因此我们只需要识别到函数调用事件之后，由调用函数向被调用函数连一条边即可。但是比较麻烦的点在于，除了函数调用，expr还有多条解释规则，而&lt;code&gt;Listener&lt;/code&gt;并不会对上述规则作区分，我们能看到的只有&lt;code&gt;expr Enter&lt;/code&gt;以及&lt;code&gt;expr Exit&lt;/code&gt;,要在此基础上识别出函数调用，我们还需要补充大量的if语句做判断，这显然不是我们希望的。&lt;/p&gt;&#10;&lt;p&gt;事实上，在antlr中，是支持对每条规则用&lt;code&gt;#&lt;/code&gt;来加标签的:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1&#10;&lt;/span&gt;&lt;span class="lnt"&gt; 2&#10;&lt;/span&gt;&lt;span class="lnt"&gt; 3&#10;&lt;/span&gt;&lt;span class="lnt"&gt; 4&#10;&lt;/span&gt;&lt;span class="lnt"&gt; 5&#10;&lt;/span&gt;&lt;span class="lnt"&gt; 6&#10;&lt;/span&gt;&lt;span class="lnt"&gt; 7&#10;&lt;/span&gt;&lt;span class="lnt"&gt; 8&#10;&lt;/span&gt;&lt;span class="lnt"&gt; 9&#10;&lt;/span&gt;&lt;span class="lnt"&gt;10&#10;&lt;/span&gt;&lt;span class="lnt"&gt;11&#10;&lt;/span&gt;&lt;span class="lnt"&gt;12&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;expr : ID &amp;#39;(&amp;#39; exprList ? &amp;#39;)&amp;#39; # functionCall&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | expr &amp;#39;[&amp;#39; expr &amp;#39;]&amp;#39; &#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | &amp;#39;-&amp;#39; expr&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | &amp;#39;!&amp;#39; expr&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | expr &amp;#39;^&amp;#39; expr&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | expr (&amp;#39;*&amp;#39; | &amp;#39;/&amp;#39;) expr&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | expr (&amp;#39;+&amp;#39; | &amp;#39;-&amp;#39;) expr&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | expr (&amp;#39;==&amp;#39; | &amp;#39;!=&amp;#39;) expr&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | &amp;#39;(&amp;#39; expr &amp;#39;)&amp;#39;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | ID&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | INT;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;exprList : expr (&amp;#39;,&amp;#39; expr)* ;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;这样，我们就可以通过&lt;code&gt;Listener&lt;/code&gt;得到事件:&lt;code&gt;functionCall Enter/Exit&lt;/code&gt;了。&lt;/p&gt;&#10;&lt;h2 id="语法分析的基本原理1-cfg"&gt;语法分析的基本原理（1） CFG&#10;&lt;/h2&gt;&lt;p&gt;我们先来看文法的组成:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;functionDecl : type ID &amp;#39;(&amp;#39; formalParameters? &amp;#39;)&amp;#39; block;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;每个文法规则都由&lt;code&gt;:&lt;/code&gt;分隔为两部分，其中前面部分我们称为头部(Head),后者为规则体(Body)。整个规则我们称作产生式(Production).&lt;/p&gt;&#10;&lt;p&gt;每个产生式的头部都是非终结符。而所有没有出现在头部的符号，我们称作终结符，他们对应于我们词法分析器中产生的词法单元。&lt;/p&gt;&#10;&lt;p&gt;这种文法，我们称作上下文无关文法（Context-Free Grammar CFG）。&lt;/p&gt;&#10;&lt;p&gt;一个上下文无关文法通常写作:&lt;/p&gt;&#10;$$&#10;G = (V,T,P,S)&#10;$$&lt;p&gt;分别代表:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;V：非终结符&lt;/li&gt;&#10;&lt;li&gt;T：终结符&lt;/li&gt;&#10;&lt;li&gt;P：产生式&lt;/li&gt;&#10;&lt;li&gt;S：开始符号&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;用数学符号表示就是:&lt;/p&gt;&#10;$$&#10;A\in N\to \alpha \in (T \cup N)^*&#10;$$&lt;p&gt;接下来我们介绍一下CFG的语义:&lt;/p&gt;&#10;&lt;p&gt;这里会涉及到几个基本概念:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;strong&gt;推导&lt;/strong&gt;：推导顾名思义就是用产生式对终结符进行替换，例如:&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;我们用规则:&lt;/p&gt;&#10;$$&#10;E \to E + E | E * E | (E) | -E | id&#10;$$&lt;p&gt;从E得到字符串：&lt;code&gt;-(id + id)&lt;/code&gt;&lt;/p&gt;&#10;&lt;p&gt;那么推导的流程我们可以写作 :&lt;/p&gt;&#10;$$&#10;E \to -E \to -(E)\to-(E+E)\to -(id+E)\to-(id + id)&#10;$$&lt;p&gt;这其中还会涉及到一个&lt;strong&gt;最左推导&lt;/strong&gt;和&lt;strong&gt;最右推导&lt;/strong&gt;的概念，二者的区别在于，前者在推导时总是选择最左侧的非终结符进行推导，而后者则是选择最右侧的非终结符。&lt;/p&gt;&#10;&lt;p&gt;另外，我们会标记:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;$E \Rightarrow -E $：经过一步推导得出&lt;/li&gt;&#10;&lt;li&gt;$E\xRightarrow{+}-(id+E)$：经过一步或多步推导得出&lt;/li&gt;&#10;&lt;li&gt;$E\xRightarrow{*}-(id+E)$：经过零步或多步推导得出&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;ol start="2"&gt;&#10;&lt;li&gt;&lt;strong&gt;句型&lt;/strong&gt;: 如果 $S \xRightarrow{*} \alpha,\alpha \in (T\cup N)^*$,则称 $\alpha$是文法G的一个&lt;strong&gt;句型。&lt;/strong&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;句子&lt;/strong&gt;：如果 $S \xRightarrow{*} \omega,\omega \in T*$,则称 $\omega$为文法G的一个&lt;strong&gt;句子&lt;/strong&gt;&lt;/li&gt;&#10;&lt;li&gt;文法G的&lt;strong&gt;语言&lt;/strong&gt;L(G)是它能推导出的&lt;strong&gt;所有句子&lt;/strong&gt;构成的集合 : $L(G) = \{\omega | S\xRightarrow{*} \omega\}$&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;关于文法G，我们主要关心两个主要问题:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Membership 问题，即给定一个字符串，该字符串是否属于该文法产生的语言L(G)？&lt;/li&gt;&#10;&lt;li&gt;L(G)究竟是什么？&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;其中，第一个问题就是编译器语法分析器的任务，为输入的词法单元流寻找推导，构建语法分析树或者报错。&lt;/p&gt;&#10;&lt;p&gt;而第二个问题则是程序设计者需要考虑的问题。&lt;/p&gt;&#10;&lt;p&gt;这个问题通常体现在这样的题目上: 请给出文法，满足: $\{x \in \{a,b\}^*| x中a,b数目相同\}$&lt;/p&gt;&#10;&lt;p&gt;一个可能的文法是: $V\to VV|aVb|bVa|\epsilon$&lt;/p&gt;&#10;&lt;p&gt;下面简单证明一下为什么？&lt;/p&gt;&#10;&lt;p&gt;我们可以先证明&lt;strong&gt;文法生成的串一定满足a,b数目相同&lt;/strong&gt;:&lt;/p&gt;&#10;&lt;p&gt;我们记: $\#_a(x)$表示串x中字符a的数目。&lt;/p&gt;&#10;&lt;p&gt;我们证明采用归纳法。&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;$V \Rightarrow \epsilon$,显然，此时满足 $\#_a(\epsilon) = \#_b(\epsilon)=0$&lt;/li&gt;&#10;&lt;li&gt;若 $V\xRightarrow{*} x$,且x中字符a和b的数目相同，那么有: $V\Rightarrow aVb\xRightarrow{*}axb$,同时增加一个a和一个b，所以仍然相同，同理&lt;code&gt;bVa&lt;/code&gt;以及VV仍然成立。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;所以我们有: $L(G)\subseteq L$&lt;/p&gt;&#10;&lt;p&gt;接下来证明&lt;strong&gt;所有a,b数目相同的字符串都能被这个文法生成&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;对串长 $|x|$做归纳。&lt;/p&gt;&#10;&lt;p&gt;若$|x|=0$，则 $x=\epsilon$，有 $V \Rightarrow \epsilon$&lt;/p&gt;&#10;&lt;p&gt;假设所有长度小于n，且有: $\#_a(x) = \#_b(x)$的串都能由V生成。&lt;/p&gt;&#10;&lt;p&gt;考虑长度为n的串$x = x_1x_2\dots x_n$,且有 $\#_a(x) = \#_b(x)$。&lt;/p&gt;&#10;&lt;p&gt;首先讨论首尾字符不同的串，比如 &lt;code&gt;x=ayb&lt;/code&gt;,由于整个x中a,b数目相同，去掉一个a和一个b后，y中仍然满足:&lt;/p&gt;&#10;&lt;p&gt;$\#_a(y) = \#_b(y)$.由归纳假设 $V \xRightarrow{*} y$.&lt;/p&gt;&#10;&lt;p&gt;因此 $V \Rightarrow aVb \xRightarrow{*}ayb=x$,若&lt;code&gt;x=bya&lt;/code&gt;，同理。&lt;/p&gt;&#10;&lt;p&gt;然后考虑首尾字符相同的串，我们假设首尾字符都是a，且定义前缀的差值:&lt;/p&gt;&#10;$$&#10;d(k) = \#_a(x_1\dots x_k) - \#_b(x_1\dots x_k)&#10;$$&lt;p&gt;因为第一个字符是a所以有: $d(1)=1$,而最后一个字符为a，因此 $d(n-1)=-1$,而 $d(k)$每读一个字符只会变化+1或者-1。因此从 $d(1)=1$到 $d(n-1)=-1$的过程中肯定存在某个k满足: $d(k)=0$.&lt;/p&gt;&#10;&lt;p&gt;于是我们可以把字符串x分为:$x=yz$,其中y，z都非空，且分别满足:&lt;/p&gt;&#10;$$&#10;\#_a(y) = \#_b(y),\#_a(z)=\#_b(z)&#10;$$&lt;p&gt;又因为: $|y|,|z| &lt; |x|$,由归纳假设我们知道:&lt;/p&gt;&#10;$$&#10;V\xRightarrow{*} y,V\xRightarrow{*}z&#10;$$&lt;p&gt;因此使用 $V \Rightarrow VV$,有:&lt;/p&gt;&#10;$$&#10;V\Rightarrow VV\xRightarrow{*}yz=x&#10;$$&lt;p&gt;对于首尾为b的串同理。因此，我们证明了:&lt;/p&gt;&#10;$$&#10;L \subseteq L(G)&#10;$$&lt;p&gt;综上&lt;/p&gt;&#10;$$&#10;L(G) = \{x\in\{a,b\}^*|\#_a(x)=\#_b(x)\}&#10;$$&lt;h2 id="语法分析的基本原理2-ll"&gt;语法分析的基本原理（2） LL&#10;&lt;/h2&gt;&lt;p&gt;接下来我们重点考虑一下和我们语法分析器背后原理相关的问题：&lt;br&gt;&#10;Membership 问题，即给定一个字符串，该字符串是否属于该文法产生的语言L(G)&lt;/p&gt;&#10;&lt;p&gt;我们语法分析要做的事情，无非就是根据词法单元流构建语法分析树。在构建时，我们有两种思路，自顶向下和自底向上。&lt;/p&gt;&#10;&lt;p&gt;我们先来介绍第一种: &lt;strong&gt;自顶向下的，递归下降的，基于预测分析表的，适用于LL(1)文法的LL(1)语法分析器。&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;其中自顶向下就是说我们在构建语法分析树时，是从根节点（文法的起始符号）往叶节点（词法单元）构建的。而中间每个中间节点表示对某个非终结符应用某个产生式进行推导。&lt;/p&gt;&#10;&lt;p&gt;而递归下降就是说，我们会为每个非终结符写一个递归函数，内部按需调用其他非终结符对应的递归函数，下降一层。&lt;/p&gt;&#10;&lt;p&gt;我们以下面这个文法的匹配来展示一下递归下降的过程:&lt;/p&gt;&#10;&lt;p&gt;$S \to F,S\to (S+F),F\to a$,匹配的文本是:&lt;code&gt;((a+a)+a)&lt;/code&gt;&lt;/p&gt;&#10;&lt;p&gt;我们从起始符号S开始，首先第一步显然是匹配文法: $S\to (S+F)$&lt;/p&gt;&#10;&lt;p&gt;接下来遍历目前的词法单元，对于终结符直接跳过，否则进行递归展开，比如我们遇到第一个S时，将其展开: $S\to (S+ F)$,而第一个F时， $F\to a$,从而得到串: $((S+F)+a)$&lt;/p&gt;&#10;&lt;p&gt;按照上一步的流程，不断展开非终结符，最终匹配&lt;code&gt;((a+a)+a)&lt;/code&gt;&lt;/p&gt;&#10;&lt;p&gt;那么在语法分析的过程中，自然就会产生一些问题，应该选择哪个终结符进行展开？应该选择采用哪个产生式进行推导？&lt;/p&gt;&#10;&lt;p&gt;对于LL(1)语法分析器而言，我们在推导的每一步都是选择最左边的非终结符进行展开。&lt;/p&gt;&#10;&lt;p&gt;而对于第二个问题，我们在LL(1)语法分析器中，通常借助预测分析表确定:&lt;/p&gt;&#10;&lt;p&gt;在上面的例子中，我们会通过某种算法得到如下预测分析表:&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;(&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;)&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;a&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;+&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;$&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;S&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;2&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;F&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;3&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;这张表指明了每个非终结符在面对不同的词法单元或文件结束符时，该选择哪个产生式或者报错。&lt;/p&gt;&#10;&lt;p&gt;有了这张表，匹配就很简单了，我们只需要当需要对非终结符进行推导时，看看此时匹配的串的非终结符，然后选择相应的规则进行推导即可。&lt;/p&gt;&#10;&lt;p&gt;而重点是，我们如果根据文法，构建出预测分析表。&lt;/p&gt;&#10;&lt;p&gt;我们定义: $\text{FIRST}(\alpha)$是可以从 $\alpha$推导得到的句型的首个终结符的集合。形式化定义为:&lt;/p&gt;&#10;$$&#10;\text{FIRST}(\alpha)=\{ t\in T \cup \{\epsilon\} | \alpha \xRightarrow * t\beta \lor \alpha \xRightarrow * \epsilon \}&#10;$$&lt;p&gt;我们定义: $\text{FOLLOW}(A)$是可能在某些句型中紧跟在A右侧的终结符集合。形式化定义为:&lt;/p&gt;&#10;$$&#10;\text{FOLLOW}(A) = \{t\in T \cup \{\$\} | \exist s. S\xRightarrow* s \triangleq \beta At\gamma \}&#10;$$&lt;p&gt;我们接下来看如何计算这两个集合。&lt;/p&gt;&#10;&lt;p&gt;我们这样计算每个符号X的 $\text{FIRST}$集合:&lt;/p&gt;&#10;&lt;p&gt;若X是终结符，那么 $\text{FIRST}(X) = X$;&lt;/p&gt;&#10;&lt;p&gt;若X是非终结符，那么 $\text{FIRST}(X) \leftarrow \text{FIRST}(X)\cup \{\text{FIRST}(Y_1) / \epsilon\}$,其中 $X \to Y_1Y_2\dots Y_k$&lt;/p&gt;&#10;&lt;p&gt;另外，对于 $Y_2-Y_k$,若 $\epsilon \in L(Y_1,\dots,Y_i)$,那么有: $\text{FIRST}(X) \leftarrow \text{FIRST}(X) \cup \{\text{FIRST}(Y_i) / \epsilon\}$&lt;/p&gt;&#10;&lt;p&gt;特别地，若: $\epsilon \in L(Y_1,\dots,Y_k)$,那么 $\text{FIRST}(X) \leftarrow \text{FIRST}(X) \cup \{ \epsilon\}$&lt;/p&gt;&#10;&lt;p&gt;我们以下面的文法进行一次 $\text{FIRST}(X)$计算的演示(为了方便，我们用F(x)代替):&lt;/p&gt;&#10;$$&#10;(1) X \to Y, (2)X \to a,(3)Y\to \epsilon,(4)Y\to c,(5)Z\to d,(6)Z\to XYZ&#10;$$&lt;p&gt;我们先算 F(x),我们根据文法 $X\to Y$知道，我们需要先算F(Y).&lt;/p&gt;&#10;&lt;p&gt;由文法 $Y\to \epsilon,Y\to c$我们可以知道: $F(Y) = \{c,\epsilon\}$&lt;/p&gt;&#10;&lt;p&gt;计算完F(Y)后，我们可以进一步得到 $F(X) = \{a,c,\epsilon\}$，其中F(X)包含 $\epsilon$则是根据最后一条规则&lt;/p&gt;&#10;&lt;p&gt;最后我们计算F(Z),首先由 $Z\to d$，我们有 $\{d\}\subseteq F(Z)$,再看: $Z\to XYZ$&lt;/p&gt;&#10;&lt;p&gt;根据计算方法，我们先看X，此时我们有:&lt;br&gt;&#10;$F(Z)\leftarrow F(Z) \cup \{F(X) / \epsilon\} = \{a,c,d\}$&lt;/p&gt;&#10;&lt;p&gt;而由于 $X \Rightarrow \epsilon$,因此我们还需要看Y，此时我们得到 $\{a,c,d\}\subseteq F(Z)$,而由于Y也可以推出 $\epsilon$&lt;/p&gt;&#10;&lt;p&gt;因此我们最后还需要看Z，就得到了: $F(Z) = \{a,c,d\}$&lt;/p&gt;&#10;&lt;p&gt;需要注意的是，Z推不出 $\epsilon$，因此F(Z)中并不会含有它。&lt;/p&gt;&#10;&lt;p&gt;我们这样计算每个符号X的 $\text{FOLLOW}$(X)&lt;/p&gt;&#10;&lt;p&gt;若X是开始符号，那么有: \(\text{FOLLOW}(X)\leftarrow \text{FOLLOW}(X)\cup \{\$\}\)&lt;/p&gt;&#10;&lt;p&gt;若X是某个产生式右部的最后一个符号： $A \to \alpha X$： $\text{FOLLOW}(X)\leftarrow \text{FOLLOW}(X) \cup \text{FOLLOW}(A)$&lt;/p&gt;&#10;&lt;p&gt;若X是某个产生式的右部的中间的一个符号: $A\to \alpha X \beta$: $\text{FOLLOW}(X)\leftarrow \text{FOLLOW}(X)\cup (\text{FIRST}(\beta) / \{\epsilon\})$&lt;/p&gt;&#10;&lt;p&gt;特别地，若此时 $\epsilon \in \text{FIRST}(\beta)$,那么 $\text{FOLLOW}(X)\leftarrow \text{FOLLOW}(X)\cup \text{FOLLOW}(A)$&lt;/p&gt;&#10;&lt;p&gt;还是一样，我们以上面的文法为例，展示 $\text{FOLLOW}$集合的计算方法。&lt;/p&gt;&#10;&lt;p&gt;我们首先处理开始符号X，因为X是开始符号，我们显然有：&lt;/p&gt;&#10;&lt;p&gt;\(\$ \in \text{FOLLOW}(X)\),所以目前： \(\text{FOLLOW}(X) = \{\$\}\)&lt;/p&gt;&#10;&lt;p&gt;接下来根据生产式: $X\to Y$，以及规则2，我们有: $\text{FOLLOW}(X)\subseteq \text{FOLLOW}(Y)$&lt;/p&gt;&#10;&lt;p&gt;因此目前: \(\text{FOLLOW}(Y) = \{\$\}\)&lt;/p&gt;&#10;&lt;p&gt;接下来看 $Z\to XYZ$.&lt;/p&gt;&#10;&lt;p&gt;对X而言，后面跟着YZ，所以要把 $\text{FIRST}(YZ) -\{\epsilon\}$,加入到 $\text{FOLLOW}(X)$中，因此我们需要计算:&lt;/p&gt;&#10;$$&#10;\text{FIRST}(YZ) = \{ a,c,d\}&#10;$$&lt;p&gt;因此: \(\text{FOLLOW}(X) = \{\$,a,c,d\}\)&lt;/p&gt;&#10;&lt;p&gt;对于Y而言，后面跟着Z，所以有:&lt;/p&gt;&#10;&lt;p&gt;$\text{FIRST}(Z) -\epsilon \subseteq \text{FOLLOW}(Y)$,而: $\text{FIRST}(Z) = \{a,c,d\}$&lt;/p&gt;&#10;&lt;p&gt;因此: \(\text{FOLLOW}(Y) = \{\$,a,c,d\}\)&lt;/p&gt;&#10;&lt;p&gt;而Z根据规则1知道是一个空集。&lt;/p&gt;&#10;&lt;p&gt;得到了FIRST集和FOLLOW集后，我们可以开始构造给定文法的预测分析表了。&lt;/p&gt;&#10;&lt;p&gt;通常而言，预测分析表写作: &lt;code&gt;M[A,a]&lt;/code&gt;,其中行为非终结符A，列尾终结符a，单元格内则填写应该选择哪个产生式。&lt;/p&gt;&#10;&lt;p&gt;填表规则只有两条。&lt;/p&gt;&#10;&lt;p&gt;对于产生式 $A\to \beta$&lt;/p&gt;&#10;&lt;p&gt;若 $a\in \text{FIRST}(\beta)-\{\epsilon\}$那么有: $M[A,a] = A\to \beta$;&lt;/p&gt;&#10;&lt;p&gt;若 $\epsilon \in \text{FIRST}(\beta)$,那么对于所有的: $b\in \text{FOLLOW}(A)$都有: $M[A,b] = A\to \beta$.&lt;/p&gt;&#10;&lt;p&gt;在实际计算中我们通常会先计算产生式 $A\to\beta$的 $\text{SELECT}$集合&lt;/p&gt;&#10;&lt;p&gt;我们定义: $\text{SELECT}(A\to\beta)$&lt;/p&gt;&#10;&lt;p&gt;若： $\epsilon \notin \text{FIRST}(\beta)$,那么 $\text{SELECT}(A\to\beta) = \text{FIRST}(\beta)$&lt;/p&gt;&#10;&lt;p&gt;若： $\epsilon \in \text{FIRST}(\beta)$,那么 $\text{SELECT}(A\to \beta) = (\text{FIRST}(\beta)-\{\epsilon\})\cup \text{FOLLOW}(A)$&lt;/p&gt;&#10;&lt;p&gt;得到 $A\to\beta$的SELECT集后，对该集合中每一个终结符a，把生产式 $A\to\beta$填入&lt;code&gt;M[A,a]&lt;/code&gt;&lt;/p&gt;&#10;&lt;p&gt;还是上面那个例子，我们来手算一遍:&lt;/p&gt;&#10;&lt;p&gt;我们知道:&lt;/p&gt;&#10;$$&#10;\begin{aligned} (1)\quad &amp;X\to Y\\ (2)\quad &amp;X\to a\\ (3)\quad &amp;Y\to \epsilon\\ (4)\quad &amp;Y\to c\\ (5)\quad &amp;Z\to d\\ (6)\quad &amp;Z\to XYZ \end{aligned}&#10;$$$$&#10;\begin{aligned} FIRST(X)&amp;=\{a,c,\epsilon\}\\ FIRST(Y)&amp;=\{c,\epsilon\}\\ FIRST(Z)&amp;=\{a,c,d\} \end{aligned}&#10;$$$$&#10;\begin{aligned} FOLLOW(X)&amp;=\{a,c,d,\$\}\\ FOLLOW(Y)&amp;=\{a,c,d,\$\}\\ FOLLOW(Z)&amp;=\varnothing \end{aligned}&#10;$$&lt;p&gt;那么对于产生式（1） $X\to Y$,因为: $\text{FIRST}(Y) = \{c,\epsilon\}$,其中包含了 $\epsilon$，因此:&lt;/p&gt;&#10;$$&#10;\text{SELECT}(X\to Y) = \{a,c,d,\$\}&#10;$$&lt;p&gt;对于产生式（2） $X \to a$,这里 $\text{FIRST}(a) = \{a\}$，不包含 $\epsilon$,因此: $\text{SELECT}(X\to a) =\{a\} $&lt;/p&gt;&#10;&lt;p&gt;按照同样的思路，我们可以求出每个产生式的 $\text{SELECT}$集，从而得到最终的预测分析表:&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;非终结符&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;a&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;c&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;d&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;$&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;X&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;(1), (2)&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;(1)&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;(1)&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;(1)&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Y&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;(3)&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;(3), (4)&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;(3)&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;(3)&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Z&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;(6)&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;(6)&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;(5), (6)&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;error&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;</description></item><item><title>[从零开始的编译原理][理论] Chapter 1：词法分析</title><link>https://july-h5kf3.github.io/p/compiler-ch1-lexical-analysis/</link><pubDate>Mon, 28 Sep 2026 20:15:14 +0800</pubDate><guid>https://july-h5kf3.github.io/p/compiler-ch1-lexical-analysis/</guid><description>&lt;p&gt;这一章我们学习的是词法分析。&lt;/p&gt;&#10;&lt;p&gt;我们知道编译器通常由以下几个部分组成:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;前端: 通过词法分析和语法分析，将源代码解析为抽象语法树（AST）。通过语义分析，扫描抽象语法树，检查其是否存在语义错误&lt;/li&gt;&#10;&lt;li&gt;中端：将AST转化为中间表示IR，并在此基础上完成一些机器无关优化&lt;/li&gt;&#10;&lt;li&gt;后端：将中间表示IR转换为目标平台的汇编代码，并在此基础上完成一些机器相关优化&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h2 id="词法分析"&gt;词法分析&#10;&lt;/h2&gt;&lt;p&gt;词法分析简单来说就是把字节流转换为单词流 (token stream). 词法分析器(lexer)会按照某种规则读文件，并将文件的内容拆分成一个个 token 作为输出, 传递给语法分析器 (parser). 同时, lexer 还会忽略文件里的一些无意义的内容, 比如空格, 换行符和注释.&lt;/p&gt;&#10;&lt;p&gt;Lexer 生成的 token 会包含一些信息, 用来让 parser 区分 token 的种类, 以及在必要时获取 token 的内容.&lt;/p&gt;&#10;&lt;p&gt;例如我们有这样一个程序:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1&#10;&lt;/span&gt;&lt;span class="lnt"&gt;2&#10;&lt;/span&gt;&lt;span class="lnt"&gt;3&#10;&lt;/span&gt;&lt;span class="lnt"&gt;4&#10;&lt;/span&gt;&lt;span class="lnt"&gt;5&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// 我是注释诶嘿嘿&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;那么Lexer可能会将他转化为如下token 流:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1&#10;&lt;/span&gt;&lt;span class="lnt"&gt;2&#10;&lt;/span&gt;&lt;span class="lnt"&gt;3&#10;&lt;/span&gt;&lt;span class="lnt"&gt;4&#10;&lt;/span&gt;&lt;span class="lnt"&gt;5&#10;&lt;/span&gt;&lt;span class="lnt"&gt;6&#10;&lt;/span&gt;&lt;span class="lnt"&gt;7&#10;&lt;/span&gt;&lt;span class="lnt"&gt;8&#10;&lt;/span&gt;&lt;span class="lnt"&gt;9&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;种类: 关键字, 内容: int.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;种类: 标识符, 内容: main.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;种类: 其他字符, 内容: (.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;种类: 其他字符, 内容: ).&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;种类: 其他字符, 内容: {.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;种类: 关键字, 内容: return.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;种类: 整数字面量, 内容: 0.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;种类: 其他字符, 内容: ;.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;种类: 其他字符, 内容: }.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;目前为止，我们实现词法分析从易到难有三种方法：&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;词法分析器生成器&lt;/li&gt;&#10;&lt;li&gt;手写词法分析器&lt;/li&gt;&#10;&lt;li&gt;自动化词法分析器&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;接下来我们重点看看前面两项&lt;/p&gt;&#10;&lt;h2 id="词法分析器生成器"&gt;词法分析器生成器&#10;&lt;/h2&gt;&lt;p&gt;在本课程中，我们使用的词法分析生成器是antlr。&lt;/p&gt;&#10;&lt;p&gt;在使用antlr时，我们的输入是一个包含词法单元规约的g4文件，那么antlr会自动生成一个词法分析器&lt;/p&gt;&#10;&lt;p&gt;如果我们使用antlr这样的语法分析器生成器，我们主要做的事情就是就是写清楚语法规则。&lt;/p&gt;&#10;&lt;p&gt;我们接下来尝试用antlr实现一个类似C++的词法分析器:&lt;/p&gt;&#10;&lt;p&gt;首先我们知道一个C++程序肯定是由若干条Statement组成的，那么我们就有第一条语法规则：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1&#10;&lt;/span&gt;&lt;span class="lnt"&gt;2&#10;&lt;/span&gt;&lt;span class="lnt"&gt;3&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;grammer SimpleSysY; // 注意在g4中第一行需要定义grammer，并且保持和文件名一致&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;prog: stat* EOF; //这里加入EOF来说明结束条件&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;而statement都是由表达式expr组成的，为了简便，我们先初步规定expr只支持赋值语句和输出，那么我们可以有下一条规则:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1&#10;&lt;/span&gt;&lt;span class="lnt"&gt;2&#10;&lt;/span&gt;&lt;span class="lnt"&gt;3&#10;&lt;/span&gt;&lt;span class="lnt"&gt;4&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;stat: expr &amp;#39;;&amp;#39; // 需要注意&amp;#39;&amp;#39;内的分号说明一条statement要以分号结尾&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | ID &amp;#39;=&amp;#39; expr &amp;#39;;&amp;#39;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | &amp;#39;print&amp;#39; expr &amp;#39;;&amp;#39;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;接下来我们就可以去描述一个表达式的语法规则了。那么显然，这是一个递归的描述:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1&#10;&lt;/span&gt;&lt;span class="lnt"&gt;2&#10;&lt;/span&gt;&lt;span class="lnt"&gt;3&#10;&lt;/span&gt;&lt;span class="lnt"&gt;4&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;expr: expr (&amp;#39;+&amp;#39; | &amp;#39;-&amp;#39; | &amp;#39;*&amp;#39; | &amp;#39;/&amp;#39; ) expr&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | &amp;#39;(&amp;#39; expr &amp;#39;)&amp;#39;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | ID&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;但是这样写会有点问题，这里会涉及到符号优先级的问题，例如乘法和除法的优先级肯定是高于加法和减法的，想要在antlr中表达优先级我们可以:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1&#10;&lt;/span&gt;&lt;span class="lnt"&gt;2&#10;&lt;/span&gt;&lt;span class="lnt"&gt;3&#10;&lt;/span&gt;&lt;span class="lnt"&gt;4&#10;&lt;/span&gt;&lt;span class="lnt"&gt;5&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;expr: expr (&amp;#39;*&amp;#39; | &amp;#39;/&amp;#39; ) expr&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | expr (&amp;#39;+&amp;#39; | &amp;#39;-&amp;#39; ) expr&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | &amp;#39;(&amp;#39; expr &amp;#39;)&amp;#39;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | ID&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;当然除了优先级以外，我们还要考虑结合性的影响，在g4中，如果我们不显式指定是左结合还是右结合，就会默认是左结合，这和我们的计算是符合的，但是我们后续还会继续探讨这部分的内容。&lt;/p&gt;&#10;&lt;p&gt;整体上来看，语法结果就是这样，接下来我们看看词法结构:&lt;/p&gt;&#10;&lt;p&gt;我们知道在C++中一个标识符是以下划线或字母开头，以字母数字下划线结尾的字符串:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ID: (&amp;#39;_&amp;#39; | [a-zA-Z])(&amp;#39;_&amp;#39; | [a-zA-Z0-9])*;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;而如果此时我们用上面的写好的语法规则和词法规则去对一个语句去做测试: &lt;code&gt;a = b + c;&lt;/code&gt;&lt;/p&gt;&#10;&lt;p&gt;会发现有部分的报错，这是因为空格没有被识别到，因此我们还需要加入空格的词法规则:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;WS: [ \t\r\n]+ -&amp;gt; skip;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;其中我们用+是因为至少要有一个才能被识别，而后面的skip则是识别到了就跳过，否则我们实际上grammar中没有相关的规则，会导致语法规则识别不出来。&lt;/p&gt;&#10;&lt;p&gt;这样，上面那个语句我们就可以画出语法分析树了:&lt;/p&gt;&#10;&lt;p&gt;&lt;img alt="a = b + c; 的语法分析树" class="gallery-image" data-flex-basis="287px" data-flex-grow="119" height="918" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://july-h5kf3.github.io/p/compiler-ch1-lexical-analysis/parse-tree-a-eq-b-plus-c.png" srcset="https://july-h5kf3.github.io/p/compiler-ch1-lexical-analysis/parse-tree-a-eq-b-plus-c_hu_4c3a487dd017927f.png 800w, https://july-h5kf3.github.io/p/compiler-ch1-lexical-analysis/parse-tree-a-eq-b-plus-c.png 1098w" width="1098"&gt;&lt;/p&gt;&#10;&lt;p&gt;当然在我们实际的实验中，这样简单的语法规则和词法规则肯定是不够的，具体的我们可以在Lab中实现，这里只做简单的介绍。&lt;/p&gt;&#10;&lt;p&gt;另外这里可以补充一点写语法规则和词法规则时一些需要注意的点:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;特殊的规则尽量写在前面，比如docs comment和mul comment的匹配规则分别写作:&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1&#10;&lt;/span&gt;&lt;span class="lnt"&gt;2&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;DOCS_COMMENT : &amp;#39;/**&amp;#39; .*? &amp;#39;*/&amp;#39;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;MUL_COMMENT : &amp;#39;/*&amp;#39; .*? &amp;#39;*/&amp;#39;; // 其中 .*?中的？表示非贪婪匹配模式&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;那么此时明显可以看到DOCS_COMMENT也可以被MUL_COMMENT给匹配到的，因此我们在写词法规则的时候应该把DOCS_COMMENT的规则写在MUL_COMMENT前面，让前者优先匹配。&lt;/p&gt;&#10;&lt;ol start="2"&gt;&#10;&lt;li&gt;在antler这类词法分析器生成器中，提供了&lt;code&gt;fragment&lt;/code&gt;的功能来进行助记:&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1&#10;&lt;/span&gt;&lt;span class="lnt"&gt;2&#10;&lt;/span&gt;&lt;span class="lnt"&gt;3&#10;&lt;/span&gt;&lt;span class="lnt"&gt;4&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;fragment LETTER: [a-zA-Z]&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;fragment NUMBER: [0-9]&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;fragment WORD: &amp;#39;_&amp;#39; | LETTER | NUMBER&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ID: (&amp;#39;_&amp;#39; | LETTER)(WORD)*;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;ol start="3"&gt;&#10;&lt;li&gt;antlr中有三个比较重要的优先匹配规则来解决冲突，分别是最前优先匹配，最长优先匹配(例如1.23会被匹配为float而不是INT和FLOAT，&amp;gt;=不会被识别为&amp;gt;和=)，非贪婪匹配&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h2 id="词法分析生成器的基本原理1"&gt;词法分析生成器的基本原理（1）&#10;&lt;/h2&gt;&lt;p&gt;在Antlr这类词法分析器生成器中，我们只需要在g4文件中描述词法单元的正则表达式就能自动生成词法分析器，这背后的流程可以分为若干个步骤。&lt;/p&gt;&#10;&lt;p&gt;比较经典的流程是:&lt;/p&gt;&#10;&lt;p&gt;Regix-&amp;gt;NFA-&amp;gt;DFA-&amp;gt;Transition Table-&amp;gt;Machine Code&lt;/p&gt;&#10;&lt;p&gt;接下来我们会逐一介绍上面的各个部分&lt;/p&gt;&#10;&lt;h3 id="正则表达式"&gt;正则表达式&#10;&lt;/h3&gt;&lt;p&gt;首先我们给出正则表达式的定义:&lt;/p&gt;&#10;&lt;p&gt;给定字母表$\Sigma$，$\Sigma$上的正则表达式有且仅有以下规则定义:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;$\epsilon$是正则表达式&lt;/li&gt;&#10;&lt;li&gt;$\forall a \in \Sigma $,a是正则表达式&lt;/li&gt;&#10;&lt;li&gt;如果r是正则表达式，则(r)也是正则表达式&lt;/li&gt;&#10;&lt;li&gt;如果r与s都是正则表达式，则 r|s,rs,r*也是正则表达式&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;此外，我们规定正则表达式中的运算优先级如下:&lt;/p&gt;&#10;$$&#10;() &gt; * &gt; connect &gt; |&#10;$$&lt;p&gt;对应的正则语言如下:&lt;/p&gt;&#10;&lt;p&gt;我们规定每个正则表达式r对应一个正则语言L(r)&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;$L(\epsilon) = \{\epsilon\}$&lt;/li&gt;&#10;&lt;li&gt;$L(a) = \{a\},\forall a \in \Sigma$&lt;/li&gt;&#10;&lt;li&gt;$L((r)) = L(r)$&lt;/li&gt;&#10;&lt;li&gt;$L(r|s)=L(r)\cup L(s)$ $L(rs)=L(r)L(s)$ $L(r*)=(L(r))*$&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;下面是常用到的正则表达式符号以及对应的含义:&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;符号&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;含义&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;例子&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;例子含义&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;a&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;匹配字符本身&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;a&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;匹配字符 a&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;.&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;匹配除换行符以外的任意单个字符&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;a.b&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;如 acb、a1b&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;r|s&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;或 / 选择&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;a|b&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;匹配 a 或 b&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;r*&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;前面的表达式重复 0 次或多次&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;a*&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&amp;ldquo;&amp;quot;、a、aa、…&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;r+&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;前面的表达式重复 1 次或多次&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;a+&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;a、aa、…&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;r?&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;前面的表达式出现 0 次或 1 次&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;a?&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&amp;quot;&amp;rdquo; 或 a&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;(r)&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;和r相同&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;(ab)*&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&amp;ldquo;&amp;quot;、ab、abab、…&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;[s]&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;字符集合，匹配其中任意一个字符&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;[abc]&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;a、b 或 c&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;[^s]&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;字符集合取反&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;[^abc]&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;除 a、b、c 外的字符&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;^&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;通常表示字符串/行开头&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;^abc&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;以 abc 开头&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;$&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;通常表示字符串/行结尾&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;abc$&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;以 abc 结尾&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;\&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;转义特殊字符&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;\*&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;匹配字符 * 本身&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;r{m}&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;恰好重复 m 次&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;a{3}&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;aaa&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;r{m,n}&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;重复 m 到 n 次&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;a{2,4}&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;aa、aaa、aaaa&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;&amp;quot;s&amp;quot;&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;串s的字面值&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;&amp;quot;*a&amp;quot;&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;*a&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;h3 id="nfa-非确定性有穷自动机"&gt;NFA 非确定性有穷自动机&#10;&lt;/h3&gt;&lt;p&gt;我们定义非确定性有穷自动机A是一个五元组 $A = (\Sigma,S,s_0,\delta,F)$:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;字母表 $\Sigma\ \ (\epsilon \notin \Sigma)$&lt;/li&gt;&#10;&lt;li&gt;有穷的状态集合 S&lt;/li&gt;&#10;&lt;li&gt;唯一的初始状态 $s_0$&lt;/li&gt;&#10;&lt;li&gt;状态转移函数 $\delta$：$\delta: S\times(\Sigma \cup \{\epsilon\})\to 2^S$&lt;/li&gt;&#10;&lt;li&gt;接受状态集合 $F \subseteq S$&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;NFA的主要特点是一个输入可以有多个下一状态，且允许 $\epsilon$转移&lt;/p&gt;&#10;&lt;p&gt;自动机A定义了一种语言 L(A),它能接受的所有字符串构成的集合&lt;/p&gt;&#10;&lt;h3 id="dfa-确定性有穷自动机"&gt;DFA 确定性有穷自动机&#10;&lt;/h3&gt;&lt;p&gt;我们定义确定性有穷自动机A是一个五元组 $A = (\Sigma,S,s_0,\delta,F)$:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;字母表 $\Sigma\ \ (\epsilon \notin \Sigma)$&lt;/li&gt;&#10;&lt;li&gt;有穷的状态集合 S&lt;/li&gt;&#10;&lt;li&gt;唯一的初始状态 $s_0$&lt;/li&gt;&#10;&lt;li&gt;状态转移函数 $\delta$：$\delta: S\times \Sigma \to S$&lt;/li&gt;&#10;&lt;li&gt;接受状态集合 $F \subseteq S$&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;与NFA不同的点在于DFA一个输入只有一个状态，不允许空转移&lt;/p&gt;&#10;&lt;p&gt;NFA简洁易于理解，便于描述语言L(A)，而DFA容易判断 $x \in L(A)$,适合产生词法分析器&lt;/p&gt;&#10;&lt;p&gt;因此我们通常用NFA描述语言，DFA实现词法分析器&lt;/p&gt;&#10;&lt;h2 id="词法分析生成器的基本原理2"&gt;词法分析生成器的基本原理（2）&#10;&lt;/h2&gt;&lt;p&gt;在上一节中我们提到了Regix以及NFA，DFA的基本含义，我们接下来介绍他们之间的相互转化。&lt;/p&gt;&#10;&lt;h3 id="从re到nfa-thompson-构造法"&gt;从RE到NFA: Thompson 构造法&#10;&lt;/h3&gt;&lt;p&gt;Thompson构造法的基本思想是按结构归纳，即把正则表达式递归地拆分成小的子表达式，每个子表达式先构造一个小NFA，再通过 $\epsilon$边把这些小的NFA连接起来。&lt;/p&gt;&#10;&lt;p&gt;依据正则表达式的定义，我们可以拆开看:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;基本字符:&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;对于一个字符&lt;code&gt;a&lt;/code&gt;，我们可以构造:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1&#10;&lt;/span&gt;&lt;span class="lnt"&gt;2&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; a&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;(q0) ---&amp;gt; (q1)&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;其中q0是入口，q1是出口&lt;/p&gt;&#10;&lt;ol start="2"&gt;&#10;&lt;li&gt;连接:rs&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;假设我们已经构造好了正则表达式r和s的NFA，那么对于连接我们可以通过将r的出口和s的入口以一个 $\epsilon$边连接即可:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1&#10;&lt;/span&gt;&lt;span class="lnt"&gt;2&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; r s&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --&amp;gt; [ NFA ] --ε--&amp;gt; [ NFA ] --&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;ol start="3"&gt;&#10;&lt;li&gt;选择 r|s&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;我们此时希望NFA可以选择走r的入口，也可以走s的入口，那么我们只需要新建一个入口和一个新的出口，并用 $\epsilon$边将他们分别与r和s的入口，出口连接即可&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1&#10;&lt;/span&gt;&lt;span class="lnt"&gt;2&#10;&lt;/span&gt;&lt;span class="lnt"&gt;3&#10;&lt;/span&gt;&lt;span class="lnt"&gt;4&#10;&lt;/span&gt;&lt;span class="lnt"&gt;5&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ε --&amp;gt; [NFA(r)] --ε&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; / \&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;start --- ---&amp;gt; end&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; \ /&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ε --&amp;gt; [NFA(s)] --ε&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;ol start="4"&gt;&#10;&lt;li&gt;Kleene 星号: r*&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;对于Kleene星号，我们需要具备两种能力，即一次都不执行和执行完以后重新执行:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1&#10;&lt;/span&gt;&lt;span class="lnt"&gt; 2&#10;&lt;/span&gt;&lt;span class="lnt"&gt; 3&#10;&lt;/span&gt;&lt;span class="lnt"&gt; 4&#10;&lt;/span&gt;&lt;span class="lnt"&gt; 5&#10;&lt;/span&gt;&lt;span class="lnt"&gt; 6&#10;&lt;/span&gt;&lt;span class="lnt"&gt; 7&#10;&lt;/span&gt;&lt;span class="lnt"&gt; 8&#10;&lt;/span&gt;&lt;span class="lnt"&gt; 9&#10;&lt;/span&gt;&lt;span class="lnt"&gt;10&#10;&lt;/span&gt;&lt;span class="lnt"&gt;11&#10;&lt;/span&gt;&lt;span class="lnt"&gt;12&#10;&lt;/span&gt;&lt;span class="lnt"&gt;13&#10;&lt;/span&gt;&lt;span class="lnt"&gt;14&#10;&lt;/span&gt;&lt;span class="lnt"&gt;15&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ε&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; +-------------&amp;gt; end&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; |&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; start&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; |&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | ε&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; v&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; start_r ---- r ----&amp;gt; end_r&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ^ |&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; |_______ ε _________|&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; |&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ε&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; |&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; v&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; end&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;前者我们通过将新的入口和新的出口直接通过 $\epsilon$边连接完成，后者我们则在r的NFA上加入一条由出口指向入口的 $\epsilon $边。&lt;/p&gt;&#10;&lt;p&gt;我们可以通过下面这个例子来展示Thompson构造法转化RE到NFA的完整流程:&lt;/p&gt;&#10;&lt;p&gt;假设我们有正则表达式&lt;code&gt;a(b|c)*&lt;/code&gt;&lt;/p&gt;&#10;&lt;p&gt;我们可以先按照语法树来决定构造顺序：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1&#10;&lt;/span&gt;&lt;span class="lnt"&gt;2&#10;&lt;/span&gt;&lt;span class="lnt"&gt;3&#10;&lt;/span&gt;&lt;span class="lnt"&gt;4&#10;&lt;/span&gt;&lt;span class="lnt"&gt;5&#10;&lt;/span&gt;&lt;span class="lnt"&gt;6&#10;&lt;/span&gt;&lt;span class="lnt"&gt;7&#10;&lt;/span&gt;&lt;span class="lnt"&gt;8&#10;&lt;/span&gt;&lt;span class="lnt"&gt;9&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 连接&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; / \&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; a *&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; |&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; |&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; |&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; b|c&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; / \&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; b c&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;然后我们可以自底向上构造，首先构造一下a,b,c：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1&#10;&lt;/span&gt;&lt;span class="lnt"&gt;2&#10;&lt;/span&gt;&lt;span class="lnt"&gt;3&#10;&lt;/span&gt;&lt;span class="lnt"&gt;4&#10;&lt;/span&gt;&lt;span class="lnt"&gt;5&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;a → NFA(a)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;b → NFA(b)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;c → NFA(c)&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;接下来构造&lt;code&gt;b|c&lt;/code&gt;:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1&#10;&lt;/span&gt;&lt;span class="lnt"&gt;2&#10;&lt;/span&gt;&lt;span class="lnt"&gt;3&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; /-- b --\&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;-- ε - - ε --&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; \-- c --/&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;然后给&lt;code&gt;b|c&lt;/code&gt;套上Kleene星号:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1&#10;&lt;/span&gt;&lt;span class="lnt"&gt;2&#10;&lt;/span&gt;&lt;span class="lnt"&gt;3&#10;&lt;/span&gt;&lt;span class="lnt"&gt;4&#10;&lt;/span&gt;&lt;span class="lnt"&gt;5&#10;&lt;/span&gt;&lt;span class="lnt"&gt;6&#10;&lt;/span&gt;&lt;span class="lnt"&gt;7&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ε----------------------+&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | |&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; v |&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;start --&amp;gt; [ b|c ] --&amp;gt; merge ---+&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | |&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; +----------ε----------------&amp;gt; end&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; merge --ε-----&amp;gt; end&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;最后再把a的按照连接的方式连起来:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;start --&amp;gt; [a] --&amp;gt; [(b|c)*] --&amp;gt; end&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;h3 id="从nfa到dfa子集构造法"&gt;从NFA到DFA:子集构造法&#10;&lt;/h3&gt;&lt;p&gt;&lt;img alt="(a|b)*abb 对应的 NFA" class="gallery-image" data-flex-basis="567px" data-flex-grow="236" height="660" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://july-h5kf3.github.io/p/compiler-ch1-lexical-analysis/nfa-a-or-b-star-abb.png" srcset="https://july-h5kf3.github.io/p/compiler-ch1-lexical-analysis/nfa-a-or-b-star-abb_hu_2f25b54a3a0b0fa8.png 800w, https://july-h5kf3.github.io/p/compiler-ch1-lexical-analysis/nfa-a-or-b-star-abb.png 1560w" width="1560"&gt;&lt;/p&gt;&#10;&lt;p&gt;子集构造法的核心思想在于DFA的一个状态，对应于NFA的一组状态。&lt;/p&gt;&#10;&lt;p&gt;由于DFA在描述状态转移时只允许目标状态有且仅有一个，而NFA可以有多个，因此我们干脆把NFA中所有可能的状态整体看成DFA的一个状态。&lt;/p&gt;&#10;&lt;p&gt;在描述具体方法前，我们通过上面的NFA介绍两个名词:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;$\epsilon-\text{closure}$： 表示在NFA中从状态集合S出发，只走任意条 $\epsilon$边，能够到达的状态，包括S自己。&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;例如下图中，从状态 $q_0$出发，只走任意条 $\epsilon$边，能够到达的状态为: $\{q_0,q_1,q_2,q_4,q_7\}$,因此我们有: $\epsilon-\text{closure}(\{q_0\})=\{q_0,q_1,q_2,q_4,q_7\}$&lt;/p&gt;&#10;&lt;ol start="2"&gt;&#10;&lt;li&gt;Move: 表示在NFA中从状态集合S出发，读取字符 &lt;code&gt;a&lt;/code&gt;一步后能到达的状态集合&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;借用这两个名词，以及上面这个例子，我们可以很好的描述子集构造法的流程了。&lt;/p&gt;&#10;&lt;p&gt;首先，从初始状态出发，计算: $\epsilon-\text{closure}(q_0)$,计算出来后记作初始状态 $D_0 = \{q_0,q_1,q_2,q_4,q_7\}$&lt;/p&gt;&#10;&lt;p&gt;之后从 $D_0$读字符a，算 $\epsilon-\text{closure}(\text{move}(D_0,a)) = D_1 = \{q_1,q_2,q_3,q_4,q_6,q_7,q_8\}$&lt;/p&gt;&#10;&lt;p&gt;接着从 $D_0$读字符b，得到 $D_2 = \{q_1,q_2,q_4,q_5,q_6,q_7\}$&lt;/p&gt;&#10;&lt;p&gt;再从 $D_1$读字符a，算 $\epsilon-\text{closure}(\text{move}(D_1,a)) = \{q_1,q_2,q_3,q_4,q_6,q_7,q_8\} = D_1$&lt;/p&gt;&#10;&lt;p&gt;再从 $D_1$读取字符b，算 $\epsilon-\text{closure}(\text{move}(D_1,b))=D_3=\{q_1,q_2,q_3,q_4,q_5,q_6,q_7,q_9\}$&lt;/p&gt;&#10;&lt;p&gt;然后从 $D_2$读字符a，得到 $D_1$，从 $D_2$读字符b，得到 $\epsilon-\text{closure}(\text{move}(D_2,b)) = D_2 = \{q_1,q_2,q_4,q_5,q_6,q_7\}$&lt;/p&gt;&#10;&lt;p&gt;最后从 $D_3$读字符a，得到 $D_1$ ，从 $D_3$读字符b，得到 $\epsilon-\text{closure}(\text{move}(D_3,b)) = D_4^* = \{q_1,q_2,q_4,q_5,q_6,q_7,q_{10}\}$,因为它包含终态 $q_{10}$，所以我们会给它一个特殊的标记。&lt;/p&gt;&#10;&lt;p&gt;而从 $D_4$读字符a和字符b分别可以得到 $D_1,D_2$&lt;/p&gt;&#10;&lt;p&gt;总结下来，我们可以得到如下状态转移图:&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;DFA 状态&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;对应 NFA 状态集合&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;a&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;b&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;$D_0$&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;{0,1,2,4,7}&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;$D_1$&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;$D_2$&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;$D_1$&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;{1,2,3,4,6,7,8}&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;$D_1$&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;$D_3$&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;$D_2$&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;{1,2,4,5,6,7}&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;$D_1$&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;$D_2$&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;$D_3$&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;{1,2,4,5,6,7,9}&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;$D_1$&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;$D_4$&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;$D_4^*$&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;{1,2,4,5,6,7,10}&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;$D_1$&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;$D_2$&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;转化为DFA，可以画成:&lt;/p&gt;&#10;&lt;p&gt;&lt;img alt="子集构造法得到的 DFA" class="gallery-image" data-flex-basis="412px" data-flex-grow="171" height="622" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://july-h5kf3.github.io/p/compiler-ch1-lexical-analysis/dfa-subset-construction.png" srcset="https://july-h5kf3.github.io/p/compiler-ch1-lexical-analysis/dfa-subset-construction_hu_2e42a1f8d5ed6600.png 800w, https://july-h5kf3.github.io/p/compiler-ch1-lexical-analysis/dfa-subset-construction.png 1068w" width="1068"&gt;&lt;/p&gt;&#10;&lt;h3 id="最小化dfa"&gt;最小化DFA&#10;&lt;/h3&gt;&lt;p&gt;DFA最小化的目标是在保持语言完全不变的前提下，把行为等价的DFA状态合并，得到状态数最少的DFA。&lt;/p&gt;&#10;&lt;p&gt;首先，我们需要明确什么叫两个状态是等价的:&lt;/p&gt;&#10;&lt;p&gt;我们假设DFA中存在两个状态p,q。从他们出发，如果输入任意的字符串w，最终要么都接受，要么都拒绝，那么我们可以认为: $p\equiv q $。&lt;/p&gt;&#10;&lt;p&gt;知道了怎样的状态是等价，也就是可以合并的，我们可以开始考虑怎么最小化DFA了。&lt;/p&gt;&#10;&lt;p&gt;首先我们可以知道的是，接受状态和非接受状态是不可合并的，因此我们可以把DFA的状态集合分为两组:&lt;/p&gt;&#10;$$&#10;P_0 = \{F,Q-F\}&#10;$$&lt;p&gt;接下来则可以进一步不断细分，我们对于同一组中的两个状态p和q，观察他们读入每个字符后的去向。如果:&lt;/p&gt;&#10;$$&#10;\delta(p,a) =\epsilon-\text{closure}(\text{move}(p,a)) \neq \delta(q,a)&#10;$$&lt;p&gt;那么此时p和q不能继续待在一个组，于是把他们拆开。&lt;/p&gt;&#10;&lt;p&gt;不断重复上述流程，直至无法继续拆分为止。&lt;/p&gt;&#10;&lt;p&gt;最终每一个分组中状态都是等价状态，可以合并为一个DFA状态。&lt;/p&gt;&#10;&lt;p&gt;为了方便我们理解，我们利用我们上面根据子集构造法得到的DFA来做一遍上述流程。&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;状态&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;a&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;b&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;是否接受&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;$D_0$&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;$D_1$&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;$D_2$&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;否&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;$D_1$&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;$D_1$&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;$D_3$&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;否&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;$D_2$&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;$D_1$&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;$D_2$&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;否&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;$D_3$&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;$D_1$&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;$D_4$&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;否&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;$D_4$&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;$D_1$&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;$D_2$&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;是&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;其中唯一接受状态： $F = \{D_4 \}$&lt;/p&gt;&#10;&lt;p&gt;第一步，按接受/不接受进行第一步划分，得到:&lt;/p&gt;&#10;$$&#10;P_0 = \{ \{D_4\},\{D_0,D_1,D_2,D_3\}\}&#10;$$&lt;p&gt;为了方便，我们记作: $P_0 = \{A,B\},A=\{ D_4\}$&lt;/p&gt;&#10;&lt;p&gt;第二步，检查B是否可以进一步拆分。&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;$D_0$: 根据表格，我们发现无论输入a还是b，最后仍然落入B中，因此我们记作: $D_0:(B,B)$&lt;/li&gt;&#10;&lt;li&gt;$D_1$: 类似的，我们可以得到: $D_1:(B,B)$&lt;/li&gt;&#10;&lt;li&gt;$D_2$: $D_2:(B,B)$&lt;/li&gt;&#10;&lt;li&gt;$D_3$: $D_3(B,A)$&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;于是我们发现只有 $D_3$与B中的其他状态不同，因此我们可以进一步划分出:&lt;/p&gt;&#10;$$&#10;P_1 = \{\{D_4\},\{D_3\},\{D_0,D_1,D_2\}\}&#10;$$&lt;p&gt;接着，我们继续检查 $C=\{D_0,D_1,D_2\}$,按照上面的流程，我们可以得到:&lt;br&gt;&#10;$D_0:(C,C)$ $D_1:(C,B)$ $D_2:(C,C)$&lt;/p&gt;&#10;&lt;p&gt;发现 $D_1$，不同因此继续拆分: $P_2 = \{\{D_4\},\{D_3\},\{D_1\},\{D_0,D_2\}\}$&lt;/p&gt;&#10;&lt;p&gt;最后，我们可以发现 $D_0,D_2$显然是不可拆分的，他们是等价的，因此我们得到了最小的DFA状态:&lt;/p&gt;&#10;$$&#10;S_0 = \{D_0,D_2\},S_1=\{D_1\},S_2=\{D_3\},S_3=\{D_4\}&#10;$$&lt;p&gt;其中 $S_3$是终态，我们可以得到状态转移表格:&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;状态&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;a&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;b&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;$S_0$&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;$S_1$&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;$S_0$&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;$S_1$&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;$S_1$&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;$S_2$&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;$S_2$&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;$S_1$&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;$S_3$&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;$S_3^*$&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;$S_1$&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;$S_0$&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;画成DFA就是&lt;/p&gt;&#10;&lt;p&gt;&lt;img alt="最小化后的 DFA" class="gallery-image" data-flex-basis="653px" data-flex-grow="272" height="406" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://july-h5kf3.github.io/p/compiler-ch1-lexical-analysis/dfa-minimized.png" srcset="https://july-h5kf3.github.io/p/compiler-ch1-lexical-analysis/dfa-minimized_hu_76083d304868456a.png 800w, https://july-h5kf3.github.io/p/compiler-ch1-lexical-analysis/dfa-minimized.png 1106w" width="1106"&gt;&lt;/p&gt;&#10;</description></item></channel></rss>