COBOLの命令区切りルール

COBOLの PROCEDURE DIVISION における命令の区切りは基本的にピリオド(.)だ。 ピリオドの直後に別の命令を記述すると1行に複数命令が入ることになる。

COBOL — 複数命令の同居
* 正規化前(1行に複数命令)
DISPLAY 'START'. MOVE 'A' TO WS-VAR. DISPLAY WS-VAR.

* 正規化後(1行1命令)
DISPLAY 'START'.
MOVE 'A' TO WS-VAR.
DISPLAY WS-VAR.

ピリオドの3つの罠

対処
① 文字列リテラル中のピリオド MOVE 'END.' TO WS-MSG. 引用符内ではピリオドを命令区切りと判定しない
② 数値リテラルの小数点 MOVE 1.5 TO WS-AMT. 数字に挟まれたピリオドは小数点(直後が数字かどうかで判定)
③ PICTURE句の小数点 PIC 9(5)V99 は通常Vで表現するが古い形式に PIC 9.99 が存在 PIC句コンテキスト中のピリオドは小数点として扱う

ピリオドがない場合の区切り — END-xxx

COBOL 1985 以降の構造化構文では END-IFEND-PERFORMEND-EVALUATE などのスコープ終端が導入された。 これらを使うと ピリオドなしで 個々の命令を区切ることができる。

COBOL — END-xxx によるスコープ終端
* ピリオドなしの命令区切り例
IF WS-FLG = 'Y'
    MOVE 'YES' TO WS-RESULT
    DISPLAY WS-RESULT
END-IF
MOVE 'DONE' TO WS-STATUS.

この場合 END-IF の後ろでも1命令として区切りを入れる必要がある。 本実装では END- で始まるトークンが行の末尾に来たとき(後ろにピリオドが続かなくても)に命令区切りとみなす。

ステートマシンの設計

1文字ずつ走査し、以下の3状態を管理する。

状態意味ピリオドの扱い
NORMAL通常の命令テキスト命令区切りと判定する
IN_STRING文字列リテラル内命令区切りとしない
IN_PICTUREPICTURE / PIC 句内命令区切りとしない(小数点)

