语法分析

313 阅读4分钟

语法分析

通过上篇的讲解,我们已经拿到了词法分析解析出来的tokens,那接下来就是通过拿到的tokens进行语法分析来构造一棵抽象语法树(AST)。那么什么是AST呢?

什么是AST?

如果你了解或者使用过 'ESLint' 、'Babel' 及 'Webpack' 这类工具,那么你已经对AST的强大之处有所了解了,比如这里举一个浅显的例子,ESLint是如何修复你的代码的呢? 首先将你的js代码解析成抽象语法树,然后对其进行修整,比如对于一些空格,或者将var转为const,修整完之后再转换为js代码,这里有副不太严谨的图可以更详细的描述

image.png 那对于目前来说,随着 JavaScript 语言的发展,由一些大佬创建的项目ESTree用于更新 AST 规则,目前已成为社区标准。然后社区中一些其它项目比如 ESlint 和 Babel 就会使用 ESTree 或在此基础上做一些修改,然后衍生出自己的一套规则,并制作相应的转换工具,暴露出一些 API 给开发者使用。那么现在我们来简易实现一下 AST

实现AST

首先判断拿到的tokens是什么类型,比如这里如果是number类型,那就创造出一个节点numberNode,并将其push到根节点rootNode里面。

import { Token, TokenTypes } from "./tokenizer";
function parser(tokens: Token[]) {
    let current = 0
    let token = tokens[current];
    const rootNode:any = {
        type: "Program",
        body: []
    }
    if (token.type === TokenTypes.Number) {
        const numberNode = {
            type: "Number",
            value: token.value
        }
        rootNode.body.push(numberNode)
    }
    return rootNode
}

这里我们还需要去重构一下,也就是类型的问题。这里可以定义一个枚举类型来指明根节点以及创建节点的类型

enum NodeTypes {
    Root,
    Number
}
function parser(tokens: Token[]) {
    let current = 0
    let token = tokens[current];
    const rootNode: any = {
        type: NodeTypes.Root,
        body: []
    }
    if (token.type === TokenTypes.Number) {
        const numberNode = {
            type: NodeTypes.Number,
            value: token.value
        }
        rootNode.body.push(numberNode)
    }
    return rootNode
}

然后我们需要对这里也就是rootNode的any类型进行重构一下,这里我们可以创建一些对应的接口。首先可以看到这里根节点rootNode和numberNode节点都有一个共同的,那就是type,那我们就可以先创建一个Node接口

interface Node {
    type: NodeTypes
}

然后对根节点以及创建的节点进行一个详细的类型,这里body我们还不知道里面放什么,所以先写一个any类型

interface RootNode extends Node {
    body: any[]
}
interface NumberNode extends Node {
    value: string
}

那这样的话,下面我们就需要指明类型了

const rootNode: RootNode = {
        type: NodeTypes.Root,
        body: []
    }
    if (token.type === TokenTypes.Number) {
        const numberNode: NumberNode = {
            type: NodeTypes.Number,
            value: token.value
        }
        rootNode.body.push(numberNode)
    }

但是可以看到,这样其实可读性不是很好,所以我们可以通过创建两个函数来表示节点

function createRootNode(): RootNode {
    return {
        type: NodeTypes.Root,
        body: []
    }
}
function createNumberNode(value: string): NumberNode {
    return {
        type: NodeTypes.Number,
        value
    }
}
function parser(tokens: Token[]) {
    let current = 0
    let token = tokens[current];
    const rootNode = createRootNode()
    if (token.type === TokenTypes.Number) {
        const numberNode = createNumberNode(token.value)
        rootNode.body.push(numberNode)
    }
    return rootNode
}

那这样的话我们就已经得到了两个创建节点的函数以及类型,然后我们就要去思考一下当他遇到什么字符的时候或者说什么token的时候,应该把它当成表达式来处理,比如( add 2 3 ) 那这里这个基本的算法流程是这样的:

1.当遇到左括号的时候,就将其当表达式来处理了。

2.然后我们获取到一个name也就是这里的add,创造出对应的节点

3.之后遇到的2,4都作为他的params

