
偃师 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/SafeLineyanshi 是 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。为什么不用 RagelREADME 明确指出创作 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 buildrelease从 Makefile 可以看到两种构建的差异Debug 构建使用-g3 -stdc1y -fsanitizeundefined,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.ccNFA 确定化、union_test.cc、intersection_test.cc、difference_test.cc、minimize_test.cc等均通过unittest/目录下的测试辅助代码读取 NFA、执行相应自动机运算并校验状态数。快速上手生成并运行一个独立 C 程序编写源文件创建一个文件a.ysexport 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 hellopress C-d0 h 1 e 2 l 3 l 4 o 5 len: 5 pref: 5 state: 5 final: true输出解读状态用黄色打印并与转移标签交错终结状态为粗体黄色。四行输出分别表示len输入码点或标签的长度pref不进入死状态的最长前缀长度state消费输入后进入的状态final该状态是否为终结状态。当不提供命令行参数时程序从标准输入读取示例中hellopress 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至少两次。该文法匹配hello、gello、helllo等等。组合运算符运算符含义示例\|并Unionc a \| b交Intersectionc a b-差Differencec a - b空格连接Concatenationc a b~补Complementc ~ a这些运算符对应源码 syntax.hh 中的UnionExpr、IntersectExpr、DifferenceExpr、ConcatExpr、ComplementExpr等 AST 节点而 fsa.hh 提供了对应的自动机运算intersect、difference、determinize用于将 NFA 确定化为 DFA、distinguishDFA 最小化、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 5import b.ys as B带限定名的导入引用其中的定义需写作B::barimport 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 meowpre的终结状态与post的起始状态之间会用一条特殊有向弧连接导出时从该弧的尾部到bar的起始状态增加一条 epsilon 转移从bar的各个终结状态到该弧的头部各增加一条 epsilon 转移。CollapseExpr的行为像函数调用但不保存返回地址因此命名为 collapse状态在穿过bar后可能走到其他调用点。上例中穿过bar后状态可能走向foo或quz从而产生误报。CallExpr修饰符export foo pre bar post bar 4这是CollapseExpr的改良。假设状态B位于A的定义中即A调用BB会被表示成一条伪弧u - v其中u是B之前的状态v是B之后的状态如果u的弧与B的弧不发生冲突那么当当前状态集合包含u且没有其他转移时转移函数会把v压入返回栈注意B与A是断开的这与CollapseExpr不同。机器会在自动机B上贪心转移如果没有转移则弹出返回地址此处即v并跳转到它。从 syntax.hh 可以看到三种引用分别对应EmbedExpr、CollapseExpr、CallExpr三个 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 集成Vimyanshi/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/Zshyanshi/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依次完成获取定义列表为每个import递归加载模块解析引用并把使用关联到定义used_as_call/used_as_collapse/used_as_embed三类引用分别记录见 loader.hh从EmbedExpr构建依赖图按拓扑序为每个非终结符编译自动机其中CollapseExpr与CallExpr用特殊有向弧表示为export的非终结符生成代码解析CollapseExpr与CallExpr。有限状态自动机的构建语法树的每个节点都对应一个自动机。父节点根据子节点的语义由子节点构造自己的自动机——父节点的自动机可能包含某个子节点的自动机中的状态也可能是父节点自己引入的新状态。assoc[i]记录了自动机树中的关联节点语法树中哪些部分与该状态有关联以及状态i的位置起始状态、终结状态或内部状态其用途有三检查应该触发哪个动作在子串文法的实现中查找内部状态既非起始也非终结检查该状态是否关联到CallExpr或CollapseExpr。该结构对应 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),仅供参考