如何用OCaml实现一个JSON查询语言?query-json架构解析:从词法分析到解释执行
如何用OCaml实现一个JSON查询语言?query-json架构解析:从词法分析到解释执行
【免费下载链接】query-jsonFaster, simpler and more portable implementation of jq-inspired language in OCaml项目地址: https://gitcode.com/gh_mirrors/qu/query-json
query-json是用 OCaml 实现的一款快速、简洁且可移植的JSON 查询语言,可以理解为「更快更好用的 jq」:它把一条 JSON 查询语句经过词法分析 → 语法解析 → 抽象语法树(AST)→ 解释执行四个阶段,最终输出查询结果。本文面向新手,不堆砌代码,带你完整拆解这套 JSON 查询引擎的架构设计。
🗂️ 总体架构:一条查询的旅程
当你输入query-json '.store.books[0].title' bookstore.json时,项目内部的Core.ml模块会编排一次完整流水线:
| 阶段 | 负责模块 | 作用 |
|---|---|---|
| ① 词法分析 | source/Lexer.ml | 把查询文本切分成 token 流 |
| ② 语法分析 | source/Parser.ml | 把 token 组装成语法树 |
| ③ AST 表示 | source/Ast.ml | 定义表达式、函数、运算符等数据结构 |
| ④ 函数库 | source/Language.ml | 管理所有内置函数的注册与命名 |
| ⑤ 解释执行 | source/Interpreter.ml | 递归遍历 AST,对 JSON 数据求值 |
| ⑥ JSON 编解码 | source/jotason/ | 高性能读取与美化输出 JSON |
整条链路非常清晰:文本 → token → AST → 执行结果,这也是所有脚本语言(如 jq、grep 方言)的标准做法。
🔤 词法分析:把文本切成"单词"
词法器source/Lexer.ml基于 OCaml 的sedlex库编写,它把查询字符串逐个字符扫描,产出带类型的 token。核心 token 类型定义在source/Lexer.mli中,大致分为几类:
- 字面量:
INT、STRING、BOOL、NULL - 标识符:
IDENTIFIER(键名)、FUNCTION(函数名)、VARIABLE($var) - 运算符:
PIPE(管道|)、DOT(.)、ALTERNATIVE(??) - 关键字:
IF、THEN、ELSE、FN、TRY、CATCH等控制流词
值得一提的是,token 类型里对数字做了精细区分:INT、INT64、BIG_INT(大整数)三种,这让 JSON 查询能无损处理超大整数,避免精度丢失。
词法器还有一个巧妙设计——字符串插值:模板字符串会被切成INTERP(插值片段)和TEMPLATEtoken,支持在查询中动态拼接表达式。
🌳 语法分析:构建抽象语法树
source/Parser.mli只暴露一个入口函数program,它消费 token 流,产出Ast.expression。AST 的定义全部在source/Ast.ml中,值得关注的几组结构:
- 表达式:
Identity(.)、Pipe(管道)、Operation(二元运算)、If_then_else、Try(异常处理) - 访问模式:
Key(.foo)、Index(.[1])、Slice(.[0:3])、Dynamic_access(.[$expr]动态取键) - 函数节点:
Fn0/Fn1/Fn2分别对应 0 参、1 参、2 参函数——这是整个语言函数体系的骨架 - 构造函数:
List(数组字面量)、Object(对象字面量)
这种"函数按参数个数分型"的做法,让类型检查器可以提前约束每个函数能接几个参数,是比纯字符串匹配更稳健的设计。
📚 函数库:200+ 内置函数的注册中心
source/Language.ml是内置函数的"注册中心"。它为每个函数记录元信息:函数名、别名(兼容 jq 旧名)、描述、示例、适用类型(字符串/数组/对象…)和参数个数。这套元数据被多处复用:
- CLI 的
query-json --functions分类列出全部函数 - REPL 的自动补全提示
- 解析期的参数数量校验与报错
语言层面它做了一次"现代化改造":全部函数统一snake_case命名(如to_uppercase取代 jq 的ascii_upcase),null 访问默认报错而非静默传播,需要宽松行为时显式写.foo?。完整对照见 docs/JQ_COMPATIBILITY.md 文档。
⚙️ 解释执行:树遍历求值
source/Interpreter.ml是引擎心脏。它采用经典的树遍历解释器:execute函数接收 AST 与 JSON 数据,递归地对每个表达式节点求值,最终返回Ok of Json.t list | Error of string | Halt of int三种结果——查询可能产出多个结果、失败、或以状态码中止。
解释器直接操作source/jotason/中的 JSON 值类型,省去了中间转换开销。jotason 模块内置了Json.ml(值模型)、Read.mll(手写词法器读取 JSON)、Write.ml(美化输出),是整个项目里性能优化最集中的地方。
🎯 错误系统:告诉你"为什么错、怎么改"
source/Error.ml让报错信息带上源代码位置。相比 jq 直接输出null,query-json 会指出哪个 token 出了问题、给出可用键名、甚至附上修复建议:
这类"可操作的错误信息"对新手极其友好,也说明错误处理被当作核心功能而非附属品来设计。
🖥️ 一份代码,三端运行
OCaml 的多后端能力是 query-json 最大的架构红利:
- 原生二进制:编译为 macOS / Linux / Windows 可执行文件,基准测试显示比 jq 快 2~4 倍
- 浏览器:通过 js 编译目标输出 JavaScript,驱动在线 playground(
website/目录) - Node.js 库:
js/Js.ml提供 JS 绑定,可npm install使用
交互式 REPL 支持上下文感知补全:输入.提示键名,输入|提示函数:
更高级的过滤、分组、聚合用法可以参考cli/test/下的集成测试脚本,它们就是可执行的查询用例集:
📌 小结
query-json 展示了一个教科书级的查询语言实现路径:
- sedlex 词法分析→ token 流(数字类型精细分级)
- 递归下降解析→ 类型化的 AST(函数按元数分型)
- 元数据驱动的函数库→ 解析校验、补全、文档同源
- 树遍历解释器→ 直接操作 JSON 值,零中间层
- 位置感知的错误系统→ 新手友好的排错体验
- OCaml 多后端→ 一份源码覆盖 CLI / 浏览器 / Node.js
如果你想动手研究,可以从source/Core.ml的parse与run两个函数读起——50 行代码内就能看懂整条流水线的全貌。
【免费下载链接】query-jsonFaster, simpler and more portable implementation of jq-inspired language in OCaml项目地址: https://gitcode.com/gh_mirrors/qu/query-json
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
