こんにちは。長年、金融や流通の基幹系メインフレームでPL/Iバッチの海を泳いできたシステムアーキテクトの私だ。
今これを読んでいる君は、ひょっとすると夜間バッチの突然のABEND(異常終了)に頭を抱えているか、あるいはレガシーマイグレーションの過程で「謎のメモリー破壊」や「データ化け」の調査に奔走しているところかもしれない。
PL/Iという言語は、1960年代に生まれたにもかかわらず、その設計思想は驚くほどモダンで、そして時にプログラマに全幅の信頼を置くあまり、「ミスをしたときには致命傷を与える」というスパルタンな一面を持っている。
今日は、その中でも特に現場のエンジニアを救う救世主、`SUBSCRIPTRANGE` 条件と配列境界チェックの極意について、実務のノウハウを交えて徹底的に解説しよう。
—
1. 予約語を持たないPL/Iと「恐怖の配列オーバーラン」
まず、PL/Iのユニークかつ恐ろしい仕様について触れておこう。C言語やJavaとは異なり、PL/Iには厳密な意味での「予約語(Reserved Words)」が存在しない。
どういうことか? 極端な話、キーワードであるはずの `IF` や `READ`、さらには今回解説する `SUBSCRIPTRANGE` すら、プログラマが変数名として宣言して使うことができてしまう。コンパイラは前後の文脈(コンテキスト)から「お、ここは命令文だな」「ここは変数名だな」と空気を読んで解釈しているのだ。
この柔軟性がレガシーコードの可読性を悪くする一因でもあるのだが、それ以上に怖いのが「配列添字の範囲外アクセス(オーバーラン)」に対するデフォルトの挙動だ。
C言語などでよくあるバッファオーバーランを想像してほしい。PL/Iでも、デフォルトのコンパイルオプションのままでは、配列の定義域を超えてアクセス(例:`DIMENSION(10)` なのに 11番目に書き込み)を行っても、コンパイラは文句を言わず、隣接する別の変数の領域を盛大に破壊していく。
結果どうなるか?
- まったく関係のない業務項目の金額データが書き換わる。
- 突然の `S0C4`(保護例外)ではなく、数世代後のステップで謎のデータ異常(S0C1や論理エラー)として爆発する。
この「原因特定が極めて困難なバグ」を未然に防ぎ、あるいは一発で炙り出し、エラー発生行を特定するための強力な武器が、コンパイラオプション `STGOWNG` / `SUBSCRIPTRANGE` と `ON SUBSCRIPTRANGE` ユニット なのだ。
—
2. 境界チェックを有効にするための二段構え
`SUBSCRIPTRANGE` によるデバッグを機能させるには、コンパイル時と実行時(ソースコード内)の両方で正しく設定を行う必要がある。現場のコーディング標準としても必須の知識だ。
① コンパイラオプションの指定
JCLのCOMPILEステップにおいて、オプションに `CHECK` または `SUBSCRIPTRANGE`(略称 `SUBR`)を指定する。
//COMPILE EXEC PGM=IBMZPLI,PARM=’STG(NONE),MARGIN(1,72),SUBR’
※本番稼働時はオーバーヘッド(実行時のオーバヘッド)を考慮して `NOSUBR` にすることが多いが、単体テストや結合テスト、そして本番障害時の原因追及(再현テスト)では必ず `SUBR` を有効にするのが鉄則だ。
② ソースコード側での有効化
実は、コンパイルオプションで `SUBR` を有効にしただけでは、条件が発生したときにプログラムが強制終了してくれない、あるいは独自のリカバリを行えない。PL/Iでは、ONユニットを使って「その条件をどう扱うか」を明示的に制御する。
—
3. 実践:VSAM入出力とON SUBSCRIPTRANGEの制御フロー
それでは、実際のメインフレーム開発現場を想定したサンプルコードを見てほしい。
VSAM(KSDS)からレコードを読み込み、内部の可変長テーブル(配列)にデータを展開する際、データ側の件数が想定を超えていた場合に `SUBSCRIPTRANGE` が発火するシナリオだ。
1
TESTPRG: PROC OPTIONS(MAIN);
/—————————————————/
/ 変数宣言およびワーク領域 /
/—————————————————/
DCL WS-EOF-FLG CHAR(1) INIT(‘0’);
DCL WS-IDX FIXED BIN(31) INIT(0);
DCL MAX-SIZE CONSTANT(100); / 配列の最大境界 /
/ 検査対象の配列(最大100要素) /
DCL 1 WS-TBL-AREA,
3 WS-ITEM CHAR(50) DIM(MAX-SIZE);
/ VSAMファイルの定義(KSDS想定) /
DCL VSAM-FILE FILE RECORD SEQUENTIAL INPUT;
DCL 1 VSAM-REC,
3 REC-KEY CHAR(10),
3 REC-DATA CHAR(40);
/—————————————————/
/ 異常系イベント(SUBSCRIPTRANGE)の捕捉宣言 /
/—————————————————/
ON SUBSCRIPTRANGE
BEGIN;
DISPLAY(‘ 致命的エラー: 配列添字が境界を超えました ‘);
DISPLAY(‘発生時のインデックス値: ‘ ||
TRIM(CHAR(WS-IDX)));
DISPLAY(‘定義済み最大サイズ : ‘ ||
TRIM(CHAR(MAX-SIZE)));
/ ここでダンプを取得して異常終了させる /
SIGNAL ERROR;
END;
/—————————————————/
/ 処理メインフロー /
/—————————————————/
OPEN FILE(VSAM-FILE);
READ FILE(VSAM-FILE) INTO(VSAM-REC);
DO WHILE (WS-EOF-FLG = ‘0’);
WS-IDX = WS-IDX + 1;
/ 意図的に境界(100)を超えるデータが存在すると仮定 /
/ WS-IDX が 101 になった瞬間にON SUBSCRIPTRANGEへ突入する /
WS-ITEM(WS-IDX) = REC-DATA;
READ FILE(VSAM-FILE) INTO(VSAM-REC);
END;
CLOSE FILE(VSAM-FILE);
DISPLAY(‘正常終了: 処理件数 = ‘ || TRIM(CHAR(WS-IDX)));
RETURN;
END TESTPRG;
コードの解説とアーキテクトからの助言
1. `ON SUBSCRIPTRANGE BEGIN … END;` の捕捉
PL/Iの `ON` ユニットは、一種の例外ハンドラとして機能する。このブロックを定義しておけば、プログラムのどこであっても、`DIM(100)` の配列に対して `101` 番目以降を指定した瞬間に、通常の処理フローが中断され、この `BEGIN` ブロックへと制御がジャンプする。
2. `SIGNAL ERROR` による安全なABEND
単に警告を出すだけでなく、シスログ(JESMSGLG)に明確なメッセージを残しつつ、意図的に `ERROR` シグナルを送ることで、リージョンをクリーンに異常終了(U3039やシステムコード)させることができる。これにより、後続のバッチジョブ連鎖(JCLのCONDパラメータ)を正しく止めることが可能になる。
3. `TRIM` と `CHAR` ビルトイン関数の活用
PL/Iの `CHAR` や `TRIM` などのビルトイン関数は、デバッグ時のメッセージ出力において非常に強力だ。変数の値を安全に文字列化し、オペレータコンソールやシスログに詳細な状況を残すことができる。
—
4. デバッグ効率を劇的に高める現場のテクニック
最後に、保守現場で培った「SUBSCRIPTRANGEにまつわる実践知恵袋」をいくつか授けよう。
- 本番環境とテスト環境のコンパイルJCLを分ける
パフォーマンス(CPU使用量)をシビアに求められる巨大な基幹バッチでは、全モジュールに `SUBR` を常時有効にすると、わずかではあるがオーバーヘッドが生じる。単体・結合テスト環境では `PARM=’SUBR’` を標準とし、本番リリース時のみ `NOSUBR` に切り替えるデプロイメントパイプラインを構築するのがプロのやり方だ。
- 「ON-source」や「ON-size」との組み合わせ
配列の添字ミスだけでなく、数値の桁あふれ(`OVERFLOW`, `SIZE`)や文字データの切り捨て(`STRINGSIZE`)も、PL/I開発における三大地雷だ。これらも `ON` ユニットでまとめて監視体制を作っておくことで、レガシーシステムの品質は劇的に安定する。
PL/Iは古い言語だと言われることもあるが、言語仕様のレイヤーでここまで堅牢なエラー検知の仕組みを持っている言語は、現代でもそう多くはない。
仕組みを正しく理解し、コンパイラとONユニットを味方につければ、どんなに複雑な巨大バッチの改修であっても、怖がる必要は全くない。
先輩である君たちがこの仕組みを使いこなし、若手エンジニアたちを導いていってくれることを期待している。
