资讯详情

资讯详情

Verible 解析器设计详解:Flex/Bison 解耦、上下文消歧与未预处理 SystemVerilog 语法分析

开发工具Lint静态分析硬件开发【免费下载链接】veribleVerible is a suite of SystemVerilog developer tools, including a parser, style-linter, formatter and language server项目地址https://gitcode.com/gh_mirrors/ve/verible点击查看免费下载本文基于 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::BisonParserAdapterverilog_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彼此独立可替换。LexerFlex 生成的全功能词法分析器设计文档指出词法分析器由 Flex 生成在一套实现中同时支持 SystemVerilog 语言本身的词法 token 以及预处理指令、宏和其他指令生成的代码被封装进一个逐个返回 token、直到以特殊 EOF token 结束的接口中。实现位于 verilog.lex约 1677 行。关键配置选项包括%option 8bit %option c %option prefixverilog %option stack %option yyclassverilog::VerilogLexer其中prefixverilog意味着 Flex 生成的类名为verilogFlexLexeryyclass将生成的代码绑定到 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::FlexLexerAdapterverilogFlexLexer { // ... 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::istringstreamCodeStreamHolder中转注释说明这允许在预处理时切换缓冲区。设计文档见 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_views 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展示了这种组合结构先改写 tokenMutateToken再更新主状态机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/kGotColonExpectingLabelConstraintBlockStateMachine跟踪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.husing TokenSequence std::vectorTokenInfo; // TokenStreamView is the type that is transformed and returned by filters. using TokenStreamView std::vectorTokenSequence::const_iterator; // TokenStreamReferenceView is TokenStreamView with writeable iterators. using TokenStreamReferenceView std::vectorTokenSequence::iterator; // Tokens that evaluate to false with these predicates are removed. using TokenFilterPredicate std::functionbool(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。ParserBison 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 节点。ParserParamparser-param.h在解析过程中承担多职责从TokenGenerator取 token、保存 Bison 错误恢复时丢弃的 tokenRecordSyntaxError、动态 resize Bison 的状态栈与值栈ResizeStacks并产出ConcreteSyntaxTree。Bison 的生成函数verilog_parse再被 verilog_parse_wrapper 包一层用--verilog_trace_parser命令行开关控制verilog_debug以输出 Bison 的详细解析 traceABSL_FLAG(bool, verilog_trace_parser, false, Trace verilog parser); int verilog_parse_wrapper(::verible::ParserParam *param) { const verible::ValueSaverint save_global_debug( verilog_debug, absl::GetFlag(FLAGS_verilog_trace_parser) ? 1 : 0); return verilog_parse(param); }文法显式容纳预处理构造未预处理代码可解析并非一句空话verilog.y 中有专门的非终结符族preprocessor_directive、preprocessor_control_flowif/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-bodylibrary/include/incdir→parse-as-library-map推断并重试另一种解析模式且以谁的第一个错误位置更靠后来裁决两次尝试的结果——这是有限但未预处理输入在实际工程中被兜底处理的典型案例。小结各环节与源码位置对照设计文档环节源码位置关键点Lexerverilog.lex / verilog-lexer.h / flex-lexer-adapter.hFlex 生成语言词法 预处理指令 all-in-one适配为逐 token EOF接口Contextualizerverilog-lexical-context.h/.cctoken 流上的消歧 pass主状态机 多个并行子状态机就地改写 token 枚举Preprocessorverilog-preprocess.h/.cc无独立预处理器pseudo-preprocessor 做单文件 best-effort分支过滤/宏展开可配置Filtertoken-stream-view.h / KeepSyntaxTreeTokenstoken 流过滤为迭代器视图隐藏注释/属性/空白后可解析Parserverilog.y / bison-parser-adapter.h / parser-param.hBison LALR(1)api.pureParserParam注入 token文法显式容纳预处理构造产出 CST整套架构的取舍可以概括为用解耦 多 pass 变换换取对未预处理SystemVerilog 的鲁棒解析能力代价是词法层需要维护大量 start condition 与上下文状态机每个 pass 职责单一、可独立测试各_test.cc均在同目录中这也是该设计文档中decoupled理念在工程上的完整落地。赞分享开发工具Lint静态分析硬件开发【免费下载链接】veribleVerible is a suite of SystemVerilog developer tools, including a parser, style-linter, formatter and language server项目地址https://gitcode.com/gh_mirrors/ve/verible点击查看免费下载相关推荐Verible项目中的SystemVerilog解析器设计解析Verible项目中的SystemVerilog解析器设计解析 前言 在数字电路设计和验证领域SystemVerilog作为一门复杂的硬件描述和验证语言其解开发工具Lint静态分析硬件开发探索文本解析新境界Linux下Flex与Bison资源包全面解读探索文本解析新境界Linux下Flex与Bison资源包全面解读 在当今软件开发的浩瀚宇宙中编译器与解释器的构建无疑是一片充满挑战与乐趣的领域。对于热衷于创LangBot项目中的Chat处理器与请求器解耦设计解析LangBot项目中的Chat处理器与请求器解耦设计解析 在LangBot项目中开发者对聊天功能模块进行了重要的架构优化将原本耦合在一起的处理器 Proce人工智能大模型AI Agent即时通讯RAG后端MCP 服务本地部署创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →