予約語なき世界の狂気:`ON CONVERSION` とデータ変換エラーの深淵
メインフレームの現場で長く生きていると、何度となく「データに裏切られる瞬間」に立ち会うことになる。
特に、外部システムから流入するファイルや、COBOLからPL/Iへ混成移行したレガシー資産の海において、数値項目(`PIC S9(9) COMP` や `FIXED DECIMAL`)に突如として混入するスペースや漢字、あるいは不正なゾーンニードル。これらはシステムを一撃で阿鼻叫喚の深夜アベンドへと導く。
JavaやC#のモダンな言語であれば、型安全なパース処理や例外機構(`try-catch`)が標準装備されている。しかし、PL/Iの世界は少し違う。
PL/Iには、C#やJavaのような「キーワードとしての厳格な予約語(Reserved Words)」がほとんど存在しない。`IF` や `DO` といった制御文ですら、文脈によってはただの変数名として定義できてしまうという、コンパイラ泣かせの自由度を誇る。
この「予約語を持たない」という仕様の裏返しとして、PL/Iは言語仕様レベルで「条件(Condition)」という高度な例外・割込み処理機構を備えている。
今回は、その中でもバッチ処理の夜間バッチで最もエンジニアを悩ませる `CONVERSION` 条件を取り上げ、極限の信頼性を求められる基幹システムにおけるデータ変換エラーのハンドリング術を、コンパイラの内部挙動から実務のトラブルシューティングまで徹底的に紐解いていこう。
—
1. `CONVERSION` 条件のメカニズムと「予約語なき世界」の罠
PL/Iにおける文字型(`CHARACTER`)から数値型(`FIXED DECIMAL` や `FLOAT`)への暗黙的、あるいは明示的な型変換において、表現できない文字(非数字や不正な符号など)が検出された際、コンパイラが生成したコードは即座に `CONVERSION` 条件を発生させる。
もしこの `ON` ユニット(例外ハンドラ)が定義されていない場合、OS(z/OS)の通常動作としてジョブはアベンド(U4038やS0C7類似のソフト異常など、処理系依存の致命的エラー)へと直行する。
ここで、PL/I特有の「予約語がない」という言語特性が牙を剥く。例えば、以下のようなコードを見てほしい。
DCL CONVERSION FIXED BIN(31) INIT(0); / なんとCONVERSIONという変数名が定義できてしまう /
PL/Iでは、組み込みの条件名やキーワードの多くがコンテキスト依存であるため、うっかり予約語的な感覚を忘れてロジックを組むと、コンパイラがそれを変数名やラベル名と誤認し、期待した `ON CONVERSION` が機能しない、あるいはコンパイルエラーの迷宮に迷い込むことがある。
この「何でも変数にできてしまう」自由度こそが、レガシー移行時のコード解析を困難にする最大の元凶なのだ。
—
2. 実践:`ON CONVERSION` による動的リカバリと不正データの特定
基幹システムのバッチにおいて、たった1件の文字化け・不正データで数千万件の処理が止まることは許されない。
「エラーデータはスキップしてログに退避し、処理を継続する」というリジリエント(回復力のある)な設計を行うには、`ON CONVERSION` ブロックの中で `ONCHAR()` などのビルトイン関数を駆使し、どの変数が、どの不正文字でコケたのかを特定する必要がある。
以下に、実務で使える堅牢なPL/Iプログラムの構造を示す。
CONV_HANDLING: PROC OPTIONS(MAIN);
DCL RAW_INPUT_DATA CHAR(10);
DCL TARGET_VAL FIXED DECIMAL(9,0);
DCL ERROR_FLAG BIT(1) INIT(‘0’B);
DCL I FIXED BIN(31);
/ テスト用の不正データを含む配列(実際はファイルから読み込む) /
DCL 1 INPUT_TABLE(3),
3 EMP_ID CHAR(5),
3 SALARY_CH CHAR(5);
/ サンプルデータの初期化(3番目に数値に変換できない文字を入れる) /
INPUT_TABLE(1).EMP_ID = ‘00001’; INPUT_TABLE(1).SALARY_CH = ‘12345’;
INPUT_TABLE(2).EMP_ID = ‘00002’; INPUT_TABLE(2).SALARY_CH = ‘05000’;
INPUT_TABLE(3).EMP_ID = ‘00003’; INPUT_TABLE(3).SALARY_CH = ‘A999X’; / ← 変換エラーの元凶 /
/ CONVERSION条件の捕捉定義 /
ON CONVERSION BEGIN;
PUT SKIP EDIT (‘[警告] データ変換エラー検出: 値[‘, ONCHAR(), ‘] は数値化できません。ID=’, EMP_ID)
(A, A, A, A);
ERROR_FLAG = ‘1’B;
/ エラー発生時は安全なデフォルト値(ゼロ等)を割り当てて処理を続行させる /
TARGET_VAL = 0;
END;
DO I = 1 TO 3;
ERROR_FLAG = ‘0’B;
/ 文字型から数値型への代入(ここでCONVERSION条件が発火する可能性がある) /
TARGET_VAL = INPUT_TABLE(I).SALARY_CH;
IF ERROR_FLAG THEN do;
PUT SKIP LIST (‘-> レコード ‘ || EMP_ID || ‘ はスキップ処理またはデフォルト値適用されました。’);
END;
ELSE do;
PUT SKIP EDIT (‘-> 正常処理: ID=’, EMP_ID, ‘ 給与=’, TARGET_VAL) (A, A, F(10));
END;
END;
END CONV_HANDLING;
このコードのポイントは、`ON CONVERSION` ブロック内で `ONCHAR()` を使い、「どの文字が原因でパースに失敗したか」をリアルタイムにキャッチしている点にある。
さらに、エラー発生時にそのままアベンドさせるのではなく、`ERROR_FLAG` を立てて処理をループバックさせることで、バッチ全体の耐障害性を飛躍的に高めている。
—
3. アーキテクチャの暗部:パックデシマルの内部符号反転とエッジケース
ここからが、単なるリファレンス解説を超えたスペシャリスト領域だ。
JavaやC#、あるいはDB2へのマイグレーション(リライト)を行う際、最も頭を悩ませるのが「PL/I固有のパックデシマル(`FIXED DECIMAL`)の内部表現と符号反転バグ」である。
メインフレーム(IBM Z)のハードウェア命令は、パックデシマル(ゾーン10進数およびパック10進数)をネイティブで処理する。
例えば、マイナス値を表す際に、最下位ニブル(4ビット)に `D` や `C` などのゾーン符号が入り込む。外部ファイル(VSAMや順編成ファイル)からデータを読み込む際、これが文字型として誤認されたり、あるいはCOBOLとの混成環境で符号の解釈(IBMホストと富士通・日立製メインフレーム間での符号の差異)がズレたりすると、`CONVERSION` すら経由せずに、いきなりデータが化けるかシステムが破壊される。
① 埋め込みSQL(DB2)連携時の罠
PL/IからDB2を叩く際、ホスト変数に `FIXED DECIMAL` を用いるが、データベース側の型(`DECIMAL` や `NUMERIC`)との間で暗黙の型変換や精度落ちが発生すると、コンパイラオプション(`TRUNC` や `STGOWNS` など)の指定次第で挙動が変わる。
特に、コンパイル時に `TRUNC(BIN)` が指定されていないと、BINARY変数の領域外あふれが原因で予期せぬ `CONVERSION` やデータ破損を引き起こす。マイグレーション時には、この「コンパイラの最適化オプションの差」がバグの温床となる。
② CICSオンライン処理におけるエッジケース
CICSの3270画面から飛んできた電文(COMMAREA)をベース変数(`based variable`)とポインタを用いてマップする際、画面上の未入力フィールド(通常はヌルやスペース、あるいは低位値 `LOW-VALUES`)をそのまま数値型変数にポインタキャストしてムーブすると、一瞬で `CONVERSION` 条件が発火、あるいはCICSのトランザクションが異常終了(ABEND: AICAやASRAなど)を起こす。
オンライン処理ではバッチのように「ログを出して次のループへ」とはいかず、タスク全体がアベンドするため、画面入力の受け口では必ず文字型(`CHAR`)として全件受けてから、アプリケーション層でバリデーションとパースを行うのが鉄則だ。
—
4. マイグレーション設計への提言:PL/Iからモダン言語へ移行する者たちへ
現在、多くの企業がIBMメインフレームからAWSやAzure上のJava(Spring Boot)やC#(.NET Core)へのマイグレーションを推進している。
その際、PL/Iの `ON CONVERSION` のような暗黙的かつ強力な例外捕捉機構を、どうモダンな言語に置き換えるかがプロジェクトの成否を分ける。
JavaやC#には、PL/Iの `ON-unit` のように「プログラムのグローバル/ブロック単位で特定の例外をフックして値を書き換える」ようなエレガント(あるいはトリッキー)な機能は標準ではない。
そのため、移行時には以下の設計アプローチが求められる。
1. インジェスト層での厳格なバリデーション
PL/Iの「とりあえず受けてから `ON` で拾う」という甘えを捨て、データ入力の最前線(API Gatewayやバッチのリーダースレッド)で、正規表現や型安全なパーサーを用いて不正データを完全に弾くアーキテクチャへリファクタリングする。
2. 符号とゾーンのデシリアライゼーションの再現
バイナリファイルやメインフレーム由来の固定長データをJavaで処理する場合、`ByteBuffer` や専用のカスタムデコーダーを作成し、パックデシマルの符号反転(`C`, `D`, `F`)を完全にエミュレートしなければならない。ここを疎かにすると、移行後の結合テストで「なぜか数値のマイナスが反転する・消える」という不可解なバグに悩まされ続けることになる。
—
おわりに
PL/Iの `ON CONVERSION` は、単なるエラー処理の構文ではない。それは、予測不能な現実世界のノイズ(汚れたデータ)から、巨大な基幹システムの心臓部を守るための、レガシーの知恵が詰まった防壁である。
予約語を持たない自由奔放な構文の裏で、コンパイラとハードウェアが密かに連携してエラーを検知し、プログラマに制御を委ねる――この骨太なアーキテクチャの思想を理解しているか否かで、レガシー移行の成否、ひいては新しいシステムにおける堅牢性の設計品質が決定的に変わる。
コードの背後にあるコンパイラの息づかいを感じ取ること。それこそが、真のシステムアーキテクトの仕事なのだ。
