六十行程式碼完成 四則運算 語法解析器

黃子毅發表於2018-09-11

syntax-parser 是完全利用 JS 編寫的詞法解析+語法解析引擎,所以完全支援在瀏覽器、NodeJS 環境執行。

它可以幫助你快速生成 詞法解析器,亦或進一步生成 語法解析器,將字串解析成語法樹,語法解析器還支援下一步智慧提示功能,輸入游標位置,給出輸入推薦。

目前 syntax-parser 功能逐漸穩定,核心效能還在逐步優化中,我們會利用 syntax-parser 引擎的能力,完成一些令人驚喜的小 DEMO,如果與你的業務場景恰好契合,歡迎使用!

這次的 DEMO 是:利用 syntax-parser 快速完成四則運算語法解析器!

1. 生成詞法解析器

通過下面 20 行配置,生成一個能解析英文、數字、加減乘除、左右括號的詞法解析器,so easy!

import { createLexer } from 'syntax-parser'

const myLexer = createLexer([
  {
    type: 'whitespace',
    regexes: [/^(\s+)/],
    ignore: true
  },
  {
    type: 'word',
    regexes: [/^([a-zA-Z0-9]+)/] // 解析數字
  },
  {
    type: 'operator',
    regexes: [
      /^(\(|\))/, // 解析 ( )
      /^(\+|\-|\*|\/)/ // 解析 + - * /
    ]
  }
]);
複製程式碼

我們可以使用 myLexer 將字串解析為一個個 Token:

myLexer('1 + 2 - 3 * b / (x + y)')
複製程式碼

不過這次的目的是生成語法樹,所以我們會把 myLexer 作為引數傳給語法解析器。

2. 生成語法解析器

五個文法,20 行程式碼搞定,表示四則運算的文法,可以參考 此文

利用 chain ,可以高效表示每一個文法表示式要匹配的字串、表示匹配次數,還支援嵌入新的文法函式。這些相互依賴的文法組成了一個文法鏈條,完整表達了四則運算的邏輯:

import { chain, createParser, many, matchTokenType } from 'syntax-parser'

const root = () => chain(term, many(addOp, root))(parseTermAst);

const term = () => chain(factor, many(mulOp, root))(parseTermAst);

const mulOp = () => chain(['*', '/'])(ast => ast[0].value);

const addOp = () => chain(['+', '-'])(ast => ast[0].value);

const factor = () => chain([
    chain('(', root, ')')(ast => ast[1]),
    chain(matchTokenType('word'))(ast => ast[0].value)
])(ast => ast[0]);

const myParser = createParser(
  root, // Root grammar.
  myLexer // Created in lexer example.
);
複製程式碼

createParser 函式第一個引數接收根文法表示式,第二個引數是詞法解析器,我們將上面建立的 myLexer 傳入。 parseTermAst 函式單獨提出來,目的是輔助生成語法樹,一共 20 行程式碼:

const parseTermAst = (ast: any) =>
  ast[1]
    ? ast[1].reduce(
        (obj: any, next: any) =>
          next[0]
            ? {
                operator: next[0],
                left: obj || ast[0],
                right: next[1]
              }
            : {
                operator: next[1] && next[1].operator,
                left: obj || ast[0],
                right: next[1] && next[1].right
              },
        null
      )
    : ast[0];
複製程式碼

這個函式是為了將語法樹變得更規整,否則得到的 AST 解析將會是陣列,而不是像 left right operator 這麼有含義的物件。

PS:本文的 DEMO 沒有考慮乘除高優先順序問題。

3. 執行詞法解析器

最後得到的 myParser 就是語法解析器了!直接執行就能拿到語法樹結果!

const result = myParser('1 + 2 - (3 - 4 + 5) * 6 / 7');
console.log(result.ast)
複製程式碼

我們列印出語法樹,執行結果如下:

{
  "operator": "/",
  "left": {
    "operator": "-",
    "left": {
      "operator": "+",
      "left": "1",
      "right": "2"
    },
    "right": {
      "operator": "*",
      "left": {
        "operator": "+",
        "left": {
          "operator": "-",
          "left": "3",
          "right": "4"
        },
        "right": "5"
      },
      "right": "6"
    }
  },
  "right": "7"
}
複製程式碼

4. 錯誤提示

不僅語法樹,我們構造一個錯誤的輸入試試!

const result = myParser('1 + 2 - (3 - 4 + 5) * 6 / ');
console.log(result.error)
複製程式碼

這次我們列印錯誤資訊:

{
  "suggestions": [
    {
      "type": "string",
      "value": "("
    },
    {
      "type": "special",
      "value": "word"
    }
  ],
  "token": {
    "type": "operator",
    "value": "/",
    "position": [
      24,
      25
    ]
  },
  "reason": "incomplete"
}
複製程式碼

精準的提示了最後一個 / 位置不完整,建議是填寫 ( 或者一個單詞。這都是根據文法自動生成的建議,提示不多一個,不少一個!

5. 任意位置輸入提示

最精髓的功能到了,這個語法解析器就是為了做自動提示,所以支援多傳一個引數,告訴我當前你游標的位置:

const result = myParser('1 + 1', 5)
console.log(result.nextMatchings)
複製程式碼

假設語句寫到這裡,我們游標位置定位到 5 的位置,也就是最後一個 1 後面,nextMatchings 屬性會告訴我們後面的可能情況:

[
  {
    "type": "string",
    "value": "-"
  },
  {
    "type": "string",
    "value": "+"
  },
  {
    "type": "string",
    "value": "/"
  },
  {
    "type": "string",
    "value": "*"
  }
]
複製程式碼

6. 結合業務,用在文字編輯器

筆者拿 monaco-editor 舉例,利用上面的語法樹解析,我們可以輕鬆完成下面的效果:

游標智慧補全

image.png | left | 747x124

錯誤提示

image.png | left | 747x182

無論是智慧補全,還是錯誤提示都是 100% 精準無誤的(根據上面編寫的文法表示式)。

相比普通的語法解析器在解析錯誤時直接丟擲錯誤,syntax-parser 不僅提供語法樹,還能根據文法智慧提示游標位置的輸入推薦,哪怕是輸入錯誤的情況下,是不是解決了大家的痛點呢?如果覺得好用,歡迎給 syntax-parser 提 建議 或者 pr

相關文章