状態遷移のルールは次のとおり。

  • NORMALIN_STRING: 引用符(' または ")を検出
  • IN_STRINGNORMAL: 同じ引用符を検出(閉じ)
  • NORMALIN_PICTURE: トークン PIC または PICTURE を検出
  • IN_PICTURENORMAL: スペースの後に次のトークンが始まったとき(PIC句は1トークンで完結)

実装 — split_to_one_statement

Python — ステートマシンによる1行1命令分割
import re

# COBOL スコープ終端キーワード
_END_SCOPE_PATTERN = re.compile(
    r'\b(END-IF|END-PERFORM|END-EVALUATE|END-READ|END-WRITE|'
    r'END-REWRITE|END-DELETE|END-START|END-RETURN|END-RECEIVE|'
    r'END-CALL|END-COMPUTE|END-ADD|END-SUBTRACT|END-MULTIPLY|'
    r'END-DIVIDE|END-STRING|END-UNSTRING|END-INSPECT|END-SEARCH)\b'
)

# PICTURE句の開始を示すパターン
_PIC_START_PATTERN = re.compile(r'\bPICTURE\b|\bPIC\b', re.IGNORECASE)


def split_to_one_statement(lines: list[str]) -> list[str]:
    """
    正規化済み行リストを1行1命令に分割する。
    複数行にまたがるケースはないため、各行を独立してトークナイズする。
    """
    result = []
    for line in lines:
        statements = _tokenize_line(line)
        result.extend(statements)
    return result


def _tokenize_line(line: str) -> list[str]:
    """
    1行をステートマシンで走査し、命令区切りで分割して返す。
    """
    STATE_NORMAL = 'NORMAL'
    STATE_STRING = 'IN_STRING'
    STATE_PICTURE = 'IN_PICTURE'

    state = STATE_NORMAL
    quote_char = ''
    current = []          # 現在構築中の命令バッファ
    statements = []       # 確定した命令リスト
    i = 0
    n = len(line)

    while i < n:
        ch = line[i]

        if state == STATE_STRING:
            current.append(ch)
            if ch == quote_char:
                state = STATE_NORMAL
            i += 1
            continue

        if state == STATE_PICTURE:
            # PIC句: スペースの後に次のトークンが来たらNORMALに戻る
            if ch == ' ':
                # スペース以降に別トークンがあるか先読み
                rest = line[i:].lstrip()
                # rest が空 or 次が普通のトークン(引用符や数字でない)なら PIC句終了
                # 簡易的に: スペースを出力してNORMALに戻る
                current.append(ch)
                state = STATE_NORMAL
            else:
                current.append(ch)
            i += 1
            continue

        # STATE_NORMAL
        if ch in ("'", '"'):
            state = STATE_STRING
            quote_char = ch
            current.append(ch)
            i += 1
            continue

        if ch == '.':
            # ピリオドが命令区切りかどうか判定
            # 数値リテラルの小数点: 前が数字かつ後が数字
            prev_ch = current[-1] if current else ''
            next_ch = line[i+1] if i+1 < n else ''
            if prev_ch.isdigit() and next_ch.isdigit():
                # 数値の小数点
                current.append(ch)
            else:
                # 命令区切りピリオド
                current.append(ch)
                stmt = ''.join(current).strip()
                if stmt and stmt != '.':
                    statements.append(stmt)
                current = []
            i += 1
            continue

        # PIC / PICTURE キーワードの検出(簡易: 先読み)
        # 現在位置から始まるサブ文字列でマッチするか確認
        pic_match = _PIC_START_PATTERN.match(line, i)
        if pic_match:
            token = pic_match.group(0)
            current.extend(list(token))
            i += len(token)
            state = STATE_PICTURE
            continue

        current.append(ch)
        i += 1

    # バッファに残った命令を処理
    remaining = ''.join(current).strip()
    if remaining:
        # END-xxx で終わっていれば区切り
        parts = _split_by_end_scope(remaining)
        statements.extend(parts)

    return [s for s in statements if s]


def _split_by_end_scope(text: str) -> list[str]:
    """
    END-xxx キーワードで文を分割する補助関数。
    """
    parts = []
    last = 0
    for m in _END_SCOPE_PATTERN.finditer(text):
        end_pos = m.end()
        chunk = text[last:end_pos].strip()
        if chunk:
            parts.append(chunk)
        last = end_pos
    remainder = text[last:].strip()
    if remainder:
        parts.append(remainder)
    return parts if parts else [text]

動作確認

Python — 動作確認
lines = [
    "DISPLAY 'START'. MOVE 'A' TO WS-VAR. DISPLAY WS-VAR.",
    "MOVE 'END.' TO WS-MSG. DISPLAY WS-MSG.",
    "MOVE 1.5 TO WS-AMT. DISPLAY WS-AMT.",
    "IF WS-FLG = 'Y' MOVE 'YES' TO WS-RESULT END-IF MOVE 'DONE' TO WS-STATUS.",
]

result = split_to_one_statement(lines)
for line in result:
    print(repr(line))
実行結果
"DISPLAY 'START'."
"MOVE 'A' TO WS-VAR."
"DISPLAY WS-VAR."
"MOVE 'END.' TO WS-MSG."
"DISPLAY WS-MSG."
"MOVE 1.5 TO WS-AMT."
"DISPLAY WS-AMT."
"IF WS-FLG = 'Y' MOVE 'YES' TO WS-RESULT END-IF"
"MOVE 'DONE' TO WS-STATUS."

文字列内の 'END.' は誤検出されず、数値 1.5 も正しく保持されている。 END-IF でのスコープ終端も独立した行として切り出されている。

次の章では…

PART 07 では全ステップを CobolNormalizer クラスに統合し、実際の COBOL ソースファイルで動作確認する。

→ PART 07 — 完成コードへ