四則運算表達式如何轉換成AST

做者:吳冠禧javascript

0 前言

曉強哥在他的上篇文章裏介紹了 裏面提到得到抽象語法樹的過程爲:代碼 => 詞法分析 => 語法分析 => AST,抱着深究技術細節的目的,我決定研究這裏的詞法分析和語法分析,寫一個簡單的四則運算表達式轉換成AST的方法,因而就有了下面的內容。html

1 人類和計算機對於表達式的見解是不一樣的

人類習慣 a + b 這種表達叫作「中序表達式」,優勢是比較簡單直觀,缺點是要用一堆括號來肯定優先級 (a + b) * (c + d)java

這裏說簡單直觀是相對人類的思惟結構來講的,對計算機而言中序表達式是很是複雜的結構。node

爲了計算機計算方便,咱們須要將中序表達式轉換成樹形結構,也就是「抽象語法樹AST」。python

2 javascript 與抽象語法樹 AST

咱們知道,幾乎任何語言中,代碼在 "編譯"(解釋型語言在運行時也有編譯的過程) 的過程當中,都會生成一種樹狀的中間狀態,這就是 AST。有些語言會直接把相似 AST 的語法暴露給程序員(例如:lisp、elixir、python等)。可是 javascript 並無這個能力,可是咱們能夠用 javascript 自身實現這個過程。git

得到抽象語法樹的過程爲:代碼(字符串) => 詞法分析(Lexer)=> Tokens => 語法分析(Parser) => AST程序員

3 詞法分析(Lexer)

詞法分析有點像中文的分詞,就是將字符串流根據規則生成一個一個的有具體意義的 Token ,造成 Token 流,而後流入下一步。github

咱們看一個簡單的例子,微信

1 + 2.3
複製代碼

很明顯這個表達式是能夠分紅三個 Token ,分別是 1 , + , 2.3函數

詞法分析這裏,咱們能夠用有限狀態機來解決。

3.1 有限狀態機

絕大多數語言的詞法部分都是用狀態機實現的,咱們下面就畫出有限狀態機的圖形,而後根據圖形直觀地寫出解析代碼,整體圖大概是這樣。

ast

下面拆開細看。

3.2 開始(start)狀態

ast

狀態機的初始狀態是 start

start 狀態下輸入數字(0 ~ 9)就會遷移到 inInt 狀態。

start 狀態下輸入符號(.)就會遷移到 inFloat 狀態。

start 狀態下輸入符號(+ - * /)就會輸出 「符號 Token」 ,並回到 start 狀態。

start 狀態下輸入 EOF 就會輸出 「EOF Token」 ,並回到 start 狀態。

代碼大概是這個樣子:

start(char) {
    // 數字
    if (["0","1","2","3","4","5","6","7","8","9"].includes(char)) {
      this.token.push(char);
      return this.inInt;
    }
    // .
    if (char === "."){
      this.token.push(char);
      return this.inFloat;
    }
    // 符號
    if (["+","-","*","/"].includes(char)) {
      this.emmitToken("SIGN", char);
      return this.start;
    }
    // 結束符
    if (char === EOF){
      this.emmitToken("EOF", EOF);
      return this.start;
    }
  }
複製代碼

3.3 在整數(inInt)狀態

start 狀態下輸入輸入數字(0 ~ 9)就會遷移到 inInt 狀態。

ast

inInt 狀態下輸入輸入符號(.)就會遷移到 inFloat 狀態。

inInt 狀態下輸入數字(0 ~ 9)就繼續留在 inInt 狀態。

inInt 狀態下輸入非數字和.(0 ~ 9 .)就會就會輸出 「整數 Token」 ,並遷移到 start 狀態。

代碼:

inInt(char) {
    if (["0","1","2","3","4","5","6","7","8","9"].includes(char)) {
      this.token.push(char);
      return this.inInt;
    } else if (char === '.') {
      this.token.push(char);
      return this.inFloat;
    } else {
      this.emmitToken("NUMBER", this.token.join(""));
      this.token = [];
      return this.start(char); // put back char
    }
  }
複製代碼

3.4 在浮點數(inFloat)狀態

start 狀態下輸入符號(.)就會遷移到 inFloat 狀態。

inInt 狀態下輸入輸入符號(.)就會遷移到 inFloat 狀態。

