はじめに:ゼロ除算とPL/Iの美学
メインフレームの基幹システムを長く支えてきたPL/Iという言語には、CやJavaのような現代の言語とは一線を画す、独特の「懐の深さ」と、同時に「牙を剥く仕様」が同居しています。その最たるものが、例外処理機構であるONユニット(ON-units)です。
Javaであれば `ArithmeticException` がスローされ、Cであればシグナル捕捉やハードウェア割り込みに怯えるところですが、PL/Iは「例外が起きても、処理の流れを断絶させずにコンテキストを維持してリカバリする」という極めて高度な抽象化を、言語仕様のコアとして標準装備していました。
今回は、その中でも夜間バッチの勘定系などで突如として牙をむく「ゼロ除算(ZERODIVIDE)」のトラップとリカバリを取り上げます。単なる基本文法の解説にとどまらず、コンパイラ最適化の罠、パックデシマルの内部構造、CICSやDB2が絡むエッジケース、そしてJava/C#へのマイグレーション設計まで、現場のテックリードが知るべきすべてを紐解いていきましょう。
—
PL/Iの割り込み処理(ON-unit)の本質
PL/Iの `ON ZERODIVIDE` は、単なるエラーキャッチの `TRY-CATCH` ではありません。これは「同期シグナルに対する動的な文脈のすり替え」です。
ハードウェアレベル(IBM System zのプロセッサ)で浮動小数点数や固定小数点数のゼロ除算例外(Fixed-Point Divide Exception等)が発生すると、OSのレジスタ割り込みを経て、PL/Iのランタイム環境(LE: Language Environment)がそれを捕捉します。そして、プログラムが制御フローのどこにいようとも、該当する `ON ZERODIVIDE` ブロックへ制御をジャンプさせます。
ここで重要なのは、「例外を発生させた演算命令の直後のアドレスへ制御を戻せる(RESIU_ME文脈)」という点です。まずは、最も堅牢とされる標準的な実装パターンを見てみましょう。
実装コード例:安全なゼロ除算リカバリ
ZERODIVIDE_SAMPLE: PROC OPTIONS(MAIN);
DCL TOTAL_AMT DEC FIXED(11,2) INIT(1000.00);
DCL DIVISOR_CNT DEC FIXED(5,0) INIT(0); / ここがゼロ /
DCL AVG_AMT DEC FIXED(11,2);
DCL ERROR_FLAG BIT(1) INIT(‘0’B);
/ ZERODIVIDE例外に対するONユニットの定義 /
ON ZERODIVIDE
BEGIN;
DISPLAY(‘【警告】ゼロ除算を検知しました。デフォルト値を適用します。’);
/ デフォルト値としてゼロまたは安全な数値を代入 /
AVG_AMT = 0;
ERROR_FLAG = ‘1’B;
/ 異常終了を回避し、演算の次の処理へ安全に戻るためのGOTO /
/ 注: LE環境下ではゴシックなGOTO外跳びよりも、フラグ制御が推奨される /
END;
DISPLAY(‘— 処理開始 —‘);
/ 意図的なゼロ除算の発生(ここでON ZERODIVIDEが発動する) /
AVG_AMT = TOTAL_AMT / DIVISOR_CNT;
IF ERROR_FLAG THEN
DISPLAY(‘リカバリ処理を経て正常に続行されました。平均金額: ‘ || AVG_AMT);
ELSE
DISPLAY(‘通常計算完了。平均金額: ‘ || AVG_AMT);
DISPLAY(‘— 処理終了 —‘);
END ZERODIVIDE_SAMPLE;
このコードの肝は、例外が発生した瞬間にプログラムがアベンド(ABEND: S379やU4038など)せず、`BEGIN-END` ブロック内の処理を完遂して次の行へ進む点です。しかし、実際の夜間バッチでこれをそのまま動かすと、いくつかの「コンパイラの暗黙の挙動」に足元をすくわれることになります。
—
現場の罠:コンパイラ最適化とパックデシマルの影
現代のEnterprise PL/Iコンパイラ(IBM Compiler)は、非常に高度な最適化を行います。ここでアーキテクトが頭を悩ませるポイントが2つあります。
1. 最適化レベル(OPT)による命令の巻き戻し・消滅
コンパイラオプションで `OPT(2)` や `OPT(3)` を指定している場合、コンパイラは「この変数はどうせゼロで割られる運命にある」と静的解析で予見し、割り算の機械語命令そのものをインライン展開の過程で最適化(省力化・変形)してしまうことがあります。
結果として、`ON ZERODIVIDE` の捕捉タイミングが期待通りにならなかったり、例外そのものがスローされずにハードウェアの別例外に化けたりする現象が発生します。信頼性を最優先するモジュールでは、該当箇所周辺のコンパイルオプションを個別に `NOOPTIMIZE` に落とす、あるいは明示的な `IF` ガードを設ける防衛的プログラミングが不可欠です。
2. パックデシマル(DEC FIXED)の内部表現と符号反転バグ
メインフレームの真骨頂である `DEC FIXED`(COMP-3)は、BCD(Binary Coded Decimal)形式でメモリ上に保持されます。
例えば `DEC FIXED(5,0)` の領域が、外部ファイルからの不正なインプットや初期化漏れによって、ゾーン部が壊れているか、あるいは符号ニブル(最下位バイトの後半4ビット)が予期せぬ値(通常 `C`, `D`, `F` 等)になっている状態で算術演算を行うと、ハードウェア側でデシマル例外(Data Exception: S0C7)が先回りして発生します。
「ゼロ除算(ZERODIVIDE)」をトラップするつもりが、その手前で「S0C7アベンド」で容赦なく夜間バッチが墜落する――これはレガシー移行の現場で最も頻繁に見る悪夢のパターンです。`ON ZERODIVIDE` を過信せず、必ず演算前の `IF DIVISOR_CNT = 0 THEN` による事前チェック(防御的ガード)を併用すべき理由はここにあります。
—
エッジケース:CICSオンラインとDB2(埋め込みSQL)の交差点
バッチ処理であればまだONユニットによるリカバリでログを吐いて継続、というシナリオも描けますが、これがCICS(Customer Information Control System)オンライン画面や、DB2トランザクションの文脈になると話は全く別次元になります。
CICS環境での注意点
CICS配下で稼働するPL/Iプログラムにおいて、未処理の例外やONユニット内での異常終了は、タスクの異常終了(ASRAやAEY9など)を引き起こし、最悪の場合、CICS地域全体のトランザクション整合性を揺るがします。
CICS環境では、PL/IのONユニット内で独自に重い処理(ファイルI/Oや複雑なメッセージ出力)を行うべきではありません。例外を検知した場合は、速やかにロギングを行い、CICSの異常終了マクロや `EXEC CICS ABEND` を安全に発行して、リソースのロールバック(Syncpointの巻き戻し)を確実に実行させる設計が求められます。
DB2(埋め込みSQL)との干渉
算術演算の前に、DB2から取得した集計値(SUM関数などの結果)をホスト変数に受けて割る処理を想像してください。
EXEC SQL SELECT TOTAL_VAL, COUNT_VAL
INTO :H-TOTAL, :H-COUNT
FROM TABLE_X;
AVG_VAL = H-TOTAL / H-COUNT; / ここでH-COUNTが0の可能性 /
DB2の `COUNT` 関数は、行が存在しない場合は `0` を返しますが、SQL自体は正常終了(SQLCODE = 0)します。したがって、データベース側の特性を熟知していないと、「まさかゼロ除算が起きるとは思わなかった」という状況を生み出します。
さらに、ONユニット内でDB2のホスト変数を書き換えたり、リカバリ処理内でさらにSQLを発行したりする場合、PL/IのランタイムスタックとDB2のスレッドコンテキストの整合性に細心の注意を払う必要があります。
—
マイグレーション(Java / C#への近代化)への設計指針
現在、多くの企業がIBMメインフレームから、オープン系(Java / Spring Boot あるいは .NET)へのマイグレーションを進めています。この「PL/IのON ZERODIVIDE」という極めて特異な仕様を、モダン言語へどうトランスレーション(またはリライト)すべきでしょうか。
1. 自動変換ツールの限界
レガシーマイグレーションツール(コンバージョンベンダーのツール)は、`ON ZERODIVIDE` を検出すると、多くの場合、すべての割り算の前後を `try { … } catch (ArithmeticException e)` で囲むコードに機械的に置換します。
しかし、これではPL/Iが持っていた「演算命令のコンテキストへの正確な復帰(RECURSIVE/RESUME的挙動)」のニュアンスが完全に失われます。Javaの `try-catch` は例外が発生した時点でそのブロックを「脱出」してしまうため、PL/Iの元の制御フローとは挙動が乖離するリスクがあります。
2. アーキテクトが取るべき設計アプローチ
モダン言語へ移行する際は、単なる構文の直訳(リフト&シフト)ではなく、「ビジネスロジックの健全化」として再設計を行うべきです。
- 明示的なバリデーション層の確立:
PL/IのONユニットに頼るのではなく、ドメインモデルやサービスクラスに入る前段階、あるいは計算ユーティリティ(Calculatorクラス)の内部で、分母がゼロであるかどうかのバリデーションを完全義務化します。
- Optional型やSafe Mathの採用:
Javaであれば、割り算を行うユーティリティメソッドを定義し、分母がゼロの場合は `BigDecimal.ZERO` を返す、あるいは `Optional` で包むといった、例外に依存しない堅牢なエラーハンドリングモデルへと昇華させます。
// Javaでのモダンな安全除算の例
public static BigDecimal safeDivide(BigDecimal total, BigDecimal count) {
if (count == null || count.compareTo(BigDecimal.ZERO) == 0) {
// ログ出力を行い、安全なデフォルト値を返す
LoggerFactory.getLogger(“MathUtils”).warn(“ゼロ除算を検知しました。デフォルト値を適用します。”);
return BigDecimal.ZERO;
}
return total.divide(count, 2, RoundingMode.HALF_UP);
}
—
おわりに
PL/Iの `ON ZERODIVIDE` は、ハードウェアと密に結びついたメインフレーム時代の「泥臭くも美しい生命維持装置」です。この仕様の裏にあるランタイムの挙動やコンパイラの最適化、そしてデータベースやCICSとの関係性を深く理解しているか否かは、レガシーシステムの保守性、ひいては大規模マイグレーションの成否を分ける決定的なファクターとなります。
動的メモリ操作やポインタ、パックデシマルのバイナリ構造までを見通す「真のシステムアーキテクト」の視座を持って、日々のコードと、そして未来への移行プロジェクトに向き合っていきましょう。
