语法分析
通过上篇的讲解,我们已经拿到了词法分析解析出来的tokens,那接下来就是通过拿到的tokens进行语法分析来构造一棵抽象语法树(AST)。那么什么是AST呢?
什么是AST?
如果你了解或者使用过 'ESLint' 、'Babel' 及 'Webpack' 这类工具,那么你已经对AST的强大之处有所了解了,比如这里举一个浅显的例子,ESLint是如何修复你的代码的呢? 首先将你的js代码解析成抽象语法树,然后对其进行修整,比如对于一些空格,或者将var转为const,修整完之后再转换为js代码,这里有副不太严谨的图可以更详细的描述
那对于目前来说,随着 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
}