Very Hard Delight Life

内容はLinux, HW, プログラミング, HaFaBra.

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: 暗示する、という意味合い通り入力を解析して alignflush-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 だとフォーマットルールに解釈の幅が生じそうですので、複数人で作業する場合はalignflush-leftpreserveのいずれかを設定した方が無難です。

--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が必要になるため、バイナリを直接インストールする方法がもっとも簡単です。 ここではWindowsMacでの開発、およびクラウドコンピューター上で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.svsrc/fifo.svに対してオプションなしでフォーマットしようとしたとき、

docker run --rm -it -v $(pwd):/work -w /work \
    verible:latest verible-verilog-format tb/testbench.sv src/fifo.sv

と実行することになります。 なお、実行環境によって作業ディレクトリの指定方法は異なりますので適宜読み替えてください。


  1. 一応国内のコンソーシアムであるSTARCのスタイルガイドがありますが、STARCはすでに解散しているためスタイルガイドも更新されていません。もちろん、 HDLがその間に大きく進化したかといわれるとまったくそんなことはないですが。

  2. Pythonであれば black が有名ですし、Go言語であれば gofmt が開発環境に含まれています。

  3. 2つで十分ですよ。

  4. もしParameterもインデントのルールを適用したい場合は--named_parameter_indentation=indentとすれば可能です。

  5. ヘルプにも記載がありますが、このオプションは「短期的な危害のリスクを減らす」とある通り、可読性は向上するものの本質的な複雑さの解消には至りませんので注意が必要です。消費リソースの削減のためにもリファクタリングが必要です。