Claude Media
Claude CodeでFortranの保守を始める — 暗黙の型をgfortranで、COMMONをgrepで洗う

Claude CodeでFortranの保守を始める — 暗黙の型をgfortranで、COMMONをgrepで洗う

-Wallだけでは暗黙の型の打ち間違いは出ません。-fimplicit-noneで洗い出し、PostToolUse hookで編集のたびに警告を返し、出力比較で数値が動いていないことを確かめる手順です。

研究室や解析ソフトに残るFORTRAN 77流のコードは、暗黙の型とCOMMONブロックで変数が受け渡されていることが多く、Claude Codeに触らせる前に「どこが危ないか」を機械に洗わせておくと安全です。結論から言うと、gfortran -Wallだけでは暗黙の型の打ち間違いは1件も出ません。-fimplicit-noneを足して初めて見えます。

この記事は、gfortranの警告をPostToolUseフックで編集のたびにClaude Codeへ返し、implicit noneの追加、COMMONの一覧化、数値結果が変わっていないことの確認まで進める手順です。試した環境はGNU Fortran 13.2.0(Homebrew版GCC)です。

暗黙の型の打ち間違いは-Wallに出ない

まず、わざと誤りを入れた小さな固定形式のコードで試します。サブルーチンの中で、DTと書くべきところをDTTと打ち間違えています。

      PROGRAM CALC
      COMMON /PARAM/ N, DT, TOTAL
      DIMENSION X(10)
      N = 10
      DT = 0.1
      DO 10 I = 1, N
         X(I) = I*DT
   10 CONTINUE
      CALL SUMUP(X)
      PRINT *, 'TOTAL =', TOTAL
      END
 
      SUBROUTINE SUMUP(X)
      COMMON /PARAM/ M, DT, TOTAL
      DIMENSION X(10)
      TOTAL = 0.0
      DO 20 K = 1, M
         TOTAL = TOTAL + X(K)*DTT
   20 CONTINUE
      RETURN
      END

DTTはどこにも宣言されていませんが、Fortranは宣言のない名前を頭文字で型づけします(IからNは整数、それ以外は実数)。そのためコンパイラはエラーにしません。このコードを-fsyntax-only -Wallで通すと、結果は次のとおりです。

gfortran -std=legacy -fsyntax-only -Wall calc.f
# 出力なし(警告0件)

-Wallが有効にする警告は、公式ドキュメントでは-Waliasing、-Wconversion、-Wtabs、-Wunused、-Wundefined-varsなどと列挙されています。暗黙の型を咎めるものは含まれません。実行すると、DTTは未初期化のまま0として扱われ、合計は0になります。

gfortran -std=legacy -O0 calc.f -o calc_old && ./calc_old
#  TOTAL =   0.00000000

エラーも警告も出ずに、答えだけが間違うわけです。これが古い計算コードの怖いところです。

-fimplicit-noneは、すべての手続きの先頭にimplicit noneを足したのと同じ扱いにするオプションです。同じコードに付けると、宣言のない名前が一覧で出ます。

gfortran -std=legacy -fsyntax-only -fimplicit-none calc.f 2>&1 \
  | grep Error | sort | uniq -c
   2 Error: Symbol 'dt' at (1) has no IMPLICIT type
   1 Error: Symbol 'dtt' at (1) has no IMPLICIT type
   1 Error: Symbol 'i' at (1) has no IMPLICIT type
   1 Error: Symbol 'k' at (1) has no IMPLICIT type
   1 Error: Symbol 'm' at (1) has no IMPLICIT type
   1 Error: Symbol 'n' at (1) has no IMPLICIT type
   2 Error: Symbol 'total' at (1) has no IMPLICIT type
   2 Error: Symbol 'x' at (1) has no IMPLICIT type

一覧にdttが1回だけ現れ、他のどの名前とも対応しないことが分かります。本物の変数は複数の手続きに現れ、打ち間違いは1か所にしか現れません。この「1回だけ」の名前から調べると速く見つかります。

-std=legacyは、古い書式の拡張を許可するための指定です。付けないと、FORTRAN 77流の記法そのものが別の診断で埋もれることがあります。

呼び出し側の宣言漏れは別の警告で拾う

引数の型や数を検査できない呼び出しは、-Wimplicit-interfaceで出ます。「明示的なインターフェースなしで手続きを呼んだ」ことを警告するオプションで、-Wallには含まれません。

gfortran -std=legacy -fsyntax-only -Wimplicit-interface calc.f 2>&1 \
  | grep Warning
# Warning: Procedure 'sumup' called with an implicit interface at (1)

ドキュメントには、このオプションは明示的なインターフェースの有無しか見ず、プログラム単位間で宣言が整合しているかは検査しないと書かれています。古いコードでは全部の呼び出しに出るので、件数が多すぎて最初は使いにくいかもしれません。件数を数えて、モジュール化を進める際の進捗指標にする使い方が向きます。

