- 开发工具
- Lint
- 静态分析
- 硬件开发
【免费下载链接】verible
Verible is a suite of SystemVerilog developer tools, including a parser, style-linter, formatter and language server
本文基于 Verible 的设计文档 doc/parser_design.md,逐节拆解 Verible 的 SystemVerilog 解析器架构:Flex 词法分析器、上下文消歧(Contextualizer)、伪预处理器(pseudo-preprocessor)、词法过滤(Filter)与 Bison 语法分析器五个环节是如何"完全解耦"地协作的。读完后,你能理解 Verible 为何能直接解析未预处理的 SystemVerilog 源码、每个环节的源码实现位置,以及如何用verible-verilog-syntax工具实际观察词法 token 流与 CST(具体语法树)的输出。
总体设计:非传统的 Flex/Bison 用法
Verible 使用 Flex 和 Yacc/Bison 这两类传统工具,但并不采用常规方式——即"生成的yyparse()函数直接调用yylex()"的模式。相反,生成的词法分析器(lexer)与语法分析器(parser)是完全解耦的。
这一设计在源码中体现得非常清晰。在 verilog-parser.h 中,VerilogParser被定义为:
// TODO(fangism): Use a pseudo-preprocessor between the lexer and parser. using VerilogParser = verible::BisonParserAdapter<verilog_parse_wrapper>;而 BisonParserAdapter 模板类的构造参数是一个TokenGenerator *,而不是 lexer 对象。类头文件中的注释直接说明了动机:
"With this design, the parser is not directly tied to a particular lexer, so it is easier to transform the token stream before feeding it to the parser."(解析器不与某个特定 lexer 绑定,因此更容易在喂给解析器之前转换 token 流。)
正因为解耦,才能在两者之间插入过滤(Filter)、上下文消歧(Contextualizer)、伪预处理等多个 pass。完整的调用链在 VerilogAnalyzer::Analyze() 中可见:
// Lex into tokens. RETURN_IF_ERROR(Tokenize()); // Here would be one place to analyze the raw token stream. FilterTokensForSyntaxTree(); // ① 过滤注释/空白 // Disambiguate tokens using lexical context. ContextualizeTokens(); // ② 上下文消歧 // pseudo-preprocess token stream. VerilogPreprocess preprocessor(preprocess_config_); // ③ 伪预处理 preprocessor_data_ = preprocessor.ScanStream(...); auto generator = MakeTokenViewer(Data().GetTokenStreamView()); VerilogParser parser(&generator, filename_); // ④ 解析 parse_status_ = FileAnalyzer::Parse(&parser);即整条管线为:Lexer → Filter → Contextualizer → (pseudo-)Preprocessor → Parser,每一步都只操作 token 流(TokenStreamView),彼此独立可替换。
Lexer:Flex 生成的全功能词法分析器
设计文档指出:词法分析器由 Flex 生成,在一套实现中同时支持 SystemVerilog 语言本身的词法 token 以及预处理指令、宏和其他指令;生成的代码被封装进一个"逐个返回 token、直到以特殊 EOF token 结束"的接口中。
实现位于 verilog.lex(约 1677 行)。关键配置选项包括:
%option 8bit %option c++ %option prefix="verilog" %option stack %option yyclass="verilog::VerilogLexer"其中prefix="verilog"意味着 Flex 生成的类名为verilogFlexLexer,yyclass将生成的代码绑定到 C++ 子类verilog::VerilogLexer。verilog.lex 定义了 30 多个 Flex 起始条件(start condition),覆盖语言词法的方方面面:
%x TIMESCALE_DIRECTIVE /* 时间尺度指令 */ %s UDPTABLE /* UDP 真值表 */ %x MACRO_CALL_EXPECT_OPEN /* 宏调用 */ %x MACRO_CALL_ARGS %x PP_EXPECT_DEF_ID /* `define 等预处理指令 */ %x PP_MACRO_FORMALS %x PP_BETWEEN_ID_AND_BODY %x DEC_BASE %x BIN_BASE %x OCT_BASE %x HEX_BASE /* 数字字面量基 */词法规则本身遵循 SystemVerilog 的词法定义,例如标识符、转义标识符、数字字面量:
BasicIdentifier {IdentifierStart}({Letter}|{Digit})* /* treat `id constants like plain identifiers */ MacroIdentifier `{BasicIdentifier} EscapedIdentifier "\\"[^ \t\f\b\n]+ DecBase \'[sS]?[dD]值得注意的是MacroIdentifier:宏调用(`define 名)被"像普通标识符一样处理",这正是"预处理构造与语言词法混在一套 lexer 中"的直接体现。
逐 token 接口与 EOF token
"逐个返回 token 直到 EOF"的封装由通用适配器 FlexLexerAdapter 完成,VerilogLexer继承自它:
// verilog-lexer.h class VerilogLexer : public verible::FlexLexerAdapter<verilogFlexLexer> { // ... int yylex() final; // 由 Flex 生成 };适配器中DoNextToken()的核心逻辑:
const TokenInfo &DoNextToken() override { if (at_eof_) { // Do not call yylex(), because that will result in the fatal error: // "fatal flex scanner internal error--end of buffer missed" last_token_ = TokenInfo::EOFToken(code_); } else { last_token_.set_token_enum(this->yylex()); } return last_token_; }除了"返回 int 枚举"的原始 Flex 行为,适配器将每个 token 包装为TokenInfo(枚举 + 原文子串 + 字节偏移),并提供Restart()以便用新输入重用 lexer。输入经std::istringstream(CodeStreamHolder)中转,注释说明这允许"在预处理时切换缓冲区"。
设计文档(见 verible/verilog/parser/README.md)还强调词法流的两个关键性质,它们是后续所有 pass 可靠工作的前提:
- Continuity(连续性):一个 token 的文本区间末端等于下一个 token 的起点;
- Completeness(完备性):首尾 token 覆盖的文本区间等于原始输入文本。
这意味着空格、换行、注释等"无关文本"也作为 token 出现(例如TK_SPACE、TK_NEWLINE、TK_EOL_COMMENT),可以在送入解析阶段前轻松过滤掉——词法流对原始输入是无损的。
Contextualizer:基于上下文的 token 消歧
设计文档说:Contextualizer 是对 lexer 产生 token 流的一次遍历(pass),用于消歧那些存在多种解释的 token,从而简化 parser 端文法的实现——这是纯上下文无关的 LR 文法做不到的事情。
实现是 LexicalContext 类,通过 VerilogAnalyzer::ContextualizeTokens() 在过滤之后、解析之前被调用:
void VerilogAnalyzer::ContextualizeTokens() { LexicalContext context; context.TransformVerilogSymbols(MutableData().MakeTokenStreamReferenceView()); }TransformVerilogSymbols()逐个遍历 token 视图,对每个 token 调用AdvanceToken();绝大多数 token 原样通过,只有需要上下文消歧的 token 会被就地改写枚举值("Postcondition: tokens_view's tokens must not be tagged with (TK*) enumerations")。典型的歧义例子包括:
->:既可以是右箭头运算符(=>/->用于约束蕴含、端口方向),也可以被重新解释为其他角色;;在property/sequence声明体结尾的重新枚举(见下文LastSemicolonStateMachine);#delay在initial/always构造与模块声明中的不同含义。
状态机组合式设计
LexicalContext头文件中的设计哲学注释写得很明确:
"This class itself is a state machine while employing smaller, simpler, concurrent state machines... The smaller state machines will be inactive most of the time, and activated on certain keywords in certain states."
AdvanceToken()的实现(verilog-lexical-context.cc)展示了这种组合结构:先改写 token(MutateToken),再更新主状态机(UpdateState),最后把 token 转发给并行的子状态机:
void LexicalContext::AdvanceToken(TokenInfo *token) { MutateToken(token); // only modifies token, not *this UpdateState(*token); // only modifies *this, not token // The following state machines require a mutable token reference: property_declaration_tracker_.UpdateState(token); sequence_declaration_tracker_.UpdateState(token); // Maintain one token look-back. previous_token_ = token; }internal命名空间(verilog-lexical-context.h)中定义了若干专职子状态机:
| 子状态机 | 职责 |
|---|---|
KeywordLabelStateMachine | 在begin、if等关键词之后解析可选标签(4 状态:kItemStart/kItemMiddle/kGotLabelableKeyword/kGotColonExpectingLabel) |
ConstraintBlockStateMachine | 跟踪constraint_block/constraint_set,用栈处理可嵌套的{ },并为->token 返回消歧后的枚举 |
RandomizeCallStateMachine | 解析randomize()调用及其约束块 |
ConstraintDeclarationStateMachine | 解析(非 extern 的)constraint 声明 |
LastSemicolonStateMachine | 在成对关键词(如property/endproperty)之间跟踪最后一个分号并将其替换为新枚举,以消歧property_declaration与sequence_declaration中因"可选前导列表"造成的冲突 |
其中LastSemicolonStateMachine的构造方式最能说明"LR 文法做不到"这一点——它通过参数记住触发词、结束词和替换枚举:
// verilog-lexical-context.cc LexicalContext::LexicalContext() : property_declaration_tracker_( TK_property, TK_endproperty, SemicolonEndOfAssertionVariableDeclarations), sequence_declaration_tracker_( TK_sequence, TK_endsequence, SemicolonEndOfAssertionVariableDeclarations) {}另外,LexicalContext内部还维护了若干主状态:in_module_declaration_、in_function_body_、in_task_declaration_等不可嵌套状态用简单布尔位,flow_control_stack_(if/for/case 的头部与身体)、block_stack_(begin/end 配对)、balance_stack_(圆/方/花括号配对)用栈跟踪,并且明确要求"对无效输入宽容——括号不配对的代码绝不能导致致命错误"。这些行为有大量单元测试覆盖,见 verilog-lexical-context_test.cc(如ScanRandomizeWithConstraintBlock、ConstraintDeclarationImplication、MacroCallBalance等用例)。
Preprocessor:没有独立预处理器,但有"伪预处理器"
设计文档明确写道:"There is no standalone preprocessor yet."(目前没有独立的预处理器。)结合 verilog-parser.h 中的TODO(fangism): Use a pseudo-preprocessor between the lexer and parser.,可以确认设计意图是把预处理作为 lexer 与 parser 之间的可选 pass。
仓库中实际存在的实现是 VerilogPreprocess,头文件注释自称为pseudo-preprocessor(伪预处理器),其约束与普通预处理器截然不同:
"Unlike a conventional preprocessor, this pseudo-preprocessor does not open included files, nor does it evaluate preprocessor expressions. Instead, it does a best-effort handling of preprocessor directives locally within one file, and no additional context."
其Config提供三个可独立开关的行为(对应Analyze()管线中的第 ③ 步):
struct Config { // Filter out non-matching `ifdef and `ifndef branches depending on // which defines are set. bool filter_branches = false; // Inlude files with `include. bool include_files = false; // Expand macro definition bodies, this will relexes the macro body. bool expand_macros = false; };注意注释中的取舍:"for formatting, we donotwant to filter the output as we want to emit all tokens"——格式化器需要保留所有 token,而语法检查可能希望过滤掉不匹配的ifdef分支。ScanStream()返回VerilogPreprocessData,包含预处理后的 token 流、宏定义注册表、被拒绝的 token 序列(errors/warnings)等。
此外,VerilogAnalyzer::AnalyzeAutomaticPreprocessFallback() 展示了这一层的"有限但有用"策略:当常规解析失败时,它会以{filter_branches: false/true} × {expand_macros: false/true}四种组合重试解析。解析成功后还有一步锦上添花的ExpandMacroCallArgExpressions():对语法树中可解析为表达式(或 property)的MacroArgtoken,递归地用AnalyzeVerilogExpression/AnalyzeVerilogPropertySpec建立子分析结果,并把子语法树**拼接(splice)**回原树,使宏调用参数也获得结构化表示。
Filter:从 token 流到可解析的 token 流视图
设计文档说:token 流可以通过多种方式被过滤为token stream view;最有用的一种过滤是隐藏注释和属性(attributes),使得过滤后的视图可以进入解析。
"Token stream view" 这一数据结构定义在 token-stream-view.h:
using TokenSequence = std::vector<TokenInfo>; // TokenStreamView is the type that is transformed and returned by filters. using TokenStreamView = std::vector<TokenSequence::const_iterator>; // TokenStreamReferenceView is TokenStreamView with writeable iterators. using TokenStreamReferenceView = std::vector<TokenSequence::iterator>; // Tokens that evaluate to false with these predicates are removed. using TokenFilterPredicate = std::function<bool(const TokenInfo &)>; void FilterTokenStreamView(const TokenFilterPredicate &keep, const TokenStreamView &src, TokenStreamView *dest); void FilterTokenStreamViewInPlace(const TokenFilterPredicate &keep, TokenStreamView *);即视图是"指向底层 token 序列的迭代器数组"——过滤不复制、不破坏原始流,只改变被访问的迭代器集合。TokenStreamReferenceView(可写迭代器)则专门供 Contextualizer 这类需要就地改写 token 枚举的变换使用。
Verible 中最核心的过滤谓词是 VerilogLexer::KeepSyntaxTreeTokens():
bool VerilogLexer::KeepSyntaxTreeTokens(const TokenInfo &t) { switch (t.token_enum()) { case TK_COMMENT_BLOCK: // fall-through case TK_EOL_COMMENT: // fall-through case TK_ATTRIBUTE: // fall-through case TK_SPACE: // fall-through case TK_NEWLINE: case TK_LINE_CONT: // TODO(fangism): preserve newlines until after some preprocessing steps. return false; default: return true; } }它在 VerilogAnalyzer::FilterTokensForSyntaxTree() 中被应用:
void VerilogAnalyzer::FilterTokensForSyntaxTree() { MutableData().FilterTokens(&VerilogLexer::KeepSyntaxTreeTokens); }被过滤掉的是注释、属性与空白类 token(空格/换行/续行符);而TK_SPACE/TK_NEWLINE之所以能被安全移除,正依赖于上文提到的词流完备性——注释等 token 仍保留在原始 token 流中,只是不出现在"语法树视图"里。这也解释了为什么 token 枚举头文件注释里要区分"raw, unfiltered"与"filtered"两种打印形态(见下文 syntax 工具的--printrawtokens/--printtokens)。
Parser:Bison LALR(1) 文法与 CST 构造
设计文档说:解析器由 Bison 用LALR(1)算法生成;其文法非常规之处是显式处理预处理构造、指令与宏——这使得解析器能够在"有限但有用"的未预处理代码子集上工作;解析器构造出捕获所有非注释 token(包括标点与语法糖)的CST(具体语法树)。
文法文件 verilog.y(约 8756 行)的头部配置印证了与解耦架构的对接:
%code requires{ #include "verible/common/parser/bison-parser-common.h" } %define api.pure %param { ::verible::ParserParam* param } %define api.prefix {verilog_} %define api.value.type {::verible::SymbolPtr}%define api.pure+%param:生成"纯"的verilog_parse(ParserParam *param)函数,token 通过ParserParam::FetchToken()从外部注入(而不是调用yylex())——这正是解耦的落地点;- 值栈元素类型是
::verible::SymbolPtr,语义动作直接构造Symbol/SyntaxTreeNode,即 CST 节点。
ParserParam(parser-param.h)在解析过程中承担多职责:从TokenGenerator取 token、保存 Bison 错误恢复时丢弃的 token(RecordSyntaxError)、动态 resize Bison 的状态栈与值栈(ResizeStacks),并产出ConcreteSyntaxTree。Bison 的生成函数verilog_parse再被 verilog_parse_wrapper 包一层,用--verilog_trace_parser命令行开关控制verilog_debug以输出 Bison 的详细解析 trace:
ABSL_FLAG(bool, verilog_trace_parser, false, "Trace verilog parser"); int verilog_parse_wrapper(::verible::ParserParam *param) { const verible::ValueSaver<int> save_global_debug( &verilog_debug, absl::GetFlag(FLAGS_verilog_trace_parser) ? 1 : 0); return verilog_parse(param); }文法显式容纳预处理构造
"未预处理代码可解析"并非一句空话,verilog.y 中有专门的非终结符族:preprocessor_directive、preprocessor_control_flow(if/elsif头 + 嵌套分支)、preprocessor_action(如define),以及preprocessor_balanced_class_items等"平衡"变体——后者允许在 class 项列表中把ifdef/elsif/else分支连同其中未展开的代码一起平衡配对地跳过:
preprocessor_directive : preprocessor_control_flow | preprocessor_action ; preprocessor_control_flow : preprocessor_if_header ... ; preprocessor_balanced_class_items : preprocessor_if_header class_items_opt preprocessor_elsif_class_items_opt preprocessor_else_class_item_opt ...文法头部注释也坦承局限:"without preprocessing, we cannot know the type of an identifier that..."——即未预处理代码只能覆盖"有限但有用"的子集。文法文件还声明其语法树是脆弱的("The syntax tree constructed by the semantic actions in this file are fragile and subject to change without notice"),依赖该结构的代码应隔离在verible/verilog/CST/...之下并配套单元测试——CST 节点由 NodeEnum 枚举打标签,具体非终结符语义类型(ModuleDeclaration、PortDeclaration等)定义在verible/verilog/CST/目录中。
端到端验证:用 verible-verilog-syntax 观察词法与 CST
仓库提供了独立工具 verible-verilog-syntax 来检查 SystemVerilog 语法并检查词法/解析表示,是排查 Verible 问题的第一入口(见 verible/verilog/tools/syntax/README.md)。主要选项:
usage: verible-verilog-syntax [options] <file(s)...> --error_limit (Limit the number of syntax errors reported. (0: unlimited)); default: 0 --export_json (Uses JSON for output. Intended to be used as an input for other tools.); default: false --lang (auto: SystemVerilog-2017, may auto-detect alternate parsing modes; sv: strict SystemVerilog-2017; lib: Verilog library map language LRM Ch. 33); default: auto --printrawtokens (Prints all lexed tokens, including filtered ones.); default: false --printtokens (Prints all lexed and filtered tokens); default: false --printtree (Whether or not to print the tree); default: false --verifytree (Verifies that all tokens are parsed into tree, prints unmatched tokens); default: false以这段代码为例:
// This is module foo. module foo(input a, b, output z); endmodule : foo--printrawtokens会打印未经过滤的完整 token 流(含注释与空白,验证上文"无损词法"的性质):
All lexed tokens: (#"// end of line comment" @0-22: "// This is module foo.") (#"<<\n>>" @22-23: "\n") (#"module" @23-29: "module") (#"<<space>>" @29-30: " ") (#SymbolIdentifier @30-33: "foo") (#'(' @33-34: "(") ... (#$end @74-74: "")--printtokens则显示过滤后的流(空白类 token 被KeepSyntaxTreeTokens剔除),--printtree打印 CST,例如:
Parse Tree: Node @0 (tag: kDescriptionList) { Node @0 (tag: kModuleDeclaration) { Node @0 (tag: kModuleHeader) { Leaf @0 (#"module" @23-29: "module") Leaf @2 (#SymbolIdentifier @30-33: "foo") Node @5 (tag: kParenGroup) { ... } } ... } }节点由 NodeEnum 打标签,叶子封装 token 并带字节偏移。需要说明:CST 的确切结构是脆弱的实现细节,节点枚举与子树形状随时可能变化,工具文档建议通过verible/verilog/CST库提供的抽象函数访问,而非依赖裸树形结构。
另外,--lang auto(默认)模式下,解析器支持替代解析模式:文件头部注释中可写// verilog_syntax: parse-as-module-body之类的指令;当按顶层解析失败时,AnalyzeAutomaticMode 还会根据第一个被拒绝 token 的关键词(如initial/always_ff→parse-as-module-body,library/include/incdir→parse-as-library-map)推断并重试另一种解析模式,且以"谁的第一个错误位置更靠后"来裁决两次尝试的结果——这是"有限但未预处理"输入在实际工程中被兜底处理的典型案例。
小结:各环节与源码位置对照
| 设计文档环节 | 源码位置 | 关键点 |
|---|---|---|
| Lexer | verilog.lex / verilog-lexer.h / flex-lexer-adapter.h | Flex 生成,语言词法 + 预处理指令 all-in-one;适配为"逐 token + EOF"接口 |
| Contextualizer | verilog-lexical-context.h/.cc | token 流上的消歧 pass,主状态机 + 多个并行子状态机,就地改写 token 枚举 |
| Preprocessor | verilog-preprocess.h/.cc | 无独立预处理器;pseudo-preprocessor 做单文件 best-effort,分支过滤/宏展开可配置 |
| Filter | token-stream-view.h / KeepSyntaxTreeTokens | token 流过滤为迭代器视图,隐藏注释/属性/空白后可解析 |
| Parser | verilog.y / bison-parser-adapter.h / parser-param.h | Bison LALR(1),api.pure+ParserParam注入 token;文法显式容纳预处理构造;产出 CST |
整套架构的取舍可以概括为:用"解耦 + 多 pass 变换"换取对未预处理SystemVerilog 的鲁棒解析能力,代价是词法层需要维护大量 start condition 与上下文状态机;每个 pass 职责单一、可独立测试(各_test.cc均在同目录中),这也是该设计文档中"decoupled"理念在工程上的完整落地。
- 开发工具
- Lint
- 静态分析
- 硬件开发
【免费下载链接】verible
Verible is a suite of SystemVerilog developer tools, including a parser, style-linter, formatter and language server
相关推荐
Verible项目中的SystemVerilog解析器设计解析
Verible项目中的SystemVerilog解析器设计解析 前言 在数字电路设计和验证领域,SystemVerilog作为一门复杂的硬件描述和验证语言,其解
开发工具Lint静态分析硬件开发探索文本解析新境界:Linux下Flex与Bison资源包全面解读
探索文本解析新境界:Linux下Flex与Bison资源包全面解读 在当今软件开发的浩瀚宇宙中,编译器与解释器的构建无疑是一片充满挑战与乐趣的领域。对于热衷于创
LangBot项目中的Chat处理器与请求器解耦设计解析
LangBot项目中的Chat处理器与请求器解耦设计解析 在LangBot项目中,开发者对聊天功能模块进行了重要的架构优化,将原本耦合在一起的处理器 Proce
人工智能大模型AI Agent即时通讯RAG后端MCP 服务本地部署
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考