Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
175 lines
6.8 KiB
Python
175 lines
6.8 KiB
Python
"""문장 분리 — '.' 종결, 체인(:` `,`) 전개. (계획서 §4.1)"""
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from dataclasses import dataclass, field
|
|
|
|
from .tokenizer import Line, read_lines, tokenize_code
|
|
|
|
_NAME_HEAD = re.compile(r"^[<A-Za-z_/]")
|
|
|
|
|
|
def assignment_eq_index(up: list[str]) -> int | None:
|
|
"""평문 대입이면 최상위 '=' 토큰의 위치, 아니면 None.
|
|
|
|
받아들이는 형태 — 쓰기 지점 추출(dataflow)과 미인식 리포트(run)가 같은 판정을 써야 하므로
|
|
여기 한 곳에만 둔다:
|
|
X = ... → 1
|
|
X[] = ... → 3 (내부테이블 본문 대입 — 흔한 관용구)
|
|
X[ key = v ] = ... → 대괄호를 건너뛴 위치 (테이블 표현식)
|
|
X(3) = ... → 괄호를 건너뛴 위치 (오프셋·길이 쓰기)
|
|
X+4(2) = ... → 위와 같음
|
|
"""
|
|
if len(up) < 3 or not _NAME_HEAD.match(up[0]):
|
|
return None
|
|
i = 1
|
|
# 오프셋 표기 X+4(2) — '+'와 숫자를 먼저 건너뛴다
|
|
if i + 1 < len(up) and up[i] == "+" and up[i + 1].isdigit():
|
|
i += 2
|
|
for opener, closer in (("[", "]"), ("(", ")")):
|
|
if i < len(up) and up[i] == opener:
|
|
depth = 0
|
|
while i < len(up):
|
|
if up[i] == opener:
|
|
depth += 1
|
|
elif up[i] == closer:
|
|
depth -= 1
|
|
if depth == 0:
|
|
i += 1
|
|
break
|
|
i += 1
|
|
return i if i < len(up) and up[i] == "=" else None
|
|
|
|
|
|
@dataclass
|
|
class Statement:
|
|
include: str
|
|
line_start: int
|
|
line_end: int
|
|
tokens: list[str] # 원문 케이스
|
|
comment_text: str = "" # 같은 줄 / 직전 주석
|
|
raw_text: str = ""
|
|
# --- 체인(`DATA: a, b, c.`) 항목의 원문 복원용 (parser/declarations.py) ---
|
|
chain_head: str = "" # ':' 앞 토큰들 ("DATA") — 체인에서 전개된 항목만
|
|
chain_start: int = 0 # 체인 전체의 시작 줄
|
|
chain_end: int = 0 # 체인 전체의 끝 줄('.' 이 있는 줄)
|
|
|
|
@property
|
|
def upper(self) -> list[str]:
|
|
return [t.upper() for t in self.tokens]
|
|
|
|
def text(self) -> str:
|
|
return " ".join(self.tokens)
|
|
|
|
|
|
def _expand_chain(tokens: list[str], token_lines: list[int]) -> list[tuple[list[str], int, int]]:
|
|
"""'DATA: a TYPE i, b TYPE i' → [(['DATA','a','TYPE','i'], 줄, 줄), (['DATA','b',...], 줄, 줄)]
|
|
|
|
괄호 안의 ','는 분리하지 않는다. 항목마다 **자기 토큰이 놓인 줄 범위**를 함께 돌려준다 —
|
|
체인 항목의 선언 원문을 그 항목만큼만 잘라내려면(정의부 수집) 항목별 줄이 있어야 한다.
|
|
체인이 아니면 빈 목록을 돌려준다(호출 측이 문장 전체 범위를 쓴다).
|
|
"""
|
|
if ":" not in tokens:
|
|
return []
|
|
ci = tokens.index(":")
|
|
head, head_lines = tokens[:ci], token_lines[:ci]
|
|
rest, rest_lines = tokens[ci + 1 :], token_lines[ci + 1 :]
|
|
groups: list[tuple[list[str], list[int]]] = []
|
|
cur: list[str] = []
|
|
cur_lines: list[int] = []
|
|
depth = 0
|
|
for t, ln in zip(rest, rest_lines):
|
|
if t == "(":
|
|
depth += 1
|
|
elif t == ")":
|
|
depth = max(0, depth - 1)
|
|
if t == "," and depth == 0:
|
|
groups.append((cur, cur_lines))
|
|
cur, cur_lines = [], []
|
|
else:
|
|
cur.append(t)
|
|
cur_lines.append(ln)
|
|
groups.append((cur, cur_lines))
|
|
|
|
out: list[tuple[list[str], int, int]] = []
|
|
fallback = head_lines[0] if head_lines else (token_lines[0] if token_lines else 0)
|
|
for g, gl in groups:
|
|
if not (head or g):
|
|
continue
|
|
start = gl[0] if gl else fallback
|
|
end = gl[-1] if gl else start
|
|
out.append((head + g, start, end))
|
|
return out
|
|
|
|
|
|
def split_statements(text: str, include: str) -> tuple[list[Statement], list[Line]]:
|
|
"""인클루드 소스 → 문장 목록. 주석 줄은 직후 문장의 comment_text 로 전달."""
|
|
lines = read_lines(text)
|
|
statements: list[Statement] = []
|
|
pending_tokens: list[str] = []
|
|
pending_lines: list[int] = [] # pending_tokens 와 같은 길이 — 토큰이 놓인 줄
|
|
pending_start: int | None = None
|
|
pending_comments: list[str] = []
|
|
inline_comments: list[str] = []
|
|
|
|
def flush(end_line: int) -> None:
|
|
nonlocal pending_tokens, pending_lines, pending_start, pending_comments, inline_comments
|
|
if pending_tokens:
|
|
comment = " ".join(c for c in (pending_comments + inline_comments) if c).strip()
|
|
start = pending_start or end_line
|
|
items = _expand_chain(pending_tokens, pending_lines)
|
|
if not items: # 체인이 아니면 문장 = 전체 범위
|
|
items = [(pending_tokens, start, end_line)]
|
|
chain_head = ""
|
|
else:
|
|
chain_head = " ".join(pending_tokens[: pending_tokens.index(":")])
|
|
for i, (toks, ls, le) in enumerate(items):
|
|
if not toks:
|
|
continue
|
|
statements.append(
|
|
Statement(
|
|
include=include,
|
|
line_start=ls,
|
|
# 마지막 항목은 종결 '.' 이 있는 줄까지 — 원문을 잘라도 문장이 닫힌다
|
|
line_end=end_line if i == len(items) - 1 else le,
|
|
tokens=toks,
|
|
comment_text=comment[:500],
|
|
chain_head=chain_head,
|
|
chain_start=start,
|
|
chain_end=end_line,
|
|
)
|
|
)
|
|
pending_tokens = []
|
|
pending_lines = []
|
|
pending_start = None
|
|
pending_comments = []
|
|
inline_comments = []
|
|
|
|
for ln in lines:
|
|
if ln.is_full_comment:
|
|
if pending_tokens:
|
|
inline_comments.append(ln.comment)
|
|
else:
|
|
pending_comments.append(ln.comment)
|
|
continue
|
|
if ln.comment:
|
|
inline_comments.append(ln.comment)
|
|
toks = tokenize_code(ln.code)
|
|
if not toks:
|
|
if not pending_tokens:
|
|
# 빈 줄이 이어지면 이전 주석은 다음 문장과 무관해질 수 있으나, 그대로 유지(요약 컨텍스트용)
|
|
pass
|
|
continue
|
|
for t in toks:
|
|
if t == ".":
|
|
flush(ln.no)
|
|
else:
|
|
if pending_start is None:
|
|
pending_start = ln.no
|
|
pending_tokens.append(t)
|
|
pending_lines.append(ln.no)
|
|
flush(len(lines))
|
|
for st in statements:
|
|
st.raw_text = st.text()[:400]
|
|
return statements, lines
|