おい、最近配属された若手の手塩にかけたプログラムが、夜間バッチのテストランで見事にS0C7…いや今回はS0C9(ゼロ除算)で派手に散ったらしいな。
「先輩、なんでここでゼロ除算なんて起きたんですか?ちゃんとIF文で弾いたはずなのに……」って青い顔して走ってきたが、まあ落ち着け。メインフレームの荒波にもまれりゃ、そんなもん日常茶飯事だ。
PL/Iという言語は、CやJavaみたいに「予約語」という概念を極力排した、懐の深い(というか油断ならない)怪物だ。変数名に `IF` や `THEN` すら使えてしまう自由度がある反面、ランタイムの挙動や例外処理(ONユニット)のメカニズムをハラに収めておかないと、本番稼働の真夜中に冷や汗をかくことになる。
今日は、このZERODIVIDE条件の発生メカニズムと、PL/Iが誇る強力なエラーハンドリング機構であるONCODEの識別について、現場のノウハウを交えて徹底的に叩き込んでやる。しっかりついてこいよ。
—
1. ZERODIVIDE条件の発生メカニズム:なぜその「ゼロ」は検知されなかったのか?
メインフレーム(z/Architecture)上で稼働するPL/Iプログラムにおいて、ゼロによる除算(割り算の分母が `0`、またはFIXED変数の場合はオーバーフローを引き起こす演算など)が発生すると、ハードウェアレベルでプログラム割込み(System Completion Code: S0C9)が発生する。
C言語やJavaに慣れた連中なら、「あぁ、ArithmeticExceptionね」で済むかもしれないが、PL/Iの世界ではこれがランタイム環境(Language Environment: LE)にフックされ、PL/Iの条件処理機構へと引き渡される。
ここで重要なのは、「計算するデータそのものがゼロだった場合」だけでなく、マイグレーションや他システムからの電文・VSAMレコード連携でよくある、「パック十進数(COMP-3 / FIXED DECIMAL)のアンパックミスや、空白・ゴミデータ(ゾーンエラー)が混入したまま演算に巻き込まれた場合」にも、予期せぬ例外として跳ね上がってくる点だ。
「いや、分母がゼロにならないようにチェックしました」?
甘いな。ファイルから読み込んだVSAMレコードの数値項目が、初期化不良でハイパッチ(X’00’やX’40’など)のまま演算式に放り込まれれば、コンパイラが生成した機械語命令(`DR` や `DS`、あるいはDECIMAL演算命令)は容赦なくハードウェア例外を叩き出す。これがメインフレームの現実だ。
—
2. ONユニットによる例外の捕捉とONCODEの識別
PL/Iの真骨頂は、このハードウェア・ソフトウェアの例外を、構造化されたプログラミング言語の構文内で優雅に(あるいは泥臭く確実に)トラップできるONユニット(ON-unit)にある。
例外が発生した際、システム任せで異常終了させるのではなく、`ON ZERODIVIDE` 文を使って処理を横取り(インターセプト)し、その場でリカバリしたり、詳細なログを残して安全に避難させることが可能だ。
ここで頼りになるのが、組み込み関数(BUILTIN)の `ONCODE` だ。
どの例外が発生したのかを数値コードで正確に教えてくれる。ZERODIVIDEに関連する主なONCODEを頭に叩き込んでおけ。
- ONCODE 310: FIXED BINARYでのゼロ除算
- ONCODE 311: FIXED DECIMALでのゼロ除算
- ONCODE 320: FLOATでのゼロ除算
これらをONユニット内で判定することで、「単なる入力データの不備によるものか」「ロジックの致命的なバグか」をコードレベルで切り分けることができるのだ。
—
3. 実践コード:堅牢なバッチ処理のためのONユニット実装例
百聞は一見にしかずだ。実際の商用バッチプログラムを想定した、安全な例外処理のコードテンプレートを見せてやろう。
大文字で記述された、いぶし銀のPL/Iコードだ。しっかりとインデントを追って読んでくれ。
1
TESTCALC: PROC OPTIONS(MAIN);
DCL SALES_AMT FIXED DEC(11,2) INIT(0);
DCL UNIT_QTY FIXED DEC(5,0) INIT(0);
DCL UNIT_PRICE FIXED DEC(7,2) INIT(0);
DCL ERR_CODE FIXED BIN(31);
/ ———————————————— /
/ ZERODIVIDE条件に対するONユニットの定義 /
/ ———————————————— /
ON ZERODIVIDE BEGIN;
ERR_CODE = ONCODE();
DISPLAY(‘ 警告: ゼロ除算例外を検知しました ‘);
DISPLAY(‘ 発生したONCODEは: ‘ || TRIM(ERR_CODE) || ‘ です ‘);
SELECT (ERR_CODE);
WHEN (311)
DISPLAY(‘詳細: 固定小数点数(FIXED DECIMAL)の除算で分母がゼロ、または無効データです。’);
WHEN (310)
DISPLAY(‘詳細: 2進整数(FIXED BINARY)の除算でゼロ除算が発生しました。’);
OTHERWISE
DISPLAY(‘詳細: 未知の算術例外です。システム担当者へ連絡してください。’);
END;
/ 異常終了を回避してデフォルト値(0)を代入して続行する場合の処理 /
UNIT_PRICE = 0;
/ ここであえてGOTOを使って正常系へ復帰させる、あるいは強制終了させる選択をする /
/ 今回は安全に処理をスキップするためのラベルへジャンプ /
GOTO CALC_ERROR_EXIT;
END;
/ 模擬的なメイン処理ループ(実際にはVSAMや順編成ファイルを読む) /
DISPLAY(‘— 単価計算バッチ処理 開始 —‘);
SALES_AMT = 150000.00;
UNIT_QTY = 0; / わざと数量をゼロにしてテスト /
DISPLAY(‘売上金額: ‘ || SALES_AMT);
DISPLAY(‘販売数量: ‘ || UNIT_QTY);
/ ———————————————— /
/ ゼロ除算が発生する危険な演算 /
/ ———————————————— /
DISPLAY(‘単価の計算を実行します…’);
UNIT_PRICE = SALES_AMT / UNIT_QTY; <-- ここでZERODIVIDE発生!
/ 通常の正常終了ルート /
DISPLAY('計算結果単価: ' || UNIT_PRICE);
GOTO NORMAL_EXIT;
CALC_ERROR_EXIT:
DISPLAY('>>> 異常データを検知したため、当レコードの計算をスキップしました。処理を継続します。’);
NORMAL_EXIT:
DISPLAY(‘— 単価計算バッチ処理 正常終了 —‘);
RETURN;
END TESTCALC;
—
4. シニアアーキテクトからの現場の教訓・アドバイス
このコードを見て、「なるほど、ON-unitの中で `GOTO` を使って処理を逃がせば、バッチ全体が落ちずに済むんだな」と思ったそこのお前。半分正解で、半分は大間違いだ。
実務の現場において、ONユニットでのリカバリは「最後の砦(セーフティネット)」であって、これを乱用してはならない。
データ不正が原因でZERODIVIDEが起きているのに、それをエラーログだけ出して「単価=0」でスルーさせたらどうなる? 後続の総額計算や財務諸表の突合で大穴(データ不整合)が空き、翌朝のユーザー部門からの大クレームに直結する。
原則として、バッチプログラムにおける正しい設計方針はこうだ:
1. 事前バリデーションを徹底する
VSAMやQSAMからの入力レコードをGETした時点で、分母となる項目がゼロでないか、あるいはスペースや文字化け(パック十進数のゾーンエラー)が含まれていないかを `IF` 文や総称関数で厳密にチェックしろ。
2. ONユニットは「異常終了時の証拠保全(ダンプとメッセージ出力)」に使う
本番稼働中に万が一想定外のデータが飛び込んできた場合、ダンプリストに `ONCODE` の値を綺麗に吐き出させ、どのキー情報のレコードで死んだのかをロギングして安全にABEND(異常終了)させるのが、基幹系システムエンジニアとしてのプロの作法だ。
PL/Iは古い言語だなどと侮るなよ。この言語の例外処理機構とONCODEの仕組みを完全に手の内に入れた時、お前はどんな巨大なレガシーシステムの改修でも、ビクともしない堅牢なコードを書ける一流のメインフレーム・エンジニアになっているはずだ。
さて、理論はここまでだ。さっそくテスト環境にログインして、今日のデバッグをやり直してこい!