ast

inFloat 狀態下輸入數字(0 ~ 9)就繼續留在 inFloat 狀態。

inFloat 狀態下輸入非數字(0 ~ 9 )就會就會輸出 「浮點數 Token」,並遷移到 start 狀態。

代碼:

inFloat(char) {
    if (["0","1","2","3","4","5","6","7","8","9"].includes(char)) {
      this.token.push(char);
      return this.inFloat;
    } else if (char === ".") {
      throw new Error("不能出現`..`");
    } else {
      if (this.token.length === 1  && this.token[0] === ".") throw new Error("不能單獨出現`.`");
      this.emmitToken("NUMBER", this.token.join(""));
      this.token = [];
      return this.start(char); // put back char
    }
  }
複製代碼

3.5 輸出的 Token 種類 和定義

我將 「浮點數 Token」「整數 Token」 合併爲 [NUMBER Token] , 其餘的 Token 還有 「SIGN Token」「EOF Token」

Token 的 定義:

interface Token{
    type:String,
    value:String,
  }
複製代碼

3.6 完整的 Lexer 代碼

const EOF = Symbol('EOF');

  class Lexer {
    constructor(){
      this.token = []; // 臨時 token 字符存儲
      this.tokens = []; // 生成的正式 token
      // state 默認是 start 狀態,後面經過 push 函數實現狀態遷移
      this.state = this.start;
    }
    start(char) {
      // 數字
      if (["0","1","2","3","4","5","6","7","8","9"].includes(char)) {
        this.token.push(char);
        return this.inInt;
      }
      // .
      if (char === "."){
        this.token.push(char);
        return this.inFloat;
      }
      // 符號
      if (["+","-","*","/"].includes(char)) {
        this.emmitToken("SIGN", char);
        return this.start;
      }
      // 結束符
      if (char === EOF){
        this.emmitToken("EOF", EOF);
        return this.start;
      }
    }
    inInt(char) {
      if (["0","1","2","3","4","5","6","7","8","9"].includes(char)) {
        this.token.push(char);
        return this.inInt;
      } else if (char === '.') {
        this.token.push(char);
        return this.inFloat;
      } else {
        this.emmitToken("NUMBER", this.token.join(""));
        this.token = [];
        return this.start(char); // put back char
      }
    }
    inFloat(char) {
      if (["0","1","2","3","4","5","6","7","8","9"].includes(char)) {
        this.token.push(char);
        return this.inFloat;
      } else if (char === ".") {
        throw new Error("不能出現`..`");
      } else {
        if (this.token.length === 1  && this.token[0] === ".") throw new Error("不能單獨出現`.`");
        this.emmitToken("NUMBER", this.token.join(""));
        this.token = [];
        return this.start(char); // put back char
      }
    }
    emmitToken(type, value) {
      this.tokens.push({
        type,
        value,
      })
    }
    push(char){
      // 每次執行 state 函數都會返回新的狀態函數,實現狀態遷移
      this.state = this.state(char);
      return this.check();
    }
    end(){
      this.state(EOF);
      return this.check();
    }
    check(){
      // 檢測是否有 token 生成並返回。
      const _token = [...this.tokens];
      this.tokens = [];
      return _token;
    }
    clear(){
      this.token = [];
      this.tokens = [];
      this.state = this.start;
    }
  }

  const lexer = new lexer();

  const input = `1 + 2.3`;

  let tokens = [];

  for (let c of input.split('')){
    tokens = [...tokens,...lexer.push(c)];
  }

  tokens = [...tokens,...lexer.end()];
複製代碼

效果以下圖:

ast

自此,咱們成功實現了詞法分析,後面進入到語法分析。

4 語法分析(Parser)

前面的詞法分析,已經將字符串劃分紅一個個有意義的 Token 進入到語法分析(Parser)。語法分析在編譯原理裏面屬於比較高深的學問,我是沒有怎麼看懂。但總的來講就是把 Token流 組裝成 AST , AST 的結構是既定的,後面我就經過對不一樣節點制定不一樣規則把 AST 組裝起來。

4.1 定義 AST 結構 和 節點(Node)

簡單來講 AST 就是一棵樹形結構,由節點(Node)和 葉子(字面量 Literal )組成,節點 下面能夠鏈接 其餘節點 或者 字面量。最頂端的節點就是 根節點。

