【テクニカル・上級編】INDEXおよびVERIFY関数によるスキャンアルゴリズム – PL/Iの基本構文とデータ制御実践ガイド

【PL/Iアーキテクチャ再考】INDEXとVERIFY関数が暴く、レガシー文字列スキャンの深淵とマイグレーションの罠

メインフレームの現場で何十年も稼働し続けている基幹システム。その心臓部であるPL/Iコードを覗くと、現代の洗練されたJavaやC#のモダンな言語仕様とは一線を画す、独特の「無骨だが強靭な」世界が広がっている。

PL/Iの特筆すべき言語仕様の一つに、「予約語を持たない(厳密には文脈依存キーワード)」という設計思想がある。変数名に `IF` や `INDEX` といった組み込み関数名すら宣言して使用できるこの柔軟性は、時としてコンパイル時のトリッキーな挙動を生み出し、コードを解析するテックリードを悩ませる。

今回は、その中でも文字列操作の根幹をなす `INDEX` 関数 と `VERIFY` 関数を取り上げる。単なる「文字列検索の便利関数」として片付けてはならない。これらが内部スキャン処理でどのように振る舞い、一致しなかった場合に何を返すのか。そして、オープン系へのマイグレーションやDB2、CICSとの統合において、どのような致命的な罠(エッジケース)を仕掛けてくるのか。極限の信頼性を求められるメインフレームの現場視点から、その内部挙動の深淵へと迫る。

—

1. `INDEX` と `VERIFY` の内部スキャンアルゴリズムと仕様の鉄則

まずは、両関数の基本仕様と、コンパイラが生成する内部機械語(S390/z/Architectureのアセンブラレベル)の挙動を整理しておこう。

`INDEX(string, substring)` — 部分文字列の位置特定

  • 動作: `string` の中から `substring` が最初に一致する位置(文字単位のオフセット)を返す。
  • 戻り値の仕様: 一致した場合は `1` 起算の整数(位置)を返す。見つからなかった場合は `0` を返す。
  • 内部処理: ハードウェア命令レベルでは、古き良き `CLC`(Compare Logical Character)や `TRT`(Translate and Test)、あるいは近年の z13/z14 以降であれば高効率なベクトル命令(Vector String Search)を活用してスキャンが行われる。

`VERIFY(string, check_string)` — 文字種チェックのスキャン

  • 動作: `string` の文字を左から順にスキャンし、`check_string` に含まれない文字が最初に出現した位置を返す。
  • 戻り値の仕様: 全ての文字が `check_string` に含まれている(完全一致・包含)場合は `0` を返す。見つからなかった場合ではなく、「検査対象外の不正文字を見つけた位置(1起算)」を返す点に注意せよ。文字列自体が空の場合も `0` だ。

> ⚠️ 現場の教訓:逆転する「0」の意味
> `INDEX` は「見つからないと `0`」だが、`VERIFY` は「違反がない(すべて合格)と `0`」である。この戻り値のセマンティクスの違いを混同したジュニアプログラマが、バリデーションロジックで誤判定を起こし、不正なデータをDB2へスルーさせてしまう障害は、レガシー移行前のバッチ改修で後を絶たない。

—

2. 実践コード:基幹バッチにおける堅牢な文字列バリデーション

実際の商用バッチプログラム(Enterprise PL/I)を想定し、両関数を駆使したデータ検証ルーチンのサンプルコードを提示する。ここでは、動的メモリ制御やポインタ演算の前提となる構造体(`BASED` 変数)との組み合わせを意識した設計にしている。

1
—————————————————————-

  • プログラム名: SCLN0010
  • 概要 : 外部電文のフォーマット検証と不正文字スキャン

—————————————————————-
SCLN0010: PROC OPTIONS(MAIN);

DCL 1 INPUT_RECORD BASED(P_REC),
5 REC_ID CHAR(4),
5 ACCOUNT_NO CHAR(10),
5 USER_DATA CHAR(100);

DCL P_REC POINTER;
DCL WORK_BUFFER CHAR(100) INIT((100)’ ‘);
DCL POS FIXED BIN(31) INIT(0);
DCL ERR_FLG CHAR(1) INIT(‘0’);

/ 有効な口座番号の文字セット(数字のみを許可)ガイドライン /
DCL VALID_NUMS CHAR(10) CONSTANT(‘0123456789’);

/ 擬似的に動的メモリ(ストレージ)を割り当てたと仮定 /
/ 実際にはCICSのGETMAINやDB2ホスト変数からの入力に対応 /
ALLOCATE INPUT_RECORD SET(P_REC);

————————————————————

  • 1. INDEX関数の活用: 特定の区切り文字や制御コードの検知

————————————————————
POS = INDEX(USER_DATA, X’00’); / ヌル文字の混入チェック /
IF POS > 0 THEN DO;
DISPLAY(‘【重度エラー】ユーザデータ内にヌル文字を検知: ポジション ‘ || TRIM(POS));
ERR_FLG = ‘1’;
END;

————————————————————

  • 2. VERIFY関数の活用: 純粋な数値項目(口座番号)の厳密検証

————————————————————

  • ACCOUNT_NO内に ‘0’~’9′ 以外の文字が含まれていないかスキャン
  • 戻り値が > 0 の場合、その位置に非数字が存在する

————————————————————
POS = VERIFY(ACCOUNT_NO, VALID_NUMS);
IF POS > 0 THEN DO;
DISPLAY(‘【データエラー】口座番号に非数字が混入: ポジション ‘ || TRIM(POS));
DISPLAY(‘該当文字: ‘ || SUBSTR(ACCOUNT_NO, POS, 1));
ERR_FLG = ‘1’;
END;

IF ERR_FLG = ‘1’ THEN;
/ 異常終了(U9999アベンド)を意図的に発生させるルーチンへ /
SIGNAL ERROR;
ELSE
DISPLAY(‘正常処理: データ検証完了’);

FREE INPUT_RECORD;
RETURN;

END SCLN0010;

このコードにおけるポイントは、`BASED` 変数を用いたストレージの直接参照と、コンパイル時最適化を見据えた定数(`CONSTANT`)の定義である。メインフレームの限られたCPUサイクルを極限まで削るため、コンパイラは `VALID_NUMS` のような定数をインライン展開し、スキャン処理のループをアンロール(展開)して高速化を図る。

—

3. アベンド(ABEND)とコンパイラ最適化の罠、エッジケース対策

ここからが、システムアーキテクトとしての真価が問われる領域だ。実務において、`INDEX` や `VERIFY` の背後にある「目に見えないリスク」をいかにして回避するか。

A. 可変長文字列(VARCHAR / VARYING)とストレージオーバーレイ

PL/Iで `CHAR(100) VARYING` を扱う場合、実際のデータ長を示す2バイトのプレフィックス(長さフィールド)と実データがメモリ上に連続して配置される。
ポインタ操作や `BASED` 変数を誤り、このプレフィックス領域を破壊した状態で `INDEX` や `VERIFY` を実行すると、コンパイラが想定外のメモリアドレスを参照し、S0C4(Protection Exception) や S0C7(Data Exception) のアベンドを引き起こす。特にマイグレーション時に、C言語やJavaの感覚でポインタ演算を自作したレガシーコードの移植を行う際、このストレージ境界違反が頻発する。

B. パックデシマル(COMP-3)の符号反転と文字列スキャン

金融系システムで多用される `FIXED DEC`(パックデシマル)のデータを、誤って文字型(`CHAR`)にキャストした上で `INDEX` や `VERIFY` に放り込むケースがある。
パックデシマルの下位4ビットには符号(`C`, `D`, `F` など)が格納されているため、これが意図しないバイナリ値となり、スキャンアルゴリズムを狂わせる。特にDB2のインラインビューから取得したホスト変数をPL/I側で受ける際、定義ミスマッチがあると、`VERIFY` が予期せぬ位置でヒットし、データ破損判定の誤作動(サイレント・データコラプション)に繋がる。

C. CICSオンラインおよび埋め込みSQL(DB2)環境でのエッジケース

CICSのトランザクション内(コミット境界前)において、大量のテキストデータを `INDEX` で繰り返しスキャンするバッチライクな処理を実装した場合、CPUタイムアウト(AICAアベンド)のリスクが生じる。
また、DB2の `VARCHAR` カラムを `INDEX` で検索する場合、データベース側の文字コード(EBCDICのCCSID 930やシフトJIS等)と、PL/Iプログラム側のコンパイル時コードページ設定(`CODEPAGE` コンパイラオプション)が一致していないと、全角文字の2バイト目と半角文字のコードポイントが衝突し、文字化けによる検索漏れが発生する。マイグレーション時には、ターゲット環境(Linux/x86のUTF-8等)における文字長とバイト長の不一致(例:日本語1文字のバイト数差異)がそのまま致命傷になるため、移行ツールの自動変換を鵜呑みにせず、このスキャンロジックを徹底的に洗い出す必要がある。

—

4. レガシー移行(マイグレーション)アーキテクトへの提言

JavaやC#、あるいはクラウドネイティブなマイクロサービスへと基幹システムを移行するプロジェクトにおいて、PL/Iの `INDEX` や `VERIFY` が担っていたロジックをどうモダン化すべきか。

1. セマンティクスの完全な移換:
Javaの `String.indexOf()` は「見つからないと `-1`」を返し、C#の `IndexOf()` も同様である。一方、PL/Iの `INDEX` は `0` である。この「-1 vs 0」の差異を自動変換ツールだけに頼ると、条件分岐の反転バグ(オフバイワンエラー)が大量に産出される。移行先言語のラッパーライブラリとして、PL/Iの仕様を完全模倣したヘルパー関数を用意するのが、アーキテクトとしての定石だ。
2. パフォーマンスの再検証:
メインフレームのハードウェア支援(ベクトル命令)に最適化されていたスキャン処理を、オープン系の汎用CPU上で単純なループに置き換えると、バッチ処理時間が数倍に膨れ上がるケースがある。大量データを扱うバッチでは、正規表現やネイティブな文字列探索アルゴリズム(KMP法やBoyer-Moore法など)を適用した等価な高パフォーマンスコンポーネントを設計段階で定義すべきである。

PL/Iのコードは古い遺物ではない。そこには、数十年かけて洗練されたハードウェアとソフトウェアの極限の最適化が刻み込まれている。その挙動の深層を理解した者だけが、安全かつ確実な次世代へのブリッジを架けることができるのだ。

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