ゼロ除算の罠:なぜPL/Iの `ON ZERODIVIDE` は甘美な毒なのか
メインフレームの現場で長年システムを支えてきたシニアアーキテクトなら、深夜のバッチ処理で突如発生する `S806` や `SOC7`、そして最も冷や汗をかく算術例外(Arithmetic Exception)のトラウマを一度や二度は経験しているはずだ。JavaやC#といったモダン言語の世界では、ゼロ除算(Division by Zero)は `ArithmeticException` としてスローされ、try-catchで安全にハンドリングされるか、あるいは単にプロセスがクラッシュして終わる。
しかし、PL/I(Programming Language One)の世界は違う。
PL/Iには、C言語のような「予約語」という概念が希薄であり、コンテキストによって識別子がキーワードにも変数名にも化ける極めて柔軟、裏を返せば極めてトリッキーな構文規則を持っている。そして、この言語が持つ例外処理機構 `ON条件(Condition Prefix / ON-unit)` は、適切に扱えば無類の信頼性を発揮するが、一歩誤ればデバッグ地獄への片道切符となる。
今回は、基幹システムのテックリードや、Java/C#等へのマイグレーション(レガシー移行)を控えたアーキテクトに向けて、`ON ZERODIVIDE` の深淵と、実務で直面するエッジケースの回避策について徹底的に解説しよう。
—
1. `ON ZERODIVIDE` の基本挙動と「予約語を持たない」構文の罠
PL/Iの最大の特徴の一つは、多くのプログラミング言語にあるような「厳格な予約語(Reserved Words)」をほとんど持たない点にある。例えば、`IF` や `DO` でさえも、コンテキストによっては単なる変数名として宣言して使うことができてしまう(非推奨ではあるが)。
この柔軟性は、例外処理である `ON` ユニットの記述においても発揮される。しかし、この「何でもあり」の仕様が、コンパイラ最適化やマイグレーション時に牙を剥く。
以下のコードを見てほしい。実務でよく見かける、ゼロ除算をトラップして処理を継続させるための典型的なパターンである。
;&————————————————————–
- ゼロ除算トラップの基本実装例
—————————————————————-
DEMO_DIV: PROC OPTIONS(MAIN);
DCL TOTAL_AMT FIXED DEC(11,2) INIT(1000.00);
DCL ITEM_CNT FIXED DEC(5,0) INIT(0); ここがゼロ
DCL UNIT_PRICE FIXED DEC(9,2);
DCL ERROR_FLG BIT(1) INIT(‘0’B);
- ZERODIVIDE 条件の捕捉定義
ON ZERODIVIDE
BEGIN;
DISPLAY(‘【警告】ゼロ除算を検知しました。デフォルト値を適用します。’);
ERROR_FLG = ‘1’B;
UNIT_PRICE = 0.00;
- 処理を安全に続行させるためのGOTO(例外発生元への復帰)
GOTO RESUME_POINT;
END;
DISPLAY(‘— 演算処理開始 —‘);
- 割算実行(ここでZERODIVIDEが発生する)
UNIT_PRICE = TOTAL_AMT / ITEM_CNT;
RESUME_POINT:
IF ERROR_FLG THEN
DISPLAY(‘リカバリ処理を経て正常終了ルートへ移行します。’);
ELSE
DISPLAY(‘計算結果: ‘ || CHAR(UNIT_PRICE));
END;
DISPLAY(‘— 演算処理終了 —‘);
END DEMO_DIV;
一見すると、例外を綺麗に捕捉して `GOTO` で処理を復帰させている優良なコードに見える。しかし、ここにメインフレーム特有の「コンパイラ最適化」と「内部データ表現」の罠が潜んでいる。
—
2. コンパイラ最適化と `ON` ユニットの隠れたコスト
IBM Enterprise PL/I コンパイラでは、高レベルの最適化(`OPTIMIZE(2)` や `OPTIMIZE(FULL)`)を有効にすると、変数のレジスタ割り当てやコードの並び替えがアグレッシブに行われる。
ここで問題になるのが、「どの時点で例外が発生したか」の正確な特定である。
`ON ZERODIVIDE` が発動した際、コントロールは `BEGIN-END` ブロックにジャンプする。しかし、最適化によって機械語命令レベルで演算が前倒しされたり、ループ展開(Loop Unrolling)されたりしていると、ダンプリスト(SYSUDUMP / CEEDUMP)を覗いても「どのレコードの、どのフィールドの割り算でゼロになったのか」を特定することが極めて困難になる。
テックリードが押さえるべき対策:
1. ストリートファイター的な `GOTO` 復帰の排除
`ON` ユニット内からの `GOTO` による外部ラベルへの脱出は、非構造化プログラミングを招くだけでなく、スタックフレームの整合性を崩し、CICS環境下ではストレージリークやタスク異常終了の原因となる。
2. ビルトイン関数 `ONCODE` と `ONSOURCE` の活用
例外ハンドラ内では、必ず `ONCODE()` を評価し、エラーの正確な原因コードをログに記録すべきである。ZERODIVIDEの場合は通常 `IBM0141` などのシグナルが飛ぶが、コンパイラやLE(Language Environment)のバージョンによって挙動が異なるため、検証が不可欠である。
—
3. エッジケース:パックデシマル(FIXED DECIMAL)の内部符号反転バグ
基幹システムのデータは、その多くが `FIXED DECIMAL`(Packed Decimal / ゾーン10進数)で保持されている。
ここで恐ろしいのが、外部ファイルやDB2(Embedded SQL)から読み込んだデータが、何らかの理由で「破損した符号(Invalid Sign)」を持っているケースである。
例えば、マスターデータの件数項目のゾーン部分や符号部分が化けた状態で読み込まれ、パッと見は `0`(ゼロ)に見えるが、内部のビットパターンとしては正規のゼロ(`C0` や `F0` など)ではなく、算術演算時にハードウェア例外(あるいはソフトウェアシミュレーションによる例外)を引き起こすケースがある。
このとき、単純な `ON ZERODIVIDE` だけでは不十分だ。ゼロ除算だけでなく、データ例外(ON CONVERSION / `S0C7`) が同時に発生する可能性が高いためである。
- データ例外とゼロ除算の二重防衛ネット
ON CONVERSION
BEGIN;
DISPLAY(‘【致命的エラー】数値データに無効な文字・符号が含まれています。’);
- ダンプ採取と異常終了への誘導
SIGNAL ERROR;
END;
ON ZERODIVIDE
BEGIN;
DISPLAY(‘【警告】分母がゼロです。スキップ処理を行います。’);
GOTO NEXT_RECORD;
END;
実務の現場では、例外が発生してから `ON` ユニットで受けるのではなく、演算の直前で明示的にガード節(Guard Condition)を設けるのが、最も堅牢なシステムアーキテクチャである。
—
4. 埋め込みSQL(DB2)およびCICSオンラインにおけるエッジケース
バッチ処理であればまだ `ON` ユニットで粘ることもできるが、これが CICSオンライン処理 や DB2のカーソル処理(ホスト変数を用いた演算) になると話は別だ。
CICS環境での注意点
CICS配下のPL/Iプログラムで未処理の算術例外が発生した場合、タスク全体がアベン(ASRA / AEY9 等)となり、最悪の場合はトランザクションがロールバックされ、TSQやTDQの整合性が崩れる。
さらに、CICSのタスク共用領域やポインタを用いた動的ストレージ操作(`ALLOCATE` / `FREE`)を行っている最中に `ON` ユニットから強制 `GOTO` で脱出すると、ヒープ管理の制御ブロックが破壊され、CICS領域全体を巻き込む大障害(CICSのフリーズや強制シャットダウン)に発展する。
DB2(埋め込みSQL)との連携
SQL文内で `SUM()` や `AVG()` を実行し、分母がゼロになるケースをSQL側(`NULLIF` や `CASE` 式)でハンドリングせず、フェッチしたホスト変数同士をPL/I側で割り算している設計を時々見かける。これは地雷原を裸足で歩くようなものである。
;&————————————————————–
- 推奨されるガード節による事前チェック(CICS/DB2共通)
—————————————————————-
IF ITEM_CNT = 0 OR ITEM_CNT = ” THEN
- 分母がゼロ、または空白(スペースパディングされた数値)の防御
UNIT_PRICE = 0;
- ログ出力またはエラーカウンターインクリメント
CALL LOG_ZERO_DIV_WARNING();
ELSE
UNIT_PRICE = TOTAL_AMT / ITEM_CNT;
END;
プログラミングの鉄則として、「例外処理機構(ONユニット)は、予期せぬハードウェア障害やシステム全体の最終防衛ラインとしてのみ使用し、ビジネスロジック上の分岐(ゼロ除算の回避など)には絶対に使わない」 という原則をチーム全体に徹底すべきである。
—
5. Java / C# へのマイグレーション(レガシー移行)における設計指針
もし、現在動いているこのPL/IレガシーシステムをJavaやC#へマイグレーションするプロジェクトを率いているなら、この `ON ZERODIVIDE` の挙動の違いは最大の設計難所の一つになる。
1. 例外の型と伝播の差異
- PL/Iの `ON` ユニットは、動的スコープ(Dynamic Scoping)に似たブロック構造の例外捕捉を行う。
- Java/C#の `try-catch` は静的スコープ(Static Scoping)であるため、PL/Iの複雑な `ON` ユニットの構造をそのまま1:1で自動変換ツールにかけようとすると、スパゲッティのような例外処理コードが生成される。
2. FIXED DECIMALの丸め誤差とゼロ除算の仕様
- Javaの `BigDecimal` でゼロ除算を行った場合、`java.lang.ArithmeticException: / by zero` が即座にスローされる。
- マイグレーション後のコードでは、全ての割り算の箇所にプレコンパイラや共通ユーティリティ(例: `SafeMath.divide(a, b, scale)`)を挟み込むリファクタリング設計が必須となる。自動変換ツール任せにすると、本番稼働後に「バッチが突如として例外で落ちる」という致命的なregression(退行バグ)を引き起こす。
—
結びにかえて:システムアーキテクトとしての矜持
PL/Iという言語は、コンパイラの仕様やOS(z/OS)のアーキテクチャと深く結合しているが故に、書き手の技量がコードの生死をダイレクトに分ける。
`ON ZERODIVIDE` は、その強力さゆえにプログラマの怠惰な設計を許容してしまう「甘美な毒」である。
テックリードとして現場を率いるあなたには、単に「エラーをトラップして止まらないようにする」のではなく、「なぜゼロが発生したのか」「データの根源的矛盾はどこにあるのか」を見極め、データガバナンスとコードの堅牢性を両立させる設計を貫いてほしい。
メインフレームの巨匠たちが遺したこの深い言語仕様の海を泳ぎ切ることは、現代のモダンな分散系開発においても、必ずやあなたを最高峰のシステムアーキテクトへと押し上げる最大の武器となるはずだ。
