☰
深入解析 Verible SystemVerilog Formatter:Inter-Token 标注、Token 分区树与换行优化
2026/10/14 0:44:05 网站建设 项目流程
  • 开发工具
  • Lint
  • 静态分析
  • 硬件开发

【免费下载链接】verible

Verible is a suite of SystemVerilog developer tools, including a parser, style-linter, formatter and language server

项目地址:https://gitcode.com/gh_mirrors/ve/verible
点击查看免费下载

本文以 doc/formatter.md 这份开发者指南为骨架,逐层拆解 Verible 中 SystemVerilog 格式化器(verible-verilog-format)的内部架构:从相邻 Token 间的间距标注(InterTokenInfo),到把语法树“解包”成TokenPartitionTree的TreeUnwrapper,再到基于最短路搜索的换行优化与表格对齐(tabular alignment)。读完后,你不仅能理解verible-verilog-format各阶段在做什么、哪些源码文件负责哪个环节,还能掌握官方给出的故障排查命令与环境变量组合,独立定位格式化行为异常。

格式化器整体数据流

doc/formatter.md把格式化器描述为若干子系统:Inter-Token Annotation(Token 间标注)、Token Partition Tree(Token 分区树)、Custom Policies(自定义策略)、Wrapping(换行优化)与 Alignment(对齐)。从源码结构看,这些子系统在 Formatter::Format() 中按如下顺序串联:

  1. 构建UnwrapperData:把词法 Token 流过滤成PreFormatToken序列(保留注释、去除纯空白),见 tree-unwrapper.h 中的UnwrapperData定义。
  2. Token 间标注:调用 AnnotateFormattingInformation() 为每对相邻 Token 填充InterTokenInfo。注释明确要求此步骤必须先于任何“是否展开分区”的决策,因为决策依赖最小间距与必须换行等信息(formatter.cc)。
  3. 计算禁用区间:DisableFormattingRanges()解析// verilog_format: off/on注释指令,PreserveSpacesOnDisabledTokenRanges()将这些区间标记为“保留原始空白”。
  4. 解包语法树:tree_unwrapper.Unwrap()把 CST 转换为TokenPartitionTree。
  5. 策略重塑:按分区策略执行kAppendFittingSubPartitions的重塑(ReshapeFittingSubpartitions)、布局优化器策略(OptimizeTokenPartitionTree)以及kTabularAlignment的表格对齐(TabularAlignTokenPartitions),见 formatter.cc。
  6. 换行搜索:对每个可独立工作的UnwrappedLine调用SearchLineWraps()最小化换行/间距决策的总代价。
  7. 校验与收敛:格式化结果会重新解析并做词法等价检查,全文件格式化时还会验证format(format(text)) == format(text)收敛性(FormatVerilog())。

此外,ExecutionControl 结构体承载了大量诊断开关(show_token_partition_tree、show_inter_token_info、show_largest_token_partitions、max_search_states、verify_convergence等),它们既是开发调试工具,也是命令行上的正式参数。

Inter-Token Annotation:相邻 Token 之间的格式属性

doc/formatter.md指出:在被格式化文本的每一对相邻 Token 之间,都有一组影响间距与换行决策的属性,即InterTokenInfo,例如两 Token 之间的最小间距。Token annotator 的职责就是依据相邻 Token 的属性、类型和语法上下文来填充这些属性——围绕括号、方括号等标点 Token 的格式化规则往往高度依赖语法上下文。

在源码中,InterTokenInfo是一个结构体(format-token.h),核心字段为:

字段含义
spaces_required该 Token 前应插入的空格数(最小间距)。若删除空格会改变词法流(错误),该值必须非零;也可用于格式化行首 Token 的缩进
break_penalty在该 Token 前换行所引入的惩罚值,供优化阶段使用
break_decisionSpacingOptions枚举,编码换行探索选项
preserved_space_start指向原始空白文本起点的迭代器,配合当前 Token 可还原SpacingOptions::kPreserve情形下的既有空白

SpacingOptions与最终决策SpacingDecision的取值(format-token.h):

enum class SpacingOptions { kUndecided, // 未决定,待优化(默认) kMustAppend, // 此处不能换行 kMustWrap, // 此处必须换行 kAppendAligned, // 追加时允许左侧填充空格 kPreserve, // 不优化,使用原始间距 }; enum class SpacingDecision { // 优化后的最终决策,刻意没有“未决定” kPreserve, // 保留原始间距 kAppend, // 追加到当前行,带适当间距 kWrap, // 换到下一行,带适当缩进 kAlign, // 类似 Append,但强制左侧填充空格,即使在行首 };

规则与理由(Rules and Reasons)

原文档特别强调了标注函数的编写风格:确定InterTokenInfo各属性的主函数都以优先级排序的规则形式书写,即一系列if (condition) return ...;。相邻 Token 对的组合可能有 O(N²) 种,但目标是用少量规则覆盖尽可能大的组合空间;而 token-annotator_test.cc 这类测试则应尽可能显式覆盖,防止无意回归。

另一个值得注意的调试设计:开启日志运行时,这些函数返回的每个值或属性都会附带一个reason字符串,例如"Always insert a space after X"。它不仅告诉你返回值,还告诉你是哪一行/哪条规则触发的,这对调试至关重要。文档给出的维护建议也很具体:修复问题时不要立刻想“该加什么代码”,先考虑调整规则顺序是否可行——把相似的 case 条件归组、或把某条规则泛化到更宽的范围,有时反而能简化代码。

Token Partition Tree:格式化子问题的层次化表示

TokenPartitionTree 是(格式化)Token 区段的语言无关的层次化表示。每个节点承载的数据是一个 UnwrappedLine,其中包含一段格式化 Token 区间(用轻量级const_iterator范围表示,可无拷贝开销地增长)以及缩进信息。TokenPartitionTree满足两条不变式(原文档原文):

  • 树中某节点跨越的区间等于其所有子节点跨越的区间之和(若有子节点);
  • 相邻兄弟节点的 Token 区间端点相等,即相邻兄弟节点之间没有区间空隙。

doc/formatter.md对每个节点的语义给出了一个很有用的心智模型:把它看作一个格式化子问题——给定框宽约束,这段文本的最佳格式化方式是什么?TokenPartitionTree的叶分区则是当前由换行优化阶段处理的“工作单位”。

UnwrappedLine的另一个关键字段是分区策略 PartitionPolicyEnum,原文档提到的两个策略(kTabularAlignment、kAppendFittingSubPartitions)都在其中:

enum class PartitionPolicyEnum { kUninitialized, // 未设置策略 kAlwaysExpand, // 仅用于分组,始终展开子分区 kFitOnLineElseExpand, // 不超过列限时压成一行,否则展开 kTabularAlignment, // 子分区之间用空格填充实现垂直对齐 kAlreadyFormatted, // 已完成格式化,跳过 SearchLineWraps kInline, // kAlreadyFormatted 行的切片(布局优化器用) kAppendFittingSubPartitions, // 子分区作为整体,能放同一行就放 kJuxtaposition, kStack, kWrap, kJuxtapositionOrIndentedStack, // LayoutOptimizer 策略 };

Tree Unwrapping:把 CST 变成分区树

TreeUnwrapper 负责把 SystemVerilog 具体语法树(CST)转换成TokenPartitionTree(即VectorTree<UnwrappedLine>)。一旦进入TokenPartitionTree形式,原始语法结构就被抽象掉了,对后续所有格式化阶段不再相关。文档由此引出一个重要的架构推论:如果要实现一个不依赖合法语法树的格式化器,只需能产出TokenPartitionTree即可复用后续全部机制——例如可以直接从词法 Token 流生成分区树。这种策略能更好地容纳预处理繁重的代码(否则会在语法树产生之前就被解析器拒绝),代价是需要做一点“伪解析”。

语言无关库函数与 Verilog 特定代码的分离

原文档明确指出:TreeUnwrapper的实现拆分为语言无关的库函数与Verilog 特定代码两部分,并给出简化类继承图(见文首插图,图源 doc/formatter_simplified_class_diagram.png)。

TreeUnwrapper类利用 Visitor 模式递归处理输入 CST 树:

// SymbolVisitor is an abstract visitor class from which visitors can be derived // SymbolVisitor only visits a single Symbol, and does not recurse on nodes // // Usage: // SymbolPtr tree = ... // SymbolVisitorSubclass visitor; // tree->Accept(visitor); // class SymbolVisitor { public: virtual ~SymbolVisitor() {} virtual void Visit(const SyntaxTreeLeaf& leaf) = 0; virtual void Visit(const SyntaxTreeNode& node) = 0; };

在 verible/common/text/visitors.h 中可以看到,SymbolVisitor只访问单个Symbol且不自动递归,递归控制完全交给子类——这正是解包器需要精细控制遍历与分区创建的原因。Verilog 侧的 verilog::formatter::TreeUnwrapper(final派生类)通过一组辅助方法简化构建过程,原文档列出的三个关键方法在源码中均有对应实现:

  • TraverseChildren()—— 只遍历 CST;
  • VisitIndentedSection()—— 创建缩进节区并遍历 CST;
  • VisitNewUnwrappedLine()—— 开启一个新的 Token 分区并遍历 CST;
  • 另有VisitNewUnindentedUnwrappedLine(),用于 flush-left 构造,新建的UnwrappedLine不追加缩进。

自定义策略的应用(Custom Policies)

格式化流程的下一阶段是按所选策略对分区树做重塑。Formatter::Format()中有一遍前序遍历(ApplyPreOrder),按PartitionPolicyEnum分派:

  • kAppendFittingSubPartitions:调用ReshapeFittingSubpartitions()重塑子分区结构——这是原文档提到的第二个示例策略(unwrapped-line.h 注释说明其语义:把子分区当作单位,能放进同一行就追加,否则换行并对齐到第一个元素的位置);
  • kJuxtaposition/kStack/kWrap/kJuxtapositionOrIndentedStack:交给 LayoutOptimizer(OptimizeTokenPartitionTree()),注释特别警告:一旦这些策略被赋给某个分区,其整棵子树都由 LayoutOptimizer 处理;
  • kTabularAlignment:调用TabularAlignTokenPartitions()(后文详述)。

一个体现“策略决定展开与否”的实现细节在 DeterminePartitionExpansion():以kAppendFittingSubPartitions为例,先用FitsOnLine()判断能否压进单行——能则Unexpand(),否则Expand();而kTabularAlignment对函数调用(kArgumentList)有个特殊优化:整个调用能放进一行时直接Unexpand(),否则才展开做表格对齐。

Wrapping:换行优化(最短路搜索)

原文档“Wrapping”一节说明:换行优化当前通过在“append/wrap 决策空间”上执行Dijkstra 最短路搜索完成,搜索跨越一段格式化 Token 区间;每当某处采用换行(line break),优化状态就会计入一个在 Token 标注阶段确定的惩罚值。文档同时坦诚该算法“可扩展性不算很好,有计划替换成类似动态规划的实现”。

对应源码即 SearchLineWraps():输入一个已标注的UnwrappedLine与样式结构,返回一组同等最优的FormattedExcerpt(格式化决策已固化)。max_search_states限制搜索规模,超限时返回一个贪心格式化结果并标记!CompletedFormatting();Formatter::Format()发现这类“未完成”行时会返回ResourceExhaustedError(formatter.cc)。惩罚项由样式参数控制:line_break_penalty(每引入一个换行的惩罚,默认 2)与over_column_limit_penalty(超过column_limit的基准惩罚,默认 100,每超 1 字符再加 1),定义见 basic-format-style-init.cc。CLI 侧的--max_search_states默认值为 100000(见 tools/formatter/README.md)。

Disable Control 与 Preservation:格式禁用区

原文档的 “Disable Control and Preservation” 一节标题下的内容较简,其完整行为需要结合源码与工具 README 来看:

  • 用户通过注释指令排除某些文本:
// verilog_format: off ... untouched code ... // verilog_format: on

或块注释形式/* verilog_format: off */ ... /* verilog_format: on */。良好实践是在指令后附上原因,例如// verilog_format: off // my alignment is prettier than the tool's(完整说明见 tools/formatter/README.md)。这些指令优先于--lines指定生效。

