--- robots: noindex, nofollow tags: 計算欄位 --- # 簡易計算欄位 - Parsing 在[前一篇][1]提到怎麼設計一個簡單的程式語言並求值。這篇會介紹一下怎麼用 JavaScript 生態系的工具來把原始碼 parse 成我們設計的 AST 。 這次選用的工具是 [parsimmon][2] ,他是一套 parser combinator 工具。不像 parser generator 會幫我們生出 parser 程式碼,它讓我們可以用小的 parser 組合出大的 parser ,所以才叫 combinator 。 ## `Str`, `Num` 和 `Field` parsers 先準備要用的 syntax node : ```javascript= import { Empty, Str, Num, Field, Binary, } from './syntax'; ``` 再準備針對個別 node 的 parser : ```javascript= const Parser = P.createLanguage({ str: () => string.map(Str), num: () => number.map(Num), }); const string = token(P.regexp(/"((?:\\.|.)*?)"/, 1)) .desc('string'); const number = token(P.regexp(/-?(0|[1-9][0-9]*)([.][0-9]+)?([eE][+-]?[0-9]+)?/)) .map(Number) .desc('number'); function token(parser) { return parser.skip(P.optWhitespace); } ``` 這裡的 `token` 是個一產生新 parser 的小工具。他會產生一個「 parse 我們要的東西,並且忽略後面任意空白」的新 parser 。 接著處理欄位參考,參考欄位的語法是 `{field}` : ```javascript= const Parser = P.createLanguage({ str: () => string.map(Str), num: () => number.map(Num), field: () => lbrace .then(symbol) .skip(rbrace) .map(Field), }); // 省略 string 和 number const lbrace = word('{'); const rbrace = word('}'); const symbol = token(P.regexp(/[a-zA-Z_-][a-zA-Z0-9_-]*/)) .desc('symbol'); function word(str) { return P.string(str).thru(token); } ``` `word` 也是一個產生新 parser 的小工具,它產生一個「 parse 特定字串,再忽略後面任意數量的空白」的新 parser 。 `lbrace.then(symbol).skip(rbrace).map(Field)` 用中文講就是:「先拿一個左括號,接著拿一個 symbol (並且不要左括號,把 symbol 當成結果),再跳過一個右括號,最後把結果變成一個 `Field` node 」。 有了這些 parser 後,我們可以告訴 `createLanguage` 說,我們的 expression parser `expr` 是由 `num`, `str` 和 `field` 這三種 term parser 組出來的: ```javascript= const Parser = P.createLanguage({ expr: (r) => P.alt( r.field, r.num, r.str, ).thru(parser => P.optWhitespace.then(parser)), str: () => string.map(Str), num: () => number.map(Num), field: () => lbrace .then(symbol) .skip(rbrace) .map(Field), }); ``` 用白話讀起來就是:「 `expr` 的開頭會有任意數量的空白,接著可能是 `field`, `num`, `str` 三種東西其中一種」。 ## `Binary` operator binary operator 比較難處理,例如我們會希望可以做到「先乘除後加減」,同時又得知道遇上 `str`, `num` 或 `field` 時,就不用再看它們是不是一個有著 binary operator 的式子: ```javascript= const Parser = P.createLanguage({ expr: (r) => P.alt( r.binary, r.field, r.num, r.str, ).thru(parser => P.optWhitespace.then(parser)), str: () => string.map(Str), num: () => number.map(Num), field: () => lbrace .then(symbol) .skip(rbrace) .map(Field), binary: (r) => { const notBinary = P.alt(r.func, r.field, r.listField, r.num, r.str); const base = lparenthesis.then(r.binary).skip(rparenthesis).or(notBinary); return [ [binaryLeft, [word('*'), word('/')]], [binaryLeft, [word('+'), word('-')]], ].reduce( (acc, [parser, ops]) => parser(P.alt(...ops), acc), base, ); }, }); const lparenthesis = word('('); const rparenthesis = word(')'); function binaryLeft(operator, operand) { return P.seqMap( operand, P.seq(operator, operand).many(), (first, rest) => rest.reduce((left, [op, right]) => Binary(op, left, right), first), ); } ``` 於是先準備一個 helper function `binaryLeft` ,它會幫我們產生「左結合的 binary operator parser 」。 接著再用 `reduce` 組合出:「先 parse 加減,再 parse 乘除,最後 parse 其他東西或者用 `()` 包起來的自己」這樣的 parser 。 這個順序是從外往內看,和一般講的「先乘除後加減」相反。但從 `reduce` 裡面看,可以看到我們是從 `base` 這個 parser 開始,先和乘除 parser 組合起來,再和加減 parser 組合起來,由內往外組合。 再把 `r.binary` 加到 `expr` parser 的最前面,希望我們的 parser 先試試看 binary operator parser 。 最後暴露一個 function `parse` 把 `Parser` 包裝起來,就能讓人使用我們的 parser 了: ```javascript= export function parse(str) { return Parser.expr.tryParse(str); } ``` ## 小結 現在我們知道該怎麼用 [parsimmon][2] 組合出想要的 parser ,也知道怎麼從小 parser 組合出大 parser 了。 [1]: https://hackmd.io/@gsscsid/form-editor-formula [2]: https://github.com/jneen/parsimmon