ゾンビシステムを蘇らせたアーキテクトの独り言:FIXED BINARYとFIXED DECIMALの迷宮
メインフレームのモダナイゼーションや、Java/C#へのマイグレーション現場に立ち会うたび、私はいつも同じ光景に出会う。
「なぜ、この単純な数値の代入でS0C7(データ例外)が起きるのか?」
「なぜ、移行先のオープン系システムで、金額の末尾が1円ズレるのか?」
犯人は大抵、PL/Iの根幹であり、同時に最も誤解されているデータ型――`FIXED BINARY`(固定小数点二進数)と`FIXED DECIMAL`(固定小数点十進数、いわゆるパック10進数)の暗黙の型変換、そして精度の境界条件にある。
IBM Enterprise PL/Iコンパイラは非常に優秀だが、プログラマが意図した「型」の裏で、コンパイラは勝手に中間変数を作り、勝手に四捨五入や切り捨てを行い、時にはメモリの深淵で予期せぬパディングや符号反転を引き起こす。今回は、この2つの数値型の相互変換ルールを、コンパイラの挙動、ダンプ解析、そしてDB2やCICSのエッジケースに至るまで、徹底的に解剖しよう。
—
1. FIXED BINARY と FIXED DECIMAL の基本思想とメモリ上の現実
まず、敵を知ることから始めよう。
- `FIXED DECIMAL(p, q)`:
IBM汎用機のハードウェア命令(PACK, UNPK, ZAPなど)と直接マッピングされる。1バイトに2桁の数字を詰め込み、最後の4ビットに符号(C, D, Fなど)を置く「パック10進数(Packed Decimal)」だ。金融計算において誤差が許されない場面では絶対の信頼を置かれている。
- `FIXED BINARY(p, q)`:
CPUのレジスタ演算に直結する二進数(半精度、単精度、倍精度)。純粋なバイナリ値であり、高速な演算が可能だが、10進数の世界との行き来には必ず「変換」というコストと、桁あふれ・丸め誤差の罠が伴う。
厄介なのは、PL/Iが「異なる型同士の演算や代入では、どちらかに型を合わせる(プロモーションする)」という仕様を持っている点だ。基本方針として、PL/Iは`FIXED DECIMAL`よりも`FIXED BINARY`を優先する傾向がある(あるいは演算の複雑さに応じてコンパイラが決定する)。この暗黙の変換が、夜間バッチの暗雲となる。
—
2. 相互変換の優先順位と「精度ロスト」の境界条件
算術演算や代入において、`FIXED BINARY`と`FIXED DECIMAL`が混在する場合、コンパイラは次のようなルールで内部表現を決定する。
1. 混合演算のルール:
`FIXED BINARY` と `FIXED DECIMAL` の二項演算では、原則として`FIXED DECIMAL`が一時的に`FIXED BINARY`に昇格(プロモート)されるか、あるいはその逆の精密なスケール調整が行われる。
2. 精度の逆転現象:
ここが最大のポイントだ。`FIXED BINARY(31, 0)` は約9桁(10進数)の表現力を持つ。一方、`FIXED DECIMAL(15, 2)` は15桁の整数部と2桁の小数部を持つ。
これらを演算させるとどうなるか? コンパイラは `FIXED BINARY` の精度(ビット幅)に合わせて中間領域を確保するため、`FIXED DECIMAL`側の有効桁数が強制的に切り詰められたり、オーバーフローを起こしてS0C7アベンドに直結する。
実務で頻発する危険なコード例
以下のPL/Iコードを見てほしい。一見、何の問題もないように見えるが、コンパイラの最適化と型変換の罠が潜んでいる。
DCL W_RATE FIXED BINARY(15, 0) INIT(3); / 金利係数 (2進数) /
DCL W_AMOUNT FIXED DECIMAL(11, 2) INIT(12345.67);/ 元金 (10進数) /
DCL W_RESULT FIXED DECIMAL(13, 2); / 結果格納用 /
/ 異なる型同士の掛け算と代入 /
W_RESULT = W_AMOUNT W_RATE;
何が起きているか?
`W_AMOUNT`(`FIXED DECIMAL(11,2)`)と `W_RATE`(`FIXED BINARY(15,0)`)が乗算される際、コンパイラは `W_RATE` を一時的な `FIXED DECIMAL` に拡張するか、あるいは両者を一時的なバイナリ領域に展開して計算する。
ここで問題になるのがスケール(小数点位置)の維持だ。`FIXED BINARY(15,0)` は小数の概念を持たないため、掛け算の結果、意図せぬ丸め(TRUNCATE)が発生するか、あるいはコンパイラオプション(`RULES(NOVALLIST)`など)の設定次第では、警告なしに精度が削ぎ落とされる。
—
3. ポインタとベース変数(BASED)を用いた動的メモリ操作における罠
レガシーな基幹システムでは、ストレージの節約や可変長レコードの処理のために、`BASED` ストレージとポインタを駆使してメモリを直接叩くことが多い。ここでも型変換の無慈悲なルールが適用される。
DCL P_BUF POINTER;
DCL 1 X_HEADER BASED(P_BUF),
3 X_ID FIXED BINARY(31, 0),
3 X_VAL FIXED DECIMAL(9, 2);
Dcl 1 Y_REC EXT,
3 Y_ID FIXED BINARY(31, 0),
3 Y_VAL FIXED BINARY(31, 0); / ここを誤ってBINARYにしてしまったとする /
/ ポインタ経由でデータをムーブする際、型の解釈違いが発生 /
Y_VAL = X_VAL; / FIXED DECIMAL から FIXED BINARY への暗黙の代入 /
`X_VAL` はメモリ上でパック10進数(5バイト)として存在しているが、これをポインタ経由や単純代入で `FIXED BINARY` の変数に受け渡す際、PL/Iランタイムは「パック10進数のゾーン/数値形式をバイナリの整数値へと変換するルーチン」を裏で走らせる。
もし、このときメモリ上の生データがパディングのゴミや不正なゾーンビット(例えば `X’00’` やスペース `X’40’` など)を含んでいた場合、変換ルーチンは容赦なくS0C7(Data Exception)を引き起こしてタスクを異常終了させる。
—
4. アベンド(ABEND:S0C7)発生時のダンプ解析アプローチ
システムが夜間バッチで突如S0C7で落ちたとき、シニアアーキテクトが真っ先に見るべきポイントを伝授しよう。
1. CEEDUMP または SYSUDUMP の取得:
まずはPSW(Program Status Word)のアドレスから、どの命令のどのステートメントで落ちたかを特定する。
2. 作業領域(WORKING-STORAGE相当)の逆算:
ダンプリストのストレージ・ダンプから、問題の変数が配置されているオフセットを確認する。
- `FIXED DECIMAL` の領域に、不正な文字データやバイナリのゴミが入り込んでいないか?
- 外部ファイルやDB2からのフェッチ時に、ホスト変数の定義(例: `PIC S9(9) COMP` -> `FIXED BINARY(31)`)と、実際のデータベース側の定義(例: `DECIMAL(9,0)` -> `FIXED DECIMAL(9,0)`)に乖離がなかったか?
3. コンパイラオプションの確認:
コンパイル時に `TRUNC(BIN)` が指定されているかどうかも重要だ。`TRUNC(STD)`(デフォルト)の場合、`FIXED BINARY(15)` の変数に真のバイナリ値(32767を超える値など)が入ると、ハードウェアのレジスタロード時に切り捨てや例外が発生する。この挙動の差異は、オープン系(Java等)への移行時にも致命的なバグの原因となる。
—
5. 埋め込みSQL(DB2)とCICSオンライン処理のエッジケース
基幹システムの心臓部であるDB2やCICSにおいて、この型変換の不一致はデータ破壊の引き金になる。
DB2ホスト変数での落とし穴
DB2の定義が `DECIMAL(7,2)` であるカラムに対し、PL/I側のホスト変数をうっかり `FIXED BINARY(31,0)` で定義したとする。
/ DB2: AMOUNT DECIMAL(7,2) に対し、ホスト変数を誤って定義 /
DCL HV_AMOUNT FIXED BINARY(31, 0);
EXEC SQL
SELECT AMOUNT INTO :HV_AMOUNT
FROM SALES_TABLE
WHERE SALE_ID = :W_ID;
DB2プリコンパイラはこれを許容することがあるが、実行時にDB2は10進数のデータを2進数に変換して渡そうとする。このとき、小数のスケール(2桁)がどこに消えるか?
多くの場合、小数部が丸められるか、あるいは予期せぬスケーリングエラー(SQLCODE -420など)を引き起こす。「金額を扱う項目は、DB2側がDECIMALであれ、PL/I側は必ず厳密に `FIXED DECIMAL` で受ける」。これが鉄則だ。
CICS画面(BMSマップ)とのインタフェース
CICSの画面から渡ってくる入力データは、基本的に文字(CHARACTER)の羅列だ。これを画面マップの定義から `FIXED BINARY` や `FIXED DECIMAL` に転記する際、画面上の未入力フィールド(空欄、つまり `X’00’` や `X’40’`)がそのまま数値変数に流れ込むと、一瞬でS0C7の餌食になる。
そのため、実務では以下のような「文字から数値への安全な変換サブルーチン」や `ON ERROR` / `ON CONVERSION` 割り込み処理を記述して防御壁を築く必要がある。
/ 変換エラー(コンバージョンエラー)のトラップ /
ON CONVERSION
BEGIN;
PUT SKIP LIST(‘数値変換エラーを検出しこグレースフルシャットダウンを試みます’);
W_SAFE_FLAG = ‘0’B;
GOTO ERROR_ROUTINE;
END;
/ 安全な代入処理 /
W_TARGET_DEC = FIXED(W_CHAR_INPUT, 9, 2);
—
6. マイグレーション(Java/C#)への提言:型安全性の幻想を打ち破る
今、多くの企業がメインフレームからJavaやC#への移行を進めている。しかし、「PL/Iの `FIXED DECIMAL` を Java の `java.math.BigDecimal` に、`FIXED BINARY` を `int` や `long` にそのまま置き換えればいいだろう」と考えているなら、それは大間違いだ。
1. 丸めモード(Rounding Mode)の差異:
PL/Iの切捨て・四捨五入のハードウェア挙動と、Javaの `BigDecimal` のデフォルトの丸めモード(`HALF_EVEN` など)の間には微妙な差異が存在する。金融システムにおいて「1円の差異」は監査上の大問題となる。移行ツールが自動生成したコードをそのまま信じてはならない。
2. オーバーフローの挙動:
PL/Iでは桁あふれ時に特定のON条件やハードウェア例外が発生するが、Javaのプリミティブ型(`int`, `long`)はオーバーフローしても黙ってラップアラウンド(値が一周してマイナスになる等)を起こす。この「サイレント・バグ」は、移行後の結合テストの終盤まで発見されないことが多い。
—
結びにかえて
PL/Iの `FIXED BINARY` と `FIXED DECIMAL` は、単なる「数値の入れ物」ではない。それは、何十年もの間、日本の経済を裏側で支え続けてきたハードウェアの物理的制約と、プログラマの知恵が織りなした「厳格な契約」の結晶である。
その背景にあるメモリ構造、コンパイラの最適化の癖、そして例外発生時のメカニズムを理解せずして、真のシステムアーキテクトとは言えない。
レガシーコードを紐解くとき、あるいはモダンな言語へリプレースするとき――この変数の裏で何が起きているのかを想像する目を、私たちは決して忘れてはならないのだ。
