Javascript 简单的递归下降解析器?

Javascript 简单的递归下降解析器?,javascript,parsing,templates,lexer,tokenize,Javascript,Parsing,Templates,Lexer,Tokenize,我正在为编译成JS的模板语言编写解析器(如果相关的话)。我开始时使用了一些简单的正则表达式,这似乎是可行的,但正则表达式非常脆弱,所以我决定编写一个解析器。我首先编写了一个简单的解析器,通过从堆栈中推/弹出来记住状态,但事情一直在升级,直到我手头上有一个递归下降解析器 不久之后,我比较了以前所有解析方法的性能。递归下降解析器是迄今为止最慢的。我被困住了:是否值得为简单的事情使用递归下降解析器,或者我有理由走捷径?我很想使用纯正则表达式,它的速度快得离谱(几乎是RD解析器的3倍),但在某种程度上非

我正在为编译成JS的模板语言编写解析器(如果相关的话)。我开始时使用了一些简单的正则表达式,这似乎是可行的,但正则表达式非常脆弱,所以我决定编写一个解析器。我首先编写了一个简单的解析器,通过从堆栈中推/弹出来记住状态,但事情一直在升级,直到我手头上有一个递归下降解析器


不久之后,我比较了以前所有解析方法的性能。递归下降解析器是迄今为止最慢的。我被困住了:是否值得为简单的事情使用递归下降解析器,或者我有理由走捷径?我很想使用纯正则表达式,它的速度快得离谱(几乎是RD解析器的3倍),但在某种程度上非常粗糙且无法维护。我认为性能并不是非常重要,因为编译的模板是缓存的,但是递归下降解析器是每个任务的正确工具吗?我想我的问题更像是一个哲学问题:为了性能而牺牲可维护性/灵活性在多大程度上是值得的

先可读,后性能


因此,如果您的解析器使代码更具可读性,那么它就是正确的工具。

递归下降解析器可以非常快

这些标记通常由lexer组织,lexer使用正则表达式来识别提供给解析器的langauge标记。大多数处理源文本的工作是由lexer使用REs经常编译成的速度惊人的fsa逐个字符地完成的

与lexer看到字符的速率相比,解析器只偶尔看到标记,因此其速度通常并不重要。然而,当比较解析器和解析器的速度时,忽略标记lex所需的时间,递归下降解析器可以非常快,因为它们使用函数调用来实现解析器堆栈,与普通解析器在模拟堆栈上推送当前状态相比,函数调用已经非常有效

所以,你可以吃蛋糕,也可以吃蛋糕。对词素使用regexp。使用解析器(任何类型,递归下降都可以)处理词素。你应该对表演感到满意

这种方法也满足了其他答案的观察结果:以一种可维护的方式编写。我向您保证,Lexer/Parser分离可以很好地实现这一点

在多大程度上值得牺牲 可维护性/灵活性 表演

我认为把编写清晰的可维护代码作为首要任务是非常重要的。直到你的代码不仅表明它是一个瓶颈,而且你的应用程序性能也受到了影响,你应该总是把清晰的代码看作是最好的代码。 同样重要的是不要重新发明轮子。关于查看另一个解析器的评论是非常好的。通常会发现编写这样的例程的常见解决方案


当应用于可应用的事物时,回避是非常优雅的。根据我自己的经验,递归导致的代码速度慢是一个例外,而不是常态。

递归下降解析器应该更快

…或者你做错了什么

首先,您的代码应该分为两个不同的步骤。Lexer+解析器

一些在线参考示例将首先将整个语法标记化为一个大的中间数据结构,然后将其传递给解析器。既有利于示范,;不要这样做,它会使时间和内存复杂度加倍。相反,一旦lexer确定匹配,就通知解析器状态转换或状态转换+数据

至于lexer。这可能是您将发现当前瓶颈的地方。如果lexer与解析器完全分离,那么可以尝试在正则表达式和非正则表达式实现之间切换,以比较性能

无论如何,Regex都不比读取原始字符串快。默认情况下,它只是避免了一些常见错误。特别是,不必要的字符串对象创建。理想情况下,您的lexer应该扫描您的代码并生成一个包含零中间数据的输出,除了跟踪解析器内状态所需的最小值之外。记忆方面,您应该具备:

  • 原始输入(即源)
  • 解析器状态(例如isExpression、ISstatement、row、col)
  • 数据(例如AST、树、2D数组等)
例如,如果您当前的lexer匹配一个非终端,并逐个复制每个字符,直到它到达下一个终端;实际上,您正在为每个匹配的字母重新创建该字符串。请记住,字符串数据类型是不可变的,concat将始终创建一个新字符串。您应该使用指针算法或其他等效算法扫描文本

要解决此问题,您需要从非终端的startPos扫描到非终端的末尾,并仅在匹配完成时进行复制

默认情况下,Regex支持所有这些,这就是为什么它是编写lexer的首选工具。与其尝试编写一个解析整个语法的正则表达式,不如编写一个只关注匹配终端和非终端作为捕获组的正则表达式。跳过标记化,并将结果直接传递到解析器/状态机

这里的关键是,不要试图将Regex用作状态机。充其量,它只适用于正则(即Chomsky类型III,无堆栈)声明性语法——因此命名为正则表达式。例如,HTML是一种上下文无关(即Chomsky Type II,基于堆栈)的声明性语法,这就是为什么单独使用Rexeg永远不足以解析它的原因。您的语法,以及通常所有模板语法,都属于这一类。很明显,你已经达到了正则表达式的极限,所以你走上了正确的道路

仅将正则表达式用于标记化。如果你真的关心性能,重写你的lexer以消除任何和所有不必要的字符串