予約語なき世界の罠と、SUBSCRIPTRANGEが暴く「配列の暴走」
メインフレームの現場で長年PL/Iコードと向き合っていると、C#やJava出身の若いエンジニアから決まってこんな質問を受ける。
「なぜPL/Iには、CやJavaのような厳格な予約語(Reserved Words)がほとんど存在しないのですか? 変数名に `IF` や `READ` を使ってもコンパイルが通るなんて、狂気としか思えません」と。
ふっ、無理もない。PL/Iは1960年代の産物であり、あらゆるプログラミングパラダイムを飲み込もうとした「巨大な野獣」だ。コンパイラは前後の文脈(Context)からそれが変数なのかキーワードなのかを神業的に判断する。だから `IF = IF + 1;` なんという悪夢のようなコードすら文法的には成立してしまう(※絶対に書いてはいけないが)。
しかし、この「何でもあり」の自由度は、基幹システムにおいては諸刃の剣だ。特に、ポインタ演算やベース変数(Based Variables)、そして動的メモリ割り当てが絡む複雑なデータ構造のデバッグにおいて、その自由度ゆえのしっぺ返しは甚大なものとなる。
今回は、そんなPL/Iの奥深い世界の中でも、バッチの夜間バグ調査やJava/C#へのマイグレーション設計において「知っているか、いないかで生死が分かれる」極上のテーマを取り上げよう。ON条件における `SUBSCRIPTRANGE` の実戦的な活用法だ。
—
1. 予約語なき世界と、ポインタ・ベース変数の危険な関係
PL/Iの真骨頂は、`POINTER` と `BASED` ストレージクラスを用いた、アセンブラ並みのダイナミックなメモリ操作にある。C言語のポインタ演算ほど野放図ではないにせよ、ストレージの直接制御は一歩間違えばメモリ破壊の温床となる。
例えば、以下のようなCICSオンライン画面の入出力域や、DB2の埋め込みSQLで取得した可変長データを処理するルーチンを考えてみてほしい。
DCL 1 W_COMM_AREA BASED(P_COMM),
5 W_REC_COUNT FIXED BIN(31),
5 W_TABLE(1000) CHAR(50);
DCL P_COMM POINTER;
DCL I FIXED BIN(31);
/ ポインタの設定(GET STORAGEまたはCICS ADDRESS等で取得済みと仮定) /
P_COMM = / 何らかのアドレス /;
/ ループ処理:ここで致命的なバグが潜む /
DO I = 1 TO 1005; / 1000を超えるオーバーループ! /
W_TABLE(I) = ‘ERROR_DATA’;
END;
JavaやC#であれば、配列の境界チェック(Bounds Check)が走り、親切に `IndexOutOfBoundsException` を投げて安全にアベンドしてくれる。
しかし、ネイティブのPL/I(コンパイラオプションで最適化された状態)では、デフォルトでは境界チェックを行わない。なぜか? 汎用機における極限のパフォーマンス追求のためだ。毎回の配列アクセスのたびに境界チェックのオーバーヘッドを入れることは、何千万回もループする基幹系バッチにおいて致命的な遅延を招くからである。
その結果どうなるか?
`W_TABLE(1000)` の後ろに隣接していた重要なお金に関するデータや、制御ブロック(Control Block)が容赦なく上書きされる。だが、プログラムはその瞬間には止まらず、数ステップ進んだ全く関係のないモジュールで突然の `S0C4`(Protection Exception)やデータ化けアベンドを引き起こす。こうなると、ダンプリストの海から原因を特定するのに丸一日を費やすことになる。
—
2. SUBSCRIPTRANGE条件の有効化とそのメカニズム
この「静かにメモリを破壊する時限爆弾」を確実にその場で捉えるための唯一にして最強の武器が、`SUBSCRIPTRANGE`(略称: SUBRG)というON条件だ。
PL/Iコンパイラに `CHECK(SUBRG)` または `LIMIT` 系のオプションを与えてコンパイルすると、コンパイラは配列参照のたびに「現在の添字が宣言された上限・下限の範囲内にあるか」を検証するコードを埋め込む。
これをソースコードレベルで有効にするには、以下のように記述する。
/ サブスクリプト範囲チェックの有効化宣言 /
(SUBSCRIPTRANGE):
PROCEDURE OPTIONS(MAIN);
DCL W_TBL(10) FIXED BIN(15) INIT((10)0);
DCL I FIXED BIN(31);
/ 意図的に範囲外エラーを起こすONユニット(例外処理)の定義 /
ON SUBSCRIPTRANGE
BEGIN;
DISPLAY (‘ ERROR: 配列の添字が範囲外です ‘);
DISPLAY (‘不正な添字の値: ‘ || I);
/ 必要に応じてダンプを取得して強制終了 /
SIGNAL ERROR;
END;
/ バグを含んだループ /
DO I = 1 TO 11;
W_TBL(I) = I 10; / I = 11 でサブスクリプトランジ例外が発生 /
END;
END;
このコードが本番・テストで持つ意味
もしこの `ON SUBSCRIPTRANGE` を仕込んでおけば、配列 `W_TBL(11)` にアクセスした瞬間、OSやランタイムの無慈悲な `S0C4` アベンドを回避し、自分が定義した親切なメッセージと「どの変数・何番目の添字で踏み抜いたか」という決定的証拠を残して安全に異常終了できる。
—
3. 本番環境におけるパフォーマンス影響とデバッグ戦略のトレードオフ
ここで、テックリードやアーキテクトとして避けて通れない議論がある。
「この強力なチェック機能を、すべての本番稼働プログラムに常時入れておけば良いのではないか?」
答えは、「No。基幹システムのパフォーマンスを殺すことになるため、原則として本番では外すべきである」だ。
アーキテクトの判断基準
1. 開発・単体・結合テスト環境(TEST / STG)
- すべてのモジュールで `TEST` コンパイラオプションと共に `SUBSCRIPTRANGE`、`STRINGRANGE`、`ZERODIVIDE` などの例外監視を完全有効化する。
- マイグレーション案件(PL/IからJava/C#への移行時など)において、レガシー側の「潜在的な仕様やバグ」を洗い出すためのリバースエンジニアリングとしても、この環境下でのエラーログ収集は極めて有効。
2. 本番環境(PROD)
- コンパイルオプションは `NOTEST`、`OPTIMIZE(2)` または `(3)` を指定し、パフォーマンスを最優先する。
- 境界チェックのオーバーヘッドは、1回あたりはナノ秒単位であっても、1日あたり数億回実行されるバッチ基幹系においては、バッチウィンドウ(夜間処理の制限時間)を数十分〜数時間オーバーさせる原因になり得る。
ただし、「どうしても本番で原因不明のデータ破壊が起きており、犯人を特定したい」という極限の状況に限り、特定の疑わしいプログラム群だけを一時的に `SUBSCRIPTRANGE` 有効版に差し替えて本番同等のテスト、あるいは限定的な本番投入を行うという「外科的手法」が取られることもある。
—
4. マイグレーション(Java/C#化)におけるエッジケース対策への応用
現在、多くの企業がレガシーなPL/I資産から、Java(Spring Boot)やC#(.NET Core)へのマイグレーションを進めている。ここでシステムアーキテクトが直面する最大の壁が、「PL/Iの緩い仕様と、モダン言語の厳格な仕様のギャップ」だ。
例えば、PL/Iのパックデシマル(`DECIMAL FIXED`)や、ポインタ経由での領域オーバーレイ(Based変数による構造体の重ね合わせ)は、JavaやC#にはそのまま直訳できない。
- PL/Iでのバグ: 配列のオーバーランが、隣接する数値項目の内部符号(Zone/Signニブル)を破壊し、パックデシマルの演算時に `S0C7`(Data Exception)を引き起こす。
- 移行先(Java)での挙動: Javaで単純に配列外アクセスをすれば `ArrayIndexOutOfBoundsException` になるが、もしバイト配列(`ByteBuffer` など)をこねくり回すような移行設計にしてしまうと、PL/I時代にあった「メモリ破壊によるサイレントバグ」が、Java側では「原因不明の業務データの辻褄合わせエラー」として数日後に発覚するという最悪のシナリオを生む。
だからこそ、マイグレーションの初期段階(アセスメントおよび旧システム解析フェーズ)において、PL/Iのソースコード解析ツールやコンパイラ情報を使い、`SUBSCRIPTRANGE` が警告を発するような危ういポインタ操作・配列操作がどこで行われているかをあらかじめ炙り出す必要があるのだ。
—
5. 結びにかえて:レガシーの知見を未来のアーキテクチャへ
予約語を持たない自由奔放なPL/Iという言語は、使いこなせばハードウェアの限界を引き出す最高の相棒だが、一歩間違えばシステム全体を奈落の底に突き落とす諸刃の剣である。
今回解説した `SUBSCRIPTRANGE` によるデバッグ手法は、単なるエラー検知のテクニックにとどまらない。それは、目に見えないメモリの挙動を可視化し、システム全体の信頼性を担保するための「アーキテクトの眼」そのものだ。
レガシーシステムのモダナイゼーションやマイグレーションを手掛けるとき、私たちは古いコードをただ「古いから捨てるべき悪」として見るのではなく、そのコードが何十年もの間、どのような例外処理やコンパイラの挙動に守られて生き延びてきたのかを理解しなければならない。その深い洞察力こそが、次世代の堅牢なシステムを構築するための確かな礎となる。
さあ、今日の夜間バグ解析にも、この知見を役立ててほしい。コンパイラと対話し、メモリの息吹を感じ取るエンジニアリングの醍醐味は、いつの時代も変わらないのだから。
