关于构建编译器扫描器的问题
Question about building a compiler scanner
我想从头开始构建一个编译器。所以在第一步中,我想构建一个扫描仪。但是我遇到了一个问题,我应该如何对待像“>=、==、<=”这样的关系运算符?我应该简单地将它们视为 "> then =, = then =, < then =" 还是作为一个整体?顺便问一下,“++”和“--”怎么样?感谢您的帮助!
扫描算法通常设计为匹配最大可能的标记。为了做到这一点,他们采用了“最大咀嚼”算法,只要遇到错误就会回溯,并通过重置状态继续。
词法分析器最简单的实现策略是构建一个 DFA,对每个标记的正则表达式的并集建模。因此,如果您的语言的标记由由空格分隔的 =
或 ===
(但不是 ==
)组成(我将明确为该答案建模为 _
),那么您的词法分析器基本上会使用接受 =|===|__*
的 DFA 的状态(注意空格规则是“至少一个 _
”、_+
)。
如您所见,有一个中间状态(您会在输入 ==
时到达,但不会接受,因为它不是接受状态)。因此,假设您已经计算了 DFA(实际上,每个输入字符都有压缩范围和其他通用谓词的快捷方式),您可以开始编写状态机。
DFA 可以写成一个循环,对于每个状态(使用 switch
语句和每个状态 case
),根据源缓冲区中的当前字符仔细检查当前状态。如果转换有效 - 即当前状态 w/ 当前输入字符作为 DFA 中的传出箭头存在 - 您将当前状态设置为该状态(并跟踪它是否正在接受)。
为了使这项工作以最大的咀嚼方式进行,您在输入缓冲区中维护两个偏移量,previous
和 current
。最初,它们都是 0。您前进 current
并在每次转换期间跟踪最后的接受状态。这个想法是,如果在任何时候都不存在这样的转换,您可以 backtrack/rewind 到最后一个有效的接受状态。因此,如果不存在这样的转换,您可以倒带,产生一个令牌,然后更新之前的(以匹配当前的、错误的、偏移量),并将状态重置为初始状态。
假设我们正在对 ===____=
进行词法分析。词法分析器将处于初始状态,参见 =
,然后进入中间(接受)状态。从那里,它将看到下一个 =
并决定进入下一个(非接受)状态。然后,它会看到最终的 =
并进入该链上的最终接受状态。一旦它看到 ===
,它将通过注意到 ===
和 _
的接受状态没有有效的转换来看到新词素的开始。所以它会产生 ===
(通过取长度为 current-prev
的子串,从 prev
开始),重置机器,然后开始检查 _
的序列初始状态。类似的故事会发生 ____
和 =
(使用特殊的 EOF
标记,当输入耗尽时所有词法分析器都倾向于使用该标记)。
我在这里描述的是最大 munch 算法的一种简单形式。这很简单,因为倒带永远不需要超过一个输入令牌。描述了您可能希望进一步回溯的情况,以及由于巧妙构建的回溯场景,朴素算法(执行有限簿记)可能具有最坏情况 O(n^2) 时间复杂度。
长话短说,您通过确保您的词法分析器识别两者但选择消耗尽可能大的(接受)输入来区分截断其他标记的标记。您可以通过在 DFA 上跟踪该算法并将两个偏移量保持到您正在分析的输入中来试验该算法。您可以阅读有关最大咀嚼 here 的更多信息。另一个有用的资源是描述如何使该算法线性化时间的论文,“Maximal-Munch”线性标记化
Time - 我在上面描述的算法及其改进位于第 5 页,尽管描述有所不同。
我想从头开始构建一个编译器。所以在第一步中,我想构建一个扫描仪。但是我遇到了一个问题,我应该如何对待像“>=、==、<=”这样的关系运算符?我应该简单地将它们视为 "> then =, = then =, < then =" 还是作为一个整体?顺便问一下,“++”和“--”怎么样?感谢您的帮助!
扫描算法通常设计为匹配最大可能的标记。为了做到这一点,他们采用了“最大咀嚼”算法,只要遇到错误就会回溯,并通过重置状态继续。
词法分析器最简单的实现策略是构建一个 DFA,对每个标记的正则表达式的并集建模。因此,如果您的语言的标记由由空格分隔的 =
或 ===
(但不是 ==
)组成(我将明确为该答案建模为 _
),那么您的词法分析器基本上会使用接受 =|===|__*
的 DFA 的状态(注意空格规则是“至少一个 _
”、_+
)。
如您所见,有一个中间状态(您会在输入 ==
时到达,但不会接受,因为它不是接受状态)。因此,假设您已经计算了 DFA(实际上,每个输入字符都有压缩范围和其他通用谓词的快捷方式),您可以开始编写状态机。
DFA 可以写成一个循环,对于每个状态(使用 switch
语句和每个状态 case
),根据源缓冲区中的当前字符仔细检查当前状态。如果转换有效 - 即当前状态 w/ 当前输入字符作为 DFA 中的传出箭头存在 - 您将当前状态设置为该状态(并跟踪它是否正在接受)。
为了使这项工作以最大的咀嚼方式进行,您在输入缓冲区中维护两个偏移量,previous
和 current
。最初,它们都是 0。您前进 current
并在每次转换期间跟踪最后的接受状态。这个想法是,如果在任何时候都不存在这样的转换,您可以 backtrack/rewind 到最后一个有效的接受状态。因此,如果不存在这样的转换,您可以倒带,产生一个令牌,然后更新之前的(以匹配当前的、错误的、偏移量),并将状态重置为初始状态。
假设我们正在对 ===____=
进行词法分析。词法分析器将处于初始状态,参见 =
,然后进入中间(接受)状态。从那里,它将看到下一个 =
并决定进入下一个(非接受)状态。然后,它会看到最终的 =
并进入该链上的最终接受状态。一旦它看到 ===
,它将通过注意到 ===
和 _
的接受状态没有有效的转换来看到新词素的开始。所以它会产生 ===
(通过取长度为 current-prev
的子串,从 prev
开始),重置机器,然后开始检查 _
的序列初始状态。类似的故事会发生 ____
和 =
(使用特殊的 EOF
标记,当输入耗尽时所有词法分析器都倾向于使用该标记)。
我在这里描述的是最大 munch 算法的一种简单形式。这很简单,因为倒带永远不需要超过一个输入令牌。描述了您可能希望进一步回溯的情况,以及由于巧妙构建的回溯场景,朴素算法(执行有限簿记)可能具有最坏情况 O(n^2) 时间复杂度。
长话短说,您通过确保您的词法分析器识别两者但选择消耗尽可能大的(接受)输入来区分截断其他标记的标记。您可以通过在 DFA 上跟踪该算法并将两个偏移量保持到您正在分析的输入中来试验该算法。您可以阅读有关最大咀嚼 here 的更多信息。另一个有用的资源是描述如何使该算法线性化时间的论文,“Maximal-Munch”线性标记化 Time - 我在上面描述的算法及其改进位于第 5 页,尽管描述有所不同。