Ruby 구문 분석기에서의 개행 처리: 원칙과 예외

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

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

핵심 요약

  • 1 Ruby의 개행 처리는 'lex_state'라는 복잡한 내부 상태에 의해 결정되며, 이는 언어의 문법, 오토마톤, 파서 개념과 밀접하게 연관됩니다.
  • 2 일반적으로 문장이 종료될 수 있는 곳에서 개행은 문장을 종료시키지만, 'endless range'나 'anonymous arguments'와 같은 특정 구문에서는 개행이 무시되는 예외가 존재합니다.
  • 3 'lex_state'의 복잡성을 해결하기 위해 파서와 'lex_state'를 통합된 오토마톤으로 모델링하여 개행 처리의 원칙과 예외를 명확히 이해할 수 있게 되었습니다.

도입

본 발표는 Ruby 프로그래밍 언어에서 개행(newline)이 문법적으로 어떻게 처리되는지에 대한 깊이 있는 분석을 제공합니다. 발표자인 카네코 유이치로(金子優一郎)는 Ruby의 내부 구현, 특히 파서(Parser)와 렉서(Lexer)의 작동 방식을 통해 개행의 원칙과 예외를 탐구합니다. Ruby는 개행을 통해 의미 있는 코드 블록을 구분할 수 있는 언어이지만, 특정 상황에서는 개행이 무시되거나 다르게 해석되는 경우가 있어 개발자들에게 혼란을 야기할 수 있습니다. 이러한 복잡성의 중심에는 'lex_state'라는 렉서의 내부 상태가 있으며, 발표는 이 'lex_state'가 개행 처리에 미치는 영향을 심층적으로 다룹니다. 궁극적으로 Ruby의 문법적 개행 처리에 대한 명확한 원칙을 정립하고, 그 예외 사례들을 밝히는 것을 목표로 합니다.

발표는 프로그래밍 언어를 둘러싼 네 가지 핵심 개념인 언어(Language), 문법(Grammar), 오토마톤(Automaton), 그리고 파서(Parser)에 대한 설명으로 시작합니다. 언어는 문자열의 집합을 의미하며, 문법은 언어를 정의하는 유한한 규칙의 집합입니다. 오토마톤은 특정 상태에서 다음 입력을 기반으로 다음 상태로 전이하는 기계 모델이며, 파서는 이러한 오토마톤을 사용하여 입력 문자열을 문법적으로 분석합니다. 특히 Ruby 파서는 푸시다운 오토마톤(Pushdown Automaton)으로 구현되어 스택을 활용하여 무한한 중첩 구조를 처리합니다.

Ruby의 개행 처리는 주로 렉서에 의해 결정됩니다. 렉서는 입력 문자열을 의미 있는 단위인 토큰(token)으로 분할하며, 이 과정에서 ‘lex_state’라는 13가지 플래그로 구성된 내부 상태를 활용하여 개행을 무시할지 혹은 개행 토큰을 반환할지를 판단합니다. 예를 들어, ‘EXPR_CLASS’ 상태(클래스 키워드 직후)나 ‘EXPR_DOT’ 상태(메서드 호출의 점 직후)에서는 개행이 무시되어 코드가 정상적으로 작동합니다. 반면, ‘EXPR_BEG’ 상태(표현식 시작)에서는 개행이 무시되지만, ‘EXPR_END’ 상태(표현식 종료)에서는 개행 토큰이 반환되어 문장이 종료됩니다. 이는 ‘1 + 2’와 같은 코드에서 ‘+’ 뒤의 개행이 무시되는 반면, ‘1 + \n 2’와 같이 ‘+’가 있는 줄에서 문장이 끝나는 것처럼 보이는 경우 ‘1’과 ‘+2’가 별개의 문장으로 해석되는 이유를 설명합니다.

