お疲れ様です。今日も元気にオンラインのログや夜間バッチのABEND(アベンド)解析に追われていることかと思います。
さて、今回はPL/Iの基本データ制御、それも「数あるプログラミング言語の中で、なぜPL/Iの剰余計算はこれほどまでに独特なのか」という深淵なテーマについて、メインフレームのハードウェア(アセンブラレベル)の挙動と絡めて徹底的に解説していきます。
現場で「あれ? なんかC言語やJavaと計算結果が違うぞ?」と頭を抱えた経験はありませんか?
モジュロ演算(剰余計算)における `MOD` と `REM` の違い、そしてそれがコンパイル時にどのようなマシン語(機械語)命令に展開されるのか。ここを正確に理解していないと、金額項目の端数処理やキーのハッシュ化ロジックなどで思わぬバグを踏み抜くことになります。
さっそく、ボンネットを開けて中身を見ていきましょう。
—
1. 予約語を持たないPL/Iと、組み込み関数(BUILTIN)の哲学
まず大前提として、PL/Iという言語のユニークな仕様に触れておきます。C言語やJavaとは異なり、PL/Iには厳密な意味での「予約語(Reserved Words)」がほとんど存在しません。
どういうことか分かりますか? `IF` や `DO`、さらには今回取り上げる `MOD` や `REM` さえも、実はプログラマが変数名として宣言して使うことが语法上は可能です(もちろん、そんなテロ行為をする人はいませんが)。
コンパイラは、それがキーワードなのか変数なのかを前後の文脈から判断しています。そのため、標準の数学的関数や処理系固有のルーチンを呼び出すときは、明示的にコンパイラへ「これは組み込み関数ですよ」と教えてあげる必要があります。それが `BUILTIN` 属性です。
1
/ 組み込み関数の明示的な宣言 /
DCL MOD BUILTIN;
DCL REM BUILTIN;
この宣言を怠ると、コンパイラが自前のルーチンと解釈してくれず、予期せぬシンタックスエラーや未定義参照に悩まされることになります。現場のコーディング標準でも、BUILTINの宣言漏れはレビュアーに真っ先に指摘されるポイントです。
—
2. MOD関数とREM関数の決定的な違い:数学的定義とハードウェアの影
では本題です。どちらも「割り算の余り」を求める関数ですが、その定義と計算結果はまったく異なります。特に「負の数」が絡んだときの挙動を絶対に押さえておかなきゃいけません。
数学的定義のおさらい
- `REM(x, y)` (Remainder / 剰余)
- 符号は被除数($x$)に依存します。つまり、$x \pmod y$ において、$x$ がマイナスであれば結果もマイナス(または0)になります。
- 計算式としては `REM(x, y) = x – (TRUNC(x / y) y)` と等価です。
- `MOD(x, y)` (Modulo / モジュロ)
- 符号は除数($y$)に依存します。結果の符号は常に $y$ と同じになります。
- 計算式としては `MOD(x, y) = x – (FLOOR(x / y) y)` と等価です。
メインフレームのハードウェア(S/370アーキテクチャ以降)との関係
IBM Z(System/370、ESA/390、z/Architecture)のプロセッサには、整数除算を行うためのアセンブラ命令として `DR`(Divide Register) や `DRS` が用意されています。
これらの機械語命令が返す余り(Remainder)は、基本的に被除数(商を収めるレジスタの符号)に一致する仕様になっています。
つまり、PL/Iの `REM` 関数は、ハードウェアの `DR` 命令の挙動に直結しており、CPUのネイティブな演算をそのまま引き出します。
一方で `MOD` 関数は、商の切り捨てにおいて `TRUNC`(ゼロ方向への切り捨て)ではなく `FLOOR`(負の無限大方向への切り捨て)の数学的補正を行うため、内部的に若干の命令シーケンスが追加されることになります。
—
3. 実践:VSAMレコードの分散処理とエラー制御を伴うPL/Iサンプルコード
百聞は一見に如かず。実際のバッチプログラムを想定したコードを見てみましょう。
ここでは、入力されたキー項目を `MOD` と `REM` でそれぞれ処理し、その結果によって処理分岐やVSAM(KSDS)へのアクセス制御、さらには万が一のゼロ除算に対する `ON` ユニットによるトラップを組み込んだ実戦的な構成にしています。
1
————————————————-
- 剰余計算(MOD vs REM)の挙動検証バッチプログラム
————————————————-
CALC_SAMPLE: PROC OPTIONS(MAIN);
/ 組み込み関数の明識的宣言 /
DCL MOD BUILTIN;
DCL REM BUILTIN;
DCL ABS BUILTIN;
/ 変数定義(固定小数点数) /
DCL W_DIVIDEND FIXED DEC(5,0) INIT(-10); / 被除数(負の数) /
DCL W_DIVISOR FIXED DEC(5,0) INIT( 3); / 除数(正の数) /
DCL W_RES_MOD FIXED DEC(5,0); / MOD結果格納 /
DCL W_RES_REM FIXED DEC(5,0); / REM結果格納 /
/ VSAMファイル定義(シミュレーション) /
DCL VSAM_FILE FILE RECORD SEQUENTIAL OUTPUT;
DCL 1 VSAM_REC,
5 R_KEY PIC ‘9(05)’,
5 R_DATA CHAR(50);
/ ゼロ除算トラップ用のONユニット /
ON ZERODIVIDE
BEGIN;
PUT SKIP EDIT (‘【SEVERE】ゼロ除算エラーを検知しました。処理を中断します。’)(A);
CLOSE FILE(VSAM_FILE);
SIGNAL FINISH;
END;
PUT SKIP EDIT (‘=== MOD / REM 演算テスト開始 ===’)(A);
PUT SKIP LIST (‘被除数:’, W_DIVIDEND, ‘除数:’, W_DIVISOR);
/ 1. MOD関数の実行(符号は除数(=3)に従うため、結果は正になる) /
/ 計算: -10 – (FLOOR(-10/3) 3) = -10 – ((-4) 3) = -10 – (-12) = +2 /
W_RES_MOD = MOD(W_DIVIDEND, W_DIVISOR);
/ 2. REM関数の実行(符号は被除数(=-10)に従うため、結果は負になる) /
/ 計算: -10 – (TRUNC(-10/3) 3) = -10 – ((-3) 3) = -10 – (-9) = -1 /
W_RES_REM = REM(W_DIVIDEND, W_DIVISOR);
/ 結果の出力確認 /
PUT SKIP EDIT (‘MOD結果 (-10 MOD 3) = ‘, W_RES_MOD)(A, F(3));
PUT SKIP EDIT (‘REM結果 (-10 REM 3) = ‘, W_RES_REM)(A, F(3));
/ 3. VSAMファイルへの出力制御(例:MOD結果を用いたハッシュ分散) /
OPEN FILE(VSAM_FILE) ENVIRONMENT(CONSECUTIVE);
R_KEY = ABS(W_RES_MOD); / キーには正の値を使用 /
R_DATA = ‘CALCULATED BY PL/I COMPILER ENGINE.’;
WRITE FILE(VSAM_FILE) FROM(VSAM_REC);
CLOSE FILE(VSAM_FILE);
PUT SKIP EDIT (‘=== MOD / REM 演算テスト正常終了 ===’)(A);
END CALC_SAMPLE;
—
4. 現場のシニアから後輩エンジニアへ:デバッグと移行時の注意点
このコード、特にマイグレーション(オープン系言語への移植や、他機種からのリホスト)の現場では、幾度となく地雷原になります。
1. CやJava、COBOL(`REMEMBER` や `FUNCTION REM`)との仕様の乖離
- 例えば、Javaの `%` 演算子(剰余演算子)はPL/Iの `REM` と同じ挙動(被除数の符号を引き継ぐ)をします。
- もし移行元のCOBOLや古い仕様書で「マイナスの値であっても、余りは常に正の数(または特定の範囲)で返すこと」という前提で作られていた場合、C言語やJavaにそのままベタ移植すると、計算結果の符号が反転してデータ破損や突合エラー(不一致)を引き起こします。
2. ONユニットによる例外処理の重要性
- バッチ処理において、マスターデータの欠損やマスタ未整備によって除数が `0` になることは、現場では「あるある」です。
- PL/Iでは `ON ZERODIVIDE` を記述しておくことで、アセンブラのアボート(S0C7やS0C9など)をスマートに捕捉し、ログを出力して安全にロールバックまたは終了させることができます。この堅牢性こそがメインフレームの基幹たる所以です。
—
まとめ
PL/Iにおける `MOD` と `REM` の違い、そして背後にあるハードウェアの仕組みについてイメージがつかめましたでしょうか。
「たかが余り、されど余り」。基幹システムの数字は1円の狂い、1件のズレも許されません。変数名や組み込み関数の仕様を何となくで済ませず、コンパイル結果や機械語の挙動まで想像を巡らせながらコードを書く――それこそが、私たちメインフレーム・エンジニアの矜持です。
日々の保守・開発、大変かとは思いますが、手堅いコードで安定稼働を勝ち取っていきましょう。それではまた!