4.那么最后他的结束条件是遇到右括号,那这就是实现一个基本的表达式的算法

  if (token.type === TokenTypes.Paren && token.value === "(") {
        token = tokens[++current]
        const node = {
            type: NodeTypes.CallExpression,
            name: token.value,
            params: []
        }
        token = tokens[++current]
        while (!(token.type === TokenTypes.Paren && token.value === "(")) {
            if (token.type === TokenTypes.Number) {
                const numberNode = createNumberNode(token.value)
                node.params.push(numberNode)
            }
        }
    }

然后这里还是有一个类型的问题,那具体跟之前是差不多的,我们这里添加一下就好,还有这里的抽离函数也跟之前一样,我就不过多说了。

enum NodeTypes {
    Root,
    Number,
    CallExpression
}
interface Node {
    type: NodeTypes
}
// 这里就是在后面比如params里面放的不一定是number,可能是表达式,所以我们可以将其抽离出来。
type ChildNode = NumberNode | CallExpressionNode
interface RootNode extends Node {
    body: ChildNode[]
}
interface NumberNode extends Node {
    value: string
}
interface CallExpressionNode extends Node {
    name: string,
    params: ChildNode[]
}
function createRootNode(): RootNode {
    return {
        type: NodeTypes.Root,
        body: []
    }
}
function createNumberNode(value: string): NumberNode {
    return {
        type: NodeTypes.Number,
        value
    }
}
function createCallExpressionNode(name: string): CallExpressionNode {
    return {
        type: NodeTypes.CallExpression,
        name,
        params: []
    }
}
function parser(tokens: Token[]) {
    let current = 0
    let token = tokens[current];
    const rootNode = createRootNode()
    if (token.type === TokenTypes.Number) {
        const numberNode = createNumberNode(token.value)
        rootNode.body.push(numberNode)
    }
    if (token.type === TokenTypes.Paren && token.value === "(") {
        token = tokens[++current]
        const node = createCallExpressionNode(token.value)
        token = tokens[++current]
        while (!(token.type === TokenTypes.Paren && token.value === ")")) {
            if (token.type === TokenTypes.Number) {
                const numberNode = createNumberNode(token.value)
                node.params.push(numberNode)
                token = tokens[++current]
            }
        }
        current++
        rootNode.body.push(node)
    }

    return rootNode
}

至此一个最基本的语法分析就结束了,但是这里再提出一个问题,如果是两个表达式呢?直接在外面加一个循环就可以了,以下就是完整的代码

import { Token, TokenTypes } from "./tokenizer";
enum NodeTypes {
    Root,
    Number,
    CallExpression
}
interface Node {
    type: NodeTypes
}
// 这里就是在后面比如params里面放的不一定是number,可能是表达式,所以我们可以将其抽离出来。
type ChildNode = NumberNode | CallExpressionNode
interface RootNode extends Node {
    body: ChildNode[]
}
interface NumberNode extends Node {
    value: string
}
interface CallExpressionNode extends Node {
    name: string,
    params: ChildNode[]
}
function createRootNode(): RootNode {
    return {
        type: NodeTypes.Root,
        body: []
    }
}
function createNumberNode(value: string): NumberNode {
    return {
        type: NodeTypes.Number,
        value
    }
}
function createCallExpressionNode(name: string): CallExpressionNode {
    return {
        type: NodeTypes.CallExpression,
        name,
        params: []
    }
}
function parser(tokens: Token[]) {
    let current = 0
    let token = tokens[current];
    const rootNode = createRootNode()
    while (current < tokens.length) {
        if (token.type === TokenTypes.Number) {
            const numberNode = createNumberNode(token.value)
            rootNode.body.push(numberNode)
        }
        if (token.type === TokenTypes.Paren && token.value === "(") {
            token = tokens[++current]
            const node = createCallExpressionNode(token.value)
            token = tokens[++current]
            while (!(token.type === TokenTypes.Paren && token.value === ")")) {
                if (token.type === TokenTypes.Number) {
                    const numberNode = createNumberNode(token.value)
                    node.params.push(numberNode)
                    token = tokens[++current]
                }
            }
            current++
            rootNode.body.push(node)
        }
    }

    return rootNode
}