‘lex_state’는 그 복잡성 때문에 Ruby 커미터들 사이에서도 이해하기 어렵고 “카오스(Chaos)” 또는 “개발자의 이성을 갉아먹는 상태 전이”로 묘사될 정도로 악명이 높습니다. 이는 파서와 렉서가 독립적으로 작동하면서 ‘lex_state’의 변경이 문법의 여러 부분에 예측 불가능한 영향을 미칠 수 있기 때문입니다. 특정 토큰의 ‘lex_state’ 변경이 다른 문법 규칙의 ‘lex_state’에 연쇄적으로 영향을 미치므로, 이를 모두 고려하여 관리하는 것이 매우 어렵습니다.

이러한 ‘lex_state’의 복잡성을 해결하고 개행 처리의 원칙을 명확히 하기 위해 발표자는 파서와 ‘lex_state’를 통합된 하나의 오토마톤으로 모델링하는 방법을 제안합니다. 이를 위해 YARV 문법을 확장하여 ‘lex_state’의 상태와 전이를 기술할 수 있도록 했습니다. 이 모델링을 통해 각 토큰과 문법 규칙에 따른 ‘lex_state’의 전이를 기계적으로 파악할 수 있게 되었고, 이를 바탕으로 기존의 “Ruby에서는 원칙적으로 문장이 종료될 수 있는 곳에 개행이 있으면 문장이 종료된다”는 가설을 검증했습니다.

검증 결과, 이 가설에는 몇 가지 예외가 발견되었습니다. 1. 문법적으로 개행이 허용되지만 렉서가 개행을 무시하는 경우: ‘endless range’ (예: 1.. \n b)나 ‘anonymous arguments’ (예: foo(* \n args))가 이에 해당합니다. 이들은 ‘EXPR_BEG’ 상태로 인해 개행이 무시됩니다. 2. 문법적으로 개행이 허용되지 않지만 렉서가 개행을 반환하는 경우: alias 구문에서 전역 변수 별칭 지정 시 변수 사이에 개행을 넣으면 구문 오류가 발생합니다. 또한 BEGINEND 블록 내부 키워드 사이의 개행도 구문 오류를 유발합니다. 이러한 예외들은 ‘lex_state’의 미묘한 동작 방식이나 의도하지 않은 결과로 분석됩니다.

이러한 예외에도 불구하고, 대부분의 경우 “문장이나 식이 종료될 수 있는 곳에서 개행이 있으면 종료된다”는 원칙이 적용되며, “식이 종료될 수 없는 곳에 있는 개행은 무시된다”는 원칙 또한 대체로 유효하다는 결론을 내렸습니다.

결론

Ruby의 개행 처리는 겉보기에는 단순해 보이지만, 내부적으로 'lex_state'라는 복잡한 메커니즘에 의해 제어되며 수많은 예외를 포함하고 있습니다. 'lex_state'의 혼돈스러운 특성으로 인해 문법의 근본적인 원칙을 이해하기가 어려웠습니다. 그러나 파서와 'lex_state'를 하나의 오토마톤으로 통합하여 모델링함으로써, 인간이 'lex_state'의 동작을 이해하고 예측할 수 있게 되었습니다. 이러한 분석을 통해 Ruby의 개행 처리에는 "문장이나 식이 종료될 수 있는 곳에서 개행이 있으면 종료"되는 대원칙이 존재하지만, 'endless range', 'anonymous arguments'와 같이 개행을 무시하는 예외와 'global alias', 'BEGIN'/'END' 블록과 같이 개행을 허용하지 않는 예외가 일부 존재함을 확인했습니다. 향후 'lex_state'의 기능을 문법 파일에 명시적으로 기술함으로써, 개발자들이 개행 처리의 규칙과 예외를 보다 명확하게 이해하고 관리할 수 있도록 개선할 필요가 있습니다. 이는 Ruby의 문법을 더욱 질서정연하고 예측 가능하게 만들며, 파서 구현의 복잡성을 줄이는 데 기여할 것입니다.

댓글 0

댓글 작성

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

아직 댓글이 없습니다

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