Skip to content

Acorn 解析器课程计划

课程概述

本课程将带你从零开始实现一个 JavaScript 解析器,理解 Acorn 的核心原理。

课程目标

  • 理解词法分析的原理
  • 掌握语法分析的方法
  • 学习 AST 的构建和遍历
  • 能够构建自己的解析器

课程结构

8 节课,每节 20-40 分钟,总时长约 90-120 分钟。


第 1 课:Acorn 简介

目标

  • 了解 Acorn 的背景
  • 理解解析器的概念
  • 掌握 Acorn 的核心功能

内容

  1. Acorn 背景

    • 什么是 Acorn
    • 为什么需要解析器
    • Acorn 的历史
  2. 核心概念

    • 词法分析
    • 语法分析
    • AST(抽象语法树)
  3. 解析器的应用

    • 代码转换
    • 代码分析
    • 代码格式化

实践步骤

bash
# 1. 创建项目
mkdir acorn-course
cd acorn-course

# 2. 初始化项目
npm init -y

# 3. 安装 Acorn
npm install acorn --save

预期输出

✓ 项目初始化完成
✓ Acorn 安装完成

总结

  • ✅ 理解了解析器的概念
  • ✅ 掌握了核心功能
  • ✅ 了解了应用场景

第 2 课:词法分析

目标

  • 理解词法分析的原理
  • 实现词法分析器
  • 学习标记类型

内容

  1. 词法分析

    • 什么是词法分析
    • 标记类型
    • 词法分析算法
  2. 实现词法分析器

    • 识别标记
    • 处理空白
    • 处理注释
  3. 测试词法分析器

    • 单元测试
    • 边界测试

实践步骤

bash
# 1. 创建词法分析器
cat > src/tokenizer.js << 'EOF'
export function tokenize(source) {
  const tokens = []
  let pos = 0
  
  while (pos < source.length) {
    const char = source[pos]
    
    // 跳过空白
    if (/\s/.test(char)) {
      pos++
      continue
    }
    
    // 识别标记
    if (/[a-zA-Z_$]/.test(char)) {
      tokens.push(readIdentifier(source, pos))
      pos += tokens[tokens.length - 1].value.length
    } else if (/[0-9]/.test(char)) {
      tokens.push(readNumber(source, pos))
      pos += tokens[tokens.length - 1].value.length
    } else {
      // 其他标记
      tokens.push({ type: 'Punctuator', value: char })
      pos++
    }
  }
  
  return tokens
}

function readIdentifier(source, pos) {
  let value = ''
  while (pos < source.length && /[a-zA-Z0-9_$]/.test(source[pos])) {
    value += source[pos]
    pos++
  }
  return { type: 'Identifier', value }
}

function readNumber(source, pos) {
  let value = ''
  while (pos < source.length && /[0-9.]/.test(source[pos])) {
    value += source[pos]
    pos++
  }
  return { type: 'Numeric', value: parseFloat(value) }
}
EOF

# 2. 创建测试
cat > test/tokenizer.test.js << 'EOF'
import { describe, it } from 'node:test'
import assert from 'node:assert'
import { tokenize } from '../src/tokenizer.js'

describe('词法分析器测试', () => {
  it('应该解析代码', () => {
    const tokens = tokenize('const a = 1')
    assert.ok(Array.isArray(tokens))
  })
})
EOF

# 3. 运行测试
npm test

预期输出

✓ 应该解析代码

总结

  • ✅ 实现了词法分析器
  • ✅ 理解了标记类型
  • ✅ 掌握了词法分析算法

第 3 课:语法分析

目标

  • 理解语法分析的原理
  • 实现语法分析器
  • 学习 AST 构建

内容

  1. 语法分析

    • 什么是语法分析
    • AST 结构
    • 语法分析算法
  2. 实现语法分析器

    • 构建节点
    • 构建表达式
    • 构建语句
  3. 测试语法分析器

    • 单元测试
    • 复杂测试

实践步骤

bash
# 1. 创建语法分析器
cat > src/parser.js << 'EOF'
import { tokenize } from './tokenizer.js'

