整数型の本質を見誤るな:`FIXED BINARY(15)` と `FIXED BINARY(31)` の深淵
メインフレームの基幹システムを支えるPL/I。そのコードベースに長年向き合ってきたアーキテクトであれば、データ定義のわずかな揺らぎが、深夜のバッチ処理で突如発生するS0C7やS0C4、あるいはもっと厄介な「サイレントな数値化け」を引き起こす現場を幾度となく目撃しているはずだ。
JavaやC#といったモダン言語の `short` や `int` に慣れきった若いエンジニアが、安易な気持ちでPL/Iの数値を書き換え、あるいはレガシーマイグレーションの自動変換ツールに全幅の信頼を置いた結果、本番稼働直後に痛い目を見る。その大半の原因は、PL/I特有の「予約語を持たない柔軟な構文」と「厳密すぎる算術精度(Precision)のルール」のミスマッチにある。
今回は、PL/Iの数値データ型の基本でありながら、極限の信頼性が求められる現場で最も深くまで理解していなければならない `FIXED BINARY(15)` と `FIXED BINARY(31)` の内部表現、演算時のオーバーフローの罠、そしてマイグレーションやCICS/DB2環境でのエッジケースについて、システムアーキテクトの視点から徹底的に紐解いていこう。
—
1. 内部バイナリ表現のアーキテクチャ:ハーフワードとフルワードの現実
まず、IBM Z(System/390アーキテクチャ)のハードウェアレベルに立ち返る。
PL/Iの `FIXED BINARY(p)` における精度($p$)は、ビット数ではなく「2進数の桁数(符号部を除く)」を指すという、C言語やJavaの感覚でいると強烈なしっぺ返しを喰らう仕様を持っている。
- `FIXED BINARY(15)`:
- 内部的には2バイト(16ビット)のハーフワード(Halfword)としてアロケートされる。
- 最高15桁の2進数+1ビットの符号部(最上位ビット:MSB)で構成され、表現可能な値の範囲は `-32768` から `+32767` となる。
- `FIXED BINARY(31)`:
- 内部的には4バイト(32ビット)のフルワード(Fullword)としてアロケートされる。
- 最高31桁の2進数+1ビットの符号部で構成され、表現可能な値の範囲は `-2147483648` から `+2147483647` となる。
ここで重要なのは、コンパイラ(Enterprise PL/Iなど)のデフォルト挙動だ。精度を省略して単に `FIXED BINARY` と書いた場合、コンパイラは環境依存ではなく、通常は `FIXED BINARY(15)` として扱う(コンパイラオプションの `RULES(NOLABELS)` や `DEFAULT` 設定に依存するが)。この「暗黙の15ビット化」が、件数カウントやループ変数のオーバーフローを引き起こす温床となる。
実際に動的なメモリ操作で確認する内部表現
ベース変数とポインタを用いたスト構造体の中で、これらのバイナリ値がどのようにメモリ上に配置されているか、実務でよく使われるコーディングパターンを見てみよう。
DCL 1 APPL_CONTROL_BLOCK BASED(P_ACB),
5 ACB_STATUS FIXED BIN(15), / 2バイトのステータスコード /
5 ACB_COUNTER FIXED BIN(31); / 4バイトのトランザクション件数 /
DCL P_ACB POINTER;
DCL MY_STORAGE CHAR(6) BASED;
/ 動的ストレージの獲得 /
ALLOCATE MY_STORAGE;
P_ACB = ADDR(MY_STORAGE);
/ 値の設定 /
ACB_STATUS = 100;
ACB_COUNTER = 150000;
この時、メモリダンプ(CEEDUMPやSYSUDUMP)を採取すれば、`ACB_STATUS` が `0064`(16進数)として2バイトに収まり、続く `ACB_COUNTER` が `000249F0` として4バイトにわたって綺麗にビッグエンディアン(System/390のネイティブ)で格納されているのが確認できるはずだ。この物理レイアウトを把握していなければ、CICSの通信領域(COMMAREA)や、COBOLプログラムとのデータ連携時におけるバイナリ整合性(Alignment)エラーの泥沼から抜け出すことはできない。
—
2. 演算時のオーバーフロー発生条件とコンパイラ最適化の罠
PL/Iの最も恐ろしい(そして同時に強力な)特徴は、「式評価時の自動スケーリングと中間結果の精度拡張」にある。しかし、これが予期せぬアベンド(ABEND)を誘発する。
例えば、以下のようなコードを考えてみてほしい。
DCL W_PRICE FIXED BIN(15) INIT(30000);
DCL W_QTY FIXED BIN(15) INIT(10);
DCL W_TOTAL FIXED BIN(31);
/ 乗算の実行 /
W_TOTAL = W_PRICE W_QTY;
数学的に考えれば、30000 × 10 = 300,000 であり、`FIXED BIN(31)` の範囲内(約21億まで)なのだから余裕で収まるはずだ。しかし、PL/Iの言語仕様(ANSI/ISO規格)では、「乗算の中間結果の精度は、オペランドの精度の和になる」というルールが存在する。
`FIXED BIN(15)` と `FIXED BIN(15)` を掛け合わせると、中間結果は一時的に精度30、すなわち `FIXED BIN(30)` として扱われる。これがコンパイラオプションや周囲の文脈によっては、ハードウェアのレジスタ演算の境界を超え、思わぬオーバーフロー例外(System Completion Code: `0C7` や算術例外系ABEND)を引き起こす引き金になる。特に `SIZE` 条件が有効化されている環境(`DEFAULT(SIZE)` やプロシージャ内の `ON SIZE` ユニット)では、代入先の変数が大きくても、中間演算の段階で即座に割込みが発生する。
コンパイラ最適化(OPTIMIZE)との関係
Enterprise PL/Iコンパイラで `OPTIMIZE(2)` などを指定してビルドすると、コンパイラは冗長な境界チェックや中間レジスタの退避を削ぎ落とす。
ここで `FIXED BIN(15)` 同士の演算結果を `FIXED BIN(15)` の変数に代入するようなコードを書いていると、最適化エンジンが「この範囲ならオーバーフローしない」と勝手に決め打ちし、実行時チェックをバイパスした結果、ラップアラウンド(値の巻き込み)を起こしてマイナス値に反転するという、最も発見が困難なバグを生み出す。
—
3. レガシー移行(Java / C#)における致命的なエッジケース
我々システムアーキテクトが、PL/IからJava(あるいはC#)へのマイグレーションプロジェクトを率いる際、最も慎重にならなければならないのが、この `FIXED BINARY(15)` と `FIXED BIN(31)` の「型マッピング」である。
- Javaの `short` (16bit) / `int` (32bit) への単純置換は、地雷原を裸足で歩くようなものだ。
- Javaの `int` は常に符号付き32ビットであり、PL/Iの `FIXED BIN(31)` と1対1で一致する。しかし、PL/Iの `FIXED BIN(15)` をJavaの `short` にマッピングした場合、Java側で算術演算を行った際の中間型昇格(Numeric Promotion)の挙動がPL/Iとは微妙に異なる。
- さらに厄介なのが、埋め込みSQL(DB2 for z/OS)やCICSのデータマップ(BMS)とのインタフェースだ。
DB2ホスト変数における罠
DB2のテーブル定義で `SMALLINT` となっている列はPL/I側では `FIXED BIN(15)` に、`INTEGER` は `FIXED BIN(31)` にマップするのが定石である。しかし、レガシーコードの中には、データベース側が `INTEGER` であるにもかかわらず、PL/I側で手抜きをして `FIXED BIN(15)` で受けているケースが散見される。
運良く値が小さいうちは動くが、データ量が増えて `32767` を超えた瞬間、DB2プリコンパイラやランタイムはエラーを吐くか、最悪の場合、下位16ビットだけが切り捨てられてサイレントに不正データをDBに書き込む。マイグレーション先のJava(JDBC)では、型不一致による `SQLException` が即座に飛ぶため、移行後に「PL/Iでは動いていたのにJavaだと落ちる」という不毛なクレームの原因になる。
—
4. ダンプ解析の現場から:アベンド発生時のトリアージ手法
深夜、本番バッチが `ABEND S0C7`(データ例外)で強制終了したという連絡が入る。
レガシーシステムの現場において、これほどアドレナリンが分泌される瞬間はない。CEEDUMPを手元に開き、PSW(Program Status Word)とレジスタを睨みつける。
もし原因がデシマルの符号反転バグ(例えば、パックデシマル `COMP-3` に不正なゾーンビットが混入したケースなど)であれば比較的早期に特定できるが、これが `FIXED BINARY` に起因する算術例外(`S0C1` やハードウェア例外)の場合、アベンドアドレスの逆アセンブラコード(LH, STH, C, CVB などの命令)を追う必要がある。
- `LH`(Load Halfword)命令で `FIXED BIN(15)` のデータをレジスタにロードしようとした際、参照先のアドレッシングエラーや、アライメント境界違反(Odd Address)が起きると `S0C4`(保護例外)や `S0C6`(仕様例外)となる。
- 特に、ポインタ経由で動的に割り当てられたストラクチャにおいて、オフセット計算を誤り、`FIXED BIN(31)` の領域の途中から `FIXED BIN(15)` として無理やり値を読み込もうとした場合、メモリ上のビット列が全く意図しない巨大なマイナス値として解釈され、その後の分岐処理(IF文の条件判定)を狂わせる。
このような障害に遭遇した時、我々アーキテクトは単に「ソースコードの型定義を直す」だけでは不十分だ。ストレージダンプから該当アドレスの16進数パターンを抽出し、それが本来どのようなバイナリ表現であるべきだったのかを即座に逆算できるだけの、ハードウェアレベルの知見が問われる。
—
5. 結びにかえて:レガシーの理屈を飲み込み、次世代へ継承する
PL/Iの `FIXED BINARY(15)` と `FIXED BIN(31)` は、単なる「数字を入れる箱」ではない。それはIBMメインフレームのアーキテクチャと密に結合し、CPUのレジスタとメモリバスの効率を極限まで高めるために設計された、洗練されたハードウェアの直示表現である。
予約語を持たないがゆえに自由度が高く、それゆえに書き手の技量がコードの生死を分けるPL/Iの世界。これからモダン言語へのマイグレーションを推進するテックリードであれ、現行のバッチを保守するアーキテクトであれ、この「ビットと精度の厳密な関係」を骨肉まで染み込ませておくことこそが、システム全体の信頼性を担保する唯一にして最大の防壁となる。
レガシーの呪縛を恐れるな。その内部表現の理屈を完全に支配下に置いた時、どんな巨大な基幹システムであっても、あなたのコントロール下から外れることは決してないのだから。
