렉싱과 파싱
컴파일러가 가장 먼저 하는 일은 (UTF-8 유니코드 텍스트로 된) 프로그램을 받아, 컴파일러가 문자열보다 더 편리하게 다룰 수 있는 데이터 형식으로 바꾸는 것입니다. 이 작업은 렉싱과 파싱의 두 단계로 이루어집니다.
- 렉싱은 문자열을 받아 토큰 스트림으로 바꿉니다. 예를 들어
foo.bar + buz는foo,.,bar,+,buz토큰으로 바뀝니다. 이는rustc_lexer에 구현되어 있습니다.
- 파싱은 토큰 스트림을 받아 컴파일러가 다루기 더 쉬운 구조화된 형태로 바꾸며, 이 형태를 보통 추상 구문 트리(AST)라고 부릅니다.
AST
AST는 Rust 프로그램의 구조를 메모리에 그대로 반영하며, Span을 사용하여 특정 AST 노드를 원래의 소스 텍스트와 연결합니다. AST는 rustc_ast에 정의되어 있으며, 토큰과 토큰 스트림에 대한 몇 가지 정의, AST를 변경하기 위한 자료 구조/트레이트, 그리고 컴파일러의 다른 AST 관련 부분(렉서와 매크로 확장 등)을 위한 공유 정의도 함께 들어 있습니다.
AST의 모든 노드는 고유한 NodeId를 가지며, 여기에는 구조체 같은 최상위 항목뿐만 아니라 개별 문과 표현식도 포함됩니다. NodeId는 크레이트 안에서 AST 노드를 고유하게 식별하는 식별자 번호입니다.
하지만 이 번호는 크레이트 안에서 절대적이기 때문에, AST에서 노드 하나만 추가하거나 제거해도 그 뒤에 오는 모든 NodeId가 바뀝니다. 그래서 변경되는 것을 가능한 한 적게 유지해야 하는 증분 컴파일에서는 NodeId가 사실상 쓸모가 없습니다.
NodeId는 매크로 확장이나 이름 해석처럼 AST를 직접 다루는 rustc의 모든 부분에서 사용됩니다(이에 대해서는 다음 몇 장에서 더 자세히 다룹니다).
파싱
파서는 rustc_parse에 정의되어 있으며, 여기에는 렉서에 대한 고수준 인터페이스와 매크로 확장 이후에 실행되는 몇 가지 검증 루틴도 함께 들어 있습니다. 특히 rustc_parse::parser에 파서 구현이 들어 있습니다.
파서의 주요 진입점은 rustc_parse에 있는 여러 parse_* 함수와 그 밖의 함수들입니다. 이 함수들을 사용하면 SourceFile(예: 파일 하나에 담긴 소스)을 토큰 스트림으로 바꾸고, 그 토큰 스트림으로 파서를 만든 다음, 파서를 실행하여 Crate(루트 AST 노드)를 얻는 등의 작업을 할 수 있습니다.
복사량을 최소화하기 위해 Lexer와 Parser는 모두 부모인 ParseSess에 자신을 묶는 라이프타임을 가집니다. 여기에는 파싱하는 동안 필요한 모든 정보뿐만 아니라 SourceMap 자체도 들어 있습니다.
파싱하는 동안 매크로 정의나 호출을 만날 수 있다는 점에 유의하십시오. 이것들은 나중에 확장하도록 따로 보관해 둡니다(매크로 확장을 참고하십시오). 확장 과정 자체에서 매크로의 출력을 파싱해야 할 수도 있으며, 그 결과 확장할 매크로가 더 드러날 수 있고, 이런 과정이 계속 이어질 수 있습니다.
어휘 분석 더 알아보기
어휘 분석 코드는 두 크레이트로 나뉘어 있습니다.
-
rustc_lexer크레이트는&str를 토큰을 이루는 조각들로 나누는 일을 담당합니다. 렉서는 흔히 생성된 유한 상태 기계로 구현하지만,rustc_lexer의 렉서는 손으로 직접 작성되었습니다. -
Lexer는rustc_lexer를rustc에 특화된 자료 구조와 통합합니다. 구체적으로는rustc_lexer가 반환한 토큰에Span정보를 추가하고 식별자를 인터닝합니다.