news 2026/9/13 19:42:12

偃师 yanshi:一个可内嵌 C++ 动作、支持近似上下文无关文法的有限状态自动机生成器

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
偃师 yanshi:一个可内嵌 C++ 动作、支持近似上下文无关文法的有限状态自动机生成器

偃师 yanshi:一个可内嵌 C++ 动作、支持近似上下文无关文法的有限状态自动机生成器

【免费下载链接】SafeLineSafeLine is a self-hosted WAF(Web Application Firewall) / reverse proxy to protect your web apps from attacks and exploits.项目地址: https://gitcode.com/GitHub_Trending/sa/SafeLine

yanshi 是 SafeLine 仓库中独立子项目yanshi/下实现的一个有限状态自动机(FSA)生成器,定位类似 Ragel,但通过@{}内联运算符把 C++ 代码嵌入到语言识别的每个关键节点,并额外提供了子串文法近似递归自动机近似两种能力,以逼近上下文无关文法(CFG)的表达范围。读完本文,你将掌握 yanshi 的构建与命令行用法、yanshi 语言语法(正则式、动作、模块、子串文法)、EmbedExpr/CollapseExpr/CallExpr三种非终结符引用机制的差异与实现原理,以及其编译器内部结构与调试手段。

本文全部内容基于 yanshi/README.md 展开,并结合yanshi/src/yanshi/unittest/yanshi/contrib/等目录下的源码与测试进行佐证与深化。

概述与设计动机

定位

yanshi 是一个类似 Ragel 的有限状态自动机生成器,核心特性是:

  • 使用内联运算符把 C++ 代码嵌入语言识别的过程之中;
  • 提供近似子串文法(substring grammar)的能力;
  • 提供递归自动机的近似能力,从而近似上下文无关文法(context-free grammar)。

为什么不用 Ragel

README 明确指出,创作 yanshi 的直接动机是:Ragel 没有提供序列化其有限状态自动机表示形式的机制,导致无法对生成后的自动机做后处理(post-process),也就难以从中得到子串文法识别器。

性能与内存问题的驱动

后续实践中作者发现,一个简化后的 SQL 文法可能包含超过 10000 个状态。此时:

  • 自动机生成缓慢,难以进行快速的试错实验(trial-and-error);
  • 存储自动机浪费大量内存。

为此作者引入CollapseExpr,允许循环引用CallExpr则更进一步,维护一个返回地址栈来模拟函数调用,可以视为CollapseExpr的增强版,消除了大量误报(false positive)情况。

名字的由来

名字"yanshi(偃师)"来自《列子》中关于古代中国自动机的一段记载(见 yanshi/README.md 的 Name 一节):

中国古代关于自动机的奇闻见于公元前 3 世纪的《列子》,其中记载了周穆王(公元前 1023-957 年)与一位名叫偃师(Yan Shi)的机械工程师("artificer")更早的相遇。

构建

yanshi/目录下执行:

  • 调试构建:make
  • 发布构建:make build=release

从 Makefile 可以看到两种构建的差异:

  • Debug 构建使用-g3 -std=c++1y -fsanitize=undefined,address -DDEBUG,链接-lasan -lubsan,产物在build/目录;
  • Release 构建使用-Os优化,产物在release/目录;
  • 两者均链接 ICU(-licuuc,处理 Unicode 区间与字符)与 readline(-lreadline,交互式补全);
  • lexer/parser 由 flex 与 bison 生成(src/lexer.l->src/lexer.ccsrc/parser.y->src/parser.cc),源码树中只保留.l/.y原始文件,需要先安装 flex 与 bison 才能构建。

另外make unittest会编译并运行unittest/目录下的单元测试,例如 determinize_test.cc(NFA 确定化)、union_test.ccintersection_test.ccdifference_test.ccminimize_test.cc等,均通过unittest/目录下的测试辅助代码读取 NFA、执行相应自动机运算并校验状态数。

快速上手:生成并运行一个独立 C++ 程序

编写源文件

创建一个文件a.ys

export foo = 'hello'

生成 C++ 代码

运行:

yanshi -S a.ys -o /tmp/a.cc

-S--standalone)选项会生成一个独立可编译运行的 C++ 文件(包含头文件与main())。生成的代码为foo提供三个核心函数:

  • yanshi_foo_start:起始状态为 0(状态用自然数表示);
  • yanshi_foo_is_final:忽略ret_stack后,检查状态u是否是终结状态;
  • yanshi_foo_transit:忽略ret_stack后,u是当前状态,c是下一个输入码点(codepoint)或标签(label)。

编译并运行