  • 实现上,DisableFormattingRanges()(comment-controls.h)把指令翻译成字节区间集合,Formatter::Format()将其并入disabled_ranges_,随后 PreserveSpacesOnDisabledTokenRanges() 把禁用区间内的 Token 标记为保留原始空白。
  • SelectLines()(--lines参数)也是同一机制:把“启用格式化的行集合”转成“禁用字节区间”(formatter.cc)。
  • 若某个分区被部分禁用,DeterminePartitionExpansion() 会强制展开该分区,使子分区之间的空白在Emit()阶段按禁用区逐段处理。

故障排查(Troubleshooting)

doc/formatter.md的排查章节给出了三层递进的诊断手段,以下完整保留并补充源码对应关系。

1. 查看语法树:verible-verilog-syntax --printtree显示源代码的具体语法树表示。许多格式化决策基于语法树节点类型,这些类型以kPackageDeclaration这类常量形式出现。

2. 查看分区树:verible-verilog-format --show_token_partition_tree显示TokenPartitionTree对格式化 Token 区段的划分及相对缩进;追加--show_inter_token_info会在每个 Token 前打印更多InterTokenInfo信息(间距、换行惩罚等)。这两个开关对应ExecutionControl的同名字段,在Formatter::Format()中通过TokenPartitionTreePrinter打印后直接停止、不产出格式化结果(formatter.cc)。

3. 详细日志:用环境变量

VERIBLE_LOGTHRESHOLD=0 VERIBLE_VLOG_DETAIL=8

原理解释:VERIBLE_LOGTHRESHOLD=0表示要看 INFO 及以上日志(四个级别:INFO=0、WARNING=1、ERROR=2、FATAL=3),它们打印到 stderr——不开启的话看不到任何 vlog 输出,因为 vlog 走 INFO 级;VERIBLE_VLOG_DETAIL=8表示记录不超过 8 级详细度的所有VLOG()命令,数字越大细节越多。(原文档中关于VERIBLE_VLOG_MODULE的引入仍是 TODO 状态。)

文档给出的一个真实TreeUnwrapper调试示例:

echo 'module m; initial a = b + c; endmodule' | \ VERIBLE_LOGTHRESHOLD=0 \ VERIBLE_VLOG_DETAIL=8 \ bazel run //verible/verilog/tools/formatter:verible-verilog-format -- -show_token_partition_tree -

补充两个实战建议(均来自仓库文档与代码):

  • 换行决策存疑时加--show_equally_optimal_wrappings,当某分区存在多个同等最优解时会在 stderr 打印对照(DisplayEquallyOptimalWrappings()),便于调换行惩罚;
  • 想定位“哪个分区 Token 太多”时,用--show_largest_token_partitions=N打印最大的 N 个叶分区及其行号,然后退出(PrintLargestPartitions())。

表格对齐(Alignment)

doc/formatter.md把表格对齐功能概括为:给定一段语法结构相似的代码块,插入所需空格,使相同子结构的元素实现垂直对齐。其实现同样拆分为语言无关的库函数与Verilog 特定调用。

触发:kTabularAlignment 标记

要对某段代码触发对齐,TreeUnwrapper会在TokenPartitionTree节点上打上 kTabularAlignment 标记:这表示该节点的某些子分区“可以”被对齐。

第一阶段:语言特定(Language-specific)

TokenPartitionTree进入顶层对齐处理器后,对齐分两阶段进行。第一阶段取决于每个分区对应的语法树节点类型,从一张函数表中选择处理函数,由其负责找出 AlignablePartitionGroup——一组可参与对齐格式化的子分区,可理解为构成一张“表”的行(rows)。原文档列出的行为要点在源码中均有体现:

  • 单个父节点返回的AlignablePartitionGroup可以覆盖不同的语法构造。例如一个表示语句元素的节点下,可能既有声明组,也有赋值语句组;
  • 若组跨越了对齐无关的文本(如注释),AlignablePartitionGroup可能是稀疏填充的;
  • 每个AlignablePartitionGroup都被赋予一个函数,负责把行上的 Token 切成列(column)单元格(AlignmentCellScannerFunction);
  • 候选对齐组的起止点判定可基于多种因素:语法树节点类型、注释、空行等;
  • 所有未被选入对齐的子分区,默认走非对齐格式化。

第二阶段:语言无关(Language-agnostic)

每个对齐组分析并转换自己的行与列,本质上计算每列最大宽度,并相应调整InterTokenInfo间距,同时考虑左/右对齐(flush)属性(AlignmentColumnProperties的flush_left、left_border_override等,见 align.h)。要点:

  • 每行被扫描并切分为将归入各列的单元格,这要求各行具有相同或相似的语法结构;
  • 列可以为空——可选语法构造在表中就是空白单元格;
  • 为正确分列,每个 Token 单元格都被赋予一个键:其在父语法树节点内的路径(path),形如[N,M,P](SyntaxTreePath,对应ColumnPositionEntry的键,见 align.h)。路径的层次性与键的字典序便于容纳每行的空单元格,也支持动态列数——例如数据声明的列数取决于维度个数;
  • 单元格分列形成column schema,据此计算列宽;
  • 列宽最终转化为InterTokenInfo的间距调整。

若最终列宽超过列限(column_limit样式参数),则放弃对齐,回退到非对齐格式化——AlignablePartitionGroup::Align(int column_limit)的接口签名(align.h)即携带该约束。

用户侧控制:对齐策略与分组边界

对齐行为通过一系列--*_alignment标志控制,默认全部为infer(AlignmentPolicy::kInferUserIntent,format-style-init.cc),覆盖端口声明、struct/union 成员、命名参数/端口、模块内 net/variable 声明、形式参数、parameter/localparam 声明、类成员变量、case 项、分布项、赋值语句等上下文。四种取值(align.h):

  • align—— 强制表格对齐;
  • flush-left—— 不做对齐,按间距约束左对齐;
  • preserve—— 原样保留;
  • infer—— 依据原始代码的间距推断作者意图。

infer的判定逻辑在 tools/formatter/README.md 中有完整示例:对齐与 flush-left 的间距差“足够小”时倾向于对齐;若原始代码中的 excess spaces(超出最小间距的多余空格)低于阈值,则推断作者想要 flush-left;作者若要对齐,需在组内任一行的任意两 Token 之间注入 4 个多余空格来“表态”;否则原始代码保持不动。另外,--alignment_group_boundary(默认none,取值{none,blank-lines,separator-comments,blank-lines-and-separator-comments},format-style-init.cc)控制空行或分隔注释是否打断对齐组,让各子节按各自宽度独立对齐。

样式参数速查

格式化器风格由两层标志组成:BasicFormatStyle(语言无关,basic-format-style-init.cc)与 Verilog 特定的 FormatStyle。常用参数与默认值:

参数默认值说明
--column_limit100格式化时尽量不超过的目标行宽
--indentation_spaces2每级缩进增加的空格数
--wrap_spaces4每级 wrap 增加的空格数(适用于开组后首元素换行的场景)
--line_break_penalty2每个引入的换行加入解的惩罚
--over_column_limit_penalty100超过列宽的基准惩罚,每超 1 字符加 1
--line_terminatorauto行结束符;auto按输入推断,也可固定 CR / CRLF
--try_wrap_long_linesfalse是否需要换行时让格式化器优化换行决策,否则保留原样(降低风险的短期措施)
--alignment_group_boundarynone对齐组边界控制(见上文)
--*_alignment(系列)infer各上下文对齐策略{align,flush-left,preserve,infer}
--*_indentation(系列)wrap端口/形式参数/命名参数/命名端口的缩进风格{indent,wrap}

完整参数列表可直接用verible-verilog-format --helpfull查看,或参考 tools/formatter/README.md。

收尾:failsafe 设计约束了以上所有机制

上述各阶段的任何一环失败,都会触发 tools/formatter/README.md 所述的安全行为——格式化器绝不能损坏数据:词法/解析失败(没有语法树可用)时放弃;输出前做词法等价性检查(只允许空白变化)与收敛性检查(再次格式化无进一步变化);换行搜索超出--max_search_states等内部资源限制时放弃;且只有在全部内部验证通过后才打开文件写入——任何崩溃都会保持原文件完整。理解这一点,也解释了本文各阶段为何被设计成“可诊断、可提前停止”(ExecutionControl::AnyStop())。

小结

doc/formatter.md描绘的 Verible 格式化器架构可以概括为一条清晰的流水线:词法 Token → InterTokenInfo 标注(规则 + reason)→ TokenPartitionTree(Visitor 解包)→ 分区策略重塑(对齐/布局/子分区折叠)→ 最短路换行搜索 → 等价性与收敛性校验。语言无关的通用库(verible/common/formatting/)承担表示与优化,Verilog 特定层(verible/verilog/formatting/)承担标注规则、分区策略选择与对齐扫描。排查问题时,从--printtree、--show_token_partition_tree --show_inter_token_info到VERIBLE_LOGTHRESHOLD=0 VERIBLE_VLOG_DETAIL=8的组合,可以逐层逼近决策发生的具体规则与代码位置。

  • 开发工具
  • Lint
  • 静态分析
  • 硬件开发

【免费下载链接】verible

Verible is a suite of SystemVerilog developer tools, including a parser, style-linter, formatter and language server

项目地址:https://gitcode.com/gh_mirrors/ve/verible
点击查看免费下载

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

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

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

立即咨询