에디션
이 장에서는 rustc에서 에디션 지원이 어떻게 동작하는지 개략적으로 설명합니다. 이 장은 독자가 에디션이 무엇인지 이미 알고 있다고 가정합니다(에디션 가이드를 참고하십시오).
에디션 정의
--edition CLI 플래그는 크레이트에 사용할 에디션을 지정합니다. 이 값은 Session::edition에서 접근할 수 있습니다. 크레이트의 에디션을 확인하는 Session::at_least_rust_2021 같은 편의 함수가 있지만, 전역 세션을 확인할지 스팬을 확인할지 주의해야 합니다. 아래의 에디션 위생을 참고하십시오.
at_least_rust_20xx 편의 메서드 대신 Edition 타입의 비교 기능을 사용하여 span.edition() >= Edition::Edition2021처럼 범위를 검사할 수도 있습니다.
새 에디션 추가하기
새 에디션을 추가하는 작업은 주로 Edition 열거형에 배리언트를 추가한 다음, 그 때문에 깨지는 부분을 모두 고치는 것입니다. 예시는 #94461을 참고하십시오.
기능과 에디션 안정성
에디션이 안정적인지 여부는 Edition 열거형에서 정의합니다. 안정적이지 않은 에디션을 활성화하려면 -Zunstable-options CLI 옵션을 전달해야 합니다.
새 기능을 추가할 때 향후 에디션과 관련된 안정성을 처리하는 방법은 다음 두 가지 중에서 선택할 수 있습니다.
span.at_least_rust_20xx()처럼 스팬의 에디션을 확인하거나(에디션 위생을 참고하십시오)Session::edition을 확인하기만 합니다. 이 경우 기능을 사용할 수 있는지는 암묵적으로 에디션 자체의 안정성에 따라 결정됩니다.- 새 동작을 기능 게이트 뒤에 둡니다.
비교적 단순한 변경이라면 현재 에디션만 확인해도 충분할 수 있습니다. 하지만 규모가 큰 언어 변경이라면 기능 게이트를 만드는 것을 고려해야 합니다. 기능 게이트를 사용하면 다음과 같은 여러 이점이 있습니다.
- 기능 게이트가 있으면 새 기능을 개발하고 실험하기가 더 쉽습니다.
#![feature(…)]속성을 사용할 때 새 기능을 활성화한다는 의도가 분명해집니다.- 아직 완성되지 않은 기능이 이미 완성되어 준비된 에디션별 기능의 테스트를 방해하지 않으므로 에디션을 테스트하기가 더 쉬워집니다.
- 기능이 에디션과 분리되므로, 기능이 준비되었을 때 다음 에디션에 넣을지 여부를 팀이 신중하게 결정하기가 더 쉬워집니다.
기능이 완성되어 준비되면 기능 게이트를 제거할 수 있습니다(이때 코드는 스팬이나 Session의 에디션만 확인하여 기능이 활성화되었는지 판단해야 합니다).
기능을 검사하는 방법에는 몇 가지가 있습니다.
-
에디션에 포함될지 불확실한 매우 실험적인 기능은
tcx.features().my_feature같은 일반 기능 게이트를 구현하고 당분간 에디션을 무시해도 됩니다. -
에디션에 포함될 수도 있는 실험적 기능은
tcx.features().my_feature && span.at_least_rust_20xx()로 게이트를 구현해야 합니다. 이렇게 하면 사용자가 여전히#![feature(my_feature)]를 지정해야 하므로, 이미 준비되어 에디션에 채택된 다른 에디션 기능의 테스트를 방해하지 않습니다. -
에디션에 확실히 포함되기로 확정된 실험적 기능은
tcx.features().my_feature || span.at_least_rust_20xx()로 게이트를 구현하거나, 기능 검사를 아예 없애고span.at_least_rust_20xx()만 확인해야 합니다.
여러 곳에서 기능 게이팅을 해야 한다면, 수정할 곳이 한 곳만 되도록 검사를 하나의 함수에 두는 것을 고려하십시오. 예를 들면 다음과 같습니다.
// An example from Edition 2021 disjoint closure captures.
fn enable_precise_capture(tcx: TyCtxt<'_>, span: Span) -> bool {
tcx.features().capture_disjoint_fields || span.rust_2021()
}
린트가 안정성을 어떻게 처리하는지에 대한 자세한 내용은 아래의 린트와 안정성을 참조하십시오.
에디션 파싱
대부분의 경우 렉서는 에디션과 무관하게 동작합니다. Lexer 내에서는 에디션별 동작에 따라 토큰이 수정될 수 있습니다. 예를 들어, c"foo"와 같은 C 문자열 리터럴은 2021 이전 에디션에서 여러 토큰으로 나뉩니다. 2021 에디션의 예약된 접두사 같은 것들도 이곳에서 처리됩니다.
에디션별 파싱은 비교적 드뭅니다. 한 가지 예로 async fn이 있는데, 이는 토큰의 스팬을 검사하여 2015 에디션인지 판단하고, 그런 경우 오류를 발생시킵니다. 이는 구문이 이미 유효하지 않은 경우에만 할 수 있습니다.
파서에서 에디션 검사를 해야 한다면 일반적으로 토큰의 에디션을 확인해야 합니다. 에디션 위생을 참조하십시오. 드문 경우에는 대신 ParseSess::edition에서 전역 에디션을 확인해야 할 수도 있습니다.
대부분의 에디션별 파싱 동작은 파서가 아니라 마이그레이션 린트로 처리됩니다. 이 방식은 (새로운 구문이 아니라) 구문에 변경이 있을 때 적합합니다. 이렇게 하면 이전 에디션에서는 기존 구문이 계속 동작할 수 있습니다. 그런 다음 린트가 동작의 변경을 검사합니다. 이전 에디션에서는 새 에디션으로의 마이그레이션을 돕기 위해 린트 패스가 마이그레이션 린트를 발생시켜야 합니다. 최신 에디션에서는 대신 코드가 emit_err로 하드 오류를 발생시켜야 합니다. 예를 들어, 더 이상 사용되지 않는 start...end 패턴 구문은 2021 이전 에디션에서는 ellipsis_inclusive_range_patterns 린트를 내보내고, 2021에서는 emit_err 메서드를 통해 하드 오류가 됩니다.
키워드
새 키워드는 에디션 경계를 넘어 도입될 수 있습니다. 이는 Symbol::is_used_keyword_conditional과 같은 함수로 구현되며, 이러한 함수는 키워드가 정의된 순서에 의존합니다.
새 키워드가 도입되면, 해당 키워드를 식별자로 사용하고 있을 수 있는 코드를 자동 마이그레이션으로 전환할 수 있도록 keyword_idents 린트를 갱신해야 합니다(KeywordIdents 참조). 고려할 수 있는 대안으로, 키워드가 사용되는 위치만으로 충분히 구별할 수 있다면 해당 키워드를 약한 키워드로 구현하는 방법이 있습니다.
추가로 고려할 수 있는 선택지는 RFC 3101에서 도입된 k# 접두사입니다. 이를 사용하면 키워드가 도입된 에디션 이전의 에디션에서도 해당 키워드를 사용할 수 있습니다. 이는 현재 구현되어 있지 않습니다.
에디션 위생
스팬에는 해당 스팬이 비롯된 크레이트의 에디션이 표시됩니다. 이것이 무엇을 의미하는지에 대한 사용자 중심의 설명은 에디션 가이드의 매크로 위생을 참조하십시오.
일반적으로 전역 Session의 에디션을 확인하는 대신 토큰 스팬의 에디션을 사용해야 합니다. 예를 들어, sess.at_least_rust_2021() 대신 span.edition().at_least_rust_2021()를 사용하십시오. 이렇게 하면 매크로를 여러 크레이트에 걸쳐 사용할 때에도 올바르게 동작하도록 보장하는 데 도움이 됩니다.
린트
린트는 에디션과 상호작용하기 위한 몇 가지 옵션을 지원합니다. 린트는 미래 비호환 에디션 마이그레이션 린트가 될 수 있으며, 이는 새로운 에디션으로의 마이그레이션을 지원하는 데 사용됩니다. 또는 린트가 에디션별 린트가 될 수도 있으며, 이 경우 특정 에디션부터 기본 레벨이 바뀝니다.
마이그레이션 린트
마이그레이션 린트는 프로젝트를 한 에디션에서 다음 에디션으로 마이그레이션하는 데 사용됩니다. 마이그레이션 린트는 코드를 다시 작성하여 이전 에디션과 다음 에디션 모두에서 성공적으로 컴파일되도록 하는 MachineApplicable 제안으로 구현됩니다. 예를 들어, keyword_idents 린트는 새 키워드와 충돌하는 식별자가 원시 식별자 구문을 사용하도록 바꾸어 충돌을 피합니다(예를 들어 async를 r#async로 바꿉니다).
마이그레이션 린트는 린트 선언에서 FutureIncompatibilityReason::EditionError 또는 FutureIncompatibilityReason::EditionSemanticsChange 미래 비호환 옵션으로 선언해야 합니다:
declare_lint! {
pub KEYWORD_IDENTS,
Allow,
"detects edition keywords being used as an identifier",
@future_incompatible = FutureIncompatibleInfo {
reason: fcw!(EditionError 2018 "slug-of-edition-guide-page")
};
}
이렇게 선언하면 해당 린트는 적절한 rust-20xx-compatibility 린트 그룹에 자동으로 추가됩니다. 사용자가 cargo fix --edition을 실행하면, cargo는 --force-warn rust-20xx-compatibility 플래그를 전달하여 에디션 마이그레이션 중에 이러한 린트가 모두 나타나도록 강제합니다. 또한 Cargo는 다른 린트가 에디션 마이그레이션을 방해하지 않도록 --cap-lints=allow를 전달합니다.
예제 코드가 올바른 에디션을 설정하는지 확인하십시오. 예제는 이전 에디션을 기준으로 작성해야 하며, 마이그레이션 경고가 어떻게 표시되는지 보여 주어야 합니다. 예를 들어, 2024 마이그레이션을 위한 이 린트는 2021 에디션의 예제를 보여 줍니다:
declare_lint! {
/// The `keyword_idents_2024` lint detects ...
///
/// ### Example
///
/// ```rust,edition2021
/// #![warn(keyword_idents_2024)]
/// fn gen() {}
/// ```
///
/// {{produces}}
}
마이그레이션 린트의 기본값은 Allow 또는 Warn 중 하나일 수 있습니다. Allow인 경우, 사용자가 에디션 마이그레이션을 수동으로 수행하고 있거나 마이그레이션 중에 문제가 발생하지 않는 한 사용자는 대개 이 경고를 보지 못합니다. 대부분의 마이그레이션 린트는 Allow입니다.
기본값이 Warn인 경우, 모든 에디션의 사용자가 이 경고를 보게 됩니다. 모든 사람이 해당 변경 사항을 알고 모든 에디션에서 코드를 업데이트하도록 권장하는 것이 중요하다고 생각하는 경우에만 Warn을 사용하십시오. 많은 프로젝트에 영향을 미치는 새로운 기본 경고 린트는 사용자에게 큰 혼란과 불만을 줄 수 있다는 점에 유의하십시오. 에디션이 안정화되고 몇 년이 지난 후에 Allow를 Warn으로 바꾸는 것을 고려할 수 있습니다. 이렇게 하면 아직 새 에디션으로 업데이트하지 않은 비교적 소수의 뒤처진 사용자에게만 경고가 표시됩니다.
에디션별 린트
린트는 특정 에디션부터 다른 레벨을 갖도록 표시할 수 있습니다. 린트 선언에서 @edition 마커를 사용하십시오:
declare_lint! {
pub SOME_LINT_NAME,
Allow,
"my lint description",
@edition Edition2024 => Warn;
}
여기서 SOME_LINT_NAME은 2024 이전의 모든 에디션에서는 기본값이 Allow이고, 그 이후에는 Warn이 됩니다.
다른 선택지도 있으므로 이 방법은 일반적으로 신중하게 사용해야 합니다:
-
에디션과 무관하고 영향이 작은 스타일 변경은 모든 에디션에서 해당 린트를
Warn으로 설정하기만 하면 됩니다. 사람들이 다른 작성 방식을 채택하기를 원한다면, 모든 프로젝트에서 이 린트가 표시되도록 하는 변경을 과감히 커밋하십시오.기본적으로 경고하는 새 린트가 많은 프로젝트에 걸리면 사용자에게 큰 혼란과 불만을 줄 수 있으므로 주의하십시오.
-
새 에디션에서는 새 스타일을 따르지 않으면 하드 오류가 되도록 바꾸고, 마이그레이션 린트를 사용하여 프로젝트를 새 스타일로 자동 변환하십시오. 예를 들어,
ellipsis_inclusive_range_patterns는 2021에서 하드 오류이며, 이전의 모든 에디션에서는 경고를 냅니다.이러한 변경은 에디션이 안정화된 후에는 추가할 수 없으므로 주의하십시오.
-
마이그레이션 린트도 시간이 지나면서 바뀔 수 있습니다. 예를 들어, 마이그레이션 린트는 처음에 기본값
Allow로 시작할 수 있습니다. 마이그레이션을 수행하는 사람들의 코드는 자동으로 새 코드로 갱신됩니다. 그런 다음 몇 년 후에 이전 에디션에서 해당 린트를Warn으로 바꿀 수 있습니다.예를 들어
anonymous_parameters는 2018 에디션 마이그레이션 린트(이자 2018에서는 하드 오류)였으며, 이전 에디션에서는 기본값이Allow였습니다. 그리고 3년 후에는 이전의 모든 에디션에서Warn으로 변경되어, 모든 사용자가 해당 스타일이 단계적으로 폐지되고 있다는 경고를 받게 되었습니다. 이것이 처음부터 경고였다면 많은 프로젝트에 영향을 주어 큰 혼란을 일으켰을 것입니다. 이를 에디션의 일부로 만들었기 때문에, 대부분의 사용자는 결국 새 에디션으로 업데이트하면서 마이그레이션을 통해 처리되었습니다.Warn으로 전환한 것은 업데이트하지 않은 소수의 뒤처진 사용자에게만 영향을 주었습니다.
린트와 안정성
린트는 불안정으로 표시할 수 있으며, 이는 새 에디션 기능을 개발하면서 마이그레이션 린트를 테스트해 보고 싶을 때 유용할 수 있습니다. 기능 게이트는 다음과 같이 린트 선언에 지정할 수 있습니다:
declare_lint! {
pub SOME_LINT_NAME,
Allow,
"my cool lint",
@feature_gate = sym::my_feature_name;
}
그러면 사용자가 적절한 #![feature(my_feature_name)]를 지정한 경우에만 린트가 발생합니다. 다만 마이그레이션을 테스트하는 crater 실행을 할 때가 되면 기능 게이트를 제거해야 한다는 점에 주의하십시오.
또는 기능 게이트 없이, 곧 나올 불안정한 에디션을 위한 기본 허용(allow-by-default) 마이그레이션 린트를 구현할 수도 있습니다. 기술적으로는 사용자가 에디션이 안정화되기 전에 해당 린트를 활성화할 수 있겠지만, 대부분은 새 린트가 존재한다는 것을 알아차리지 못할 것이며, 아무것도 방해하거나 망가뜨리지 않을 것입니다.
관용구 린트
2018 에디션에는 rust-2018-idioms 린트 그룹 아래에 “관용구 린트(idiom lints)“라는 개념이 있었습니다. 이 개념은 새로운 관용적 스타일을 rust-2018-compatibility 린트 그룹 아래의 강제 마이그레이션과 분리된 별도의 린트 그룹에 두어, 사람들이 특정 에디션 변경 사항을 선택적으로 적용하는 방식에 어느 정도 유연성을 주려는 것이었습니다.
전반적으로 이 접근 방식은 그다지 잘 작동하지 않은 것으로 보이며, 앞으로 관용구 그룹을 사용할 가능성은 낮습니다.
표준 라이브러리 변경 사항
프렐루드
각 에디션에는 해당 에디션 전용의 표준 라이브러리 프렐루드가 함께 제공됩니다. 이들은 core::prelude와 std::prelude에 일반 모듈로 구현되어 있습니다. 프렐루드에 새 항목을 추가할 수 있지만, 이것이 사용자의 기존 코드와 충돌할 수 있다는 점에 주의하십시오. 일반적으로 충돌을 피하려면 마이그레이션 린트를 사용하여 기존 코드를 마이그레이션해야 합니다. 예를 들어, rust_2021_prelude_collisions는 2021 에디션의 새 트레이트와의 충돌을 처리하는 데 사용됩니다.
사용자 정의 언어 동작
일반적으로 표준 라이브러리에는 호환성을 깨는 변경을 할 수 없습니다. 드물게는 팀이 동작 변경이 이 규칙을 깰 만큼 중요하다고 판단할 수도 있습니다. 단점은 이전 시그니처나 동작과 새 시그니처나 동작 중 어느 것을 언제 사용해야 하는지 구별할 수 있도록 컴파일러에서 특별한 처리가 필요하다는 것입니다.
한 가지 예는 배열의 into_iter()에 대한 메서드 해석 변경입니다. 이는 IntoIterator 트레이트에 #[rustc_skip_array_during_method_dispatch] 속성을 붙여 구현되었으며, 이 속성은 컴파일러가 에디션에 따라 다른 트레이트 해석 방식을 고려하도록 지시합니다.
또 다른 예는 panic! 매크로 변경입니다. 이를 위해 여러 패닉 매크로를 정의하고, 내장 패닉 매크로 구현이 이를 확장할 적절한 방법을 결정하도록 해야 했습니다. 여기에는 기존 코드를 새로운 형태로 조정하기 위한 non_fmt_panics 마이그레이션 린트도 포함되었는데, 이 린트는 패닉 매크로의 사용을 감지하기 위해 rustc_diagnostic_item 속성이 필요했습니다.
일반적으로 매우 가치가 높은 상황이 아니라면 이러한 특수한 경우는 피하는 것이 좋습니다.
표준 라이브러리 에디션 마이그레이션
표준 라이브러리 자체의 에디션을 업데이트하는 작업은 대략 다음 과정으로 이루어집니다.
- 새로 안정화된 에디션이 베타에 도달하고 부트스트랩 컴파일러가 업데이트될 때까지 기다리십시오.
- 마이그레이션 린트를 적용하십시오. 일부 코드는 외부 서브모듈1에 있고 표준 라이브러리는 조건부 컴파일을 많이 사용하므로, 이 과정은 복잡할 수 있습니다. 또한 표준 라이브러리 자체에
cargo fix --edition을 실행하는 것은 현실적이지 않을 수 있습니다. 한 가지 방법은 각 린트마다 각 크레이트의 맨 위에#![warn(...)]를 개별적으로 추가하고,./x check library를 실행하고, 마이그레이션을 적용한 다음,#![warn(...)]를 제거하고 각 마이그레이션을 별도로 커밋하는 것입니다. 전체 커버리지를 확보하려면 여러 타깃에 대해--target과 함께./x check를 실행해야 할 가능성이 높습니다(그렇지 않으면 CI를 통과시키는 데 며칠에서 몇 주를 소비하게 될 가능성이 큽니다)2. 더 많은 팁은 고급 마이그레이션 가이드도 참고하십시오.backtrace-rs에 마이그레이션을 적용하십시오. 2024 에디션 예시입니다. 이 작업은 크레이트 자체의 에디션을 업데이트하지 않는다는 점에 유의하십시오. 해당 크레이트는 crates.io에 독립적으로 배포되므로, 에디션을 업데이트하면 최소 Rust 버전이 제한되기 때문입니다. 해당 크레이트의 에디션이 업데이트될 때까지 회귀를 방지하기 위해#![deny()]속성을 몇 개 추가하는 것을 고려하십시오.stdarch에 마이그레이션을 적용하고, 에디션과 포매팅을 업데이트하십시오. 2024 에디션 예시입니다.- backtrace와 stdarch 서브모듈을 업데이트하는 PR을 올리고, 해당 PR이 병합될 때까지 기다리십시오.
- 표준 라이브러리 크레이트에 마이그레이션 린트를 적용하고, 에디션을 업데이트하십시오.
core부터 시작하여 한 번에 한 크레이트씩 작업하는 것이 좋습니다. 2024 에디션 예시입니다.
에디션 안정화하기
에디션 팀이 승인하면 에디션을 안정화하는 절차는 대략 다음과 같습니다.
LATEST_STABLE_EDITION을 업데이트하십시오.Edition::is_stable을 업데이트하십시오.- 에디션을 번호로 언급하는 문서를 모두 찾아 업데이트하십시오.
//@ edition헤더를 사용하는 테스트를 정리하여-Zunstable-options플래그를 제거하고, 해당 테스트가 실제로 안정화되었는지 확인하십시오. 참고: 이 작업은 자동화하는 것이 이상적입니다. #133582를 참조하십시오.- 변경되는 테스트는 모두 bless하십시오.
lint-docs가 새 에디션을 기본값으로 사용하도록 업데이트하십시오.
2024 에디션 예시를 참조하십시오.
-
향후에는 이 서브모듈들을
rust-lang/rust로 가져오는 방식으로 바뀌기를 기대합니다. ↩ -
또한 여러 타깃에 대해 많은 테스트를 수행해야 할 가능성이 높으며, 이때 docker 테스트가 유용합니다. ↩