루비 문법에서의 개행이란 무엇인가?

[JA] Ruby's Line Breaks / Yuichiro Kaneko @spikeolaf

작성자
RubyKaigi
발행일
2025년 05월 27일

핵심 요약

  • 1 루비에서 개행은 문법적으로 문(statement)이 종료될 수 있는 곳에서 문을 종료시키는 역할을 하지만, 렉서(Lexer)의 상태(lex_state)에 따라 무시되거나 강제로 토큰화되는 복잡한 예외가 존재합니다.
  • 2 `lex_state`는 루비 렉서의 핵심적인 부분이면서도 개발자들에게 '혼돈의 문'으로 불릴 만큼 관리 및 이해가 어렵지만, 이를 문법에 통합하여 오토마톤으로 모델링함으로써 동작 원리를 체계적으로 분석할 수 있었습니다.
  • 3 엔드리스 레인지, 익명 인자, 전역 변수 별칭, BEGIN/END 블록 등 특정 구문에서 개행 처리의 예외 사례가 발견되었으며, 향후 파서가 렉서에게 개행 처리 방식을 명시적으로 전달하도록 문법을 확장하는 방향을 제안합니다.

도입

루비 언어에서 개행(newline)은 코드의 의미 있는 블록을 구분하는 중요한 역할을 합니다. 발표자는 `p 1\np 2`와 `1+\n2`와 같은 코드 예시를 통해 개행이 때로는 문을 분리하고 때로는 무시되는 루비의 독특한 개행 처리 방식에 주목합니다. 본 발표는 루비 문법에서 개행이 어떤 원칙에 따라 동작하는지 탐구하며, AKR 커미터의 가설("루비에서는 원칙적으로 문이 종료될 수 있는 곳에 개행이 있으면 문이 종료된다")을 검증하는 것을 목표로 합니다. 이를 위해 프로그래밍 언어를 둘러싼 네 가지 핵심 개념(언어, 문법, 오토마톤, 파서)을 먼저 살펴본 후, 루비 파서의 내부 구현을 깊이 있게 분석합니다.

프로그래밍 언어의 핵심 개념

  • 언어(Language): 특정 규칙을 따르는 문자열의 집합으로, 루비 코드를 정의합니다.
  • 문법(Grammar): 언어를 정의하기 위한 유한한 규칙의 집합으로, 코드에 구조를 부여합니다. if 문이나 1+2*3과 같은 표현식의 구조를 정의합니다.
  • 오토마톤(Automaton): 언어의 규칙을 인식하는 추상 기계입니다. 유한 오토마톤(Finite Automaton)은 자판기 예시처럼 상태 전이를 통해 입력을 처리하며, 스택을 가진 푸시다운 오토마톤(Pushdown Automaton)은 중첩된 구조(예: 클래스 정의)를 처리하는 데 사용됩니다.
  • 파서(Parser): 토큰을 입력으로 받아 문법 규칙에 따라 구조를 분석하는 오토마톤입니다. LR 파서는 시프트(shift)와 리듀스(reduce) 동작을 통해 문법 트리를 구성하며, 다음 토큰(룩어헤드 세트)을 통해 적절한 규칙을 선택합니다.

루비 파서의 개행 처리 내부 구현

  • 렉서(Lexer): 입력 문자열을 의미 있는 토큰으로 분할하는 컴포넌트입니다. 루비의 렉서는 lex_state라는 내부 상태에 따라 개행을 토큰으로 반환하거나 무시합니다.
  • lex_state의 역할:
    • EXPR_CLASS, EXPR_DOT, EXPR_BEG 상태에서는 개행을 무시하여 코드가 한 줄처럼 처리됩니다 (예: class A, obj. method, 1+ 2).
    • EXPR_END 상태에서는 개행을 토큰으로 반환하여 문을 분리합니다.
    • 특정 상황(예: 메서드 인자 목록의 마지막 필수 키워드 인자)에서는 예외적으로 개행 토큰을 강제 반환하여 의도치 않은 파싱을 방지합니다.
  • lex_state의 복잡성: lex_state는 13가지 플래그와 그 조합으로 이루어진 복잡한 구조로, 루비 커미터들조차 “개발자의 이성을 좀먹는 상태 전이”, “파서의 심연에 숨어 있는 혼돈의 문”이라고 표현할 정도로 이해하고 관리하기 어렵습니다. 이는 파서와 렉서가 독립적이지 않고 상호작용하며, 하나의 lex_state가 여러 문법적 맥락에서 사용될 수 있기 때문입니다.

가설 검증 및 예외 사례

  • 모델링 접근 방식: lex_state의 혼돈을 해결하기 위해 파서와 lex_state를 하나의 통합된 오토마톤으로 모델링하는 시도가 이루어졌습니다. Yacc 문법을 확장하여 lex_state의 전이 규칙을 명시적으로 기술함으로써, 기계적으로 각 토큰 및 문법 규칙에 따른 lex_state의 동작을 파악할 수 있게 되었습니다.
  • 가설에 대한 반례:
    • 문법적으로 개행이 허용되나 렉서가 무시하는 경우:
      • 엔드리스 레인지 (1.. 2): ..EXPR_BEG 상태로 전환되어 개행이 무시되고 1..2로 파싱됩니다.
      • 익명 인자 (foo(* args)): *EXPR_BEG 상태로 전환되어 개행이 무시되고 foo(*args)로 파싱됩니다. **, &도 동일합니다.
    • 문법적으로 개행이 허용되지 않으나 렉서가 개행 토큰을 반환하는 경우:
      • 전역 변수 별칭 (alias $foo $bar): alias $fooEXPR_END 상태에서 개행 토큰이 반환되나 문법이 이를 허용하지 않아 구문 오류가 발생합니다.
      • BEGIN { ... }, END { ... } 블록 (BEGIN {): 키워드와 { 사이에 개행이 있을 경우 구문 오류가 발생합니다.
  • 이러한 예외 사례들은 AKR의 가설이 대부분의 경우에 유효하지만, 특정 엣지 케이스에서는 복잡한 lex_state 동작으로 인해 원칙이 깨질 수 있음을 보여줍니다.

결론

루비 문법에서 개행의 원칙을 이해하기 위해서는 `lex_state`라는 복잡한 메커니즘을 완전히 파악해야 합니다. 본 연구는 `lex_state`를 문법과 오토마톤의 질서 있는 프레임워크 안에 통합함으로써, 그동안 혼돈으로 여겨졌던 `lex_state`의 동작을 인간이 이해할 수 있는 수준으로 끌어올리는 데 성공했습니다. 결과적으로, 루비의 개행 처리에는 "문이나 식이 종료될 수 있는 곳에 개행이 있으면 종료된다"는 대원칙이 존재하지만, 엔드리스 레인지, 익명 인자, 전역 변수 별칭, `BEGIN`/`END`와 같은 특정 예외 사례들이 있음을 확인했습니다. 향후에는 파서가 렉서에게 개행 무시 또는 반환 여부를 명시적으로 전달하고, 문법 파일에 이러한 예외적 개행 처리 규칙을 추가함으로써 루비 문법의 일관성과 이해도를 높일 수 있을 것으로 기대됩니다.

댓글 0

댓글 작성

0/1000
정중하고 건설적인 댓글을 작성해 주세요.

아직 댓글이 없습니다

첫 번째 댓글을 작성해보세요!