做者:吳冠禧javascript
曉強哥在他的上篇文章裏介紹了 裏面提到得到抽象語法樹的過程爲:代碼 => 詞法分析 => 語法分析 => AST,抱着深究技術細節的目的,我決定研究這裏的詞法分析和語法分析,寫一個簡單的四則運算表達式轉換成AST的方法,因而就有了下面的內容。html
人類習慣 a + b
這種表達叫作「中序表達式」,優勢是比較簡單直觀,缺點是要用一堆括號來肯定優先級 (a + b) * (c + d)
。java
這裏說簡單直觀是相對人類的思惟結構來講的,對計算機而言中序表達式是很是複雜的結構。node
爲了計算機計算方便,咱們須要將中序表達式轉換成樹形結構,也就是「抽象語法樹AST」。python
咱們知道,幾乎任何語言中,代碼在 "編譯"(解釋型語言在運行時也有編譯的過程) 的過程當中,都會生成一種樹狀的中間狀態,這就是 AST。有些語言會直接把相似 AST 的語法暴露給程序員(例如:lisp、elixir、python等)。可是 javascript 並無這個能力,可是咱們能夠用 javascript 自身實現這個過程。git
得到抽象語法樹的過程爲:代碼(字符串) => 詞法分析(Lexer)=> Tokens => 語法分析(Parser) => AST程序員
詞法分析有點像中文的分詞,就是將字符串流根據規則生成一個一個的有具體意義的 Token ,造成 Token 流,而後流入下一步。github
咱們看一個簡單的例子,微信
1 + 2.3
複製代碼
很明顯這個表達式是能夠分紅三個 Token ,分別是 1
, +
, 2.3
。函數
詞法分析這裏,咱們能夠用有限狀態機來解決。
絕大多數語言的詞法部分都是用狀態機實現的,咱們下面就畫出有限狀態機的圖形,而後根據圖形直觀地寫出解析代碼,整體圖大概是這樣。
下面拆開細看。
狀態機的初始狀態是 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;
}
}
複製代碼
start
狀態下輸入輸入數字(0 ~ 9)就會遷移到 inInt
狀態。
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
}
}
複製代碼
start
狀態下輸入符號(.)就會遷移到 inFloat
狀態。
inInt
狀態下輸入輸入符號(.)就會遷移到 inFloat
狀態。
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
}
}
複製代碼
我將 「浮點數 Token」
和 「整數 Token」
合併爲 [NUMBER Token]
, 其餘的 Token 還有 「SIGN Token」
和 「EOF Token」
。
Token 的 定義:
interface Token{
type:String,
value:String,
}
複製代碼
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()];
複製代碼
效果以下圖:
自此,咱們成功實現了詞法分析,後面進入到語法分析。
前面的詞法分析,已經將字符串劃分紅一個個有意義的 Token 進入到語法分析(Parser)。語法分析在編譯原理裏面屬於比較高深的學問,我是沒有怎麼看懂。但總的來講就是把 Token流 組裝成 AST , AST 的結構是既定的,後面我就經過對不一樣節點制定不一樣規則把 AST 組裝起來。
簡單來講 AST 就是一棵樹形結構,由節點(Node)和 葉子(字面量 Literal )組成,節點 下面能夠鏈接 其餘節點 或者 字面量。最頂端的節點就是 根節點。
節點的定義就是一個簡單的 javascript Object
interface Node {
type:string,
children:[],// children棧 裏面能夠是 Node 或者 Literal
maxChildren:number,
}
複製代碼
語法分析(Parser)這裏,我使用的是一個棧結構,每來一個 Token 就入棧,而後經過必定的規則組裝 AST。
第一步就是壓入 根節點 <Root>
。
function RootNode(){
return {
type:"ROOT",
children:[],
maxChildren:0,
}
}
const stack = [RootNode()];
複製代碼
在說明不一樣類型節點的規則前,先說一下通用規則。
對應的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;
}
複製代碼
定義一個數字節點,其children就是 數字字面量。
function NumberNode(){
return {
type:"NUMBER",
children:[...arguments],
maxChildren:1, // 只能有一個 child
}
}
複製代碼
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);
}
}
複製代碼
定義一個符號節點,其 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
}
}
複製代碼
你們都知道,運算符有優先級,例如 * /
的優先級就比 + -
要高。我把這個優先級擴展到所有節點,全部節點都有一個優先級數值。
const operatorValue = {
"ROOT" : 0,
"+" : 1,
"-" : 1,
"*" : 2,
"/" : 2,
"NUMBER" : 3,
}
複製代碼
這個數值後面立刻就會用到。
咱們回到 1 + 2.3
這個算術表達式。前面說到 1
這個 Token 已經壓入棧了,如今輪到 +
Token 。
// 後置符號
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 棧裏。
文字有點幹,咱們配合圖一塊兒看。
前面提到 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 棧裏。
文字仍是有點幹,咱們配合圖一塊兒看。
細心的朋友應該會發現,在執行 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 棧裏。
繼續上圖。
負數能夠說是開了一個比較壞的先河,由於和減號公用一個 -
符號,致使代碼邏輯的增長。負號和減號的區別在於,負號的取值是在它的右側 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>
。
括號 (
能夠改變表達式裏的優先級,先定義括號節點。
首先須要在 詞法分析 的時候加入 (
。
// 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("(")());
}
複製代碼
反括號 )
的做用是將當前括號後面添加的節點收縮成一個穩定節點,具體方法是把 (
後面的節點 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",
}
複製代碼
括號的做用是將其內部的節點包裹起來,造成一個穩定的節點,括號 (
和反括號 )
自成一對。還有一對有一樣的功能,就是 ROOT
和 ROOT_END
。
ROOT
和 ROOT_END
標示着這個表達式的開始和結束。 ROOT
節點是初始化時就添加的,那麼 ROOT_END
對應就是 EOF
這個 Token 了。
if (token.type === "EOF") {
// EOF
return remove("ROOT");
};
複製代碼
來一個完整的流程gif。
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
複製代碼
寫到這裏,一個簡單的四則運算解析器總算完成了。一共分 3 大部分。分別是 詞法分析(Lexer)、語法分析(Parser)、計算求值(evaluate)。
詞法分析(Lexer)是將 表達式 字符串 轉化爲 Token 流,這裏用到有限狀態機。
語法分析(Parser)是將 Token 流 轉化爲 抽象語法樹(AST),這裏主要是手工寫的語法分析,用了 兩個棧 ,規定了 4 個方法 link 、 retire 、 rob 、 remove,還有定義了不一樣節點的入棧規則。
計算求值(evaluate)是將 AST 計算出表達式的 值,這裏用了遞歸求值。
弄清楚四則運算的解析方法後,咱們能夠創造本身制定規則的表達式運算了。
由於以前的項目我寫過向量運算,可是由於函數調用的寫法有點醜陋,我這裏就嘗試自定義向量運算表達式。
這裏一個 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("只有兩個數量才能生成向量");
}
}
複製代碼
向量加減乘除法取負繼續源用 +
, -
, *
, /
符號,只須要在 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;
}
}
複製代碼
向量的旋轉(@rot
)、點乘(@dot
),角度的單位轉換(@deg
),用這3個自定義符號。
這裏須要修改一下 詞法分析 的狀態機,在 start 狀態下新增一個躍遷狀態 customSgin 用 @
爲標識。而後 customSgin 狀態下輸入[a-zA-Z]都回躍遷自身 不然 躍遷回狀態 start 並輸出 Token。
// 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 度。
最後結合 Vue 寫了一個 表達式轉 AST 的可視化 demo,支持數字和向量。
rococolate.github.io/blog/ast/in…
demo 源碼: github.com/Rococolate/…
若是你以爲這篇內容對你有價值,請點贊,並關注咱們的官網和咱們的微信公衆號(WecTeam),每週都有優質文章推送: