`parse.js` 的核心思路

Article detail

学习笔记

2026/4/14 · 24 分钟阅读

parse.js 的核心思路

好,我来详细解释 parse.js 怎么写。


parse.js 的核心思路

先理解数据流

用户输入文本
    ↓  sexp.ts 的 parser.parse()
syntax[]   ← 通用 S-expression 树(不懂 UTLC)
    ↓  我们写的 parse_expr()
exp AST    ← UTLC 专用 AST(懂语法规则)+ 错误列表

syntax[] 长什么样?

拿具体例子说明:输入 (lambda (x) x)

parser.parse() 返回:
[
  group {
    children: [
      atom { leaf: token("(") }        ← 下标 0: 左括号
      atom { leaf: token("lambda") }   ← 下标 1: 标识符 lambda
      atom { leaf: token(" ") }        ← 下标 2: 空格 (space)
      group {                          ← 下标 3: (x) 这个括号组
        children: [
          atom("("),
          atom("x"),
          atom(")")
        ]
      }
      atom { leaf: token(" ") }        ← 下标 4: 空格
      atom { leaf: token("x") }        ← 下标 5: body
      atom { leaf: token(")") }        ← 下标 6: 右括号
    ]
  }
]

关键点: group.children包含了括号 token 本身(第一个和最后一个),而且空白也在里面


过滤噪音 → 提取"有意义的子节点"

写一个辅助函数,把 children 里的括号空白都过滤掉:

// 从 group.children 里取出有意义的节点(去掉括号 token 和空白)
function meaningful_children(group) {
  return group.children.filter(child => {
    // 跳过空白
    if (child.tag === syntax.tag.atom && child.leaf.kind === token.kind.atom.space) {
      return false
    }
    // 跳过括号 token(group 的第一个和最后一个 children 是括号)
    if (child.tag === syntax.tag.atom) {
      const k = child.leaf.kind
      if (k === token.kind.parenthesis.left || k === token.kind.parenthesis.right) {
        return false
      }
    }
    return true
  })
}

过滤后,(lambda (x) x) 的 meaningful_children 就变成:

[ atom("lambda"),  group((x)),  atom("x") ]
   index 0          index 1      index 2

parse_expr 的决策树

输入一个 syntax 节点,判断 tag:

├── tag == atom
│   ├── 是空白 → 跳过(返回 null)
│   └── 是标识符 → 返回 var_ { name: "x" }
│
├── tag == lone  → 孤立括号,push 错误,返回 null
│
├── tag == mismatch → 括号不匹配,push 错误,返回 null
│
└── tag == group
    取 meaningful_children(group) → items[]
    ├── items[0] 是 atom("lambda")
    │   ├── items.length == 3 → abs { param: items[1], body: parse_expr(items[2]) }
    │   │   还要验证 items[1] 是一个 group,里面只有一个标识符
    │   ├── items.length < 3  → 错误:lambda 缺少参数
    │   └── items.length > 3  → 错误:lambda 参数过多
    │
    └── 否则(普通 application)
        ├── items.length == 2 → app { func: parse_expr(items[0]), arg: parse_expr(items[1]) }
        ├── items.length < 2  → 错误:application 缺少函数或参数
        └── items.length > 2  → 错误:application 参数过多

实际代码骨架

// parse.js (CommonJS)
const { parser, syntax, token } = require('./sexp.js')  // 编译后的 sexp

/**
 * @typedef {{ tag: 'var_', name: string, range: range }} VarExp
 * @typedef {{ tag: 'abs', param: string, body: Exp, range: range }} AbsExp
 * @typedef {{ tag: 'app', func: Exp, arg: Exp, range: range }} AppExp
 * @typedef {VarExp | AbsExp | AppExp} Exp
 *
 * @typedef {{ range: range, message: string }} ParseError
 */

/**
 * 过滤 group.children 中的括号和空白
 */