ast

節點的定義就是一個簡單的 javascript Object

interface Node {
  type:string,
  children:[],// children棧 裏面能夠是 Node 或者 Literal
  maxChildren:number,
}
複製代碼

4.2 棧 和 根節點(Root)

語法分析(Parser)這裏,我使用的是一個棧結構,每來一個 Token 就入棧,而後經過必定的規則組裝 AST。

第一步就是壓入 根節點 <Root>

function RootNode(){
  return {
    type:"ROOT",
    children:[],
    maxChildren:0,
  }
}

const stack = [RootNode()];
複製代碼

ast

4.3 通用規則

在說明不一樣類型節點的規則前,先說一下通用規則。

    1. 沒有後代的節點(NoChildrenNode),就是節點的 maxChildren 屬性爲 0。
    1. 非滿的節點(NotFullNode),就是節點的 maxChildren 屬性大於 0,並且其 children.length < maxChildren。
    1. 滿的節點(FullNode),就是節點的 maxChildren 屬性大於 0,並且其 children.length >= maxChildren。

對應的3個函數:

function isFullNode(node){
    if (isNoChildrenNode(node)) return false;
    return node && node.children && node.children.length >= node.maxChildren;
  }

  function isNotFullNode(node){
    if (isNoChildrenNode(node)) return false;
    return node && node.children && node.children.length < node.maxChildren;
  }

  function isNoChildrenNode(node){
    return node.maxChildren === 0;
  }
複製代碼

4.4 數字節點(Number)

定義一個數字節點,其children就是 數字字面量。

function NumberNode(){
  return {
    type:"NUMBER",
    children:[...arguments],
    maxChildren:1, // 只能有一個 child
  }
}
複製代碼

ast

4.5 數字節點的規則

    1. 找到棧頂 top
    1. 和數字節點 number
    1. top 不能是滿項
    1. 若是 top 爲非滿的節點,number push 到 top.children
    1. 不然(top 是沒有後代的節點),number 壓棧
const top = stack[stack.length - 1]; // 棧頂
  if (token.type === "NUMBER") {
    // 1 1 
    // 1 + 1 1
    if (isFullNode(top)) throw new Error("數字前一項不能是滿項")
    const number = CreateTypeNode(token.type)(token.value);
    if (isNotFullNode(top)){
      return topChildPush(number);
    } else {
      return stackPush(number);
    }
  }
複製代碼

ast

4.6 符號節點(Sign + - * /)

定義一個符號節點,其 children 能夠是 字面量 或者 其餘節點。

function AddNode(){
  return {
    type:"+",
    children:[...arguments],
    maxChildren:2, // 能有兩個 child
  }
}
function SubNode(){
  return {
    type:"-",
    children:[...arguments],
    maxChildren:2, // 能有兩個 child
  }
}
function MulNode(){
  return {
    type:"*",
    children:[...arguments],
    maxChildren:2, // 能有兩個 child
  }
}
function DivNode(){
  return {
    type:"/",
    children:[...arguments],
    maxChildren:2, // 能有兩個 child
  }
}
複製代碼

4.7 節點的優先級

你們都知道,運算符有優先級,例如 * / 的優先級就比 + - 要高。我把這個優先級擴展到所有節點,全部節點都有一個優先級數值。

const operatorValue = {
    "ROOT" : 0, 
    "+" : 1,
    "-" : 1,
    "*" : 2,
    "/" : 2,
    "NUMBER" : 3,
  }
複製代碼

這個數值後面立刻就會用到。

4.8 retire 操做

咱們回到 1 + 2.3 這個算術表達式。前面說到 1 這個 Token 已經壓入棧了,如今輪到 + Token 。

    1. 棧頂 top (就是 number 1)
    1. 符號節點 add
    1. top 是滿的節點,因此 add 是後置符號,進入後置符號規則
    1. 比較 top 節點與 符號 add 節點 的優先級數值
    1. top < add 執行 rob 操做 ,不然 執行 retire 操做
// 後置符號
  if (isFullNode(top)) {
    if (operatorValue[token.value] > operatorValue[top.type]){
        // 1 + 2 * 
        return rob(token.value,top.children);
      } else {
        // 1 +
        // 1 + 2 + 
        link(token.value);
        return retire(token.value);
      }
  }
