一个简单的编译器

270 阅读4分钟

编译器工作过程

  1. 解析:将源代码转化为抽象的语法结构 ast,两步走:
      1. 分词(词法分析):将源代码分解成一个个词素,形如 [{type:'',value:''}]。
      • 它可以描述数字、标识符、标点符号、运算符等等
      • type: 'paren' | 'name' | 'string' | 'number'
      1. 生成 ast (语法分析):抽象语法树。
      • 抽象语法树是一个深度嵌套的对象,这个对象以一个既能够简单操作又能提供关于源码信息的形式来展现代码。
  2. 转换:接收解析生成的抽象形式,操作它,做编译器想让它做的事情
    • 接收 ast 并对它做出改动。
    • 转换阶段可以改变 ast 使代码保持在同一个语言(例如Babel,Babel接收的是JS代码生成的也是JS代码),或者编译成另外一门语言。
    • 在转换抽象语法树的时候,我们可以通过添加/删除/替换节点属性来操纵节点,我们也可以添加节点,删除节点,或者基于现有的抽象语法树创建一个全新的抽象语法树。
  3. 代码生成:基于转换后的代码形式生成目标代码
    • 有时候编译器在这个步骤也会执行转换阶段的一些行为,但是大体而言代码生成阶段的工作就是基于转换步骤产生的抽象语法树生成目标代码。
    • 代码生成器的工作方式多种多样,一些编译器会重新利用更早阶段产生的词素,还有一些编译器会创建一个独立的代码表达形式从而能够线性地打印节点。
    • 一个有效的代码生成器知道如何“打印”抽象语法树不同类型的节点,并且会递归地调用自己来打印嵌套的节点直到整个语法树被打印成一长串完整的代码字符串。

image.png

实现编译器工作过程

以 (add "2" (subtract 4 2)) 为例

1. 解析

1.1 词法分析

tokenizer 此阶段将源代码分解成一个个词素,形如 [{type:'',value:''}]。

type: 'parent' | 'name' | 'string' | 'number' // 表达式 | 函数名称 | 字符串 | 数字

(add "2" (subtract 4 2)) 会被解析为一个个词素,最终解析的形式如下:

[
    { type: 'parent', value: '(' },
    { type: 'name', value: 'add' },
    { type: 'string', value: '"2"' },
    { type: 'parent', value: '(' },
    { type: 'name', value: 'subtract' },
    { type: 'number', value: '4' },
    { type: 'number', value: '2' },
    { type: 'parent', value: ')' },
    { type: 'parent', value: ')' }
]

基本思想

  1. 对源代码进行遍历
  2. 一遍遍历后,把代码分词,通过正则或者字符串比较判断单个字符的类型,然后把单个字符匹配为 type value 形式,value 就是单个根据单个字符类型拼接一个或者多个字符为一个词。

实现 tokenizer

index.js

/**
 * 
 * @param {string} input 
 * input 循环一遍,分出不同的词素
 * (add "2" (subtract 4 2))
 * () 分别表示一个表达式的开始和结束,我们定义 type 为 parent
 * add 和 subtract 都是函数名称 type 为 name
 * "2" 是一个字符串 type 为 string
 * 4 2 是 number type 为 number
 */
function tokenizer(input) {
    // 最终生成的词素数组
    let tokens = []
    // 遍历需要一个值记录当前的位置
    let current = 0

    // 循环一遍,对当前位置的单个字符串匹配对应的type和value
    while (current < input.length) {
        // 当前位置的字符串
        let char = input[current]

        // () 分别表示一个表达式的开始和结束,我们定义 type 为 parent
        if (['(', ')'].includes(char)) {
            tokens.push({
                type: 'parent',
                value: char
            })
            ++ current
            continue
        }
        // 过滤一个或者多个空格
        const WHITESPACE = /\s/i
        if (WHITESPACE.test(char)) {
            while (WHITESPACE.test(char)) {
                char = input[++current]
            }
            continue
        }

        // number
        const NUMBERS = /[0-9]/i
        if (NUMBERS.test(char)) {
            let value = ''
            while (NUMBERS.test(char)) {
                value += char
                char = input[++current]
            }
            tokens.push({
                type: 'number',
                value
            })
            continue
        }

        // string
        if (char === '"') {
            // 跳过字符串开头的 "
            char = input[++current]
            let value = ''
            while (char !== '"') {
                value += char
                char = input[++current]
            }
            // 跳过字符串结尾的 "
            ++ current
            tokens.push({
                type: 'string',
                value
            })
            continue
        }

        // letter
        const LETTERS = /[a-z]/i
        if (LETTERS.test(char)) {
            let value = ''
            while (LETTERS.test(char)) {
                value += char
                char = input[++current]
            }
            tokens.push({
                type: 'name',
                value
            })
            continue
        }

        throw new TypeError('I dont know what this character is: ' + char)
    }
    
    return tokens
}