function meaningful_children(group_node) {
  return group_node.children.filter(child => {
    if (child.tag !== syntax.tag.atom) return true  // 非 atom 节点(group/lone/mismatch)保留
    const k = child.leaf.kind
    // 过滤空白
    if (k === token.kind.atom.space) return false
    // 过滤括号
    if (k === token.kind.parenthesis.left)  return false
    if (k === token.kind.parenthesis.right) return false
    return true
  })
}

/**
 * DFS 解析单个 syntax 节点为 exp
 * @param {import('./sexp').syntax} node
 * @param {ParseError[]} errors  - 收集错误(副作用)
 * @returns {Exp | null}
 */
function parse_expr(node, errors) {
  switch (node.tag) {

    case syntax.tag.atom: {
      const k = node.leaf.kind
      if (k === token.kind.atom.space) return null  // 空白,跳过
      // 普通标识符 → variable
      return {
        tag: 'var_',
        name: token.to_span(node.leaf, /* text */ ),  // 需要传入原始 text
        range: token.to_range(node.leaf)
      }
    }

    case syntax.tag.lone: {
      errors.push({
        range: token.to_range(node.leaf),
        message: `孤立的括号 '${token.to_span(node.leaf, text)}'`
      })
      return null
    }

    case syntax.tag.mismatch: {
      const open = syntax.open(node)
      errors.push({
        range: token.to_range(open),
        message: '括号不匹配'
      })
      return null
    }

    case syntax.tag.group: {
      const items = meaningful_children(node)
      // items[0] 是 lambda?
      if (items.length > 0
          && items[0].tag === syntax.tag.atom
          && token.to_span(items[0].leaf, text) === 'lambda') {

        // (lambda (binder) body) → 期望 items 有 3 个:lambda, (binder), body
        if (items.length !== 3) {
          errors.push({
            range: token.to_range(syntax.open(node)),
            message: `lambda 需要恰好 2 个参数 (binder 和 body),实际得到 ${items.length - 1} 个`
          })
          return null
        }

        // items[1] 应该是 (binder) 这个 group
        const binder_group = items[1]
        if (binder_group.tag !== syntax.tag.group) {
          errors.push({
            range: ???,  // 用 items[1] 的 range
            message: 'lambda 的 binder 必须是 (binder) 格式'
          })
          return null
        }
        const binder_items = meaningful_children(binder_group)
        if (binder_items.length !== 1 || binder_items[0].tag !== syntax.tag.atom) {
          errors.push({ ... , message: 'binder 必须是单个标识符' })
          return null
        }

        const param_name = token.to_span(binder_items[0].leaf, text)
        const body = parse_expr(items[2], errors)

        return { tag: 'abs', param: param_name, body, range: ... }
      }

      // 普通 application (func arg)
      if (items.length < 2) {
        errors.push({ ..., message: 'application 需要至少一个函数和一个参数' })
        return null
      }
      if (items.length > 2) {
        errors.push({ ..., message: 'application 只接受一个参数' })
        return null
      }

      const func = parse_expr(items[0], errors)
      const arg  = parse_expr(items[1], errors)
      return { tag: 'app', func, arg, range: ... }
    }
  }
}

/**
 * 顶层入口:解析整段文本
 */
function parse(text) {
  const p = parser.make(text)
  const tree = parser.parse(p)  // syntax[]
  const errors = []
  const exprs = tree
    .map(node => parse_expr(node, errors, text))
    .filter(e => e !== null)
  return { exprs, errors }
}

module.exports = { parse }

一个具体的错误例子

输入 (lambda x x) ← binder 没有括号

parse_expr(group) 被调用
  items = [atom("lambda"), atom("x"), atom("x")]
  items.length === 3  ✓
  items[1] = atom("x")  ← 不是 group!
  → push error: "lambda 的 binder 必须是 (binder) 格式"
  → return null

需要注意的一个坑

token.to_span(token, text) 需要传入原始文本字符串 text,因为 token 只存了偏移量,实际内容要从原文里截取。所以 parse_expr 需要把 text 作为参数一路传下去(或者闭包捕获)。


明白了吗?有哪部分想深入聊的?

评论

动作测试