What Is Rust Doing Behind the Curtains?

Matthias Endler

Rust는 커튼 뒤에서 무슨 일을 하고 있을까?

Rust에는 코드를 즐겁게 작성할 수 있게 해 주는 문법적 설탕이 풍부합니다. 하지만 커튼 뒤를 들여다보고 컴파일러가 우리 코드로 실제로 무슨 일을 하는지 파악하기는 때때로 어렵습니다.

Rust Belt Rust 2018에서 Tshepang Lekhonkhobe가 발표한 Syntax conveniences afforded by the compiler(컴파일러가 제공하는 문법적 편의)라는 발표를 들었습니다(영상 보기).

초록을 인용하면 다음과 같습니다.

Rust 컴파일러는 사용자의 삶을 더 편하게 만드는 여러 편의를 제공합니다. 내부에서 무슨 일이 일어나는지 신비롭게 느끼지 않으려면 이러한 편의가 무엇인지 아는 것이 좋습니다… 세상에 대해 마법 같은 생각을 덜 할수록 더 좋습니다.

그는 이어서 이러한 편의의 몇 가지 예를 들었습니다.

  • 라이프타임 생략
  • 타입 추론
  • 신택틱 슈가
  • 암시적 역참조
  • 타입 강제 변환
  • 숨겨진 코드(예: prelude)

발표 중에 그가 이러한 편의가 적용된 코드와 적용되지 않은 코드를 비교하는 모습을 보는 것은 매우 유익하고 재미있었습니다.

집에 돌아온 뒤 더 알아보고 싶어졌습니다. Rust가 커튼 뒤에서 무슨 일을 하고 있는지 보여 주는 도구가 있을지 궁금했습니다.

Reddit에서 desugar된 출력을 생성하는 컴파일러 플래그에 대한 토론을 찾았습니다. (여기서는 +nightly 플래그로 nightly 컴파일러를 실행하기 위해 rustup을 사용했습니다.)

rustc +nightly -Zunpretty=hir example.rs

HIR은 high-level intermediate representation(고수준 중간 표현)의 약자입니다. 이는 컴파일러가 사용하기에 더 적합한 추상 구문 트리(AST)라고 할 수 있습니다. 이후 컴파일 단계에서 다루기 쉬운 기본 구성 요소로 신택틱 슈가를 대체합니다. 더 자세한 내용은 Nico Matsakis의 상세한 설명 글을 참고하시기 바랍니다.

rustc 컴파일 다이어그램. HIR은 첫 번째 단계입니다.

어쨌든 출력 결과는 놀라울 정도로 읽기 쉬웠습니다(아래 참조). 구문 강조와 서식을 더하면 꽤 유용한 도구가 될 수 있겠다는 생각이 들었습니다.

출력을 rustfmt로 포맷팅해 보았는데, 터무니없을 정도로 잘 동작했습니다. 이 빠른 성과에 힘입어 이를 cargo 서브커맨드로 감싸 cargo-inspect라 이름 붙였습니다.

실제 코드로 cargo-inspect를 시험해 보겠습니다!

예제 - 범위 표현식 desugar하기

다음 예제들은 프로젝트의 examples 폴더에서도 찾아볼 수 있습니다.

입력:

for n in 1..3 {
    println!("{}", n);
}

cargo-inspect 출력:

Range 출력 결과

이것이 깔끔하게 포맷팅된 터미널 출력입니다. bat 기반 라이브러리인 prettyprint 덕분에 줄 번호와 색상이 표시됩니다. 잘 보이지 않을 수도 있으니 핵심 부분만 옮겨 보겠습니다.

match ::std::iter::IntoIterator::into_iter(
        ::std::ops::Range { start: 1, end: 3 })
  mut iter => loop {
    // ...
  },
};

1..3std::ops::Range { start: 1, end: 3 }으로 변환되는 것을 볼 수 있습니다. 컴파일러 백엔드 관점에서는 두 표현이 완전히 동일합니다. 그래서 다음 코드는 성립합니다.

assert_eq!((1..3), std::ops::Range { start: 1, end: 3 });

예제 - 파일 처리

입력:

use std::fs::File;
use std::io::Error;

fn main() -> Result<(), Error> {
    let file = File::open("file.txt")?;
    Ok(())
}

출력:

#[prelude_import]
use std::prelude::v1::*;
#[macro_use]
extern crate std;
use std::fs::File;
use std::io::Error;

fn main() -> Result<(), Error> {
  let file = match ::std::ops::Try::into_result(
      <File>::open("file.txt")) {
    ::std::result::Result::Err(err) =>
    #[allow(unreachable_code)]
    {
      #[allow(unreachable_code)]
      return ::std::ops::Try::from_error(
          ::std::convert::From::from(err))
    }
    ::std::result::Result::Ok(val) =>
    #[allow(unreachable_code)]
    {
      #[allow(unreachable_code)]
      val
    }
  };
  Ok(())
}

? 연산자가 File::openResult에 대한 match로 desugar되는 것을 볼 수 있습니다. 오류가 발생한 경우에는 std::convert::From::from을 적용해 오류 타입을 변환하고, 그렇지 않으면 단순히 Ok 값을 반환합니다.

발표

벨기에에서 열린 FOSDEM에서 이 프로젝트에 대해 자세히 발표할 기회가 있었습니다. 발표 영상은 다음과 같습니다.

향후 계획

여기서 컴파일러를 다시 만들려는 계획은 없습니다. rustc가 제가 할 수 있는 것보다 훨씬 더 훌륭하게 해내고 있습니다. 이 모든 기능은 이미 존재하던 것이며, 저는 단지 저 같은 학습자가 컴파일러에 더 쉽게 다가갈 수 있도록 하려는 것입니다.

현재 이 도구는 다소 불안정합니다. 문제가 생기면 보기 좋지 않은 오류 메시지를 출력합니다. 작고 독립적인 예제 스니펫에 실행할 때 가장 빛을 발합니다.

참여해 보세요!

GitHub에 다른 분들이 참여할 수 있도록 몇 가지 이슈를 열어 두었습니다. 특히 다음과 같은 옵션이 있으면 좋겠습니다.

또한 특히 흥미로운 코드 예제를 찾으셨다면 주저하지 말고 examples 폴더에 기여해 주시기 바랍니다.

원문은 Matthias Endler님이 에 게재했습니다.

이 글은 muse-spark-1.2-contributor 모델을 사용해 번역했습니다.