module.exports = {
    tokenizer
}

test code: test.js

const {
    tokenizer
} = require('./index')
const assert = require('assert')

const input = '(add "2" (subtract 4 2))'
const tokens = [
    { type: 'parent', value: '(' },
    { type: 'name', value: 'add' },
    { type: 'string', value: '2' },
    { type: 'parent', value: '(' },
    { type: 'name', value: 'subtract' },
    { type: 'number', value: '4' },
    { type: 'number', value: '2' },
    { type: 'parent', value: ')' },
    { type: 'parent', value: ')' }
]

assert.deepStrictEqual(tokenizer(input), tokens, 'Tokenizer should turn `input` string into `tokens` array')

执行 node index.js

1.2 语法分析

把词素数组 tokens 解析为 ast

image.png

基本思想

遍历一遍词素,使用递归的方式构造一颗抽象语法树 ast

实现 parser

function parser(tokens) {
    let ast = {
        type: 'Program',
        body: []
    }
    let current = 0

    function walk() {
        let token = tokens[current]

        if (token.type === 'number') {
            ++ current
            return {
                type: 'NumberLiteral',
                value: token.value
            }
        }

        if (token.type === 'string') {
            ++ current
            return {
                type: 'StringLiteral',
                value: token.value
            }
        }

        if (token.type === 'parent' && token.value === '(') {
            // 跳过 (
            token = tokens[++current]

            let node = {
                type: 'CallExpression',
                name: token.value,
                params: []
            }
            // 跳过 name
            token = tokens[++current]

            while (token.type !== 'parent' || token.type === 'parent' && token.value !== ')') {
                node.params.push(walk())
                // token 赋值为下一个节点
                token = tokens[current]
            }

            // 跳过 )
            ++ current
            return node
        }
    }

    while (current < tokens.length) {
        ast.body.push(walk())
    }

    return ast
}

test code:

const {
    tokenizer,
    parser,
} = require('./fullIndex')
const assert = require('assert')

const input = '(add "2" (subtract 4 2))'
const tokens = [
    { type: 'parent', value: '(' },
    { type: 'name', value: 'add' },
    { type: 'string', value: '2' },
    { type: 'parent', value: '(' },
    { type: 'name', value: 'subtract' },
    { type: 'number', value: '4' },
    { type: 'number', value: '2' },
    { type: 'parent', value: ')' },
    { type: 'parent', value: ')' }
]

const ast = {
    type: 'Program',
    body: [
        {
            type: 'CallExpression',
            name: 'add',
            params: [
                {
                    type: 'StringLiteral',
                    value: '2'
                }, {
                    type: 'CallExpression',
                    name: 'subtract',
                    params: [
                        {
                            type: 'NumberLiteral',
                            value: '4'
                        }, {
                            type: 'NumberLiteral',
                            value: '2'
                        }
                    ]
                }
            ]
        }
    ]
}

assert.deepStrictEqual(tokenizer(input), tokens, 'Tokenizer should turn `input` string into `tokens` array')

assert.deepStrictEqual(parser(tokens), ast, 'Tokenizer should turn `input` string into `tokens` array')

2. 转换

image.png

// ast
const ast = {
    type: 'Program',
    body: [
        {
            type: 'CallExpression',
            name: 'add',
            params: [
                {
                    type: 'StringLiteral',
                    value: '2'
                }, {
                    type: 'CallExpression',
                    name: 'subtract',
                    params: [
                        {
                            type: 'NumberLiteral',
                            value: '4'
                        }, {
                            type: 'NumberLiteral',
                            value: '2'
                        }
                    ]
                }
            ]
        }
    ]
}

newAst 转换的过程

  1. 定义一个transformer函数,这个函数接收一个抽象语法树对象 ast ;
  2. 为了处理节点,我们需要遍历它们。这个遍历的过程按照深度优先规则遍历每一个节点。
    • 所以针对上面这个抽象语法树我们会按照下面步骤遍历节点:
1. Program - 从抽象语法树的最顶端开始
2. CallExpression (add) - 移动到Program的body属性中的第一个元素
3. StringLiteral (2) - 移动到CallExpression的params中的第一个元素
4. CallExpression (subtract) - 移动到CallExpression的params中的第二个元素
5. NumberLiteral (4) - 移动到CallExpression的params中的第一个元素
6. NumberLiteral (2) - 移动到CallExpression的params中的第二个元素
  1. 如果我们直接操纵这个抽象语法树,而不是创建一个新的抽象语法树,那么我们就需要在这个步骤使用到很多不同的抽象概念。然而为了满足我们的需求,在这一步我们仅仅需要**访问**抽象语法树中的每一个节点即可。
  2. 基本的思想是我们会创建一个“访问者”对象,这个访问者对象有不同的方法来接受不同的节点类型,当我们遍历抽象语法树的时候,我们会根据现在“进入”的节点的类型调用访问者对象相对应的方法。为了使这个对象能够正常工作,我们需要传入当前节点以及当前节点的父节点的引用。