% make -C /tmp a make: Entering directory '/tmp' g++ a.cc -o a make: Leaving directory '/tmp' % /tmp/a hello 0 h 1 e 2 l 3 l 4 o 5 len: 5 pref: 5 state: 5 final: true % /tmp/a hello<press C-d>0 h 1 e 2 l 3 l 4 o 5 len: 5 pref: 5 state: 5 final: true

输出解读:状态用黄色打印并与转移标签交错,终结状态为粗体黄色。四行输出分别表示:

  • len:输入码点或标签的长度;
  • pref:不进入死状态的最长前缀长度;
  • state:消费输入后进入的状态;
  • final:该状态是否为终结状态。

当不提供命令行参数时,程序从标准输入读取(示例中hello<press C-d>表示输入hello后按 Ctrl-D 结束输入)。

交互式模式

-i--interactive)选项启用交互模式,适合快速试错与检查自动机内部结构:

% yanshi -i a.ys Testing foo foo :: DefineStmt .integer mode Commands available from the prompt: .automaton dump automaton .assoc dump associated AST Expr for each state .help display this help .integer input is a list of non-negative integers, macros(#define) or '' quoted strings .macro display defined macros .string input is a string .stmt <ident> change target DefineStmt to <ident> .quit exit interactive mode λ 104 101 108 108 111 0 104 1 101 2 108 3 108 4 111 5 export foo = 'hello': λ .string .string mode λ hello 0 h 1 e 2 l 3 l 4 o 5 export foo = 'hello': λ

交互模式的实现位于 repl.cc,其命令表与 README 完全对应。从源码看,交互模式下支持两种输入模式:

  • .string:输入按字符串处理;
  • .integer:输入是一系列非负整数(即码点),也可以使用#define宏或''引号字符串。

交互模式还通过 readline 提供命令补全(command_completer)、宏补全(macro_completer)与语句补全(stmt_completer),.stmt <ident>命令在源码中通过resolve()解析标识符并切换到对应的DefineStmt后,将anno指针更新为对应自动机,后续输入均针对该自动机执行。

yanshi 语言:正则式风格语法与运算符

括号表达式与重复

export hello = [gh] 'e' l{2} 'o' l = 'l'
  • [gh]是括号表达式(bracket expression),匹配一个字符;
  • l{2}表示匹配l至少两次。

该文法匹配hellogellohelllo,等等。

组合运算符

运算符含义示例
\|并(Union)c = a \| b
&&交(Intersection)c = a && b
-差(Difference)c = a - b
空格连接(Concatenation)c = a b
~补(Complement)c = ~ a

这些运算符对应源码 syntax.hh 中的UnionExprIntersectExprDifferenceExprConcatExprComplementExpr等 AST 节点,而 fsa.hh 提供了对应的自动机运算:intersectdifferencedeterminize(用于将 NFA 确定化为 DFA)、distinguish(DFA 最小化)、accessible/co_accessible(可达/共可达状态裁剪)等。单元测试 union_test.cc、intersection_test.cc、difference_test.cc、determinize_test.cc 分别对上述运算进行了验证。

动作(内嵌 C++ 代码)

c++ { #include <stdio.h> } export hello = '喵' @ { puts("meow"); } {2}
  • c++ { ... }把 C/C++ 代码原样嵌入到生成的 C++ 文件中(对应源码中的CppStmt);
  • @ { ... }在某个识别节点执行内联动作(对应InlineAction)。

注意:README 明确说明动作的执行点(executing point)可能反直觉,其实现尚未完全想清楚,使用时建议先做实验验证执行时机。

模块与导入

# a.ys import 'b.ys' as B # B::bar import 'b.ys' # qux export foo = B::bar | qux bar = '4' # b.ys bar = '3' qux = '5'
  • import 'b.ys' as B:带限定名的导入,引用其中的定义需写作B::bar
  • import 'b.ys':不带限定名的导入,其中的定义(如qux)可以直接使用;
  • 导入搜索路径可通过-I, --import <dir>命令行选项添加(见 main.cc 中opt_include_paths)。

模块相关数据结构定义在 loader.hh 的Module结构中:defined保存本模块定义,unqualified_import/qualified_import分别保存无限定名与带限定名的导入模块,macro保存#define宏。

子串文法

--substring-grammar选项生成子串文法的代码,即生成的代码匹配该文法的每一个子串。实现方式是:创建一个新的起始状态和新的终结状态,把新起始状态连到旧起始状态,把旧终结状态连到新终结状态。README 同时提到,子串文法的实现需要查找"内部状态"(既非起始也非终结的状态),这依赖assoc信息(见下文 Internals 一节)。

三种非终结符引用机制

yanshi 提供三种引用非终结符的方式,是理解其表达能力(从正则语言逼近上下文无关文法)的核心。

EmbedExpr:直接引用

foo = bar bar = [0-9]

不带任何修饰符引用非终结符时,bar的完整自动机会在每一个引用点被复制。如果被引用的自动机很大,EmbedExpr会显著增加状态数。EmbedExpr在状态间建立依赖关系,不允许循环依赖

CollapseExpr!修饰符

export foo = 'pre' !bar 'post' bar = [\u0300-\u034E] quz = 'meow' !bar 'meow'
  • 'pre'的终结状态与'post'的起始状态之间会用一条特殊有向弧连接;
  • 导出时,从该弧的尾部到bar的起始状态增加一条 epsilon 转移,从bar的各个终结状态到该弧的头部各增加一条 epsilon 转移。

CollapseExpr的行为像函数调用,但不保存返回地址(因此命名为 collapse),状态在穿过bar后可能走到其他调用点。上例中,穿过bar后状态可能走向fooquz,从而产生误报

CallExpr&修饰符

export foo = 'pre' &bar 'post' bar = '4'

这是CollapseExpr的改良。假设状态&B位于A的定义中(即A调用B),&B会被表示成一条伪弧u -> v,其中u&B之前的状态,v&B之后的状态:

  • 如果u的弧与B的弧不发生冲突,那么当当前状态集合包含u且没有其他转移时,转移函数会把v压入返回栈
  • 注意BA断开的,这与CollapseExpr不同。机器会在自动机B贪心转移
  • 如果没有转移,则弹出返回地址(此处即v)并跳转到它。

从 syntax.hh 可以看到三种引用分别对应EmbedExprCollapseExprCallExpr三个 AST 节点,它们都记录qualified(限定名)与ident(标识符),并由编译器在解析引用后回填define_stmt指针。返回栈的长度由命令行选项--max-return-stack控制(默认 100,见 main.cc)。

在交互模式与独立代码生成之外,-G, --graph <dir>选项还可以输出 Graphviz dot 文件(对应 option.hh 中的Mode::graphviz),方便可视化查看自动机拓扑。

完整的命令行选项

根据 main.cc 的print_help与参数解析逻辑,yanshi 支持以下选项:

短选项长选项含义
-b--bytes标签取值范围为[0,256),Unicode 字面量按 UTF-8 字节处理(同时将字母表大小AB设为 256)
-C生成 C 源码(默认生成 C++)
-c--check只检查语法与 use/def
-d--debug调试级别
-l--debug-output调试输出文件名(默认 stderr)
--dump-action输出每条边关联的动作
--dump-assoc输出每个状态关联的 AST Expr
--dump-automaton输出自动机
--dump-embed输出 EmbedExpr 统计信息
--dump-module输出模块的 use/def 等
--dump-tree输出 AST
--extern-c生成extern "C"说明符
-G--graph <dir>输出 Graphviz dot 文件
-I--import <dir>添加import搜索路径
-i--interactive交互模式
--max-return-stackC 生成器中返回栈最大长度(默认 100)
-k--keep-inaccessible不做 accessible/co-accessible 裁剪
-S--standalone生成头文件与main()
-s--substring-grammar构造子串文法的正则近似;标有intact的非终结符内部状态不连接到 start/final
-o--output <file>.cc输出文件名
-O--output-header <file>.hh输出文件名
-h--help显示帮助并退出

编辑器与 Shell 集成

Vim

yanshi/contrib/vim/提供语法高亮与语法检查插件(面向 Syntastic),安装方式(符号链接到~/.vim对应子目录):

ln -sr contrib/vim/compiler/yanshi.vim ~/.vim/compiler/ ln -sr contrib/vim/ftdetect/yanshi.vim ~/.vim/ftdetect/ ln -sr contrib/vim/ftplugin/yanshi.vim ~/.vim/ftplugin/ ln -sr contrib/vim/syntax/yanshi.vim ~/.vim/syntax/ ln -sr contrib/vim/syntax_checker/yanshi ~/.vim/syntax_checker/

Zsh

yanshi/contrib/zsh/_yanshi提供命令行补全,安装方式:

# ~/.zshrc fpath=(~/.zsh $fpath) # ln -sr contrib/zsh/_yanshi ~/.zsh/

注意:syntax_checker目录在 README 中写作syntax_checkers,实际源码树中的目录名为yanshi/contrib/vim/syntax_checkers/yanshi/,安装时应以实际目录名为准。

内部实现(Internals)

源码结构

src common.{cc,hh} main.{cc,hh} syntax.{cc,hh} loader.{cc,hh} fsa.{cc,hh} fsa_anno.{cc,hh} compiler.{cc,hh} parser.y lexer.l location.cc

编译流水线

  • lexer.l:词法分析;
  • parser.y:语法分析并生成语法树;
  • loader.cc依次完成:
    1. 获取定义列表;
    2. 为每个import递归加载模块;
    3. 解析引用并把使用关联到定义(used_as_call/used_as_collapse/used_as_embed三类引用分别记录,见 loader.hh);
    4. EmbedExpr构建依赖图;
    5. 按拓扑序为每个非终结符编译自动机,其中CollapseExprCallExpr用特殊有向弧表示;
    6. export的非终结符生成代码,解析CollapseExprCallExpr

有限状态自动机的构建

语法树的每个节点都对应一个自动机。父节点根据子节点的语义由子节点构造自己的自动机——父节点的自动机可能包含某个子节点的自动机中的状态,也可能是父节点自己引入的新状态。

assoc[i]记录了自动机树中的关联节点(语法树中哪些部分与该状态有关联)以及状态i的位置(起始状态、终结状态或内部状态),其用途有三:

  • 检查应该触发哪个动作;
  • 在子串文法的实现中查找内部状态(既非起始也非终结);
  • 检查该状态是否关联到CallExprCollapseExpr

该结构对应 fsa_anno.hh 中的FsaAnno(由compile()填充到 compiler.hh 的compiled映射中),--dump-assoc与交互模式的.assoc命令都会输出它。

循环引用与近似 CFG 的动机回顾

回到 README 开头的问题:简化 SQL 文法超过 10000 个状态。引入CollapseExpr后允许循环引用,不再需要把被引用的自动机整体展开复制(这是EmbedExpr的做法);CallExpr进一步通过返回地址栈消除误报。这三者的递进关系(Embed -> Collapse -> Call)正是 yanshi"以正则自动机近似上下文无关文法"这一设计路线的核心。

总结

  • yanshi 是一个"类 Ragel、但可后处理自动机"的 FSA 生成器,通过@{}内联 C++ 动作、--substring-grammar子串文法近似、CollapseExpr/CallExpr递归近似来逼近 CFG 的表达能力;
  • 三种非终结符引用中,EmbedExpr直接复制自动机(允许重复引用、不允许循环),CollapseExpr用 epsilon 弧连接调用点但不保存返回地址(可能误报),CallExpr用返回地址栈模拟真实函数调用(误报最少);
  • 调试与试错可以通过-i交互模式、-GGraphviz 输出以及--dump-*系列选项完成,单元测试位于yanshi/unittest/,实现细节可继续阅读 yanshi/src/ 下的源码。

【免费下载链接】SafeLineSafeLine is a self-hosted WAF(Web Application Firewall) / reverse proxy to protect your web apps from attacks and exploits.项目地址: https://gitcode.com/GitHub_Trending/sa/SafeLine

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 19:39:26

darktable 实战指南:RAW 到成片的 5 步工作流

darktable 实战指南&#xff1a;RAW 到成片的 5 步工作流 【免费下载链接】darktable darktable is an open source photography workflow application and raw developer 项目地址: https://gitcode.com/GitHub_Trending/da/darktable 把一堆 RAW 倒进文件夹&#xff0…

作者头像 李华
网站建设 2026/9/13 19:38:04

Label Studio 交通监控目标检测模板:Bounding Box 标注配置与实践

Label Studio 交通监控目标检测模板&#xff1a;Bounding Box 标注配置与实践 【免费下载链接】label-studio Label Studio is a multi-type data labeling and annotation tool with standardized output format 项目地址: https://gitcode.com/GitHub_Trending/la/label-st…

作者头像 李华
网站建设 2026/9/13 19:32:38

EM3DVP:从EDI到ModEM的大地电磁三维反演前处理工具

简介&#xff1a;EM3DVP是一套基于Matlab开发的三维地电磁建模与反演可视化工具包&#xff0c;主要面向地质电磁法研究人员与工程师&#xff0c;用于简化三维反演代码的输入模型、数据与参数文件准备&#xff0c;并提供结果模型和电磁响应的绘制界面。压缩包共收录243个文件&am…

作者头像 李华
网站建设 2026/9/13 19:25:36

基于Hessian矩阵与Frangi滤波器的血管分割实现详解

简介&#xff1a;基于Hessian矩阵增强的心血管分割是医学图像分析领域的重要课题&#xff0c;这份代码资源面向从事医学影像处理、计算机辅助诊断的研究者与学生&#xff0c;针对血管细长且高对比度结构难以自动提取的痛点&#xff0c;提供一套可运行的MATLAB实现方案。压缩包内…

作者头像 李华