编译原理从零到一 写一个 JSON parser

最近在做 Chrome 插件的高级功能,突然想要试试可视化选择 key 的路径,再一想,咦,这不就是个编译原理的典型例题吗?

于是我就开始着手学编译原理了,主要参考 dalao 的以下两篇文章:

编译原理一共分为四个部分:词法分析,语法(文法)分析,语义分析,代码生成。

词法分析,就是逐个解析之后加上词性,语法分析则是将词法分析后的结果解析为抽象语法树,语义分析是在语法分析时针对具体的实例加上额外的动作解释。具体的可以见上面的链接。

JSON 的首页有非常完整的词法和语法分析的 token 列表甚至是流程图,所以可以省去了一些劳动。

在词法分析上,我们要首先抽象出哪些是我们 tokens,进行归类,然后一个个字符的进行 read() 。最简单的方法只要逐个阅读看看满足哪一个 token 的条件处理并存入就行了,上文的链接中也有词法分析器的介绍,写得非常清楚。

class Lexer {
  constructor() {
    this.chunk = '';
  }

  lex(input) {
    this.chunk = input;
    this.tokens = [];
    while(!this.readEOF()) {
      let consumed = this.readWhiteSpace()
                  || this.readString()
                  || this.readNumber()
                  || this.readBoolean()
                  || this.readNull()
                  || this.readLiteral()
      this.chunk = this.chunk.slice(consumed);
    }
    return this.tokens;
  }
  pushToken(type, value) {
    this.tokens.push([type, value]);
  }

  readEOF() {
    if (!this.chunk) {
      this.pushToken('EOF', '');
      return true;
    };
  }

  readWhiteSpace() {
    return this.chunk.match(/^\s*/)[0].length;
  }

  readString() {
    let match = this.chunk.match(/^"(?:[^"\\\x00-\x1F\x7F\x80-\x9F]|\\["\\/bfnrt]|\\u[0-9a-fA-F]{4})*"/);
    if (match) {
      this.pushToken('String', match[0]);
      return match[0].length;
    }
  }

  readNumber() {
    let match = this.chunk.match(/^-?[1-9]*\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/);
    if (match) {
      this.pushToken('Number', match[0]);
      return match[0].length;
    }
  }

  readBoolean() {
    let match = this.chunk.match(/^(?:true|false)/);
    if (match) {
      this.pushToken('Boolean', match[0].toLowerCase());
      return match[0].length;
    }
  }

  readNull() {
    let match = this.chunk.match(/^null/);
    if (match) {
      this.pushToken('Null', match[0]);
      return match[0].length;
    }
  }

  readLiteral() {
    const value = this.chunk.charAt(0);
    this.pushToken(value, value);
    return value.length;
  }
}

词法分析器的流程非常简单,只要一个不拉的列出 tokens 并且存入即可,之后我们就可以得到一个词法分析的结果数组。

parser 的部分原博并没有写,实际上也就是遍历并构造一颗抽象语法树,我们这里用自顶向下的构建法,深度优先遍历得出构造的结果。

从表面上来说和词法分析器是一样的,区别只是构造数组和构造树的区别——我们利用解析后的词法分析结果数组作为输入,逐个分析类型并且处理结果。

最后,我们需要做一些自定义的操作,比如我们想要格式化或者生成 JSON,就非常容易了:只需要根据结果的 token 类型匹配再进行一些操作即可:

class Parser {
  constructor() {
    this.tokens = [];
  }

  parse(input) {
    this.i = 0;
    this.tokens = new Lexer().lex(input);
    const value = this.getValue();
    if (!value) throw new TypeError('failed to parse JSON, expect value');
    return value;
  }

  format(input, indent = '  ') {
    const ast = this.parse(input);
    return this._format(ast, indent);
  }

  _format(node, indent) {
    switch(node.type) {
      case 'String':
        return JSON.stringify(JSON.parse(node.value));
      case 'Number':
      case 'True':
      case 'False':
      case 'Null':
        return node.value;
    }
    let [begin, end] = node.type === 'Object' ? ['{', '}'] : ['[', ']'];
    let output = begin;
    const innerMembers = node.value;
    if (!innerMembers.length) return output + end;
    output += '\n';
    innerMembers.forEach((item, index, arr) => {
      output += indent;
      if (node.type === 'Object') output += item.key.value + ': ';
      const value = node.type === 'Object' ?
          this._format(item.value, indent).replace(/\n/g, '\n' + indent) :
          this._format(item, indent).replace(/\n/g, '\n' + indent);
        output += value;
        if (index !== arr.length - 1) output += ',\n';
    });
    return output += '\n' + end;
  }

  getValue() {
    return this.readString() || this.readNumber() || this.readObject() || this.readArray() || this['true']() || this['false']() || this['null']();
  }

  readString() {
    const [tag, value] = this.tokens[this.i];
    if (tag === 'String') {
      this.i++;
      return { type: 'String', value };
    }
  }

  readNumber() {
    const [tag, value] = this.tokens[this.i];
    if (tag === 'Number') {
      this.i++;
      return { type: 'Number', value };
    }
  }

  readObject() {
    const [tag] = this.tokens[this.i];
    if (tag === '{') {
      this.i++;
      const members = this.readMembers() || [];
      const [tag] = this.tokens[this.i];
      if (tag !== '}') throw new TypeError('failed to parse object, expect "}"');
      this.i++;
      return { type: 'Object', value: members };
    }
  }

  readMembers() {
    let pair = this.readPair();
    if (!pair) return [];
    let members = [ pair ];
    while (true) {
      const [tag] = this.tokens[this.i];
      if (tag !== ',') break;
      this.i++;
      pair = this.readPair();
      members.push(pair);
    }
    return members;
  }

  readPair() {
    const string = this.readString();
    if (!string) throw new TypeError('failed to parse pair, expect string');
    const [tag] = this.tokens[this.i];
    if (tag !== ':') throw new TypeError('failed to parse pair, expect colon');
    this.i++;
    const value = this.getValue();
    if (!value) throw new TypeError('failed to parse pair, expect value');
    return { key: string, value };
  }

  readArray() {
    const [tag] = this.tokens[this.i];
    if (tag === '[') {
      this.i++;
      const elements = this.readElements() || [];
      const [tag] = this.tokens[this.i];
      if (tag !== ']') throw new TypeError('failed to parse object, expect "]"');
      this.i++;
      return { type: 'Array', value: elements };
    }
  }
  
  readElements() {
    let value = this.getValue();
    if (!value) return [];
    let elements = [ value ];
    while (true) {
      const [tag] = this.tokens[this.i];
      if (tag !== ',') break;
      this.i++;
      value = this.getValue();
      elements.push(value);
    }
    return elements;
  }

  'true'() {
    const [tag, value] = this.tokens[this.i];
    if (tag === 'Boolean' && value === 'true') {
      this.i++;
      return { type: 'True', value };
    }
  }

  'false'() {
    const [tag, value] = this.tokens[this.i];
    if (tag === 'Boolean' && value === 'false') {
      this.i++;
      return { type: 'False', value };
    }
  }

  'null'() {
    const [tag, value] = this.tokens[this.i];
    if (tag === 'Null') {
      this.i++;
      return { type: 'Null', value };
    }
  }
}

这样就完成了一个 JSON parser。

植入部分

如果您觉得文章不错,可以通过赞助支持我。

如果您不希望打赏,也可以通过关闭广告屏蔽插件的形式帮助网站运作。

标签: 成品, 源码, 知识, 代码段, 语法

添加新评论