export function parse(source) {
  const tokens = tokenize(source)
  let pos = 0
  
  function parseExpression() {
    return parseAssignment()
  }
  
  function parseAssignment() {
    const left = parseAdditive()
    
    if (match('=')) {
      consume()
      const right = parseAssignment()
      return {
        type: 'AssignmentExpression',
        operator: '=',
        left,
        right
      }
    }
    
    return left
  }
  
  function parseAdditive() {
    let left = parseMultiplicative()
    
    while (match('+') || match('-')) {
      const operator = consume().value
      const right = parseMultiplicative()
      left = {
        type: 'BinaryExpression',
        operator,
        left,
        right
      }
    }
    
    return left
  }
  
  function parseMultiplicative() {
    let left = parsePrimary()
    
    while (match('*') || match('/')) {
      const operator = consume().value
      const right = parsePrimary()
      left = {
        type: 'BinaryExpression',
        operator,
        left,
        right
      }
    }
    
    return left
  }
  
  function parsePrimary() {
    if (match('Identifier')) {
      return consume()
    }
    if (match('Numeric')) {
      return consume()
    }
    if (match('(')) {
      consume()
      const expr = parseExpression()
      expect(')')
      return expr
    }
  }
  
  function match(type) {
    return pos < tokens.length && tokens[pos].type === type
  }
  
  function consume() {
    return tokens[pos++]
  }
  
  function expect(type) {
    if (!match(type)) {
      throw new Error(`Expected ${type}, got ${tokens[pos]?.type}`)
    }
    return consume()
  }
  
  return parseExpression()
}
EOF

# 2. 创建测试
cat > test/parser.test.js << 'EOF'
import { describe, it } from 'node:test'
import assert from 'node:assert'
import { parse } from '../src/parser.js'

describe('语法分析器测试', () => {
  it('应该解析表达式', () => {
    const ast = parse('1 + 2')
    assert.strictEqual(ast.type, 'BinaryExpression')
  })
})
EOF

# 3. 运行测试
npm test

预期输出

✓ 应该解析表达式

总结

  • ✅ 实现了语法分析器
  • ✅ 理解了 AST 结构
  • ✅ 掌握了语法分析算法

第 4 课:AST 遍历

目标

  • 理解 AST 遍历的原理
  • 实现 AST 遍历器
  • 学习节点访问

内容

  1. AST 遍历

    • 什么是遍历
    • 遍历策略
    • 访问者模式
  2. 实现遍历器

    • 深度优先遍历
    • 节点访问
    • 路径操作
  3. 测试遍历器

    • 单元测试
    • 复杂测试

实践步骤

bash
# 1. 创建遍历器
cat > src/traverse.js << 'EOF'
export function traverse(ast, visitor) {
  function visit(node) {
    if (!node) return
    
    // 访问当前节点
    if (visitor.enter) {
      visitor.enter(node)
    }
    
    // 遍历子节点
    for (const key in node) {
      const child = node[key]
      
      if (Array.isArray(child)) {
        child.forEach(visit)
      } else if (typeof child === 'object' && child !== null) {
        visit(child)
      }
    }
    
    // 离开节点
    if (visitor.leave) {
      visitor.leave(node)
    }
  }
  
  visit(ast)
}
EOF

# 2. 创建测试
cat > test/traverse.test.js << 'EOF'
import { describe, it } from 'node:test'
import assert from 'node:assert'
import { parse } from '../src/parser.js'
import { traverse } from '../src/traverse.js'

describe('遍历器测试', () => {
  it('应该遍历 AST', () => {
    const ast = parse('1 + 2')
    let visited = false
    
    traverse(ast, {
      enter(node) {
        if (node.type === 'BinaryExpression') {
          visited = true
        }
      }
    })
    
    assert.strictEqual(visited, true)
  })
})
EOF

# 3. 运行测试
npm test

预期输出

✓ 应该遍历 AST

总结

  • ✅ 实现了 AST 遍历
  • ✅ 理解了遍历策略
  • ✅ 掌握了节点访问

第 5 课:代码生成

目标

  • 理解代码生成的原理
  • 实现代码生成器
  • 学习代码格式化

内容

  1. 代码生成

    • 什么是代码生成
    • 生成算法
    • 格式化选项
  2. 实现代码生成器

    • AST 到代码
    • 格式化
    • 压缩
  3. 测试代码生成器

    • 单元测试
    • 格式化测试

实践步骤

bash
# 1. 创建代码生成器
cat > src/generator.js << 'EOF'
export function generate(ast, options = {}) {
  const { minify = false } = options
  let code = ''
  
  function visit(node) {
    switch (node.type) {
      case 'Identifier':
        code += node.value
        break
      case 'Numeric':
        code += node.value
        break
      case 'BinaryExpression':
        visit(node.left)
        code += node.operator
        visit(node.right)
        break
      case 'AssignmentExpression':
        visit(node.left)
        code += node.operator
        visit(node.right)
        break
      // ... 其他节点类型
    }
  }
  
  visit(ast)
  
  if (minify) {
    code = code.replace(/\s+/g, ' ').trim()
  }
  
  return code
}
EOF

# 2. 创建测试
cat > test/generator.test.js << 'EOF'
import { describe, it } from 'node:test'
import assert from 'node:assert'
import { parse } from '../src/parser.js'
import { generate } from '../src/generator.js'

