PostHog HogQL 解析器:基于 ANTLR 的语法生成流程与 C++/WASM 双端解析器构建管线
【免费下载链接】posthog:hedgehog: PostHog is the leading platform for building self-driving products. Our developer tools – AI observability, analytics, session replay, flags, experiments, error tracking, logs, and more – capture all the context agents need to diagnose problems, uncover opportunities, and ship fixes. Steer it all from Slack, web, desktop, or the MCP.项目地址: https://gitcode.com/GitHub_Trending/po/posthog
本文围绕 PostHog 仓库中 HogQL 语法目录下的说明文档展开,完整讲解如何从 ANTLR 语法文件(HogQLParser.g4与拆分式 Lexer 文件)重新生成 C++ 解析器源码:包括 ANTLR 工具在 macOS/Ubuntu 上的安装方式、pnpm run grammar:build背后的确切命令行拼接逻辑、生成产物的落盘位置,以及 CI 如何强制保证语法文件与生成代码不漂移。读完后你可以独立完成一次 HogQL 语法的修改与再生成,并理解这套语法如何同时支撑 Python 原生扩展和 WebAssembly 两个发布包。
语法文件构成:一个 Parser 加一个"拼接式"Lexer
HogQL 是 PostHog 的查询语言,其解析能力由 common/hogql_parser 目录封装为两个发布包:PyPI 上的hogql_parser(CPython 原生 C++ 扩展)和 npm 上的@posthog/hogql-parser(WebAssembly 模块)。两者共享同一套 C++ 解析器核心与 ANTLR4 语法,因此产出完全一致的 AST(见 common/hogql_parser/README.md 的说明)。
语法定义位于 posthog/hogql/grammar/ 目录,共三个.g4文件:
| 文件 | 角色 |
|---|---|
| HogQLParser.g4 | 解析器语法(parser grammar),声明所有语法规则 |
| HogQLLexer.common.g4 | 词法器(lexer)的共享规则:关键字、token、通用规则 |
| HogQLLexer.cpp.g4 | C++ 目标专用的 Lexer 头部:lexer grammar HogQLLexer声明、@header/@members中的 C++ 自定义代码 |
按 README 的说法,构建时会将HogQLLexer.common.g4(共享规则)与目标特定的HogQLLexer.cpp.g4拼接成最终喂给 ANTLR 的词法器文件。之所以要"拆分",从HogQLLexer.common.g4顶部的注释可以确认:
NB! We cat either HogQLLexter.cpp.g4 or HogQLLexter.python.g4 when generating the grammar.
即同一份共享 Lexer 规则可以被不同目标的后缀文件拼接复用,而当前仓库中 C++ 是唯一的 ANTLR 生成目标——Rust 端的解析器是手写的,不消费这些.g4文件,只是"手工镜像"它们的语法规则(见 rust/hogql/ 目录,README 中已明确此点)。这也是修改语法时需要注意的一处"双份维护"成本。
Parser 文件开头声明了与 Lexer 的绑定关系(HogQLParser.g4):
parser grammar HogQLParser; options { tokenVocab = HogQLLexer; }安装 ANTLR:macOS 与 Ubuntu 两种方式
生成源码的前提是本机装有antlr可执行文件,且版本必须是 4.13.2。README 给出两条安装路径:
macOS:Homebrew 一键安装
brew install antlr注意:如果 Homebrew 安装的版本比 4.13.2 更新,需要同步修改 .github/workflows/ci-hog.yml 中ANTLR_VERSION的值,否则本地再生成结果与 CI 校验结果不一致,PR 会卡在"生成代码未提交"这一步(原因见下文 CI 一节)。
Ubuntu(bash):手动下载 ANTLR 发行包
README 提供的完整脚本如下,它下载 ANTLR 的 complete jar,并生成一个名为antlr的 bash 包装脚本:
export ANTLR_VERSION=4.13.2 sudo apt-get install default-jre mkdir antlr cd antlr curl -o antlr.jar https://www.antlr.org/download/antlr-$ANTLR_VERSION-complete.jar export PWD=`pwd` echo '#!/bin/bash' > antlr echo "java -jar $PWD/antlr.jar \$*" >> antlr chmod +x antlr export CLASSPATH=".:$PWD/antlr.jar:$CLASSPATH" export PATH="$PWD:$PATH"执行后antlr命令即出现在当前 shell 的 PATH 中,底层等价于java -jar antlr.jar。这套"手工搭 jar + 包装脚本"的做法与 CI 中的流程完全一致(下文会看到 CI 也是这么做的),因此本地与 CI 的行为可以保持对齐。
pnpm run grammar:build:一次调用背后的完整命令链
安装好 ANTLR 后,在仓库根目录执行:
pnpm run grammar:build这一条命令会把 C++ 解析器重新生成到common/hogql_parser/。它真正包装的是根目录 package.json 中的grammar:build:cpp脚本(grammar:build仅是其别名):
cd posthog/hogql/grammar \ && cat HogQLLexer.cpp.g4 > HogQLLexer.g4 \ && tail -n +2 HogQLLexer.common.g4 >> HogQLLexer.g4 \ && antlr -o ../../../common/hogql_parser -Dlanguage=Cpp HogQLLexer.g4 \ && rm HogQLLexer.g4 \ && antlr -o ../../../common/hogql_parser -visitor -no-listener -Dlanguage=Cpp HogQLParser.g4可以把它拆解成五步理解:
- 拼接 Lexer 文件:
cat HogQLLexer.cpp.g4 > HogQLLexer.g4以 C++ 专用头(含lexer grammar HogQLLexer;声明与@header/@members自定义 C++ 代码)作为新文件开头;随后tail -n +2 HogQLLexer.common.g4 >> HogQLLexer.g4把共享规则文件跳过第 1 行追加进来——第 1 行正是 common 文件里那句会重复的lexer grammar HogQLLexer;声明,跳过它才能拼出合法的单 grammar 文件。 - 生成词法器:
antlr -o ../../../common/hogql_parser -Dlanguage=Cpp HogQLLexer.g4输出HogQLLexer.cpp/.h、.tokens、.interp等到common/hogql_parser/。 - 清理临时文件:
rm HogQLLexer.g4删除拼接产物,保持语法目录干净(仓库里只保留三个源文件,见 posthog/hogql/grammar/)。 - 生成解析器:对
HogQLParser.g4再次调用 ANTLR,参数-visitor -no-listener表示只生成 Visitor 接口(HogQLParserVisitor)而不生成 Listener 版本。 - 落盘位置:所有产物统一进入
common/hogql_parser/,与手写的桥接代码放在一起。
在 common/hogql_parser/ 目录中可以看到两类文件:ANTLR 生成物(HogQLLexer.cpp、HogQLParser.cpp、HogQLParserBaseVisitor.cpp、HogQLParserVisitor.cpp、.interp/.tokens)以及手写胶水层(parser_python.cpp供 Python 扩展调用、parser_wasm.cpp供 Emscripten 导出、parser_json.cpp负责把 AST 序列化为 JSON、index.cjs/index.d.ts供 npm 包使用)。package.json 中的build脚本通过emcmake cmake把这套 C++ 代码编译成 WASM 放入dist/,而 Python 侧则由 pyproject.toml 用 scikit-build 流程构建原生扩展——两个运行时、一套语法核心。
CI 如何强制"语法文件与生成代码零漂移"
再生成出来的 C++ 文件必须提交进仓库,否则 CI 会直接失败。.github/workflows/ci-hog.yml 中的 "Check if ANTLR definitions are up to date" 步骤做了三件事:
- 在 CI 中复现本地安装流程:下载
antlr-4.13.2-complete.jar(主源失败时回落到 Maven 镜像),生成同样的antlr包装脚本; - 执行
npm run grammar:build(即上文grammar:build:cpp全量脚本)重新生成; git diff --exit-code——只要仓库中提交的生成文件与本次再生成结果有任何差异,CI 立即报错。
版本锁定也在这一步里:env中写死ANTLR_VERSION: '4.13.2',注释说明这与 2024 年 8 月 Homebrew 提供的版本一致(apt 源中的 ANTLR 版本过旧不可用),并要求common/hogql_parser/pyproject.toml中保持相应版本配套。从 pyproject.toml 实际内容看,构建 wheel 时下载的是antlr4-cpp-runtime-4.13.1-source.zip并校验 md5,随后 cmake 编译 C++ 运行时并安装到系统目录——也就是说词法/语法生成(ANTLR 4.13.2)与运行时 C++ 库的构建是两条独立但版本配套的管线。
由此得出实操约束:任何一次对.g4文件的改动,都必须本地跑一遍pnpm run grammar:build,把common/hogql_parser/下的 diff 一并提交,且本地 ANTLR 版本必须与 CI 的 4.13.2 对齐,避免生成代码的版本性差异导致 CI 误判漂移。
语法本体速览:从 README 的三个设计点回到.g4源码
README 末尾用三句话概括了这套语法与 ClickHouse 官方 ANTLR 语法(ClickHouseParser.g4,ClickHouse 仓库自带)的差异。把这三点放回 HogQLParser.g4 源码中可以得到更具体的印证:
1. 只保留 SELECT 语句。整个 parser grammar 的顶层入口只有三类(HogQLParser.g4):
program: declaration* EOF; // Hog 程序(变量声明与语句) expr: columnExpr EOF; // 单个表达式 select: (selectSetStmt | selectStmt | hogqlxTagElement) SEMICOLON? EOF; // SELECT 查询没有 INSERT/UPDATE/DROP 等写语句规则。select入口的三选一中还混入了hogqlxTagElement(形如<Chart ...>的模板标签),对应hogqlxChildElement/hogqlxTagElement规则族(HogQLParser.g4),让查询文本里能嵌入带属性的标签元素。
2. 未实现的 ClickHouse 特性会被拒绝。README 用"ever changing list, check the code"指向代码层面的校验:被解析出的 AST 后续要在 Python 侧的 resolver/编译器中过一遍,不支持的语法(如某些 ClickHouse SQL 特性)在实现层抛出错误。语法层本身偏宽松、实现层收紧,是这类"SQL 子集"查询语言常见的分层策略。
3. 支持{val1}形式的占位符。语法中的定义是(HogQLParser.g4):
placeholder: LBRACE columnExpr RBRACE;{ ... }内是一个完整的columnExpr,可以出现在列引用(columnIdentifier的可选分支)、表表达式(TableExprPlaceholder)与采样比例(ratioExpr)等位置。README 里的例子team_id = {val1}即走这条规则。占位符的下游处理在 Python 侧,posthog/hogql/placeholders.py 中的find_placeholders/visit_placeholder会把 AST 中的Placeholder节点拆成"简单字段占位符"与"复杂表达式占位符"两类,后者会被送进 Hog 虚拟机求值——这也解释了为什么HogQLParser.g4里同时存在program(Hog 程序)入口。
值得留意的几处语法设计细节
- 两级优先级的表达式分层:
columnExpr(布尔与/或、三元、别名层)与columnExprValue(算术、比较、函数调用、主叶节点层)的拆分在 HogQLParser.g4 有长注释解释——这是 ANTLR4 左递归规则下表达BETWEEN ... AND ... AND ...正确分组的唯一方式,注释明确说拆成两条规则"the only way to express this in ANTLR4"。修改表达式规则时务必读懂这段注释,否则会破坏既有优先级。 - 无限深度的属性链:
nestedIdentifier: identifier (DOT identifier)*允许properties.b.a.a.w.a.s这类任意深度嵌套引用(HogQLParser.g4 的注释指出这与 ClickHouse SQL 不同),解析后会被折叠成单个Field节点(chain=[...])。 - 模板字符串:
templateString/fullTemplateString规则(HogQLParser.g4)用QUOTE_SINGLE_TEMPLATE等 token 支持f'...'风格内嵌{表达式}的字符串,与 npm 包 API 中的parseFullTemplateString相对应。 - Lexer 内嵌 C++ 自定义代码:
HogQLLexer.cpp.g4的@members块包含真实 C++ 实现——skipWsAndComments同时跳过//、--、#三种行注释(并特意用 ASCII 范围判断规避std::isalpha对非 ASCII 输入的未定义行为),isOpeningTag则解决<既是小于运算符又是 HogQLx 标签起始符的歧义。这意味着 C++ 目标的词法行为并非纯 ANTLR 声明式,改动 Lexer 时不能只看规则本身。 - 新增关键字的提醒:
HogQLLexer.common.g4顶部注释写着don't forget to add new keywords to the parser rule "keyword"!——对应 parser 侧的keyword规则(HogQLParser.g4),它是标识符可用作identifier的白名单来源。只加 Lexer token 而漏掉 parser 侧keyword规则,新关键字将无法作为identifier出现,是修改语法时最容易踩的坑之一。
适用前提与小结
- 流程适用前提:仓库根目录有 pnpm 环境,且本机 ANTLR 版本为 4.13.2(与 ci-hog.yml 一致);
- 生成是全量覆盖
common/hogql_parser/中 ANTLR 产物,手写文件(parser_*.cpp、json.cpp等)不受影响,但二者必须能一起通过 cmake 编译(npm run build走 Emscripten,pip install ./common/hogql_parser走 Python 扩展构建); - Rust 解析器(rust/hogql/)不走 ANTLR,语法规则变化需在其内部手工同步,这是 README 明确声明的设计决策;
- 提交前务必确认
git status中common/hogql_parser/下的 diff 已包含在变更内,因为 CI 会用git diff --exit-code对再生成结果做严格校验。
整条链路可以概括为:三个.g4源文件(语法目录)→ 拼接 + 两次 ANTLR 调用(pnpm run grammar:build)→ C++ 产物(common/hogql_parser/)→ 两条构建出口(PyPI 原生扩展 / npm WASM)→ CI 零漂移校验(ci-hog.yml)。掌握这条管线后,无论是给 HogQL 加一个新关键字、调整运算符优先级,还是排查"为什么我改了语法 PR 却红了",都有了明确的排查路径。
【免费下载链接】posthog:hedgehog: PostHog is the leading platform for building self-driving products. Our developer tools – AI observability, analytics, session replay, flags, experiments, error tracking, logs, and more – capture all the context agents need to diagnose problems, uncover opportunities, and ship fixes. Steer it all from Slack, web, desktop, or the MCP.项目地址: https://gitcode.com/GitHub_Trending/po/posthog
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考