---
# System prepended metadata

title: 簡易計算欄位 - Parsing
tags: [計算欄位]

---

---
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
