프로그래밍 언어를 해석하는 과정은 일반적으로 세 단계로 이루어집니다.
-
토큰화 (Lexing): 입력 텍스트를 의미 있는 토큰 목록으로 분해합니다.
-
파싱 (Parsing): 토큰을 분석하여 프로그램 구조를 이해하고, 이를 추상 구문 트리(AST)와 같은 데이터 표현으로 변환합니다.
-
평가 (Evaluating): 파싱된 입력을 실행하여 결과를 생성합니다.
Prism의 유용성
기존 Ruby 파서인 parse.y는 CRuby에 특화되어 JRuby, TruffleRuby와 같은 다른 Ruby 구현체들이 자체 파서를 개발해야 했습니다. 이로 인해 RuboCop, 코드 편집기 등 Ruby 분석 도구들이 최신 Ruby 문법을 따라가지 못하거나 호환성 문제가 발생했습니다. Prism은 이러한 문제를 해결하기 위해 모든 Ruby 도구 및 구현체의 사실상 표준 파서가 되었으며, 현재 CRuby, JRuby, TruffleRuby, Rails, RuboCop 등에서 활발히 사용되고 있습니다.
첫 번째 트랜스파일러 구축: Emoruby 변환기
Prism의 렉싱 기능을 활용하여 Ruby 코드를 Emoruby로 변환하는 트랜스파일러를 구축하는 과정을 소개합니다.
-
Prism.lex메서드를 사용하여 입력 코드를 토큰화합니다. -
Emoruby의 변환 파일을 참조하여 Ruby 토큰을 해당 이모지 대체제로 매핑합니다.
-
Prism::Token인스턴스의 위치 정보를 활용하여 들여쓰기 및 공백을 정확하게 처리하여 원본 코드의 가독성을 유지합니다.
Prism 파싱 활용: Struct를 Data 클래스로 리팩토링
Ruby 3.2의 Data 클래스를 활용하여 기존 Struct를 리팩토링하는 도구를 개발합니다.
-
Prism::Visitor클래스를 사용하여 AST 노드를 순회하며Struct.new호출을 식별합니다. -
CallNode타입을 필터링하여Struct.new호출을 찾아냅니다. -
Struct인수를 수집하고,Data.define으로 대체할 코드를 생성합니다. -
수정 사항은 시작 위치의 오름차순으로 적용하며, 멀티바이트 문자 처리를 위해
byteslice를 사용합니다.
뮤테이션 검사:
Data 객체는 불변성을 가지므로, 내부 상태를 변경하는 Struct는 Data로 변환할 수 없습니다. 이를 위해 중첩된 Visitor를 사용하여 Struct 본문에 뮤테이션(예: 인스턴스 변수 쓰기, 속성 쓰기)이 있는지 검사합니다.
-
“write” 작업을 수행하는 노드를 식별하여 뮤테이션을 감지합니다.
-
상수, 지역 변수, 전역 변수 쓰기는
Struct의 내부 상태 변경으로 간주하지 않습니다.