現場の勘と経験がものをいう、PL/I算術ビルトイン関数の本当の話
おい、最近入った若手が、夜間バッチの金額計算でまた「S0C7(データ例外)」を踏み抜いたって聞いたぞ。ログを見たら、勘定系マスターの売上累計フィールドに対して、場当たり的に `FIXED(15,2)` のまま掛け算をかましてオーバーフローを起こしていた。
「先輩、なんでここで桁あふれするんですかね? 変数の定義はちゃんと合ってるはずなのに……」なんて頭を抱えていたが、ここがPL/Iという言語の奥深さであり、同時にレガシー保守の腕の見せ所なんだ。
現代のオープン系言語に慣れた連中からすると、PL/Iの変数は「ただの入れ物」に見えるかもしれない。だが、IBMメインフレームのPL/Iが扱うデータは、コンパイラがその精度(Precision)をコンパイル時にどう解釈し、中間結果をどう保持するかという「精度の伝播ルール」に支配されている。特に、金融や流通の基幹システムを扱う我々にとって、計算途中のオーバーフローや思わぬ切り捨て(精度の喪失)は、絶対に許されない致命傷だ。
今回は、演算結果の精度をプログラマが完全に手元でコントロールするための切り札、`ADD`、`SUBTRACT`、`MULTIPLY`、`DIVIDE` の4大算術ビルトイン関数について、実務の現場でそのまま使えるノウハウを叩き込んでやろう。
—
1. 予約語を持たないPL/Iの懐の深さと「罠」
まず大前提として、PL/IにはJavaやC#のような「固定された予約語(Reserved Words)」という概念がほとんどない。例えば、`IF` や `DO` といった制御文のキーワードであっても、文脈さえ許せば変数名として定義できてしまう(もちろん、そんな狂ったコーディングをする奴は現場から即座に干されるが)。
この「自由度の高さ」の裏返しとして、コンパイラはソースコードを解析する際、識別子(変数名や関数名)の文脈を非常にシビアに判定している。そして、今回取り上げる `ADD` などの算術関数も、コンパイラに対して「この演算はこの精度で厳密に実行せよ」と明示的に命令するための重要な武器になる。
通常、単純な四則演算子(`+`, `-`, “, `/`)を使うと、コンパイラは言語仕様のデフォルトルールに従って中間結果の精度を勝手に決めてしまう。これが、夜間バッチの終盤で突然の「桁あふれ(S0C7や大小比較の不一致)」を引き起こす元凶なのだ。
—
2. 4大算術ビルトイン関数の仕様と精度指定の極意
それぞれの関数は、単に「足す・引く・掛ける・割る」だけでなく、第3引数以降で結果の精度(全体桁数と小数点以下桁数)を直接指定できるという強力な特徴を持っている。
1. `ADD(x, y, p [, q])`: $x + y$ の計算結果を指定精度 `p`(および小数点以下 `q`)で返す。
2. `SUBTRACT(x, y, p [, q])`: $x – y$ の計算結果を指定精度 `p`(および小数点以下 `q`)で返す。
3. `MULTIPLY(x, y, p [, q])`: $x \times y$ の計算結果を指定精度 `p`(および小数点以下 `q`)で返す。特に掛け算は桁数が爆発しやすいため、この指定が必須になる。
4. `DIVIDE(x, y, p [, q])`: $x \div y$ の計算結果を指定精度 `p`(および小数点以下 `q`)で返す。割り算で最も怖い「ゼロ割り(ZCC等)」や「無限小数による桁あふれ」を防ぐために不可欠。
—
3. 実践!VSAMファイル更新バッチにおけるコード例
百聞は一見にしかずだ。実際の基幹系バッチプログラムを想定したコードを見てみよう。
月次売上データ(VSAMのKSDSファイル)を読み込み、単価と数量から総額を算出し、さらに前月までの累計に安全な精度管理のもとで加算・更新する処理の抜粋だ。
1
—————————————————————-
- 顧客別売上累計更新プログラム
- 算術ビルトイン関数を用いた厳密な精度制御のサンプル
—————————————————————-
SALES_UPDATE: PROC OPTIONS(MAIN);
DCL 1 CUST_REC,
5 CUST_ID CHAR(5), / 顧客ID /
5 CUR_QTY FIXED DEC(7,0), / 今回数量 /
5 CUR_UNIT_P FIXED DEC(9,2); / 今回単価 /
DCL 1 CUST_MASTER,
5 M_CUST_ID CHAR(5), / 顧客ID(キー) /
5 M_TOTAL_AMT FIXED DEC(13,2); / 売上累計(総額) /
/ 作業用変数:精度あふれを防ぐため十分な領域を確保 /
DCL W_CALC_AMT FIXED DEC(13,2) VALUE(0);
DCL W_NEW_TOTAL FIXED DEC(13,2) VALUE(0);
/ VSAMファイルの定義(簡略化) /
DCL VSAM_FILE FILE RECORD SEQUENTIAL UPDATE
ENVIRONMENT(KEYLENGTH(5));
/ ビルトイン関数の明示的宣言 /
DCL (ADD, MULTIPLY) BUILTIN;
ON ENDFILE(VSAM_FILE) GO TO FINISH;
OPEN FILE(VSAM_FILE);
DO FOREVER;
READ FILE(VSAM_FILE) INTO(CUST_REC);
/——————————————————–/
/ 【重要】MULTIPLY関数による掛け算の精度制御 /
/ 単価(9,2) と 数量(7,0) の乗算結果を、 /
/ 整数部11桁、小数部2桁(全体13桁)として厳密に算出する /
/——————————————————–/
W_CALC_AMT = MULTIPLY(CUR_UNIT_P, CUR_QTY, 13, 2);
/——————————————————–/
/ 【重要】ADD関数による加算の精度制御 /
/ 既存の累計値と今回算出した売上額を正確に加算し、 /
/ 桁あふれ(OVERFLOW)を未然にブロックする /
/——————————————————–/
W_NEW_TOTAL = ADD(M_TOTAL_AMT, W_CALC_AMT, 13, 2);
/ 算出した安全な値をマスターレコードに反映 /
M_TOTAL_AMT = W_NEW_TOTAL;
REWRITE FILE(VSAM_FILE) FROM(CUST_MASTER);
END;
FINISH:
CLOSE FILE(VSAM_FILE);
RETURN;
END SALES_UPDATE;
—
4. なぜ、このコーディングが現場で求められるのか?
上のコードを見て、「なぜわざわざ `MULTIPLY` や `ADD` なんて関数を使うんだ? `W_CALC_AMT = CUR_UNIT_P CUR_QTY;` じゃダメなのか?」と思った奴は、まだ甘い。
通常の演算子(“ や `+`)を使うと、PL/Iのコンパイラはオペランドの宣言精度から算術規則に基づいて自動的に中間結果の精度を決定する。例えば、固定小数点同士の掛け算では、精度は「オペランドの精度を足し合わせたもの」になりがちだ。これが何重にもネストした複雑な計算式になると、コンパイラが勝手に桁数を巨大化させ、最終的にシステムの上限(パック十進数なら通常15桁、または31桁)を超えてしまい、コンパイルエラーか実行時例外を引き起こす。
また、割り算(`DIVIDE`)において精度指定を怠ると、割り切れない小数(循環小数など)に直面した際、コンパイラ任せのデフォルト精度では途中で不気味な丸め誤差が生じたり、最悪の場合は桁あふれで異常終了する。
デバッグとONユニット制御の現場の知恵
もし万が一、これらの計算で予期せぬオーバーフローの危険性があるシステムを保守しているなら、`ON OVERFLOW` ユニットを適切に配置しておくべきだ。
1
ON OVERFLOW BEGIN;
DISPLAY ‘ 致命的エラー: 算術演算で桁あふれが発生しました ‘;
DISPLAY ‘該当顧客ID: ‘ || CUST_ID;
/ ダンプ採取や異常終了処理をここに記述 /
SIGNAL ERROR;
END;
だが、後から `ON` ユニットで火消しをするよりも、今回解説した `ADD`、`SUBTRACT`、`MULTIPLY`、`DIVIDE` を用いて、最初から演算結果の精度をコード上でガチガチにコントロールする方が、プロフェッショナルなメインフレームエンジニアの仕事と言える。
レガシーシステムの改修は、過去のコードが積み上げてきた「歴史」との戦いだ。変数の型定義とビルトイン関数の精度指定、この2つを完全に掌握できれば、お前ももう「S0C7に怯える若手」を卒業し、現場から頼りにされるシステムアーキテクトへの道を確実に歩み始めているということだ。次のバッチ改修では、ぜひこの手法をコードに反映させてみてくれ。