COMMONの並びの食い違いは、サイズが同じだと警告されない

COMMONの危険は、同じ名前のブロックを手続きごとに別の並びで宣言できる点です。次の2つは、どちらも/PARAM/ですが、変数の並びが違います。

      SUBROUTINE A
      COMMON /PARAM/ N, DT
      INTEGER N
      REAL DT
      N = 1
      END
      SUBROUTINE B
      COMMON /PARAM/ DT, N
      REAL DT
      INTEGER N
      END

gfortran -std=legacy -Wall -cでこのファイルをコンパイルしても、警告は出ませんでした(GNU Fortran 13.2.0)。変数の並びや型が違っていても、合計サイズが同じなら診断は出なかったということです。この例のようにサイズが揃っていると診断に現れないので、食い違いの確認はコンパイラの診断だけに頼らず、一覧にして見るのが確実です。固定形式のソースでは、コメント行が1桁目から始まるので、行頭に空白があるCOMMON文を探せば足ります。

grep -n -i -E '^ +common' *.f
grep -i -h -E '^ +common' *.f | sed -E 's/^ +//' | sort | uniq -c

2本目のコマンドは、同じブロック名で宣言の文字列が異なるものを並べます。上の例なら、/PARAM/に3種類の宣言(N, DT, TOTAL、M, DT, TOTAL、DT, N)が並び、一見して揃っていないと分かります。名前が違うだけ(NとM)か、並びや型が違うのかは、一覧を見て人かClaude Codeが判断します。

Claude Codeへ頼むなら、次の形が安全です。

  • COMMONごとに「ブロック名、変数の並び、型、使っているファイルと手続き」の表を作らせる
  • ソースは書き換えさせない(読み取りだけ)
  • 並びが食い違うブロックを先頭に出させる
  • 表はdocs/common-blocks.mdのようなファイルに保存させ、以降の作業の前提にする

表ができれば、COMMONをMODULEへ置き換える作業にも、どの変数が実際に共有されているかという根拠が付きます。

PostToolUse hookで編集のたびに警告を返す

ここまでのコマンドを、Claude Codeがファイルを編集するたびに自動で走らせます。PostToolUseは、ツール呼び出しが成功した後に発火するイベントです。Prettierで自動整形する公式の例と同じく、Edit|Writeをマッチャーにします。

hookの出力がClaudeに届くかどうかは、終了コードで決まります。公式ドキュメントでは、終了コード0のときのstderrをClaudeは見ず、PostToolUseの警告をClaudeに見せるには終了コード2を使うと説明されています。PostToolUseは、ツールがすでに実行された後のイベントなので、ブロックはできません。stderrの内容がClaudeへ渡る、という動きです。

まず、検査用のシェルスクリプトを.claude/hooks/fortran-check.shとして置きます。

#!/bin/bash
f=$(jq -r '.tool_input.file_path')
case "$f" in
  *.f|*.for|*.f77|*.f90) ;;
  *) exit 0 ;;
esac
out=$(gfortran -std=legacy -fsyntax-only -fimplicit-none -Wall \
  -Wimplicit-interface "$f" 2>&1 | grep -E 'Error|Warning')
if [ -n "$out" ]; then
  echo "$out" | sort | uniq -c | head -30 >&2
  exit 2
fi

Fortran以外のファイルは終了コード0で素通りさせ、Fortranのファイルだけ検査します。警告が1件でもあれば、重複をまとめた上で30行までをstderrに出し、終了コード2で返します。jqで標準入力のJSONからtool_input.file_pathを取り出す書き方は、公式の整形フックと同じです。

.claude/settings.jsonには、次のように登録します。

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/fortran-check.sh"
          }
        ]
      }
    ]
  }
}

スクリプト単体の動作は、疑似的なJSONを標準入力に流して確かめられます。

chmod +x .claude/hooks/fortran-check.sh
echo '{"tool_input":{"file_path":"calc.f"}}' \
  | .claude/hooks/fortran-check.sh; echo "rc=$?"

上のcalc.fでは、先ほどの8種類のエラーとimplicit interfaceの警告が出てrc=2になりました。README.mdを渡すと何も出さずrc=0、implicit none済みのclean.f90もrc=0でした。実際にClaude Codeから動くかどうかは、claude --debugでログを見るか、セッション内の/hooksで登録を確かめます。

フックの設計で気をつけたい点が3つあります。

  • 暗黙の型だらけの既存ファイルでは、毎回エラーが大量に返ってきます。最初の1周は、まず警告が出ない状態にするのではなく、head -30で件数を絞ったまま「触った箇所の周辺だけ直す」運用にすると、会話が警告で埋まりません
  • -fsyntax-onlyなので、リンク時の問題や実行時の問題は出ません。出力比較は別に必要です
  • フックはツールの実行後に走るので、編集そのものは止められません。警告を踏まえて直させるのはClaude側の仕事です

hookが失敗した場合の扱いは、Claude Code hooksのonFailureで失敗時に操作を止める設定にまとめています。

