做者:吴冠禧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),每周都有优质文章推送: