COBOLの命令区切りルール
COBOLの PROCEDURE DIVISION における命令の区切りは基本的にピリオド(.)だ。
ピリオドの直後に別の命令を記述すると1行に複数命令が入ることになる。
* 正規化前(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-IF・END-PERFORM・END-EVALUATE などのスコープ終端が導入された。
これらを使うと ピリオドなしで 個々の命令を区切ることができる。
* ピリオドなしの命令区切り例
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_PICTURE | PICTURE / PIC 句内 | 命令区切りとしない(小数点) |
状態遷移のルールは次のとおり。
NORMAL→IN_STRING: 引用符('または")を検出IN_STRING→NORMAL: 同じ引用符を検出(閉じ)NORMAL→IN_PICTURE: トークンPICまたはPICTUREを検出IN_PICTURE→NORMAL: スペースの後に次のトークンが始まったとき(PIC句は1トークンで完結)
実装 — split_to_one_statement
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]
動作確認
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 でのスコープ終端も独立した行として切り出されている。