describe('代码生成器测试', () => {
  it('应该生成代码', () => {
    const ast = parse('1 + 2')
    const code = generate(ast)
    assert.ok(typeof code === 'string')
  })
})
EOF

# 3. 运行测试
npm test

预期输出

✓ 应该生成代码

总结

  • ✅ 实现了代码生成器
  • ✅ 理解了生成算法
  • ✅ 掌握了代码格式化

第 6 课:作用域分析

目标

  • 理解作用域分析的原理
  • 实现作用域分析器
  • 学习变量声明

内容

  1. 作用域分析

    • 什么是作用域
    • 作用域类型
    • 作用域链
  2. 实现作用域分析

    • 变量声明
    • 变量引用
    • 作用域嵌套
  3. 测试作用域分析

    • 单元测试
    • 复杂测试

实践步骤

bash
# 1. 创建作用域分析器
cat > src/scope.js << 'EOF'
export function analyzeScope(ast) {
  const scopes = []
  let currentScope = null
  
  function enterScope() {
    const scope = {
      parent: currentScope,
      declarations: new Set(),
      references: new Set()
    }
    scopes.push(scope)
    currentScope = scope
  }
  
  function exitScope() {
    currentScope = currentScope.parent
  }
  
  function declare(name) {
    if (currentScope) {
      currentScope.declarations.add(name)
    }
  }
  
  function reference(name) {
    if (currentScope) {
      currentScope.references.add(name)
    }
  }
  
  traverse(ast, {
    enter(node) {
      if (node.type === 'Identifier') {
        reference(node.value)
      }
    }
  })
  
  return scopes
}
EOF

# 2. 创建测试
cat > test/scope.test.js << 'EOF'
import { describe, it } from 'node:test'
import assert from 'node:assert'
import { parse } from '../src/parser.js'
import { analyzeScope } from '../src/scope.js'

describe('作用域分析测试', () => {
  it('应该分析作用域', () => {
    const ast = parse('a + b')
    const scopes = analyzeScope(ast)
    assert.ok(Array.isArray(scopes))
  })
})
EOF

# 3. 运行测试
npm test

预期输出

✓ 应该分析作用域

总结

  • ✅ 实现了作用域分析
  • ✅ 理解了作用域类型
  • ✅ 掌握了变量声明

第 7 课:错误处理

目标

  • 理解错误处理的原理
  • 实现错误处理器
  • 学习错误恢复

内容

  1. 错误处理

    • 什么是错误
    • 错误类型
    • 错误恢复
  2. 实现错误处理

    • 错误检测
    • 错误报告
    • 错误恢复
  3. 测试错误处理

    • 单元测试
    • 错误测试

实践步骤

bash
# 1. 创建错误处理器
cat > src/error.js << 'EOF'
export class ParseError extends Error {
  constructor(message, pos, source) {
    super(message)
    this.name = 'ParseError'
    this.pos = pos
    this.source = source
  }
  
  getLocation() {
    const lines = this.source.substring(0, this.pos).split('\n')
    const line = lines.length
    const column = lines[lines.length - 1].length + 1
    return { line, column }
  }
}

export function reportError(error) {
  const location = error.getLocation()
  console.error(`Error at line ${location.line}, column ${location.column}:`)
  console.error(error.message)
}
EOF

# 2. 创建测试
cat > test/error.test.js << 'EOF'
import { describe, it } from 'node:test'
import assert from 'node:assert'
import { ParseError } from '../src/error.js'

describe('错误处理测试', () => {
  it('应该创建错误', () => {
    const error = new ParseError('Test error', 0, 'test')
    assert.ok(error instanceof ParseError)
  })
})
EOF

# 3. 运行测试
npm test

预期输出

✓ 应该创建错误

总结

  • ✅ 实现了错误处理
  • ✅ 理解了错误类型
  • ✅ 掌握了错误恢复

第 8 课:总结与扩展

目标

  • 总结所有课程
  • 回顾关键概念
  • 提供扩展建议

内容

  1. 课程总结

    • 回顾所有功能
    • 总结关键概念
    • 展示完整代码
  2. 扩展建议

    • 添加更多功能
    • 优化性能
    • 支持更多特性
  3. 下一步

    • 学习 Acorn 源码
    • 参与开源项目
    • 构建自己的解析器

总结

通过本课程,你学会了:

  1. ✅ Acorn 简介
  2. ✅ 词法分析
  3. ✅ 语法分析
  4. ✅ AST 遍历
  5. ✅ 代码生成
  6. ✅ 作用域分析
  7. ✅ 错误处理

下一步

  • 学习 Acorn 源码
  • 添加更多功能
  • 参与开源项目

参考资源

架构师AI杜公众号二维码

扫描二维码关注"架构师AI杜"公众号,获取更多技术内容和最新动态