複製代碼

先說 retire 操做,retire 有退休的意思。我是想表達,這當前條件下,棧頂節點能夠退下來了,把棧頂的位置讓給新節點。

步驟是把的舊棧頂節點出棧,新節點入棧,而後舊棧頂壓進新節點的 children 棧裏。

const retire = (type) => {
  stack.push(CreateTypeNode(type)(stack.pop()));
}
複製代碼

而後到2.3 Token,根據前面的規則,由於棧頂的 add 節點是非滿節點,2.3 構建成 number 節點 後,直接 push 到 add 節點的 children 棧裏。

文字有點幹,咱們配合圖一塊兒看。

ast

4.9 rob 操做

前面提到 retire 操做的反向條件是 rob 操做。先來看一個例子1 + 2.3 * 4

接上一節,如今棧裏是<Root>,<+ 1 2.3>,現須要壓入新節點 mul,一樣的 mul 節點和棧頂 add 節點比較, 優先級 mul > add,執行 rob 操做。

rob 操做 很好理解,由於乘法比加法的優先級要高,因此原本屬於 add 節點 下的 number(2.3) 要被 mul 節點搶走了。

const rob = (type,children) =>{
    const child = children.pop();
    stack.push(CreateTypeNode(type)(child));
  }
  rob(token.value,top.children);
複製代碼

mul 節點搶走 number(2.3) 後放壓進本身的 children 棧裏,而後 mul 節點入棧,成爲新的棧頂。

而後到4 Token,根據前面的規則,由於棧頂的 mul 節點是非滿節點,4 構建成 number 節點 後,直接 push 到 mul 節點的 children 棧裏。

文字仍是有點幹,咱們配合圖一塊兒看。

ast

4.10 link 操做

細心的朋友應該會發現,在執行 retire 操做以前還執行了一個 link 操做。這個 link 是作啥的呢?咱們來看一個例子1 + 2.3 * 4 - 5

接上一節,棧裏如今是<Root>,<+ 1>,<* 2.3 4>,如今準備壓入 sub 節點,由於優先級上 sub < mul ,若是先忽略 link 直接走 retire 操做,就會變成<Root>,<+ 1>,<- <* 2.3 4>>。這個不是我想要的結果,由於+-優先級是相同的,相同優先級應該先計算先出現的符號,理想的操做下,棧裏應該變成<Root>,<- <+ 1 <* 2.3 4>>>。因此我引入了 link 操做。

link 操做會先將棧頂的滿項節點 push 到前一項的 childen 棧裏(若是前一項是非滿節點),並且這是一個循環操做 直到 前一項是滿節點 或者 前一項節點的優先級比新節點的還要低。

回看上面的例子,棧裏如今是 <Root>,<+ 1>,<* 2.3 4> ,如今準備壓入 sub 節點,由於優先級上 sub < mul ,先在 link 操做下變成 <Root>,<+ 1 <* 2.3 4>> ,而後執行 retire , 變成 <Root>,<- <+ 1 <* 2.3 4>>>

function typeValue(node){
    if (node === undefined) throw new Error("node is undefined");
    return operatorValue[node.type];
  }
  const link = (type) =>{
    const value = operatorValue[type];
    while(isFullNode(stack[stack.length -1]) &&  isNotFullNode(stack[stack.length - 2]) && (value <= typeValue(stack[stack.length -1])) && (value <= typeValue(stack[stack.length -2])) ) {
      stack[stack.length - 2].children.push(stack.pop());
    }
  }
複製代碼

而後到 5 Token,根據前面的規則,由於棧頂的 sub 節點是非滿節點,5 構建成 number 節點 後,直接 push 到 mul 節點的 children 棧裏。

繼續上圖。

ast

4.13 增長負數

負數能夠說是開了一個比較壞的先河,由於和減號公用一個 - 符號,致使代碼邏輯的增長。負號和減號的區別在於,負號的取值是在它的右側 1 + - 1 ,減號是從左到右 1 - 1 。這裏能夠經過判斷棧頂節點的狀況來肯定到底是 負號 仍是 減號。我將 負號這種取值在右邊的符號稱爲 前置符號 ,加減乘除這種左到右取值的符號稱爲 後置符號。前置符號直接壓棧。

