こんにちは!メインフレームの世界へようこそ。
JavaやCOBOLといったモダン、あるいはビジネスライクな言語をバリバリ書いてきた方にとって、IBMメインフレームの「PL/I(ピーエルワン)」という言語は、最初にソースコードを見たとき「なんだこの暗号は……?」と少し身構えてしまうかもしれませんね。
でも、安心してください。ベースにある考え方を一つずつ紐解いていけば、決して怖い言語ではありません。
今回は、基幹システムのバッチ処理などで突如として牙を剥く「ZERODIVIDE(ゼロ除算)条件の発生メカニズムと、ONCODEによるエラーハンドリング」について、実務の現場を覗くような感覚で優しく解説していきます。COBOLやJavaの例外処理との違いも含めて、リラックスして読んでいってくださいね。
—
1. 他言語とはココが違う!PL/Iの「予約語を持たない」世界観
まず、PL/Iの非常にユニークで、JavaやCOBOL出身者が最初に驚くポイントに触れておきましょう。
それは「PL/Iには厳密な意味での『予約語(Keyword)』が存在しない」という点です。
JavaやCOBOLでは、`IF`や`READ`、`ZONED`といった言葉は言語の命令として予約されているため、変数名に使うことはできませんよね。しかし、PL/Iでは次のような宣言が理論上できてしまいます。
DECLARE IF FIXED BINARY(31); / 変数名に IF を使っちゃう暴挙 /
IF = 10; / 変数 IF に 10 を代入 /
「えっ、じゃあコンパイラはどうやって `IF` が条件分岐なのか変数なのか見分けるの?」と不安になりますよね。
コンパイラは、前後の文脈(Context)から「ここは命令語の位置だな」「ここは変数名だな」と賢く判断しています。ただ、こんな書き方をするとコードを読む人間(特に後任のプログラマ)が精神崩壊してしまいますので、実務の現場では予約語っぽい名前を変数に使うのはタブーとされています。
さて、そんな柔軟かつ少しレガシーな懐の深いPL/Iにおいて、最も頭を悩ませるランタイムエラーの一つが「ゼロ除算(ZERODIVIDE)」です。次はその仕組みを見ていきましょう。
—
2. ゼロ除算はなぜ起きる?ハードウェアとPL/Iランタイムの裏側
基幹システムの夜間バッチで、ある日突然、ジョブが異常終了(ABEND)する……。その原因の多くは、計算処理で分母が「0(ゼロ)」になってしまった瞬間です。
DCL TOTAL_AMT DEC FIXED(9,2);
DCL COUNT_NUM DEC FIXED(5,0);
DCL AVG_AMT DEC FIXED(9,2);
/ もし COUNT_NUM が 0 だったら…? /
AVG_AMT = TOTAL_AMT / COUNT_NUM;
Javaであればここで `ArithmeticException: / by zero` がスローされ、COBOLであれば条件次第でサイズエラーやプロシージャ異常となります。
PL/Iの場合、この割算が実行された瞬間に何が起きるでしょうか?
裏側では、IBMメインフレームのハードウェア(プロセッサ)が「おっと、ゼロで割ろうとしたな!」と検知し、ハードウェア割り込みを発生させます。それをPL/Iのランタイム環境(LE: Language Environment)がキャッチし、デフォルトでは「IBM360I ON-CODE 321」という致命的なメッセージを出力して、容赦なくジョブを強制終了(U4038などのABEND)させます。
「おいおい、マスタデータが汚れていて分母が0になることがあるんだから、強制終了しないでログに残してスキップしたいよ!」
そんなときに力を発揮するのが、PL/Iが誇る強力な例外処理機構「ON条件(ON-unit)」と「ONCODE」です。
—
3. 実践!ONCODEを使ったスマートなエラーハンドリング
PL/Iでは、エラーが起きるかもしれないブロックのあらかじめ手前で、「もしゼロ除算が起きたら、クラッシュさせずにこの処理をしてね」と指示しておくことができます。
百聞は一見にしかず。実際のサンプルコードを見てみましょう。大文字ベースの、実務でよく見かけるスタイルで記述しています。
CALC_MODULE: PROC OPTIONS(MAIN);
/ 変数の宣言:パック十進数(DECIMAL FIXED)を使用 /
DCL TOTAL_AMT DECIMAL FIXED(9,2) INIT(1500.00);
DCL COUNT_NUM DECIMAL FIXED(5,0) INIT(0); / ここが「0」! /
DCL AVG_AMT DECIMAL FIXED(9,2);
DCL ERROR_FLAG BIT(1) INIT(‘0’B);
/ ========================================================== /
/ ZERODIVIDE 条件のトラップ(監視)を宣言する /
/ ========================================================== /
ON ZERODIVIDE BEGIN;
/ ここでONCODEを組み合わせて、発生したエラーの正確なコードを取得する /
PUT SKIP LIST(‘【警告】ゼロ除算を検知しました。ONCODE = ‘, ONCODE());
/ 異常終了を回避するためのフェイルセーフ処理 /
AVG_AMT = 0; / 平均金額に 0 を強制代入 /
ERROR_FLAG = ‘1’; / エラーフラグを立てる /
END;
PUT SKIP LIST(‘— 割り算処理を開始します —‘);
/ 意図的にゼロ除算が発生する行 /
/ 通常ならここでABENDするところですが… /
AVG_AMT = TOTAL_AMT / COUNT_NUM;
/ エラーが起きていなければ結果を表示 /
IF ERROR_FLAG = ‘0’ THEN
PUT SKIP LIST(‘平均金額: ‘, AVG_AMT);
ELSE
PUT SKIP LIST(‘ゼロ除算が発生したため、処理を継続しました(スキップ処理)。’);
PUT SKIP LIST(‘— 正常にプログラムを終了します —‘);
END CALC_MODULE;
このコードのポイントと「ONCODE」の魅力
1. `ON ZERODIVIDE BEGIN; … END;`
Javaでいう `try-catch` ブロックの `catch` に似た仕組みです。このスコープ内でゼロ除算が発生すると、プログラムが異常終了する代わりに、この `BEGIN` ブログの中身が実行されます。
2. `ONCODE()` 組み込み関数
ここが今回の最も重要なスパイスです!PL/Iの `ONCODE()` は、「今、何というエラーコードが起きたのか」を数値で正確に教えてくれる非常に優秀な関数です。
ゼロ除算の場合は、大体 `321` といった固有のコードが返ってきます。これを利用することで、「ゼロ除算なのか、それとも別の算術オーバーフロー(OVERFLOWなど)なのか」を細かく判別し、エラーログに出力したり、処理を分岐させたりすることができます。
3. 安全な継続(Resumption)
エラー処理の中で代入値を補正してあげることで、プログラムを強制終了させずにそのまま次の行へ処理を戻す(あるいは安全にルートを変える)ことができます。夜間バッチで「たった1件のデータ不備で何百万件の処理が止まる」という悪夢を防ぐための、レガシーならではの知恵ですね。
—
4. アーキテクトからのアドバイス
メインフレームの世界では、データが綺麗に整っている保証はどこにもありません。外部から連携されてくる古い電文や、長年積み上げられたマスタファイルには、思わぬ「ゼロ」や「スペース」が潜んでいます。
JavaやCOBOLの感覚からすると、PL/Iの `ON` 条件の書き方は最初は少し独特(グローバルかつ宣言的)に感じるかもしれません。「どこからどこまでが監視対象なんだっけ?」と迷うこともあるでしょう。
しかし、ハードウェアレベルの割り込みと直結しているからこそ、非常に高速かつ確実に例外をキャッチできるのがPL/Iの強みです。
「エラーが起きても慌てず騒らず、`ONCODE` で原因を特定して優しくハンドリングする」――。このアプローチが身につけば、あなたはもう立派なPL/I使いの仲間入りです。
レガシーシステムの改修やマイグレーション調査でこのコードに出会ったら、「あ、ここでエラーを上手にいなしているんだな」と、ぜひ心の中で微笑んであげてくださいね。それでは、次回の解説もお楽しみに!