数値結果が変わらないことを出力比較で確かめる

暗黙の型を直す作業で最も守りたいのは、「数値結果が変わらない」ことです。そのためには、触る前の実行結果を基準として保存しておきます。研究室のコードなら、論文や過去の解析で使った入力ファイルが残っているはずなので、それを使います。

mkdir -p ref
gfortran -std=legacy -O0 calc.f -o calc_old
./calc_old < input.dat > ref/calc_old.out

修正後に同じ入力で実行し、基準とdiffを取ります。

gfortran -std=legacy -O0 calc_new.f -o calc_new
./calc_new < input.dat > out/calc_new.out
diff ref/calc_old.out out/calc_new.out && echo same

浮動小数点の桁数が実行環境で揺れる場合は、diffで完全一致を求める代わりに、許容誤差つきの比較に切り替えます。その考え方は、Claude CodeでMATLABからPythonへ移植し、数値結果の一致を検算するの基準値と一致テストの組み立てがそのまま使えます。

ここで1つ注意があります。先ほどのDTTは、直すと数値結果が変わります。

sed 's/DTT/DT/' calc.f > calc_fix.f
gfortran -std=legacy -O0 calc_fix.f -o calc_fix && ./calc_fix
#  TOTAL =  0.550000012

修正前はTOTAL = 0、修正後は0.55です。これは意図した変更で、基準と一致しません。だからこそ、次の2種類の変更を同じコミットに混ぜない運用が必要です。

変更の種類例出力比較の期待
振る舞いを保つ変更例implicit noneの追加、宣言の追記、COMMONの一覧化出力比較の期待基準と完全一致(または許容誤差内)
結果を変えるバグ修正例DTTからDTへの打ち間違いの訂正出力比較の期待差が出るのが正しい。理由を記録する

implicit noneを足す作業は、前者に徹します。差が出たら、それが宣言を足したせいなのか、元からあった潜在バグが見えたのかを切り分けます。後者なら別のコミットにして、「なぜ値が変わるか」をコミットメッセージに残します。

実行時の添字範囲は-fcheck=allで見る

COMMON周辺では、配列の宣言サイズが手続きによって違い、範囲外の添字が静かにメモリを壊す事故も起きます。実行時の検査として-fcheck=allが使えます。公式ドキュメントでは、-fcheckのすべての実行時検査を有効にするキーワードがallで、boundsは配列の添字などが宣言された範囲に収まっているかの検査です。

gfortran -std=legacy -fcheck=all oob.f -o oob && ./oob

要素数5の配列に6番目を書き込む小さなコードで試すと、コンパイル時にも「Array reference ... out of bounds (6 > 5)」と警告が出ます。実行すると、次のように止まります。

At line 4 of file oob.f
Fortran runtime error: Index '6' of dimension 1 of array 'a' above upper bound of 5

この検査は実行を遅くします。基準の出力を作る通常のビルドとは別に、検査用のビルドを用意し、同じ入力で一度回して「検査が通る」ことを確認する使い方になります。検査で止まったら、そのコードは元から範囲外を触っており、最適化や環境次第で結果が変わる状態だったことになります。

Claude Codeに渡す指示

進め方をCLAUDE.mdに書いておくと、毎回の説明が要りません。例えば次のような断片です。

## Fortran保守のルール
- 作業前に ref/ の基準出力があることを確認する。無ければ先に作る
- 1回の変更では「振る舞いを保つ変更」と「結果が変わる修正」を混ぜない
- implicit none の追加は手続き単位で行い、追加後に宣言漏れを全て解消する
- COMMONの宣言は docs/common-blocks.md の表を正とし、勝手に並びを変えない
- 変更のたびにフックの警告と、基準との diff の結果を報告する

この指示で、Claude Codeは「宣言を足す→フックの警告が減る→diffで基準と一致を確認する」という反復を自分で回せます。警告の件数が減り続けることと、diffが空のままであることが、作業が正しく進んでいる証拠になります。

まとめ

古い計算コードを触る前に必要なのは、書き換えの技術より、安全な足場です。-Wallは暗黙の型の誤りを見せず、並びが違ってもサイズが同じCOMMONはコンパイラが警告しなかったので、-fimplicit-noneとgrepによる一覧化で補います。その上で警告をhookで返し、出力比較を基準にして、結果を保つ変更とバグ修正を分けてください。足場が先にあれば、Claude Codeの提案が本当に安全かを毎回自分の目で判断できます。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. code.claude.com/docs/en/hooks-guide
  2. code.claude.com/docs/en/hooks
  3. gcc.gnu.org/onlinedocs/gfortran/Fortran-Dialect-Options.html
  4. gcc.gnu.org/onlinedocs/gfortran/Error-and-Warning-Options.html
  5. gcc.gnu.org/onlinedocs/gfortran/Code-Gen-Options.html
この記事を共有:XはてブLinkedIn