访问者模式:模式匹配 + 递归

// 当我们向下遍历语法树的时候,我们会碰到所谓的叶子节点。我们在处理完一个节点后会“离开”这个节点。
// 所以向下遍历树的时候我们“进入”节点,而向上返回的时候我们“离开”节点。
-> Program (enter)
    -> CallExpression (enter)
        -> Number Literal (enter)
        <- Number Literal (exit)
        -> Call Expression (enter)
            -> Number Literal (enter)
            <- Number Literal (exit)
            -> Number Literal (enter)
            <- Number Literal (exit)
        <- CallExpression (exit)
    <- CallExpression (exit)
<- Program (exit)

所以最终的访问者对象如下:

var visitor = {
    StringLiteral {
         enter(node, parent) {},
         exit(node, parent) {},
     },
     NumberLiteral {
         enter(node, parent) {},
         exit(node, parent) {},
     },
     CallExpression {
         enter(node, parent) {},
         exit(node, parent) {},
     },
     
};

实现

function tranverser(ast, visitor) {
    function tranverseArray(array, parent) {
        array.forEach(child => {
            tranverseNode(child, parent)
        })
    }
    function tranverseNode(node, parent) {
        let methods = visitor[node.type]
        if (methods && methods.enter) {
            methods.enter(node, parent)
        }
        switch (node.type) {
            case 'Program':
                tranverseArray(node.body, node)
                break
            case 'CallExpression':
                tranverseArray(node.params, node)
                break
            case 'StringLiteral':
            case 'NumberLiteral':
                break
            default:
                throw new TypeError(node.type)
        }
    }
    tranverseNode(ast, null)
}

function transformer(ast) {
    let newAst = {
        type: 'Program',
        body: []
    }
    // 借助于 _context 属性存储新的抽象语法树
    // parent._context 始终指向下一个节点的 body / arguments
    ast._context = newAst.body

    tranverser(ast, {
        NumberLiteral: {
            enter (node, parent) {
                parent._context.push({
                    type: 'NumberLiteral',
                    value: node.value
                })
            }
        },
        StringLiteral: {
            enter (node, parent) {
                parent._context.push({
                    type: 'StringLiteral',
                    value: node.value
                })
            }
        },
        CallExpression: {
            enter (node, parent) {
                let expression = {
                    type: 'CallExpression',
                    callee: {
                        type: 'Identifier',
                        name: node.name
                    },
                    arguments: []
                }
                node._context = expression.arguments
                if (parent.type !== 'CallExpression') {
                    expression = {
                        type: 'ExpressionStatement',
                        expression
                    }
                }
                parent._context.push(expression)
            }
        }
    })
    

    return newAst
}

3. 代码生成

image.png

遍历语法树,深度优先遍历

function codeGenerator(node) {
    switch(node.type) {
        case 'Program':
            return node.body.map(codeGenerator).join('\n')
        case 'ExpressionStatement':
            return codeGenerator(node.expression) + ';'
        case 'CallExpression':
            return codeGenerator(node.callee) + '(' + node.arguments.map(codeGenerator).join(', ') + ')'
        case 'Identifier':
            return node.name
        case 'StringLiteral':
            return '"' + node.value + '"'
        case 'NumberLiteral':
            return node.value
        default:
            throw new TypeError(node.type)
    }
}

webpack babel 编译器

image.png

Babel 的架构

我在《透过现象看本质: 常见的前端架构风格和案例🔥》 提及 Babel 和 Webpack 为了适应复杂的定制需求和频繁的功能变化,都使用了微内核 的架构风格。也就是说它们的核心非常小,大部分功能都是通过插件扩展实现的

image.png

Babel visitor

Wiki 上面对‘宏’的定义是:宏(Macro), 是一种批处理的称谓,它根据一系列的预定义规则转换一定的文本模式。解释器编译器在遇到宏时会自动进行这一模式转换,这个转换过程被称为“宏展开(Macro Expansion)”。对于编译语言,宏展开在编译时发生,进行宏展开的工具常被称为宏展开器。

宏就是用来生成代码的代码,它有能力进行一些句法解析和代码转换。宏大致可以分为两种: 文本替换语法扩展

预处理器(宏展开器)GNU m4教程

JS '类' Lisp的宏机制

原文地址:编译器

在线转换工具:在线解析为AST 在线Babel编译