美文网首页Other
[Other] 递归下降解析器(模式)

[Other] 递归下降解析器(模式)

作者: 何幻 | 来源:发表于2021-12-25 21:56 被阅读0次

很多人工编写的解析器,会采用 “递归下降” 的解析方法,例如 typescriptswcbabel-parser 等等。

这可能是 “递归下降” 方法所编写的代码,更利于调整,结构上更加清晰。
虽然这种方法无法处理 左递归文法
但大多数场景我们都可以采用 左递归消除 的办法,对文法进行改写。

本文用几个例子,简单总结一下 “递归下降解析器” 的常用写法。

一、list 解析器

1. 字符串、文法 和 语法树

递归下降解析器是一种自上而下的解析器,由一组相互递归的程序(或等价的非递归程序)构建而成,其中每个程序都实现了文法中的一个非终结符。
—— 递归下降解析器

这样说可能比较抽象,我们先写一个简单的 list 解析器,来说明情况。
完整的代码如下:github: thzt/tiny-parser#list

它可以用来解析如下字符串:

(ab cd 
  (ee ff gg)
)

文法如下:

list       = '(' elements ')'
elements   = element | element elements
element    = identifier | list

identifier = [a-z]+
whitespace = ' ' | '\n'

它是指,list 由 括号包围的 elements 构成,而 elements 则是由 一个或多个 element 组成。
最后,element 或者是 一个标识符 identifier,或者是一个 list(又回到了 list,表明结构上是递归的)。

比如,上文提到的字符串,

(ab cd 
  (ee ff gg)
)

ab cd ee ff gg 都是 identifier(ee ff gg) 是一个 list
(ab cd (ee ff gg)) 也是一个 list

解析的过程,其实是根据文法,将字符串整理成数据结构的过程。
整理后的数据结构(语法树)如下,


题外话:
我们看到 字符串 在结构上是递归的,所以很自然的我们会想到,用递归的程序来解决问题。这个其实是跟 上下文无关语言的 泵引理 有关,它指出了 上下文无关语言 在结构上的自相似性。上下文无关语言中的字符串只要足够长,它的某一部分总能 “坍缩” 到相同的非终结符上去(即语法树的树高不会无限增加)。

2. TypeScript & namespace

为了方便编写,我们使用了 TypeScript,且用到了不太常见的 namespace 特性,
所以这里多解释一下,

namespace Parser {
  // ...
}

namespace 会编译成一个立即执行的函数(一个闭包),
namespace 导出(export) 的函数,就是闭包向外透出的值。
编译产物 可参考这里 github: thzt/tiny-parser#list out/index.js

var Parser;
(function (Parser) {  // namespace 的编译结果
    // ...
    function parse(code) {  }
    Parser.parse = parse;  // 导出的 parse 方法
    // ...
})(Parser || (Parser = {}));

3. 解析器的结构

解析器 主要有 4 个部分组成:

  • (1)解析器的初始化:源码、初始位置、结束位置
  • (2)递归解析的入口:开始调用一组互相递归的函数,从 parseList 开始
  • (3)一些互相递归的函数:parseList/parseElements/parseElement
  • (4)词法分析器:用来返回 token(单词)

可参考如下代码:github: thzt/tiny-parser#list src/parser.ts

namespace Parser {
  let sourceText: string;
  let pos: number;
  let end: number;
  let token;

  export function parse(code) {
    // 1. 解析器的初始化
    sourceText = code;
    pos = 0;
    end = sourceText.length;

    // 2. 递归解析的入口
    nextToken();
    assert(SyntaxKind.LeftBracket);
    const body = parseList();
    nextToken();
    assert(SyntaxKind.EndOfFile);

    return body;
  }

  // 3. 一些互相递归的函数:parseList/parseElements/parseElement
  function parseList() {  }
  function parseElements() {  }
  function parseElement() {  }

  // 4. 词法分析器
  function nextToken() {  }
}

这里值得一提的是,互相递归的那些函数,其实是跟 文法中的 产生式 一一对应的。

list       = '(' elements ')'                   -> parseList
elements   = element | element elements         -> parseElements
element    = identifier | list                  -> parseElement

所以只要我们写出 文法,这些互相递归的函数也就有了。
这也是很多 递归下降的 “解析器生成器” 常用的代码生成办法,例如 peg.js

4. 词法分析器(nextToken)

以上代码中的词法分析器,就是一个函数 nextToken,其实非常的容易编写。
github: thzt/tiny-parser#list src/parser.ts#nextToken
总共也就 30 多行代码,

function nextToken() {
  while (true) {
    if (pos >= end) {
      return token = createNode(SyntaxKind.EndOfFile, pos, pos, null);
    }

    let ch = sourceText.charAt(pos);
    switch (ch) {
      case '(':
        return token = createNode(SyntaxKind.LeftBracket, pos++, pos, '(');

      case ')':
        return token = createNode(SyntaxKind.RightBracket, pos++, pos, ')');

      case ' ':
      case '\n':
        pos++;
        continue;

      default:
        if (isIdentifierStart(ch)) {
          return token = scanIdentifier();
        }

        return token = createNode(SyntaxKind.RightBracket, pos++, pos, ch);
    }
  }
}

它会逐个字符进行判断,将不同类型的 “单词” 切分开,例如,

(ab cd 
  (ee ff gg)
)

