PostHog HogQL 解析器:基于 ANTLR 的语法生成流程与 C++/WASM 双端解析器构建管线
2026/9/13 15:45:19 网站建设 项目流程

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.g4C++ 目标专用的 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

可以把它拆解成五步理解:

  1. 拼接 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 文件。
  2. 生成词法器antlr -o ../../../common/hogql_parser -Dlanguage=Cpp HogQLLexer.g4输出HogQLLexer.cpp/.h.tokens.interp等到common/hogql_parser/
  3. 清理临时文件rm HogQLLexer.g4删除拼接产物,保持语法目录干净(仓库里只保留三个源文件,见 posthog/hogql/grammar/)。
  4. 生成解析器:对HogQLParser.g4再次调用 ANTLR,参数-visitor -no-listener表示只生成 Visitor 接口(HogQLParserVisitor)而不生成 Listener 版本。
  5. 落盘位置:所有产物统一进入common/hogql_parser/,与手写的桥接代码放在一起。

在 common/hogql_parser/ 目录中可以看到两类文件:ANTLR 生成物(HogQLLexer.cppHogQLParser.cppHogQLParserBaseVisitor.cppHogQLParserVisitor.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" 步骤做了三件事:

  1. 在 CI 中复现本地安装流程:下载antlr-4.13.2-complete.jar(主源失败时回落到 Maven 镜像),生成同样的antlr包装脚本;
  2. 执行npm run grammar:build(即上文grammar:build:cpp全量脚本)重新生成;
  3. 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_*.cppjson.cpp等)不受影响,但二者必须能一起通过 cmake 编译(npm run build走 Emscripten,pip install ./common/hogql_parser走 Python 扩展构建);
  • Rust 解析器(rust/hogql/)不走 ANTLR,语法规则变化需在其内部手工同步,这是 README 明确声明的设计决策;
  • 提交前务必确认git statuscommon/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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询