// 定義負數節點
  function NegNode(){
    return {
      type:"NEGATE",
      children:[...arguments],
      maxChildren:1,
    }
  }
  if (token.type === "SIGN") {
      // 後置符號
    if (isFullNode(top)) {
      if (operatorValue[token.value] > operatorValue[top.type]){
          // 1 + 2 * 
          // console.log("rob");
          return rob(token.value,top.children);
        } else {
          // 1 +
          // 1 + 2 + 
          link(token.value);
          return retire(token.value);
        }
    }
    // 前置符號
    if (
      (isNoChildrenNode(top)) || // (-
      (isNotFullNode(top)) // 1 + -
    ){
      if (token.value === "-") return stackPush(CreateTypeNode("NEGATE")()); // 取負公用符號 - 
      if (token.value === "+") return ; // + 號靜默
      throw new Error(token.value + "符號不能前置");
    }
  }
複製代碼

例子 - 1- 1 這裏開始棧 <Root> ,而後準備壓入 - ,由於 Root 節點是沒有後代的節點(NoChildrenNode),因此這裏判斷-是前置符號,生成 NE(NEGATE) 節點直接入棧 <Root><NE> 。而後是 1 , <Root><NE 1>

例子 1 - - 1 。這裏第一個 -<Root><1> ,由於 棧頂 number 節點是滿的節點(FullNode),因此第一個 - 是後置符號,生成 sub 節點。第二個 -<Root><- 1>, 棧頂的 sub 節點是未滿的節點(NotFullNode),斷定爲前置符號,生成 NE(NEGATE) 節點直接入棧 <Root><- 1><NE> 。而後是 1 , <Root><- 1><NE 1>

ast

4.14 增長括號

