PL/I SUBSTR組み込み関数の深淵:内部挙動、引数省略、そして基幹システムの信頼性
IBMメインフレームという巨艦の上で、半世紀以上にわたり基幹システムを支え続けてきたPL/I。そのコードの隅々には、一見シンプルに見えても、奥深いアーキテクチャの洞察と熟練のプログラマの知恵が息づいています。現代のJavaやC#といった言語への移行を検討する際、表面的な構文の翻訳だけでは到底捉えきれない、PL/I独自の振る舞いやメモリ管理の妙を理解することが不可欠です。
今回は、PL/Iプログラマならば誰もが一度は使うであろう、文字列操作の要である`SUBSTR`組み込み関数に焦点を当てます。特に、その第3引数を省略した場合の挙動、固定長と可変長文字列における内部メモリ操作の違い、そしてそれがアベンドや移行プロジェクトにもたらす影響について、基幹システムの極限の信頼性と移行設計の観点から深く掘り下げて解説します。
SUBSTRの基本と第3引数省略の真意
ご存知の通り、PL/Iの`SUBSTR`組み込み関数は、文字列の一部を切り出すために使用されます。その一般的な形式は次の通りです。
`SUBSTR(string, position, length)`
- `string`: 切り出し元の文字列式。
- `position`: 切り出し開始位置(1から始まる)。
- `length`: 切り出す文字数。
このうち、`length`、すなわち第3引数を省略した場合の挙動は、PL/Iの設計思想とコンパイラの緻密な仕事ぶりを如実に物語っています。マニュアルには「`position`から`string`の終わりまでの文字列が返される」と簡潔に記されていますが、その裏側では何が起きているのでしょうか。
第3引数省略時の内部計算
`SUBSTR(string, position)`と記述された場合、コンパイラは内部的に`length`を次のように計算します。
`LENGTH(string) – position + 1`
この計算結果が、`position`から`string`の末尾までの文字数として採用されます。この一見自明な挙動は、実は多くの基幹システムで暗黙の前提として利用されており、もしこの計算が期待通りに行われなかった場合、あるいは`position`が`string`の長さを超えていた場合、予期せぬ結果や致命的なアベンドを引き起こす可能性があります。
例えば、以下のコードを考えてみましょう。
DCL ORIGINAL_STR CHAR(20) VARYING INIT(‘IBM MAINFRAME’);
DCL RESULT_STR CHAR(20) VARYING;
RESULT_STR = SUBSTR(ORIGINAL_STR, 5);
/ RESULT_STR には ‘MAINFRAME’ が格納されることを期待 /
/ 内部的には LENGTH(ORIGINAL_STR) = 13 /
/ length = 13 – 5 + 1 = 9 と計算される /
この処理自体は非常にシンプルですが、`ORIGINAL_STR`が`VARYING`属性を持つことに注目してください。ここから、固定長文字列と可変長文字列で`SUBSTR`の内部挙動がどう変わるのかを見ていきます。
固定長文字列(FIXED CHAR)と可変長文字列(VARYING CHAR)のメモリ操作
PL/Iが真にその本領を発揮するのは、データ型とメモリレイアウトの厳密な制御を可能にする点にあります。`SUBSTR`関数も例外ではなく、ターゲットが`CHAR(n)`(固定長)であるか`CHAR(n) VARYING`(可変長)であるかによって、コンパイラの生成する機械語コードは大きく異なります。
固定長文字列(FIXED CHAR)の場合
`DCL MY_FIXED_STR CHAR(10) INIT(‘ABCDE’);`
この宣言では、`MY_FIXED_STR`はメモリ上に常に10バイトの領域を確保します。初期値が5バイトなので、残りの5バイトはスペースで埋められます(EBCDICではX’40’)。
`SUBSTR(MY_FIXED_STR, 3, 4)` のような操作は、メモリ上の`MY_FIXED_STR`の先頭アドレスからオフセットを計算し、指定された長さのバイト列を直接コピーする操作に変換されます。これは、非常に高速な`MVC`(Move Character)命令などによって実現されることがほとんどです。
DCL FIXED_STR CHAR(10) INIT(‘1234567890’);
DCL SUB_FIXED CHAR(5);
SUB_FIXED = SUBSTR(FIXED_STR, 3, 5);
/ SUB_FIXED には ‘34567’ が格納される /
/ 内部的には FIXED_STR のアドレス + 2バイト目から5バイトをコピー /
第3引数を省略した場合も同様で、`LENGTH(FIXED_STR)`は常に宣言された長さ(この例では10)として扱われるため、`position`が1の場合を除き、後続のスペースも切り出し対象に含まれます。
可変長文字列(VARYING CHAR)の場合
`DCL MY_VARYING_STR CHAR(10) VARYING INIT(‘ABCDE’);`
`VARYING`属性を持つ文字列は、メモリ上では宣言された最大長(この例では10バイト)に加えて、その現在の長さを示す2バイトのプレフィックスが先頭に付与されます。
| 2バイトの長さフィールド | データ本体(最大10バイト) |
| :———————- | :————————— |
| X’0005′ | ‘ABCDE’ + 5バイトの未使用領域 |
`LENGTH(MY_VARYING_STR)`関数が呼ばれると、この2バイトの長さフィールドが参照され、現在の有効長(この例では5)が返されます。
`SUBSTR`が`VARYING`文字列を操作する場合、コンパイラは以下の点を考慮したコードを生成します。
1. ソース文字列の長さフィールドの参照: `position`や`length`の計算に`string`の現在の有効長を使用します。
2. 結果文字列の長さフィールドの生成: 切り出された結果の文字列も`VARYING`である場合、その新しい有効長を計算し、2バイトの長さフィールドを適切に設定します。
3. メモリコピー: 長さフィールドを除いたデータ本体部分のみをコピーします。
DCL VARYING_STR CHAR(10) VARYING INIT(‘PL/I IS GREAT’); / 現在の長さ 13 /
DCL SUB_VARYING CHAR(10) VARYING;
SUB_VARYING = SUBSTR(VARYING_STR, 6); / 第3引数省略 /
/ VARYING_STR の現在の長さは 13 /
/ position = 6 /
/ length = 13 – 6 + 1 = 8 と計算される /
/ SUB_VARYING には ‘IS GREAT’ が格納され、長さフィールドは 8 となる /
もし`VARYING_STR`が空文字列(`”`)であった場合、長さフィールドは`X’0000’`となります。`SUBSTR(VARYING_STR, 1)`とすれば、長さは`0 – 1 + 1 = 0`と計算され、結果も空文字列になります。この挙動は非常に堅牢ですが、`position`が`LENGTH(string)`を超えた場合(例えば、空文字列に対して`SUBSTR(VARYING_STR, 2)`)、`STRINGSIZE`条件が発生する原因となります。
一時ストレージとポインタの活用
`SUBSTR`関数は、その結果を一時的なストレージに生成することが多々あります。特に複雑な式の中で`SUBSTR`がネストされていたり、左辺値として使用されたりする場合、コンパイラは一時的なシステム管理領域(DSA: Dynamic Storage Areaなど)を確保し、そこに結果を格納します。
PL/Iの真骨頂は、`POINTER`型と`BASED`変数を用いた動的なメモリ操作です。これにより、プログラムは実行時に任意のサイズの文字列領域を確保し、`SUBSTR`などの操作で得られた結果を直接そこに格納したり、ポインタを介して参照したりすることが可能です。
DCL 1 MY_RECORD BASED(PTR),
2 REC_LEN FIXED BIN(15) UNSIGNED, / 可変長レコードの長さ /
2 REC_DATA CHAR(255) VARYING; / 可変長データ /
DCL PTR POINTER;
DCL (LEN_VAL) FIXED BIN(15) UNSIGNED;
/ 動的にメモリを確保 /
ALLOCATE MY_RECORD;
PTR->REC_DATA = ‘Dynamic Data String’;
PTR->REC_LEN = LENGTH(PTR->REC_DATA); / 長さフィールドは自動更新されるが、明示的に設定するケースも /
LEN_VAL = PTR->REC_LEN; / 現在の長さを取得 /
IF LEN_VAL > 10 THEN DO;
/ ポインタを介して SUBSTR を利用 /
PUT SKIP LIST(‘Part of dynamic data: ‘ || SUBSTR(PTR->REC_DATA, 1, 10));
END;
FREE PTR->MY_RECORD; / メモリを解放 /
ポインタと`BASED`変数を活用することで、`SUBSTR`は物理メモリ上の任意の位置にある文字列データを効率的に操作できます。これはJavaやC#の`String`がイミュータブル(不変)であることとは対照的で、PL/Iがいかに低レベルなメモリ操作を許容しているかを示す一例です。移行時には、このような動的メモリ管理がJava/C#のどのようなクラスやライブラリで置き換えられるかを慎重に検討する必要があります。
パフォーマンスと最適化:コンパイラオプションの威力
PL/Iのコンパイラ、特にIBM Enterprise PL/I for z/OSは、非常に高度な最適化機能を備えています。`SUBSTR`の挙動も、コンパイラオプションによってその性能が大きく左右されることがあります。
OPTIMIZEオプション
`OPTIMIZE(0 | 1 | 2 | 3)`オプションは、生成されるコードの最適化レベルを制御します。`OPTIMIZE(3)`のような高レベル最適化が指定された場合、コンパイラは`SUBSTR`呼び出しを可能な限りインライン展開し、冗長なロード/ストア命令を削減し、CPUレジスタの利用効率を最大化しようとします。
例えば、ループ内で`SUBSTR(S, P, L)`が頻繁に呼ばれるようなケースで、`P`と`L`が定数であれば、コンパイラはループの外部で一度だけオフセット計算を行い、ループ内では直接`MVC`命令を発行するように最適化する可能性があります。しかし、`P`や`L`がループ内で変化する変数である場合、毎回計算が必要となるため、最適化の度合いは低下します。
STRINGSIZE / STRINGRANGE 条件
PL/Iは実行時エラーを検出するための「条件」メカニズムを持っています。`STRINGSIZE`と`STRINGRANGE`は、`SUBSTR`や`UNSUBSTR`操作における文字列の長さに関するエラーを検出します。
- `STRINGSIZE`: `SUBSTR(string, position)`のように第3引数を省略した場合、または`position`と`length`の組み合わせが`string`の長さを超える場合に発生します。
- `STRINGRANGE`: `position`が1未満、または`position`が`LENGTH(string)`を超える場合に発生します。
これらの条件は、デフォルトでは`ON STRINGSIZE SYSTEM;`や`ON STRINGRANGE SYSTEM;`となっており、エラーが発生するとプログラムはABENDします。しかし、`ON STRINGSIZE GO TO ERROR_HANDLER;`のように独自のハンドラを定義することで、プログラムの異常終了を防ぎ、エラーリカバリーを行うことが可能です。
`CHECK`オプションや`NOCHECK`オプションは、これらの条件チェックを有効にするか無効にするかを制御します。`NOCHECK`を指定すると、実行時オーバーヘッドが削減されパフォーマンスが向上しますが、その代わり、不正な文字列操作が発生した場合に検出されず、予測不能なメモリ破壊やアベンドに直結するリスクが高まります。基幹システムでは、信頼性確保のため`CHECK`を有効にすることが多いですが、性能がクリティカルな部分では`NOCHECK`を慎重に検討することもあります。
アベンド解析とエッジケース対策
基幹システムにおけるトラブルシューティングは、メインフレームアーキテクトの腕の見せ所です。`SUBSTR`に関連するアベンドは、多くの場合、文字列長の不整合や不正なポインタ操作に起因します。
ダンプ解析の要諦
ABEND S0C4(保護例外)やS0C1(演算例外)が発生した場合、ダンプ解析は不可欠です。`SUBSTR`が絡むケースでは、以下の点に注目します。
1. レジスタ R1, R13, R14, R15:
- R1: 引数リストのアドレスを指すことが多い。`SUBSTR`に渡された文字列のアドレスや長さフィールドを特定する手がかりになる。
- R13: ワークエリア(SAVE AREA)のアドレス。スタックフレームを辿る際に重要。
- R14: 呼び出し元に戻るアドレス。ABEND発生箇所を特定する。
- R15: 戻りコードまたはベースレジスタとして使われる。
2. PSA (Program Status Area) と TCB (Task Control Block): ABENDコード、PSW(Program Status Word)、中断レジスタ情報など、システム全体の状況を把握する。
3. コンパイラリストとの突き合わせ: `SYSPRINT`に出力されたコンパイラリストとダンプを突き合わせることで、ABEND発生時の命令アドレス(PSWのInstruction Address)がソースコードのどの行に対応するかを特定します。特に`SUBSTR`の呼び出し元や、その引数となる変数の内容を確認します。
4. 文字列変数の内部構造: ダンプ内で`DCL`された文字列変数の領域を探し、そのEBCDIC値とHEX値を比較します。
- `CHAR(n)`であれば、期待通りの内容か、予期せぬヌル文字や制御文字が混入していないか。
- `CHAR(n) VARYING`であれば、2バイトの長さフィールドが正しい値を示しているか、データ本体の長さと一致しているか。これが不正だと、`SUBSTR`内部で不正なアドレス参照を引き起こす可能性があります。
特に、`STRINGSIZE`条件が`NOCHECK`で無効化されている環境では、本来エラーになるべき不正な`SUBSTR`操作がそのまま実行され、その後、全く関係ない場所でS0C4が発生してダンプ解析を困難にすることがあります。これは、メモリのどこかに不正な書き込みが行われた結果、後になってそれが顕在化する「時限爆弾」のようなものです。
DB2/CICS環境でのエッジケース
埋め込みSQL(DB2)やCICSオンライン処理は、PL/Iプログラムにさらなる複雑性をもたらします。
- DB2ホスト変数とVARYING文字列: DB2の`VARCHAR`型は、PL/Iの`CHAR VARYING`と直接マッピングされます。しかし、`SUBSTR`で切り出した結果をDB2のホスト変数に格納する際、長さフィールドの扱いが非常に重要です。PL/I側で`SUBSTR`の結果を`CHAR VARYING`に代入すると、長さフィールドは自動で更新されますが、もし`CHAR`(固定長)に代入してしまい、その後DB2に渡すと、予期せぬ長さでデータが更新される可能性があります。
- CICS COMMAREAの連携: CICSの`COMMAREA`は、トランザクション間でデータを引き渡すための固定長領域です。`CHAR VARYING`型のデータを`COMMAREA`に直接配置すると、2バイトの長さフィールドも含まれることになります。`SUBSTR`で`COMMAREA`内の文字列を操作する場合、常にその物理的なオフセットと長さを意識する必要があります。特に、COMMAREAのサイズが不足しているにも関わらず`SUBSTR`でデータを作成しようとすると、COMMAREA領域を超えてメモリ破壊を引き起こし、CICSタスクのアベンド(ASRAなど)に繋がります。
パックデシマル(P-DEC)の符号反転バグ(補足)
`SUBSTR`とは直接関係ありませんが、PL/Iの深い知見を示すために、基幹システムの移行で遭遇しがちな別の「闇」を一つ紹介します。符号付きパックデシマル(`PIC ‘9(n)V9(m)S’`)で定義された数値フィールドを、`HIGH_VALUE`などで初期化した場合、その内部符号が`X’F’`(正の符号)ではなく`X’D’`(負の符号)になるという、一部のコンパイラバージョンやOS環境で確認された現象です。
DCL P_DEC_FIELD PIC ‘9(5)S’;
P_DEC_FIELD = HIGH_VALUE; / 内部的には X’99999D’ となるケースがあった /
/ 期待されるのは X’99999F’ /
このフィールドに対して後続で算術演算を行うと、結果が不正になったり、S0C7(データ例外)アベンドが発生したりすることがありました。これはPL/Iが数値の内部表現をバイト単位で操作できる`UNSPEC`組み込み関数などを持っているからこそ、その内部表現の「揺らぎ」が問題となる典型例です。マイグレーション時には、このような内部表現の依存性まで含めて、厳密なデータ検証が求められます。
レガシー移行への示唆
PL/Iの`SUBSTR`関数は、単なる文字列切り出しではありません。それは、メインフレームのメモリ管理、コンパイラの最適化、実行時エラー処理、さらにはプログラム間の連携メカニズムまでを内包する、複雑なシステムの一端です。
JavaやC#といったモダンプラットフォームへの移行において、PL/Iの`SUBSTR`を単純に`String.substring()`メソッドに置き換えるだけでは不十分です。
1. 文字列のミュータビリティ: Java/C#の`String`はイミュータブル(不変)です。`substring()`は常に新しい文字列オブジェクトを生成します。PL/Iの`SUBSTR`は、左辺値として使用することで元の文字列の一部を直接変更できます(`SUBSTR(MY_STR, 1, 5) = ‘HELLO’;`)。このような操作は、Java/C#では`StringBuilder`や`char[]`を介した全く異なるアプローチが必要になります。
2. 固定長/可変長の違い: Java/C#にはPL/Iのような`CHAR VARYING`の概念は直接ありません。`String`型は可変長ですが、その内部実装はPL/Iとは異なります。移行元で`FIXED CHAR`と`VARYING CHAR`が混在している場合、Java/C#側での適切なデータ型選択と、スペース埋め/トリム処理のハンドリングが重要です。
3. ポインタとメモリ操作: PL/Iのポインタと`BASED`変数によるメモリ直接操作は、Java/C#では基本的に不可能です。低レベルなバイナリデータ操作が必要な場合、Javaの`ByteBuffer`やJNI(Java Native Interface)の使用を検討することになりますが、これは設計と実装の複雑性を大幅に増大させます。
まとめ
`SUBSTR`組み込み関数は、PL/Iの奥深さを象徴する機能の一つです。第3引数省略時の挙動から始まり、固定長/可変長文字列のメモリレイアウト、ポインタと`BASED`変数による動的メモリ管理、コンパイラ最適化、そしてアベンド時のダンプ解析に至るまで、その挙動の全てはメインフレームアーキテクチャと密接に結びついています。
レガシーシステムの移行は、単に言語を翻訳する作業ではありません。それは、半世紀にわたって培われた言語の思想、その背後にあるハードウェアの特性、そしてそれらを最大限に活用してきたプログラマの知恵を、新しいプラットフォームのパラダイムへと再構築する壮大なプロジェクトです。`SUBSTR`一つをとっても、その真髄を理解し、現在のシステムがどのようにその恩恵を受け、またその制約を回避してきたのかを洞察することが、移行成功への鍵となるでしょう。
表面的な知識に惑わされず、基幹システムの深層に潜む知恵を掘り起こすこと。これこそが、私たちメインフレームスペシャリストに求められる最後の仕事であり、次世代へと繋ぐべき貴重な遺産なのです。
