SystemVerilog フォーマッタ verible のオプションの解説
Qiita HDL (SystemVerilog/Verilog/VHDL/Chisel/etc.) アドベントカレンダー 2021 9日目の記事です。
アドベントカレンダー初参加です。 久々にブログを書きました。
veribleをSystemVerilogのフォーマッタとして使ってみる
コードを一人で書いているときですらフォーマットが崩れますが、複数人で作成していると荒れます。 とくに、ハードウェア記述言語 (HDL) はフリーフォーマットが許されており、記法が言語仕様やコミュニティで厳密に定められている訳ではありません。1 したがって、複数人で開発するときはコーディングルールが必要になります。 ここで、コーディングルールの中でもフォーマットルールは白黒のつけづらい上に人間が管理するにはコスト高すぎます。 他の言語ではフォーマッタが開発されており 2 、コードの変更前にフォーマッタを利用するのがルールとなっているところも多いでしょう。
この記事ではSystemVerilogパーサである verible に含まれるフォーマッタ機能について解説します。 なるべく例示を多めに進めていく事とし、読者が手元で試しやすいようにするのが目的です。
なお、この記事は作成中の最新版であるv0.0-1761-gc8f52628をベースに記述しています。 veribleはまだまだ開発途中のツールですので、仕様が変わる可能性は高いことを念頭にご利用ください。
オプションの解説
basic format style
READMEから抜粋すると基本的な設定は以下を変更できるようです。
Flags from common/formatting/basic_format_style_init.cc: --column_limit (Target line length limit to stay under when formatting.); default: 100; --indentation_spaces (Each indentation level adds this many spaces.); default: 2; --line_break_penalty (Penalty added to solution for each introduced line break.); default: 2; --over_column_limit_penalty (For penalty minimization, this represents the baseline penalty value of exceeding the column limit. Additional penalty of 1 is incurred for each character over this limit); default: 100; --wrap_spaces (Each wrap level adds this many spaces. This applies when the first element after an open-group section is wrapped. Otherwise, the indentation level is set to the column position of the open-group operator.); default: 4;
本記事では--line_break_penalty
と--over_column_limit_penalty
以外を解説します。
--column_limit
1行分の文字数の上限を決定できます。 越えた場合は自動で折り返されますが、式はそのままです。
たとえば、デフォルトの状態で実行したとき
task automatic task_foo(logic arg1, logic [7:0] arg2 = 8'hAA, output logic arg3 = 1'b1, output logic [7:0] arg4, ref logic arg5, ref logic [15:0] arg6); $display("print some string"); $display("print %s %s %s", "some long string", "another long string", "more and more longer string"); endtask
となる場合、 --column_limit=80
とすると、
task automatic task_foo( logic arg1, logic [7:0] arg2 = 8'hAA, output logic arg3 = 1'b1, output logic [7:0] arg4, ref logic arg5, ref logic [15:0] arg6); $display("print some string"); $display("print %s %s %s", "some long string", "another long string", "more and more longer string"); endtask
となります。
--indentation_spaces
インデントの空白の個数を決めます。 デフォルトで2つです。
always_ff @(posedge (clk) or negedge (rst_n)) begin if (some_bool) begin if (another_bool) begin if (one_more_bool) begin out_0 = local_0; end end end end endmodule
のように、module
からendmodule
を起点としてインデントがスペースで挿入されます。
--indentation_spaces=4
とすると、
always_ff @(posedge (clk) or negedge (rst_n)) begin if (some_bool) begin if (another_bool) begin if (one_more_bool) begin out_0 = local_0; end end end end endmodule
のようになります3。
--wrap_spaces
括弧などでラップされたときに加えるスペースの個数です。 一番分かりやすいポートマップで比較すると、デフォルトの4のときは、
module foo #( parameter int PARAM0 = 10, parameter PARAM1 = 5, parameter string PARAMSTR = "string" ) ( input logic [7:0] in_0, input logic in_1, output logic [7:0] out_0, output logic out_1 );
となり、--wrap_spaces=8
のときは
module foo #( parameter int PARAM0 = 10, parameter PARAM1 = 5, parameter string PARAMSTR = "string" ) ( input logic [7:0] in_0, input logic in_1, output logic [7:0] out_0, output logic out_1 );
となります。
format style init
もう少し細やかな設定は以下のオプションで指定できます。
Flags from verilog/formatting/format_style_init.cc: --assignment_statement_alignment (Format various assignments: {align,flush-left,preserve,infer}); default: infer; --case_items_alignment (Format case items: {align,flush-left,preserve,infer}); default: infer; --class_member_variable_alignment (Format class member variables: {align,flush-left,preserve,infer}); default: infer; --compact_indexing_and_selections (Use compact binary expressions inside indexing / bit selection operators); default: true; --distribution_items_alignment (Aligh distribution items: {align,flush-left,preserve,infer}); default: infer; --enum_assignment_statement_alignment (Format assignments with enums: {align,flush-left,preserve,infer}); default: infer; --expand_coverpoints (If true, always expand coverpoints.); default: false; --formal_parameters_alignment (Format formal parameters: {align,flush-left,preserve,infer}); default: infer; --formal_parameters_indentation (Indent formal parameters: {indent,wrap}); default: wrap; --module_net_variable_alignment (Format net/variable declarations: {align,flush-left,preserve,infer}); default: infer; --named_parameter_alignment (Format named actual parameters: {align,flush-left,preserve,infer}); default: infer; --named_parameter_indentation (Indent named parameter assignments: {indent,wrap}); default: wrap; --named_port_alignment (Format named port connections: {align,flush-left,preserve,infer}); default: infer; --named_port_indentation (Indent named port connections: {indent,wrap}); default: wrap; --port_declarations_alignment (Format port declarations: {align,flush-left,preserve,infer}); default: infer; --port_declarations_indentation (Indent port declarations: {indent,wrap}); default: wrap; --port_declarations_right_align_packed_dimensions (If true, packed dimensions in contexts with enabled alignment are aligned to the right.); default: false; --port_declarations_right_align_unpacked_dimensions (If true, unpacked dimensions in contexts with enabled alignment are aligned to the right.); default: false; --struct_union_members_alignment (Format struct/union members: {align,flush-left,preserve,infer}); default: infer; --try_wrap_long_lines (If true, let the formatter attempt to optimize line wrapping decisions where wrapping is needed, else leave them unformatted. This is a short-term measure to reduce risk-of-harm.); default: false;
整列(alignment)の指定には4種類あり、
align
: 整える、の意味合い通りブロック内で基準となる記号に合わせて空白を挿入します。flush-left
: 左側へ詰めるように空白を1つにまとめます。preserve
: 入力のままとし変更しません。infer
: 暗示する、という意味合い通り入力を解析してalign
かflush-left
かどちらかを適用するようです。
デフォルトの状態である --assignment_statement_alignment=infer
のとき、
assign local_short = in_0; assign local_long = in_1; assign local_long_long = in_2;
となる場合、 --assignment_statement_alignment=align
とすると、
assign local_short = in_0; assign local_long = in_1; assign local_long_long = in_2;
のように等号の位置が揃うようになります。
逆に、上記のように等号の位置を揃えたとき、--assignment_statement_alignment=flush-left
を指定すると、等号前のすべての空白が1つにまとめられもう1つ前のコードブロックのようになります。
---xxx_alignment
前述の通り基準となる文字に対してどのように整列するかを指定します。
infer
だとフォーマットルールに解釈の幅が生じそうですので、複数人で作業する場合はalign
、flush-left
、preserve
のいずれかを設定した方が無難です。
--xxx_indentation
インデントの付与のルールをラップの個数かインデントの個数のどちらへ従うかを指定できます。 デフォルトではラップ(デフォルトで4つのスペース)であり、インスタンス宣言であれば、
bar #( .PARAM0 (PARAM0), .PARAM1 (PARAM1), .PARAMSTR(PARAMSTR) ) bar ( .clk(clk), .input_0(in_0), .rst_n, .out_1, .* );
のようにフォーマットされます。
一方で、--named_port_indentation=indent
を指定すると、
bar #( .PARAM0 (PARAM0), .PARAM1 (PARAM1), .PARAMSTR(PARAMSTR) ) bar ( .clk(clk), .input_0(in_0), .rst_n, .out_1, .* );
のようにポートマップのみインデントのルール(デフォルトで2つのスペース)が適用されます4。
--try_wrap_long_lines
このオプションをtrueとすると長い記述を複数行へラップします。
たとえば、デフォルトではfalseなので長い式は
always_comb begin local_0 = local_1 + local_2; if (some_bool) begin out_0 = local_1; if (anoter_bool) begin out_0 = local_1; if (last_bool) begin out_0 = local_1 + local_1 + local_1 + local_1 + local_1 + local_1 + local_1 + local_1 + local_1; end end end end
のようにそのままとなります。
--try_wrap_long_lines=true
を指定すると、
always_comb begin local_0 = local_1 + local_2; if (some_bool) begin out_0 = local_1; if (anoter_bool) begin out_0 = local_1; if (last_bool) begin out_0 = local_1 + local_1 + local_1 + local_1 + local_1 + local_1 + local_1 + local_1 + local_1; end end end end
のようにラップ(デフォルトで4つのスペース)として指定した文字数で折り返されます5。
verilog format
フォーマッタというツールとしてのオプションは以下の通りです。
Flags from verilog/tools/formatter/verilog_format.cc: --failsafe_success (If true, always exit with 0 status, even if there were input errors or internal errors. In all error conditions, the original text is always preserved. This is useful in deploying services where fail-safe behaviors should be considered a success.); default: true; --inplace (If true, overwrite the input file on successful conditions.); default: false; --lines (Specific lines to format, 1-based, comma-separated, inclusive N-M ranges, N is short for N-N. By default, left unspecified, all lines are enabled for formatting. (repeatable, cumulative)); default: ; --max_search_states (Limits the number of search states explored during line wrap optimization.); default: 100000; --show_equally_optimal_wrappings (If true, print when multiple optimal solutions are found (stderr), but continue to operate normally.); default: false; --show_inter_token_info (If true, along with show_token_partition_tree, include inter-token information such as spacing and break penalties.); default: false; --show_largest_token_partitions (If > 0, print token partitioning and then exit without formatting output.); default: 0; --show_token_partition_tree (If true, print diagnostics after token partitioning and then exit without formatting output.); default: false; --stdin_name (When using '-' to read from stdin, this gives an alternate name for diagnostic purposes. Otherwise this is ignored.); default: "<stdin>"; --verbose (Be more verbose.); default: false; --verify_convergence (If true, and not incrementally formatting with --lines, verify that re-formatting the formatted output yields no further changes, i.e. formatting is convergent.); default: true;
重要な --failsafe_success
と --inplace
について解説します。
--failsafe_success
このオプションがtrueのときは終了ステータス ($?) が常に0となります。 そのため、文法エラーでフォーマッタが失敗したときでも正常とみなされます。
もしフォーマッタがエラーとなったことを取得したい場合はfalse
を指定することとなります。
--inplace
このオプションがtrueのとき、フォーマッタが正常に実行できたときファイルを実行内容で上書きします。 git-hooksで実行するときはこのオプションをtrueにすると良いです。
付録: Dockerによる環境構築
veribleをビルドするのはbazelが必要になるため、バイナリを直接インストールする方法がもっとも簡単です。 ここではWindowsやMacでの開発、およびクラウドコンピューター上でCIへ組み込むことを想定し、Dockerを使った環境の用意の方法を紹介しておきます。
Dockerfile
FROM ubuntu:focal RUN apt-get update &&\ apt-get install -y wget curl jq &&\ rm -rf /var/lib/apt/lists/* # リリースをAPIから取得し、Ubuntu focal 用にビルドされた最新版の URL を取得してダウンロード RUN wget -qO- \ $(curl -s https://api.github.com/repos/chipsalliance/verible/releases |\ jq -r '.[0].assets[] |\ select(.name |\ test("focal-x86_64.tar.gz")) |\ .browser_download_url ') |\ tar xvz -C /tmp # 解凍されたビルド済みのバイナリを移動 RUN mv $(find /tmp -type f | grep "verible" | grep "/bin/") /usr/local/bin
使い方
上記のDockerfileをビルドし、verible:latest
とタグを付与したとして、以下のコマンドを実行します。
docker run --rm -it -v <作業ディレクトリ>:/work -w /work \ verible:latest verible-verilog-format <オプション> <対象ファイル> ...
たとえば、カレントディレクトリに対してtb/testbench.sv
とsrc/fifo.sv
に対してオプションなしでフォーマットしようとしたとき、
docker run --rm -it -v $(pwd):/work -w /work \ verible:latest verible-verilog-format tb/testbench.sv src/fifo.sv
と実行することになります。 なお、実行環境によって作業ディレクトリの指定方法は異なりますので適宜読み替えてください。
-
一応国内のコンソーシアムであるSTARCのスタイルガイドがありますが、STARCはすでに解散しているためスタイルガイドも更新されていません。もちろん、 HDLがその間に大きく進化したかといわれるとまったくそんなことはないですが。↩
-
2つで十分ですよ。↩
-
もしParameterもインデントのルールを適用したい場合は
--named_parameter_indentation=indent
とすれば可能です。↩ -
ヘルプにも記載がありますが、このオプションは「短期的な危害のリスクを減らす」とある通り、可読性は向上するものの本質的な複雑さの解消には至りませんので注意が必要です。消費リソースの削減のためにもリファクタリングが必要です。↩