会被切分成:(abcd(eeffgg)) 这样一系列 token。
效果是:ab 被合并成了一个,空格和换行符被忽略了。

5. 互相递归的函数

上文我们提到了互相递归的函数:parseList/parseElements/parseElement,
是跟文法中的 产生式 一一对应的,

list       = '(' elements ')'                   -> parseList
elements   = element | element elements         -> parseElements
element    = identifier | list                  -> parseElement

其实,不止是函数名,函数内部的编写方式,也可以根据产生式来生成(有套路)。
我们分别来看一下。

  // list       = '(' elements ')'
  function parseList() {  // 进来时当前 token 是 '('
    const elements = parseElements();  // 这里解析 `elements`
    const rb = nextToken();  // 这里的 token 为 `)`

    // 以上解析完了 `(` elements ')',返回。我们的 ast 中只保存了 elements
    return elements;
  }
  // elements   = element | element elements
  function parseElements() {
    const elements = [];  // 由一个或多个 element 组成,所以用个数组来存

    while (true) {
      nextToken();
      if (isElementsTeminate()) {  // 向前看一个字符,判断后面还是不是 element
        break;
      }

      const element = parseElement();  // 解析 element
      elements.push(element);
    }

    return elements;  // 将 elements 数组返回
  }
  // element    = identifier | list
  function parseElement() {
    if (token.kind === SyntaxKind.LeftBracket) {  // 分支判断
      return parseList();  // 如果是 list 就是递归调用 parseList
    }

    console.assert(token.kind === SyntaxKind.Identifier);  // 否则就把标识符返回
    return token;
  }

parseList 会调用 parseElementsparseElements 会调用 parseElement
最后 parseElement 又有可能递归调用 parseList
这就是一组互相递归的函数了。

二、html 解析器

解析完了最简单的 list 之后,我们来看一个较为复杂的例子,

<div id="tiny" class="parser">
  <span>
    abc
  </span>
</div>

文法如下:

html       = '<' identifier props '>' html '</' identifier '>' | identifier
props      = '' | identifier '=' '"' identifier '"'

identifier = [a-z]+
whitespace = ' ' | '\n'

解析器仍然是由 4 部分组成:

  • (1)解析器的初始化
  • (2)递归解析的入口
  • (3)一些互相递归的函数
  • (4)词法分析器
    所不同的是,互相递归的函数变了,变成了 parseHtml/parseProps
namespace Parser {
  let sourceText: string;
  let pos: number;
  let end: number;
  let token;

  export function parse(code) {
    // 1. 解析器的初始化
    sourceText = code;
    pos = 0;
    end = sourceText.length;

    // 2. 递归解析的入口
    nextToken();  // <
    assert(SyntaxKind.LeftBracket);
    const html = parseHtml();

    nextToken();  // eof
    assert(SyntaxKind.EndOfFile);

    return html;
  }

  // 3. 一些互相递归的函数:parseList/parseElements/parseElement
  function parseHtml() {  }
  function parseProps() {  }

  // 4. 词法分析器
  function nextToken() { }
}

完整的代码:github: thzt/tiny-parser#html

三、四则运算 解析器

文法:

Expr -> Term | Term + Expr
Term -> Factor | Factor * Term
Factor -> NUMBER | ( Expr )

字符串:

((1 + 2) * 3 + 4) * (5 + 6)

语法树:


完整的代码:github: thzt/tiny-parser#arithmetic

参考

相关文章

  • [Other] 递归下降解析器(模式)

    很多人工编写的解析器,会采用 “递归下降[https://zh.wikipedia.org/zh-hans/%E9...

  • ANTLR快餐教程(2) - ANTLR其实很简单

    ANTLR其实很简单 ANTLR是通过递归下降的方式来解析一个语法的。所谓的递归下降,其实很简单,不过就是一些模式...

  • 解析器模式 与 免费公交卡

    解析器模式,它能用在什么地方呢? 解析器模式,它是如何解析的? 解析器模式,它有哪些关键要素? 解析器模式(Int...

  • 递归下降总结

    先推荐一些编译原理的材料: mooc上斯坦福的compilers课程 《形式语言与自动机导论》(An Introd...

  • “领域规则”模式

    “领域规则”模式 解析器模式 模式定义 类图 要点总结

  • 奇异递归模板模式(Curiously Recurring Tem

    奇异递归模板模式(Curiously Recurring Template Pattern) 奇异递归模板模式(C...

  • 编译原理——递归分析翻译

    递归解析器中每个非终结符A都有产生式A。我们可以将解析器扩展为翻译器,如下所示: a)产生式A的参数是非终结点A的...

  • 编译原理带属性文法的LL1递归下降子程序构造

    【实验名称】 带属性文法的递归下降子程序 【实验目的】 实现该文法的递归下降子程序 “ 属性文法把二进制无符号定点...

  • 解析器模式

    一、模式简介 定义:给分析对象定义一个语言,并定义该语言的文法表示,再设计一个解析器来解释语言中的句子,用编译语言...

  • 解析器模式

    解析器模式-定义? 定义:给定一个语言,定义它的文法的一种表达式,并且定义一个解析器,该解析器使用该表示来解析语言...

网友评论

    本文标题:[Other] 递归下降解析器(模式)

    本文链接:https://www.haomeiwen.com/subject/rlfjqrtx.html