おい、そこの君。ちょっと手を止めて画面を見てくれ。
今、君が修正しようとしているそのPL/Iのバッチプログラム、`SUBSTR`を代入文の左辺(=イコールの左側)に置いていないか?
「あ、はい。文字型項目の特定の位置を書き換えるために、普通に使ってますけど……何か?」なんて、涼しい顔をして返事をされると、おじさんは冷や汗が出るんだよ。
何を隠そう、この`SUBSTR`を左辺値として使う書き方は、PL/Iという言語の懐の深さ――いや、「何でもできてしまうがゆえの魔力」を象徴するものだ。C言語のポインタ操作や、COBOLの`MOVE CORRESPONDING`のノリで安易に扱うと、本番稼働した途端に謎のS0C4(ストレージ保護例外)や、データ破壊という名のホラーショーを引き起こす。
今日は、長年このメインフレームの荒波にもまれてきた私が、`SUBSTR`組み込み関数の左辺値としての実態、そしてコンパイラが裏でこっそりやっている「内部オフセット計算」と「記述子(Descriptor)」の秘密について、みっちり叩き込んでやろう。心して聞くように。
—
1. PL/Iの「予約語レス」な世界とSUBSTRの正体
まず大前提として、PL/IにはC言語やJavaのような「強固な予約語(Keywords)」という概念がほとんどない。`IF`や`DO`といった制御文ですら、文脈によってはただの変数名や識別子として定義できてしまう。
そんな自由度の高いPL/Iにおいて、`SUBSTR`は組み込み関数(Built-in Function)として提供されている。通常、関数とは右辺に置いて「値を取り出す」ものだ。例えばこうだな。
1
DCL WORK_AREA CHAR(100) INIT(‘IBM_MAINFRAME_SYSTEM’);
DCL EXTRACT_STR CHAR(8);
EXTRACT_STR = SUBSTR(WORK_AREA, 5, 8); / 右辺値としての利用:5文字目から8文字分を取得 /
これ自体は初級の基礎だ。しかし、PL/Iの恐ろしいところ(そして強みなところ)は、文字列(CHARACTER)変数に限って、この`SUBSTR`を代入文の左辺に置くことができる点にある。
1
/ 左辺値としての利用:WORK_AREAの5文字目から8文字分を上書きする /
SUBSTR(WORK_AREA, 5, 8) = ‘Z/OS___’;
COBOLで同じことをやろうとしたら、`STRING`文を書くか、あらかじめ領域を分解して再定義(`REDEFINES`)しなければならない。しかしPL/Iなら、変数の一部分だけをピンポイントでターゲットにして値を流し込める。一見すると非常にスマートで、ソースコードの行数を削減できるように見える。
だが、この「スマートさ」の裏で、コンパイラとランタイムはどれほどの苦労をしているか、君は想像したことがあるだろうか?
—
2. 内部オフセット計算と記述子(Descriptor)の裏側
コンパイラは、私たちが書いた`SUBSTR(WORK_AREA, 5, 8)`という記述を読んだとき、一体何をしているのか。
PL/Iの文字型変数には、大きく分けて「Varying(可変長)」と「Non-varying(固定長)」がある。特に厄介なのが、引数として渡される場合や、長さが動的に変わる文字列だ。
ここで登場するのが記述子(Descriptor)である。
メインフレームのPL/I環境(LE: Language Environment)では、文字列変数をサブルーチンに渡したり、複雑なスライス操作を行ったりするとき、データの「アドレス」「最大長」「現在の長さ」などをまとめた制御ブロック――すなわち記述子を裏で生成する。
`SUBSTR`を左辺に置いた場合、コンパイラは単に「指定されたメモリアドレスに文字をコピーする」だけではない。以下のステップを瞬時に実行している。
1. オフセットの動的計算: 第2引数(開始位置)に基づき、ベースアドレスからのバイトオフセットを計算する。もしここに変数が使われていれば、実行時(Runtime)にレジスタを使ったアドレス算術が発生する。
2. 長さの検証と切り詰め(Padding / Truncation): 第3引数(長さを表す式)と、右辺から代入されるデータの長さを比較する。右辺の方が短ければスペース埋め(Padding)が行われ、長ければ容赦なく切り詰め(Truncation)が発生する。
3. ストレージの直接書き換え: 計算された実アドレスに対し、直接バイト単位のストア命令(MVCなど)を発行する。
ここで気をつけなければならないのは、「もし開始位置や長さの計算を誤り、変数の定義領域を超えたメモリを指してしまった場合、コンパイルエラーにはならず、容赦なく隣のデータを破壊する」という点だ。メインフレームの基幹バッチでメモリ破壊が起きると、夜間バッチ全体がS0C4やS0C7で盛大にクラッシュし、運用オペレータから冷たい目で見られることになる。
—
3. 実践:VSAM入出力とONユニットにおけるSUBSTR左辺値の活用
百聞は一見にしかず。実際のメインフレーム開発の現場を想定した、実用的なPL/Iのサンプルコードを見せよう。
このプログラムは、VSAM(KSDS)から読み込んだレコードの特定エリア(例えば、電文のヘッダ部)にエラーフラグやタイムスタンプをピンポイントで埋め込み、再書き込みを行うバッチ処理の断片だ。さらに、万が一のデータ異常に備えて`ON`ユニットでトラップを仕掛けている。
1
PROCESS MAP,RENT,CICS;
SUBSTR_DEMO: PROC OPTIONS(MAIN);
/ 宣言部 /
DCL VSAM_INP_REC CHAR(200) RECORD; / VSAMレコード全体(200バイト) /
DCL ERR_FLAG CHAR(1) INIT(‘E’);
DCL TIMESTAMP_AREA CHAR(14);
DCL POS_OFFSET FIXED BIN(31) INIT(51); / 埋め込み開始位置を変数で制御 /
/ 組み込み関数の明示的宣言(プログラミング標準の基本) /
DCL SUBSTR BUILTIN;
DCL TIME BUILTIN;
DCL DATE BUILTIN;
/ 条件処理(ONユニット):データ例外や領域外参照への備え /
ON ERROR BEGIN;
PUT SKIP LIST(‘ 致命的なエラーまたはストレージ例外が発生しました ‘);
/ 実際にはここでログ出力や異常終了コードのセットを行う /
SIGNAL FINISH;
END;
/ タイムスタンプの生成 (YYYYMMDDHHMMSS) /
TIMESTAMP_AREA = DATE() || SUBSTR(TIME(), 1, 6);
/ ========================================================== /
/ ここが本日のメインテーマ:SUBSTRの左辺値としての利用 /
/ ========================================================== /
/ レコードの10文字目から3文字分にステータスコードを埋め込む /
SUBSTR(VSAM_INP_REC, 10, 3) = ‘NEW’;
/ 変数で指定されたオフセット位置(51文字目)からタイムスタンプを埋め込む /
/ コンパイラはここで裏側でオフセット計算用のコードを生成する /
SUBSTR(VSAM_INP_REC, POS_OFFSET, 14) = TIMESTAMP_AREA;
/ エラーフラグを特定のコントロール領域(190文字目)に立てる /
SUBSTR(VSAM_INP_REC, 190, 1) = ERR_FLAG;
/ 処理結果の確認用出力 /
PUT SKIP EDIT (‘UPDATED RECORD: ‘, SUBSTR(VSAM_INP_REC, 1, 100))
(A, A);
END SUBSTR_DEMO;
このコードを見て、君はどう感じる?
「変数 `POS_OFFSET` を使って動的に位置を指定できるなんて、すごく柔軟で便利じゃないか!」と思ったなら、まだ修行が足りないな。便利であることと、安全であることは別問題なのだ。
—
4. 現場のシニアが教える「SUBSTR左辺値」デバッグとコーディングの鉄則
最後に、現場で数々のバグを踏んできた私から、この構文を使う際の「鉄則」を授けよう。これを守らないと、次のバージョンアップや他機種からのマイグレーション(COBOLやJavaへの移行調査など)で確実に地雷を踏むことになる。
① マジックナンバーを絶対に直書きするな
サンプルでは説明のために `SUBSTR(VSAM_INP_REC, 10, 3)` と書いたが、実務のレイアウト変更が激しい基幹システムでこれをやると、仕様変更のたびにバグの温床になる。
レコードレイアウトは必ず `DECLARE` で構造体(ストラクチャー)として定義し、名前付きフィールドでアクセスするのが王道だ。どうしても部分修正が必要な場合のみ、十分なコメントと共に`SUBSTR`を左辺に使うこと。
② はみ出し(オーバーラン)の恐怖を忘れるな
左辺に指定した長さが、元の変数の定義長を超えていないか? コンパイラや実行環境によっては、パディングや切り捨てでよしなにしてくれる場合もあるが、古いコンパイラや最適化オプション(`OPTIMIZE(2)`など)の挙動によっては、メモリの境界外を叩いてS0C4を引き起こす。
特に、引数に式(`POS_OFFSET + 5` など)を使う場合は、事前に範囲チェック(Bounds Check)のロジックを入れるのが、プロとしてのプログラミング作法というものだ。
③ マイグレーション時の罠
もし将来的に、このPL/IプログラムをJavaや現代的な言語にリライト、あるいはマイグレーションする日が来たらどうなるか。
Javaの `String` や `StringBuilder` には、PL/Iの `SUBSTR(A, X, Y) = B` に直に対応する一撃の構文はない。substringで切り出して結合し直すか、char配列に一度バラす必要がある。つまり、「SUBSTRの左辺値代入」は、PL/Iに強く依存した高度な(=保守性の低い)書き方であるという自覚を持たなければならない。
—
どうだ、少しは `SUBSTR` の裏側の挙動が見えてきたか?
言語の仕様を知り尽くすということは、単にコンパイルエラーを通すことではなく、「コンパイラが裏で生成するマシン語やメモリの動きを頭の中でトレースできること」を意味する。
次の改修では、ただ動くだけのコードではなく、メモリの安全性を配慮した美しいコードを書いてくれよ。期待しているぞ。
