발표는 프로그래밍 언어를 둘러싼 네 가지 핵심 개념인 언어(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
구문에서 전역 변수 별칭 지정 시 변수 사이에 개행을 넣으면 구문 오류가 발생합니다. 또한 BEGIN
및 END
블록 내부 키워드 사이의 개행도 구문 오류를 유발합니다. 이러한 예외들은 ‘lex_state’의 미묘한 동작 방식이나 의도하지 않은 결과로 분석됩니다.
이러한 예외에도 불구하고, 대부분의 경우 “문장이나 식이 종료될 수 있는 곳에서 개행이 있으면 종료된다”는 원칙이 적용되며, “식이 종료될 수 없는 곳에 있는 개행은 무시된다”는 원칙 또한 대체로 유효하다는 결론을 내렸습니다.