"""문장 분리 — '.' 종결, 체인(:` `,`) 전개. (계획서 §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"^[ 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