- 开发工具
- Lint
- 静态分析
- 硬件开发
【免费下载链接】verible
Verible is a suite of SystemVerilog developer tools, including a parser, style-linter, formatter and language server
本文以 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() 中按如下顺序串联:
- 构建
UnwrapperData:把词法 Token 流过滤成PreFormatToken序列(保留注释、去除纯空白),见 tree-unwrapper.h 中的UnwrapperData定义。 - Token 间标注:调用 AnnotateFormattingInformation() 为每对相邻 Token 填充
InterTokenInfo。注释明确要求此步骤必须先于任何“是否展开分区”的决策,因为决策依赖最小间距与必须换行等信息(formatter.cc)。 - 计算禁用区间:
DisableFormattingRanges()解析// verilog_format: off/on注释指令,PreserveSpacesOnDisabledTokenRanges()将这些区间标记为“保留原始空白”。 - 解包语法树:
tree_unwrapper.Unwrap()把 CST 转换为TokenPartitionTree。 - 策略重塑:按分区策略执行
kAppendFittingSubPartitions的重塑(ReshapeFittingSubpartitions)、布局优化器策略(OptimizeTokenPartitionTree)以及kTabularAlignment的表格对齐(TabularAlignTokenPartitions),见 formatter.cc。 - 换行搜索:对每个可独立工作的
UnwrappedLine调用SearchLineWraps()最小化换行/间距决策的总代价。 - 校验与收敛:格式化结果会重新解析并做词法等价检查,全文件格式化时还会验证
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_decision | SpacingOptions枚举,编码换行探索选项 |
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_limit | 100 | 格式化时尽量不超过的目标行宽 |
--indentation_spaces | 2 | 每级缩进增加的空格数 |
--wrap_spaces | 4 | 每级 wrap 增加的空格数(适用于开组后首元素换行的场景) |
--line_break_penalty | 2 | 每个引入的换行加入解的惩罚 |
--over_column_limit_penalty | 100 | 超过列宽的基准惩罚,每超 1 字符加 1 |
--line_terminator | auto | 行结束符;auto按输入推断,也可固定 CR / CRLF |
--try_wrap_long_lines | false | 是否需要换行时让格式化器优化换行决策,否则保留原样(降低风险的短期措施) |
--alignment_group_boundary | none | 对齐组边界控制(见上文) |
--*_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
相关推荐
Canopy 区块链跨链 Token 兑换(Token Swap)状态机实现深度解析
Canopy 区块链跨链 Token 兑换(Token Swap)状态机实现深度解析 本文以 fsm/swap.md https://link.gitcode.
区块链后端Verible项目中的SystemVerilog解析器设计解析
Verible项目中的SystemVerilog解析器设计解析 前言 在数字电路设计和验证领域,SystemVerilog作为一门复杂的硬件描述和验证语言,其解
开发工具Lint静态分析硬件开发Docker Swarm Join Token 管理实战:深入解析 docker swarm join-token 命令
Docker Swarm Join Token 管理实战:深入解析 docker swarm join token 命令 导读 本文围绕 Docker CE 中
容器运行时云原生
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考