ゼロ除算の罠:PL/I `ON ZERODIVIDE` が救う基幹システムの命運と、モダン移行へのロードマップ
レガシーシステムの深部、夜間バッチがうなりを上げるメインフレームの只中で、突発的なシステム停止ほどエンジニアの血圧を上げるものはない。特に、金融や流通の心臓部で突如発生する `S0CB` アベンド(プログラムチェック例外:ゼロ除算)。原因を辿れば、マスタデータの欠損や、前日までの累計処理における変数の初期化漏れという、実に古典的で泥臭いミスに行き着く。
JavaやC#といったモダン言語の世界であれば、ゼロ除算は `ArithmeticException` として安全にキャッチされ、ログを残してトランザクションをロールバックするか、デフォルト値を丸めて処理を継続させることができる。では、IBMメインフレームの重鎮であるPL/Iはどうだろうか。
PL/Iには、C言語の `if (b == 0)` のような安易な事前チェックに頼らずとも、ハードウェアレベルの例外を優雅に捕捉し、処理の継続や的確なエラーハンドリングを行うための強力な機構が備わっている。それが `ON ZERODIVIDE` 条件である。
今回は、この `ON ZERODIVIDE` の実務的な制御手法を掘り下げるとともに、ベース変数とポインタを駆使した動的メモリ上の数値演算、コンパイラ最適化の裏側、さらには将来的なオープン系(Java等)へのマイグレーションを見据えたアーキテクチャ設計の要諦を、現場の知見を交えて解説する。
—
1. `ON ZERODIVIDE` のメカニズムと実務的コーディングパターン
PL/Iの例外処理(条件処理)は、他の言語の例外機構とは一線を画す。あらかじめ `ON` 文でトラップを張っておくことで、ハードウェアが発出した割込みをOS経由でキャッチし、定義された「ONユニット(処理ブロック)」へ制御をジャンプさせる。
もしこれを怠れば、システムは容赦なく `S0CB`(あるいは浮動小数点演算であれば `S0C7` や `S0C1` の親戚筋)で異常終了し、夜間バッチのチェーン全体を巻き込んで盛大に爆発する。
以下に、実務のバッチプログラムで使える、安全かつ堅牢な `ON ZERODIVIDE` のコーディングパターンを示す。
1
/ —————————————————————- /
/ プログラム名: FIN0010 – 月次金利・手数料計算バッチ /
/ 概要: ゼロ除算発生時の動的リカバリとログ出力のサンプル /
/ —————————————————————- /
FIN0010: PROC OPTIONS(MAIN);
DCL W_TOTAL_AMT DEC FIXED(15,2) INIT(1000000.00);
DCL W_DIVISOR DEC FIXED(5,2) INIT(0.00); / ここで意図せぬゼロ /
DCL W_RESULT DEC FIXED(15,2);
DCL W_ERROR_CNT FIXED BIN(31) INIT(0);
/ ゼロ除算条件の有効化(スコープの設定) /
ON ZERODIVIDE BEGIN;
W_ERROR_CNT = W_ERROR_CNT + 1;
DISPLAY(‘【警告】ゼロ除算を検知しました。代替値で処理を続行します。’);
DISPLAY(‘ 発生時点の割愛変数: ‘ || W_DIVISOR);
/ 例外発生時のフォールバック値(安全なデフォルト)を代入 /
W_RESULT = 0.00;
/
- 注意: GOTOを使わない場合、ONユニットの終端に達すると、
- 例外発生命令の「次の命令」へ処理が戻る(RESUME動作)。
/}
/ 実際の割り算処理(マスタデータ読込ループを想定) /
DISPLAY(‘— 演算処理開始 —‘);
/ 危険な割算の実行 /
W_RESULT = W_TOTAL_AMT / W_DIVISOR;
DISPLAY(‘演算結果 (W_RESULT): ‘ || W_RESULT);
DISPLAY(‘エラー発生回数 : ‘ || W_ERROR_CNT);
DISPLAY(‘— 異常なく正常終了ルートを通過 —‘);
RETURN;
END FIN0010;
このコードのアーキテクチャ的ポイント
1. 暗黙のレジューム(RESUME): PL/Iの `ON` ユニット内で `GOTO` を使わずにブロックを抜けた場合、制御は例外を引き起こした割算命令の「直後の命令」に戻る。これにより、バッチ全体をアベンドさせずに、該当レコードをスキップあるいはゼロ補正してループを継続させることが可能になる。
2. パックデシマル(`DEC FIXED`)の挙動: メインフレーム特有のPacked Decimal演算において、分母が `+0` なのか `-0` なのか、あるいはスペース(未初期化データ)に起因するコンバージョンエラーなのかによって、捕捉すべき条件が変わる点には細心の注意が必要である。
—
2. 高度なメモリ操作とエッジケース(ポインタ・動的ストレージ)
基幹システムのチューニング現場では、巨大なテーブルや動的に構造が変わる電文データを扱うために、`BASED` 変数と `POINTER` を駆使したメモリ直接操作が多用される。ここでゼロ除算が絡むと、障害解析の難易度は跳ね上がる。
例えば、以下のように動的に割り当てられたストレージ内のパックデシマル項目に対して演算を行うケースを考えてほしい。
1
Dcl P_REC_PTR Pointer;
Dcl 1 D_REC Based(P_REC_PTR),
3 D_ID Char(5),
3 D_RATE Dec Fixed(5,4);
/ 領域の動的獲得 /
Allocate D_REC Set(P_REC_PTR);
/
- 外部ファイルやDB2からの読み込み不良で、
- D_RATE の領域に不当なゾーン/パック文字が混入していたり、
- 完全にゼロクリアされている状態で割り算の分母に使われた場合…
/
このようなケースでは、単なる `ZERODIVIDE` だけでなく、データ自体の正当性を担保する `CONVERSION` 条件も同時にトラップしなければならない。メモリ上の生データが不正な符号(例えば `0xF9` 以外のゾーンブルース等)を持っている場合、算術演算の瞬間にハードウェアは `S0C7`(データ例外)を吐く。`ZERODIVIDE` の手前で `CONVERSION` が発火することを忘れてはならない。
—
3. コンパイラ最適化とオプティマイザの罠
IBM Enterprise PL/Iコンパイラを使用する際、`OPTIMIZE(2)` や `OPTIMIZE(3)` といった高レベルの最適化をかけると、コンパイラはコードの効率化のために命令の順序を入れ替えたり、冗長な演算をインライン展開したりする。
ここで問題になるのが、「例外発生時の正確なレジスタ状態の保証」である。
高度に最適化されたコードブロック内では、`ON ZERODIVIDE` ユニットに制御が渡った時点において、どのレジスタにどの変数の値が入っているか(あるいはすでにレジスタ上で定数畳み込みが行われているか)が、ソースコード上の見た目と一致しないことがある。
- 対策: 金融計算や絶対にデータの整合性を落としたくないクリティカルな演算モジュールでは、該当箇所周辺に対して一時的に `OPTIMIZE(0)` を指定するか、あるいはコンパイラオプションで `STGIM(INIT)` を指定し、未初期化変数を確実に検知できるようにビルドパイプラインを構築することが、シニアアーキテクトとしての必須の作法となる。
—
4. 埋め込みSQL(DB2)およびCICS環境における特殊事情
バッチ処理(Batch)であれば `ON ZERODIVIDE` で優雅にトラップしてログを吐き出せば済む話だが、これが CICSオンライン や DB2のストアドプロシージャ・ホスト変数 が絡む世界になると、話は全く違ってくる。
- CICS環境: オンライン画面からの入力値がゼロであった場合、単にバッチのように `DISPLAY` を叩いて処理を続行することは許されない。CICSのタスク内で未処理の例外や意図しないアベンドが発生すると、トランザクション全体がバックアウトされ、最悪の場合はAICA(無限ループ)やASRA(プログラムチェック)により、CICSリージョン全体の保全性に影響を与えかねない。CICSではPL/Iの `ON` ユニット内で `EXEC CICS ABEND` を発行するか、あるいはあらかじめアプリケーション側で厳格な入力バリデーション(範囲チェック)を行うべきである。
- DB2環境: SQL内で `COL_A / COL_B` のような割り算を行う場合、これはPL/I側の `ON ZERODIVIDE` のスコープ外であり、DB2エンジン側(SQLCODE = -802: 算術例外)として返却される。PL/I側で捕捉できるのは、あらかじめDB2からフェッチした後のホスト変数同士をローカルメモリ上で演算する区間のみである。この「SQL内部でのゼロ除算」と「PL/Iローカル変数でのゼロ除算」の境界線を曖昧にしている設計書が非常に多い点に、現場のアーキテクトは目を光らせる必要がある。
—
5. モダナイゼーション(Java/C#移行)への架け橋
いま、多くの企業がメインフレームからの脱却、すなわち「レガシーマイグレーション」の岐路に立っている。PL/Iで書かれた何百万ステップもの基幹ロジックを、Java(Spring Boot)やC#.NETへとリライト、あるいは自動変換するプロジェクトにおいて、この `ON ZERODIVIDE` の挙動をどうモダン環境にマッピングするかは一大テーマである。
自動翻訳ツール(トランスレータ)は、単純な構文を置き換えることは得意だが、PL/I特有の「動的な条件ハンドラ(`ON` ユニットのスコープ伝播)」や「レジューム動作」を完全に忠実にJavaへ変換することは極めて困難である。
移行設計における推奨アプローチ
1. 例外ハンドリングの明示化:
Java移行時は、暗黙の `ON` ユニットによるジャンプを避け、共通の算術演算ラッパーユーティリティ(例: `MathUtils.safeDivide()`)に置き換える。
// Java移行後のリファレンス実装イメージ
public static BigDecimal safeDivide(BigDecimal numerator, BigDecimal denominator, BigDecimal defaultVal) {
if (denominator == null || denominator.compareTo(BigDecimal.ZERO) == 0) {
// PL/IのON ZERODIVIDEによるフォールバック動作を模倣
LogUtils.warn(“ゼロ除算検知。代替値を適用します。”);
return defaultVal;
}
return numerator.divide(denominator, 2, RoundingMode.HALF_UP);
}
2. データの事前検証(Fail-Fast原則の適用):
レガシー特有の「エラーをキャッチして何食わぬ顔で処理を継続する」という設計は、オープン系のマイクロサービスアーキテクチャにおいては「データのサイレント破損(不整合)」の温床になりやすい。移行を機に、ゼロ除算が発生するような不正なマスタデータやトランザクションは、前段のバッチバリデーション層で確実に弾く(Fail-Fast)設計へとシフトさせるべきである。
—
結びにかえて
PL/Iの `ON ZERODIVIDE` は、単なるエラー逃れのテクニックではない。ハードウェアの脈動を直接感じ取り、システムを絶対に止めないというメインフレーム全盛期のエンジニアリングの粋が詰まった防壁である。
その思想の背景にある「例外の制御とデータ保全の哲学」を深く理解しているアーキテクトだけが、メインフレームの保守から、次世代の堅牢なクラウドネイティブアーキテクチャへの安全な橋渡しを成し遂げることができる。
コードの奥底に潜む「ゼロ」の魔力に怯えるのではなく、コンパイラの挙動とメモリの動きを掌中に収め、システム全体の信頼性をデザインし続けよう。