括號 ( 能夠改變表達式裏的優先級,先定義括號節點。

首先須要在 詞法分析 的時候加入 (

// start 狀態裏
// 符號
if (["+","-","*","/","("].includes(char)) {
  this.emmitToken("SIGN", char);
  return this.start;
}
複製代碼
function ParNode(){
  return {
    type:"(",
    children:[],
    maxChildren:0,
  }
}
複製代碼

這裏 maxChildren 設爲 0 ,當咱們將 括號節點 push 到棧裏時,就造成一個屏障,使後面節點變更時,不會越過 括號節點 。

看例子 1 * (2 + 3 * 4)

`<Root>` 
1 `<Root><1>`
* `<Root><* 1>`
( `<Root><* 1><(>` // ( 隔離
2 `<Root><* 1><(><2>` // 把 2 和 * 隔離
+ `<Root><* 1><(><+ 2>` 
3 `<Root><* 1><(><+ 2 3>` 
* `<Root><* 1><(><+ 2><* 3>` 
4 `<Root><* 1><(><+ 2><* 3 4>` 
複製代碼

參考代碼。

if (token.value === "(" ) {
  // 1(
  // 1 + 1 (
  if (isFullNode(top)) throw new Error("not a function");
  // (
  return stackPush(CreateTypeNode("(")());
}
複製代碼

ast

4.14 增長反括號 與 remove 操做

反括號 ) 的做用是將當前括號後面添加的節點收縮成一個穩定節點,具體方法是把 ( 後面的節點 link 起來( ( 的優先級設定得比較小,旨在將括號裏的節點都鏈接起來),並推到一個臨時的棧裏,而後將 ( 節點 改寫成 ) 節點 ,再將臨時棧的節點出棧 push 到 ) 節點的 children 裏。還由於 ) 節點的優先級別設置了很高,不用擔憂會被後面的節點 rob 。

首先須要在 詞法分析 的時候加入 )

// start 狀態裏
// 符號
if (["+","-","*","/","(",")"].includes(char)) {
  this.emmitToken("SIGN", char);
  return this.start;
}
複製代碼
if (token.value === ")" ) {
  // ()
  if (isNoChildrenNode(top)) throw new Error("Unexpected token )");
  // (1+)
  if (isNotFullNode(top)) throw new Error("Unexpected token )");
  return remove("(");  // 收攏 (
}

const remove = (type) => {
  link(type);
  // 找到最近的( 其他push到tempStack
  while(stack.length > 0 && !(stack[stack.length - 1].type === type && !stack[stack.length - 1].children)){
    tempStack.push(stack.pop());
  }
  // 修改最近的( 
  const top = stack[stack.length - 1];
  if (top.type === type){
    top.type = opposite[type];  // 取反 ( => )
    top.children = [];
    // tempStack的Node壓給(
    while(tempStack.length > 0){
      top.children.push(tempStack.pop());
    }
    top.maxChildren = top.children.length; // maxChildren 設滿
  } 
}


const operatorValue = {
  "ROOT" : 0, 
  "(" : 1, // 括號的優先級低,方便 link
  "+" : 2,
  "-" : 2,
  "*" : 3,
  "/" : 3,
  "NEGATE" : 4, // 取負
  "NUMBER" : 5, // 取正
  ")" : 6, // 反括號的優先級高,防止被 rob
  "ROOT_END" : 7,
}

const opposite = {
  "(" : ")" ,
  "ROOT" : "ROOT_END",
}
複製代碼

ast

4.15 EOF

括號的做用是將其內部的節點包裹起來,造成一個穩定的節點,括號 ( 和反括號 ) 自成一對。還有一對有一樣的功能,就是 ROOTROOT_END

ROOTROOT_END 標示着這個表達式的開始和結束。 ROOT 節點是初始化時就添加的,那麼 ROOT_END 對應就是 EOF 這個 Token 了。

if (token.type === "EOF") {
  // EOF
  return remove("ROOT");
};
複製代碼

來一個完整的流程gif。

ast

ast

5 計算求值

EOF 後,咱們就能夠獲得抽象語法樹 AST 了。由於是樹形結構,咱們能夠用遞歸的方法求值。

`1 * ( 2 + 3 * 4)`
const ast = {
  "type": "ROOT_END",
  "children": [{
    "type": "*",
    "children": [{
      "type": "NUMBER",
      "children": ["1"],
    }, {
      "type": ")",
      "children": [{
        "type": "+",
        "children": [{
          "type": "NUMBER",
          "children": ["2"],
        }, {
          "type": "*",
          "children": [{
            "type": "NUMBER",
            "children": ["3"],
          }, {
            "type": "NUMBER",
            "children": ["4"],
          }],
        }],
      }],
    }],
  }],
}
function evaluate(node){
  const {type,children} = node;
  if (type === "NUMBER") return Number(children[0]);
  if (type === "+") return evaluate(children[0]) + evaluate(children[1]);
  if (type === "-") return evaluate(children[0]) - evaluate(children[1]);
  if (type === "*") return evaluate(children[0]) * evaluate(children[1]);
  if (type === "/") return evaluate(children[0]) / evaluate(children[1]);
  if (type === ")") return evaluate(children[0]);
  if (type === "ROOT_END") return evaluate(children[0]);
  if (type === "NEGATE") return evaluate(children[0]) * -1;
}
console.log(evaluate(ast)); // 14
複製代碼

6 小結

寫到這裏,一個簡單的四則運算解析器總算完成了。一共分 3 大部分。分別是 詞法分析(Lexer)、語法分析(Parser)、計算求值(evaluate)。

詞法分析(Lexer)是將 表達式 字符串 轉化爲 Token 流,這裏用到有限狀態機。

語法分析(Parser)是將 Token 流 轉化爲 抽象語法樹(AST),這裏主要是手工寫的語法分析,用了 兩個棧 ,規定了 4 個方法 link 、 retire 、 rob 、 remove,還有定義了不一樣節點的入棧規則。

計算求值(evaluate)是將 AST 計算出表達式的 值,這裏用了遞歸求值。

7 應用之自定義的向量運算

弄清楚四則運算的解析方法後,咱們能夠創造本身制定規則的表達式運算了。

由於以前的項目我寫過向量運算,可是由於函數調用的寫法有點醜陋,我這裏就嘗試自定義向量運算表達式。

7.1 向量表示之引入符號(Sign [ , ])

這裏一個 2維向量 我用 [1,2] 來表示。因此先在 詞法分析(Lexer)裏增長 [,]

// start 狀態裏
// 符號
if (["+","-","*","/","(",")","[",",","]"].includes(char)) {
  this.emmitToken("SIGN", char);
  return this.start;
}
複製代碼

[] 是一對,本質和括號對 ( ) 沒什麼區別。

, 其定位就是一個分割符,沒有成對子。並且 , 出現後,其前面的節點都要 link 起來。

function VecNode(){
  return {
    type:"[",
    children:[],
    maxChildren:0,
  }
}
function WallNode(){
  return {
    type:",",
    children:[],
    maxChildren:0,
  }
}

const opposite = {
  "(" : ")" ,
  "[" : "]" ,
  "ROOT" : "ROOT_END" ,
}

if (token.value === "[" ) {
  // 1[
  // 1 + 1 [
  if (isFullNode(top)) throw new Error("非頂端[前面不能有滿項");
  return stack.push(CreateTypeNode("[")());
}

if (token.value === "," ) {
  // ,
  // ,,
  // (,
  // [,
  if (isNoChildrenNode(top)) throw new Error(",不能接在空符後面");
  // [ 1 + ,
  if (isNotFullNode(top)) throw new Error(",不能接在非滿項後面");
  link("[");
  return stack.push(CreateTypeNode(",")());
}

if (token.value === "]" ) {
  // [1+]
  if (isNotFullNode(top)) throw new Error("]前不能有非滿項");
  return remove("[");
}
複製代碼

例子 [ 1 + 2 * 3 , 4 + 5 * 6 ]

`<Root>`
[   `<Root><[>` 
1   `<Root><[><1>` 
+   `<Root><[><+ 1>` 
2   `<Root><[><+ 1 2>` 
*   `<Root><[><+ 1><* 2>` 
3   `<Root><[><+ 1><* 2 3>` 
,   `<Root><[><+ 1 <* 2 3>><,>` 
4   `<Root><[><+ 1 <* 2 3>><,><4>` 
+   `<Root><[><+ 1 <* 2 3>><,><+ 4>` 
5   `<Root><[><+ 1 <* 2 3>><,><+ 4 5>` 
*   `<Root><[><+ 1 <* 2 3>><,><+ 4><* 5>` 
6   `<Root><[><+ 1 <* 2 3>><,><+ 4><* 5 6>` 
]   `<Root><[><+ 1 <* 2 3>><,><+ 4<* 5 6>>` 
    `<Root><] <+ 1 <* 2 3>><,><+ 4<* 5 6>>>` 
EOF `<RootEnd <] <+ 1 <* 2 3>><,><+ 4<* 5 6>>>>` 
複製代碼

最後在 evaluate 方法裏增長對向量的支持。

// evaluate 裏
if (type === "]") {
  const notWall = children.filter(item => item.type !== ",");
  const a = evaluate(notWall[0]);
  const b = evaluate(notWall[1]);
  const isNumA = typeof a === "number";
  const isNumB = typeof b === "number";
  if (isNumA && isNumB) {
    return new Vector2d(a,b);
  } else {
    throw new Error("只有兩個數量才能生成向量");
  }
}
複製代碼

ast

ast

7.2 向量加減乘除法取負

向量加減乘除法取負繼續源用 + , - , * , / 符號,只須要在 evaluate 方法裏作判斷就能夠了。

// evaluate 裏
if (type === "+") {
  const a = evaluate(children[0]);
  const b = evaluate(children[1]);
  if (Vector2d.is(a) && Vector2d.is(b)){
    return Vector2d.add(a,b);
  } else {
    return a + b;
  }
}
if (type === "-") {
  const a = evaluate(children[0]);
  const b = evaluate(children[1]);
  if (Vector2d.is(a) && Vector2d.is(b)){
    return Vector2d.sub(a,b);
  } else {
    return a - b;
  }
}
if (type === "*" || type === "/") {
  const a = evaluate(children[0]);
  const b = evaluate(children[1]);
  const isVecA = Vector2d.is(a);
  const isVecB = Vector2d.is(b);
  const isNumA = typeof a === "number";
  const isNumB = typeof b === "number";
  if ( isNumA && isNumB ){
    if (type === "*") return a * b;
    if (type === "/") return a / b;
  } else if(isVecA && isNumB) {
    if (type === "*") return Vector2d.scale(a,b);
    if (type === "/") return Vector2d.scale(a,1/b);
  } else if (isVecB && isNumA) {
    if (type === "*") return Vector2d.scale(b,a);
    if (type === "/") return Vector2d.scale(b,1/a);
  } else {
    throw new Error("兩個向量不能相乘,請用@dot");
  }
}
if (type === "NEGATE") {
  const a = evaluate(children[0]);
  if (Vector2d.is(a)){
    return Vector2d.scale(a,-1);
  } else {
    return a * -1;
  }
}
複製代碼

7.3 向量旋轉、點乘,角度的單位轉換

向量的旋轉(@rot)、點乘(@dot),角度的單位轉換(@deg),用這3個自定義符號。

這裏須要修改一下 詞法分析 的狀態機,在 start 狀態下新增一個躍遷狀態 customSgin 用 @ 爲標識。而後 customSgin 狀態下輸入[a-zA-Z]都回躍遷自身 不然 躍遷回狀態 start 並輸出 Token。

ast

// Lexer 裏
  start(char) {
    // 數字
    if (["0","1","2","3","4","5","6","7","8","9"].includes(char)) {
      this.token.push(char);
      return this.inInt;
    }
    // .
    if (char === "."){
      this.token.push(char);
      return this.inFloat;
    }
    // 符號
    if (["+","-","*","/","(",")","[","]",",","<",">"].includes(char)) {
      this.emmitToken("SIGN", char);
      return this.start
    }
    // 空白字符
    if ([" ","\r","\n"].includes(char)) {
      return this.start;
    }
    // 結束
    if (char === EOF){
      this.emmitToken("EOF", EOF);
      return this.start
    }
    if (char === "@"){
      this.token.push(char);
      return this.customSgin;
    }
  }

  customSgin(char) {
    if ("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".split("").includes(char)) {
      this.token.push(char);
      return this.customSgin;
    } else {
      this.emmitToken("SIGN", this.token.join(""));
      this.token = [];
      return this.start(char); // put back char
    }
  }
複製代碼

而後定義節點和節點優先級。

function DegNode(){
  return {
    type:"@deg",
    children:[...arguments],
    maxChildren:1,
  }
}
function DotNode(){
  return {
    type:"@dot",
    children:[...arguments],
    maxChildren:2,
  }
}
function RotNode(){
  return {
    type:"@rot",
    children:[...arguments],
    maxChildren:2,
  }
}
const operatorValue = {
  "ROOT" : 0, 
  "(" : 1,
  "[" : 1,
  "@dot" : 2, // 向量點乘
  "<" : 3,
  ">" : 3,
  "+" : 4,
  "-" : 4,
  "*" : 5,
  "/" : 5,
  "@rot" : 5, // 向量旋轉
  "NEGATE" : 6, // 取負
  "@deg" : 7, // 角度轉換
  "NUMBER" : 8, // 取正
  ")" : 9,
  "]" : 9,
  "ROOT_END" : 10,
}
複製代碼

還有在 evaluate 裏寫對應的方法。

if (type === "@dot"){
  const a = evaluate(children[0]);
  const b = evaluate(children[1]);
  const isVecA = Vector2d.is(a);
  const isVecB = Vector2d.is(b);
  if (isVecA && isVecB) {
    return Vector2d.dot(a,b);
  } else {
    throw new Error("只有向量和向量能點乘");
  }
}
if (type === "@rot"){
  const a = evaluate(children[0]);
  const b = evaluate(children[1]);
  const isVecA = Vector2d.is(a);
  const isVecB = Vector2d.is(b);
  const isNumA = typeof a === "number";
  const isNumB = typeof b === "number";
  if (isVecA && isNumB) {
    return Vector2d.rotate(a,b);
  } else if (isVecB && isNumA) {
    return Vector2d.rotate(b,a);
  } else {
    throw new Error("只有向量和數量能旋轉");
  }
}
if (type === "@deg"){
  const a = evaluate(children[0]);
  const isNumA = typeof a === "number";
  if (isNumA){
    return a / 180 * Math.PI;
  } else {
    throw new Error("非數字不能轉換deg");
  }
}
複製代碼

來一個例子 [1, 0] @rot - 90 @deg ,把 [1,0] 旋轉負 90 度。

ast

ast

8 Demo手動玩

最後結合 Vue 寫了一個 表達式轉 AST 的可視化 demo,支持數字和向量。

rococolate.github.io/blog/ast/in…

demo 源碼: github.com/Rococolate/…


若是你以爲這篇內容對你有價值,請點贊,並關注咱們的官網和咱們的微信公衆號(WecTeam),每週都有優質文章推送:

WecTeam
相關文章
相關標籤/搜索