为什么query-json放弃menhir改用手写递归下降解析器?Parser源码级剖析 📅 发布时间:2026/8/25 9:12:32 👁 浏览次数: 为什么query-json放弃menhir改用手写递归下降解析器Parser源码级剖析【免费下载链接】query-jsonFaster, simpler and more portable implementation of jq-inspired language in OCaml项目地址: https://gitcode.com/gh_mirrors/qu/query-jsonquery-json 是一个用 OCaml 编写的 jq 风格 JSON 查询语言实现主打更快、更简单、更可移植。它早期借助 menhir 生成解析器而在 1.0.0 beta 版本中项目把 menhir 解析器替换成了手写递归下降解析器。本文从源码层面拆解这次重构的动因与实现细节。项目速览query-json 是什么query-json 的定位可以概括为一句话像 sed 处理文本那样处理 JSON 的小语言。你给它一条查询语句和一个 JSON 文件它返回查询结果。它的几个特点快OCaml 编译为原生二进制基准测试里全面对标 jq简单函数统一 snake_case 命名to_string而非tostring语义比 jq 更严格可移植同一套内核既能编译成命令行工具也能编译成 JavaScript 库在浏览器里跑❓可选访问用?优雅处理缺失字段如.missing?返回 null 而非报错在终端里最基础的用法一次有记录的重构从 menhir 到递归下降这次替换在 CHANGES.md 的 1.0.0~beta-1 条目中有明确记录[REFACTOR] Replace menhir-based parser with hand-written recursive descent parser三个事实佐证这次重构已经落地依赖清单里不再有 menhir。项目根目录 dune-project 声明的核心依赖只剩sedlex词法分析、zarith大整数、mosaicREPL等构建期解析器生成器已被移除库依赖同样干净。核心库 source/dune 中只列了json sedlex re unix zarith没有任何 parser 生成代码的引用旧文档还留有案发现场。benchmarks/README.md 中仍写着 query-json uses Menhir, an LR(1) parser generator——这是项目早期确实依赖 menhir 的旁证为什么放弃 menhir源码给出的四个理由1️⃣ 精准到字符的语法错误提示menhir 生成的解析器在错误处理上比较一刀切而 query-json 对错误体验投入了大量工作。手写的 source/Parser.ml 中expect函数在每步都记录 token 的精确位置let expect stream expected let token stream.token in if token expected then advance stream else let message Printf.sprintf expected %s, got %s (Lexer.humanize expected) (Lexer.humanize token) in ...配合 source/Error.ml 中支持location、contexts、suggestion三个维度的错误类型解析器能做到指出出错的具体行列并画出^^^指针类型不匹配时给出期望 vs 实际的对比缺失键时列出可用键名并建议使用?这种每一步都掌握位置信息的写法只有控制解析流程本身才能实现——这正是手写递归下降的天然优势。2️⃣ 可移植同一内核编译到浏览器dune-project 里声明了三个包query-json原生二进制 CLIquery-json-js通过 js_of_ocaml 编译的 JS 库query-json-playground基于 Melange/ReScript 的 Web 在线 playground手写解析器是纯粹的 OCaml 函数词法部分由sedlex在编译期展开为普通代码见 source/dune 中的(pps ... sedlex.ppx)没有额外的生成物环节。去掉 menhir 意味着整条构建链对所有目标平台都更干净与项目more portable的口号完全一致。3️⃣ 语法快速演进手写更易维护从 CHANGES.md 可以看到 1.0.0 beta 一口气加入的语法特性foreach循环、reduce解构绑定as [$i, $j]、字符串插值\(expr)、切片[1:3]、try/catch/finally、复合赋值等。递归下降解析器中每增加一条语法规则就增加一个parse_xxx函数结构与文法一一对应而维护 LR(1) 文法时每加一条规则都可能触发移/约冲突排查。对处于高速迭代的语言来说手写的可控性价值巨大。4️⃣ simpler 名副其实menhir 是构建期的代码生成工具意味着构建系统多一个环节。移除后解析器就是一个 900 多行、可读性极高的 source/Parser.ml新人可以直接通读全部解析逻辑。手写递归下降解析器是如何工作的词法层sedlex 生成的 Lexersource/Lexer.ml 定义了约 60 种 token并用sedlex.regexp描述字面量模式例如let decimal_number [%sedlex.regexp? Plus digit, ., Plus digit, Opt exponent | Plus digit, exponent]数字被刻意区分成INT/INT64/BIG_INT/DECIMAL四种 token为后来按最小适配类型存储数值的高精度特性打下基础。此外还有humanize函数把 token 渲染成人类可读的形式[、$x、text专门服务于错误消息。核心骨架stream advance / peek / expect解析器的全部状态装在一个轻量记录里source/Parser.mltype stream { buf : Sedlexing.lexbuf; (* 原始输入 *) mutable token : Lexer.token; (* 当前 lookahead token *) mutable start_pos : Lexing.position; mutable end_pos : Lexing.position; }围绕它只有三个动词函数作用advance从 Lexer 取下一个 token并记录起止位置peek只读当前 token不消费expect断言当前 token 等于期望值不等则抛出带位置的解析错误这就是递归下降的最小完备工具集单 token 前瞻 位置追踪既省内存又让每处错误都能定位到行列。运算符优先级阶梯优先级通过一长串互相递归的parse_xxx实现层级从低到高依次是见 source/Parser.mlparse_pipe_expr 管道 | 赋值 - * / ?? parse_comma_expr 逗号 , parse_or_expr or parse_and_expr and parse_comparison ! parse_add_expr - parse_mul_expr * / % parse_term → parse_postfix. 键访问、[ ] 索引 parse_primary 字面量、变量、函数调用、{ } [ ] 构造每一层都是先解析左操作数再看是否出现本层运算符是则递归右侧并循环——这是递归下降处理中缀表达式的标准套路OCaml 的尾递归优化保证了长表达式栈开销恒定。一个真实例子.books[1].author这个典型查询的解析路径恰好被 source/test/Test_parse.ml 覆盖parse_primary看到DOT进入parse_dot取出键booksparse_postfix发现后面跟着[进入parse_bracket_access读数字 token1遇到]收尾生成Index [1]parse_postfix继续循环发现.author再套一层Key最终产出 ASTPipe (Pipe (Key books, Index [1]), Key author)——层层嵌套的管道节点与语法树天然同构。字符串插值Hello \(name)则展示了手写解析器的另一重灵活parse_interpolated_string 在解析表达式和扫描字符串片段之间反复横跳把文字与子查询拼成Operation链——这种嵌套扫描逻辑放在 LR 文法里会非常别扭。从这次重构学到的东西错误提示是解析器的核心竞争力。手写解析器让你在每个 token 边界注入位置信息与上下文换来的是用户能看懂并自救的报错语言处于快速迭代期时递归下降的维护成本更低。文法结构与函数结构一一对应心智负担小依赖减法也是架构决策。少一个构建期代码生成工具多平台原生 / JS / Web构建就更简单单 token 前瞻 位置追踪就足以支撑一个完整语言的前端工具不必复杂 想继续深挖的源码入口source/Parser.ml —— 递归下降解析器主体942 行可全文通读source/Lexer.ml —— sedlex 词法分析与 token 定义source/Ast.ml —— 语法树定义source/Error.ml —— 带位置、上下文与修复建议的错误模型source/test/Test_parse.ml —— 输入 → 期望 AST 的回归用例CHANGES.md —— 完整的版本演进记录【免费下载链接】query-jsonFaster, simpler and more portable implementation of jq-inspired language in OCaml项目地址: https://gitcode.com/gh_mirrors/qu/query-json创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考