【テクニカル・上級編】ON ENDFILEユニットのスタック構造と制御フロー – PL/Iの基本構文とデータ制御実践ガイド

はじめに:なぜ今、PL/Iの「ON ENDFILE」とスタック構造なのか

基幹システムの心臓部で長年稼働し続けるIBMメインフレーム。その世界において、PL/I(Programming Language One)ほど「エレガントでありながら、一歩間違えるとシステム全体を奈落の底に突き落とす」言語も珍しい。COBOLの画一的な手続き型記述とは一線を画し、ブロック構造や強力な例外処理(ON-conditions)を備えたPL/Iは、当時のプログラマにとって究極の自由度をもたらした。

しかし、その「自由度」の裏側にあるコンパイラの挙動や制御フローのメカニズムを正しく理解していないと、現代のJavaやC#へのマイグレーションプロジェクトにおいて、あるいは既存バッチの巧妙な改修において、致命的なバグを踏み抜くことになる。

今回は、PL/Iの例外処理の根幹をなす `ON ENDFILE` ユニットのスタック構造`REVERT` 文による条件復帰のメカニズム に焦点を当てる。単なる文法のおさらいではない。ベース変数とポインタによる動的メモリ操作の闇、DB2(埋め込みSQL)やCICSオンラインとの統合時におけるエッジケース、そしてアベンド(ABEND)時のダンプ解析に至るまで、極限の信頼性が求められる現場の視点から徹底的に紐解いていこう。

1. PL/Iの「予約語を持たない」柔軟性と、ON条件スタックの基本原則

PL/Iの言語仕様における最大の特異性は、「予約語(Reserved Words)を持たない」という設計思想にある。`IF` や `READ` でさえも文脈キーワード(Contextual Keywords)であり、変数名として使用できてしまう。この柔軟性は時にパーサーを悩ませ、コンパイラ最適化の裏側で複雑なシンボルテーブルの解決を要求する。

この「動的な文脈解釈」の精神は、例外処理である `ON` 条件のハンドリングにも色濃く反映されている。

スタック構造としての `ON` ユニット

ファイル入力中に物理的、あるいは論理的なファイルの終端(End of File)に達した際、PL/Iランタイム環境は `ENDFILE` 条件を発生させる。ここでプログラマが記述するのが `ON ENDFILE(filename)` ユニットだ。

1
DCL SYSIN FILE INPUT;
DCL EOF_FLAG BIT(1) INIT(‘0’B);

/ ENDFILE条件のトラップを設定 /
ON ENDFILE(SYSIN)
BEGIN;
EOF_FLAG = ‘1’B;
/ ここに制御を戻すためのREVERT、あるいはGOTOが必要 /
END;

ここで重要なのは、`ON` ユニットは実行時スタック(Execution Stack)に積み上げられる(プッシュされる) という事実だ。同じファイル名に対する `ON` 文が異なるブロックやサブルーチンで複数回実行されると、古いハンドラは上書きされるのではなく、ランタイムの内部スタックに隠蔽(ネスト)される。

[ 実行時スタックのイメージ ]
+——————————————+
| 直近のON ENDFILEユニット (現在のブロック) | <-- 現在有効 +------------------------------------------+ | ひとつ前のON ENDFILEユニット (呼び出し元) | <-- 保留中 +------------------------------------------+ ブロックが抜けたり(`RETURN` や `END`)、後述する `REVERT` 文が実行されたりすると、スタックがポップされ、一つ前のハンドラが復元される。このライフサイクルを意識していないと、サブルーチン間で意図しない例外ハンドラが発火し、無限ループや予期せぬアベンドを引き起こす。 ---

2. `REVERT` 文の真価と制御フローの罠

`ON` 条件が発生し、そのハンドラ内で処理を終えた後、プログラムはどのように元の世界へ戻るべきか。ここで登場するのが `REVERT` 文である。

`REVERT` のない世界、あるいは誤った `GOTO`

多くの若手エンジニア(あるいはレガシー移行の自動変換ツール)が犯す最大のミスは、`ON` ユニット内で `GOTO` を使ってループの外へ脱出した後、元のブロックに戻ったときに `REVERT` を行わないこと、あるいは `REVERT` の意味を履き違えることだ。

`REVERT ENDFILE(filename);` は、ランタイムスタックから現在の `ON` ユニットの有効性を解除し、直前の有効な(あるいはデフォルトのシステム動作であるアベンド等の)ハンドラ状態に戻す 命令である。

1
READ FILE(INFILE) INTO(REC_BUF);

DO WHILE (^EOF_FLAG);

ON ENDFILE(INFILE)
BEGIN;
EOF_FLAG = ‘1’B;
/ 警告: ここでREVERTを忘れると、スタックが汚染される /
REVERT ENDFILE(INFILE);
END;

READ FILE(INFILE) INTO(REC_BUF);
IF ^EOF_FLAG THEN
CALL PROCESS_RECORD(REC_BUF);
END;

もし `REVERT` を省略し、かつブロックを抜けずにループ内で何度も `ON` 文を再定義(あるいは暗黙的に再評価)させると、PL/Iランタイムのヒープ領域やコントロールブロックを圧迫し、最悪の場合は Storage Violation(ストレージ違反) によるS0C4アベンドを引き起こす。コンパイラオプション(例えば `OPTIMIZE(2)` や `TEST`)の組み合わせによっては、このスタックの巻き戻し最適化がアグレッシブに行われ、デバッグ時にトレースが追いにくくなる現象(いわゆる神隠し現象)が発生するため注意が必要だ。

3. 実務のエッジケース:ベース変数・ポインタ操作との複合汚染

基幹システムの高度なバッチ処理では、静的なデータ構造だけでなく、ポインタとベース変数(Based Variables)を用いた動的リスト構造(線形リストやツリー)が多用される。ここで `ON ENDFILE` やその他のIO例外が絡むと、一気に難易度が跳ね上がる。

以下の実用コード例を見てほしい。可変長の入力レコードを動的に割り当て、メモリリークを防ぎながらファイル終端をハンドリングする堅牢なパターンの実装例だ。

1
/ ————————————————– /
/ 動的ストレージとENDFILE制御の統合サンプル /
/ ————————————————– /
EX_BATCH: PROC OPTIONS(MAIN);

DCL MFILE FILE INPUT RECORD;
DCL 1 REC_NODE BASED(P_REC),
3 NEXT_PTR POINTER,
3 DATA_LEN FIXED BIN(15),
3 PAYLOAD CHAR(0) REFER(DATA_LEN);

DCL P_REC POINTER INIT(NULL());
DCL P_HEAD POINTER INIT(NULL());
DCL P_LAST POINTER INIT(NULL());
DCL EOF_SW BIT(1) INIT(‘0’B);
DCL IO_ERR BIT(1) INIT(‘0’B);

/ ENDFILEハンドラの定義 /
ON ENDFILE(MFILE)
BEGIN;
EOF_SW = ‘1’B;
REVERT ENDFILE(MFILE);
END;

/ エラー(ERROR)ハンドラ(入出力異常系)の定義 /
ON ERROR
BEGIN;
IO_ERR = ‘1’B;
DISPLAY(‘CRITICAL: I/O or System Error detected in MFILE.’);
GOTO ERROR_ROUTINE;
END;

OPEN FILE(MFILE) INPUT;

DO WHILE (^EOF_SW & ^IO_ERR);

/ データの長さを表すプレフィックスを読み込むための仮バッファ処理等 /
/ ここでは簡略化のため固定長として動的領域を確保する前提とする /
ALLOCATE REC_NODE SET(P_REC);
NEXT_PTR(P_REC) = NULL();

READ FILE(MFILE) INTO(REC_NODE);

IF EOF_SW THEN
BEGIN;
/ 読み込み失敗時はアロケートした領域を解放 /
FREE REC_NODE;
LEAVE;
END;

/ リスト構造への組み込み /
IF P_HEAD = NULL() THEN
P_HEAD = P_REC;
ELSE
NEXT_PTR(P_LAST) = P_REC;

P_LAST = P_REC;
END;

CLOSE FILE(MFILE);

/ 正常終了時のリスト処理へ /
CALL PROCESS_LIST(P_HEAD);
RETURN;

ERROR_ROUTINE:
/ 異常系クリーンアップ /
IF P_REC ^= NULL() THEN
FREE REC_NODE;
CLOSE FILE(MFILE);
SIGNAL FINISH;

END EX_BATCH;

アーキテクトの眼:ポインタと例外時のメモリリーク

上記のコードで最も恐ろしいのは、`READ` ステートメントの実行と `ENDFILE` 条件の発生が、ハードウェア割込みとランタイムのコンテキストスイッチの狭間で発生する点だ。`ALLOCATE` でヒープ上にメモリを確保した直後に `ENDFILE` が発生し、ハンドラ内で適切に `FREE` を行わずに制御を飛ばした場合、そのメモリ領域は二度と回収できなくなる(メモリリーク)。

メインフレームのバッチが何百万件ものレコード処理の途中で徐々にメモリーを枯渇させ、突如として S0C4アベンドCEE0374Sシステムエラー を吐いて沈没する原因の多くは、こうした「例外発生時の動的ストレージの始末漏れ」にある。

4. マイグレーション(Java/C#化)における致命的な罠

レガシーマイグレーションの現場では、「PL/Iの `ON ENDFILE` や `ON ERROR` は、Javaの `try-catch-finally` や C#の `try-catch` にそのまま置き換えればよい」と安易に考える設計者が後を絶たない。

ここに大きな落とし穴がある。

1. スコープと伝播の概念の違い
Javaの例外はコールスタックを遡って例外オブジェクトを投げるが、PL/Iの `ON` ユニットは「宣言された静的/動的ブロックの有効期間内における、イベント駆動型の条件トラップ」である。スタックされた複数の `ON` ユニットが、条件発生時にどの順番で評価されるかのセマンティクスをJavaの例外機構で完全再現しようとすると、カスタムの例外管理クラスとスレッドローカルな状態管理が必要になる。

2. パックデシマル(COMP-3)の符号反転バグとの複合
ファイル終端や不正データ読み込み時に発生する `ERROR` や `ENDFILE` のハンドリングにおいて、移行先言語(Java)へデータをコンバートする際、メインフレーム特有のゾーン10進数やパックデシマルの内部符号(最下位バイトの4ビット)の解釈違いが起きやすい。特にマイグレーションツールが自動生成したコードでは、ファイル終端直前のパディング領域や不正レコードを読んだ際の例外ハンドラが正しく動作せず、数値項目の符号反転バグ(例: 正負が逆転する、あるいはデータ例外になる)が本番稼働直前に発覚するケースが後を絶たない。

3. CICSオンライン環境およびDB2(埋め込みSQL)との干渉
CICSオンラインプログラムの中でPL/Iを使用する場合、ファイル(VSAM等)の終端や `ENDFILE` は、しばしば `RESP`コード(DFHRESP(ENDFILE)など)で処理されるべきものだ。しかし、古いシステムから移植されたコードに `ON ENDFILE` が混在していると、CICSのタスク異常終了ハンドラ(`HANDLE CONDITION`)とPL/Iのランタイム例外処理が二重にフックし、トランザクションがデッドロック状態に陥るか、不整合なABENDダンプを残してセッションが切断される。

5. アベンド発生時のダンプ解析手法:CEEDUMPを読む

万が一、本番環境でスタックの破損やストレージ違反によるアベンド(例: `S0C4` や `U4038`)が発生した場合、頼りになるのはLanguage Environment(LE)が吐き出す CEEDUMP だ。

アーキテクトとして、CEEDUMPを受け取った際に真っ直ぐ見るべきポイントを提示しよう。

1. Condition Information の確認
ダンプの冒頭付近にある `Traceback` セクションを確認し、どのモジュール(CSECT)のどのオフセットで例外が発生したか、またその時点で有効だった `ON` 条件のスタック状態がどうなっていたかを特定する。
2. Active Conditions Stack の追跡
CEEDUMPには、現在どの `ON` ユニットがアクティブであったか(Pending / Active)のリストが出力される。ここに意図しない古いハンドラが残っている場合、`REVERT` の漏れ、あるいはブロック構造の設計ミスが証明される。
3. Storage Address の検証
`S0C4`(Protection Exception)の場合、参照しようとしたポインタが指すアドレスが `00000000` なのか、あるいはすでに `FREE` された解放済み領域(Wild Pointer)を指しているのかを、レジスタ値(R14, R15等)と照らし合わせてデバッグする。

おわりに:レガシーの理知を継承するということ

PL/Iの `ON ENDFILE` ユニットとスタック管理、そして `REVERT` のメカニズムは、単なる古い文法仕様ではない。それは、ハードウェアの制約と向き合いながら、エラーや例外を優雅にいなすために先人たちが築き上げた「ソフトウェア工学の原点」の一つである。

現代のモダン言語への移行(マイグレーション)を成功させるためには、移行先言語の書き換え技術に溺れるのではなく、移行元のPL/Iコンパイラが裏側で何を行っているのか――スタックのプッシュとポップ、動的ストレージのライフサイクル、そして例外のスコープ範囲を完全に把握していなければならない。

コードの行間に隠されたコンパイラの息吹を感じ取り、堅牢なシステムをデザインし抜くこと。それこそが、真のメインフレーム・システムアーキテクトに求められる矜持である。

タイトルとURLをコピーしました