호환성을 깨는 변경을 위한 절차
이 페이지는 기존 코드가 더 이상 컴파일되지 않게 만들 수 있는 컴파일러의 버그 수정이나 건전성 수정을 수행할 때 따라야 할 모범 사례 절차를 정의합니다. 이 글은 RFC 1589를 기반으로 합니다.
동기
때때로 기존 코드가 더 이상 컴파일되지 않게 만드는 버그 수정, 건전성 수정 또는 그 밖의 변경을 컴파일러에 적용해야 할 때가 있습니다. 이런 경우에는 Rust 사용자가 순조롭게 전환할 수 있는 방식으로 변경을 처리하는 것이 중요합니다. 우리가 피하고 싶은 것은 기존 프로그램이 알아보기 힘든 오류 메시지와 함께 갑자기 컴파일되지 않는 상황입니다. 그 대신 문제가 무엇인지, 어떻게 고쳐야 하는지, 왜 변경이 이루어졌는지에 대한 명확한 안내와 함께 점진적인 경고 기간을 두는 편을 선호합니다. 이 RFC는 그러한 순조로운 전환을 이루는 것을 목표로, 호환성을 깨는 변경을 처리하기 위해 우리가 발전시켜 온 절차를 설명합니다.
이 정책의 핵심 사항 중 하나는 (a) 가능하다면 처음에는 하드 오류가 아닌 경고를 내야 하며, (b) 기존 코드가 더 이상 컴파일되지 않게 만드는 모든 변경에는 관련된 추적 이슈가 있어야 한다는 점입니다. 이 이슈는 해당 변경의 결과에 대한 피드백을 모으는 창구 역할을 합니다. 때로는 변경이 예상보다 큰 영향을 미치거나, 미처 고려하지 못한 변경 회피 방법이 있을 수도 있습니다. 그런 경우 방향을 바꾸어 변경을 되돌리거나 다른 해결책을 찾기로 결정할 수 있습니다(경고를 사용하고 있다면 이렇게 하기가 특히 쉽습니다).
무엇이 버그 수정에 해당합니까?
이 RFC는 호환성을 깨는 변경이 언제 허용되는지를 정의하려 하지 않는다는 점에 유의하십시오. 그 내용은 이미 RFC 1122에서 다루고 있습니다. 이 문서는 이루어지는 변경이 해당 정책을 따른다고 가정합니다. 다음은 RFC 1122에 나온 조건을 요약한 것입니다:
- 건전성 변경: 타입 시스템에서 발견된 허점을 수정하는 것입니다.
- 컴파일러 버그: 컴파일러가 RFC나 언어 팀 결정에 명시된 의미론을 구현하지 않고 있는 부분입니다.
- 명세가 불충분한 언어 의미론: 컴파일러가 일관되지 않게 동작하고 이전에 공식적인 동작이 정해진 적 없는 회색 지대를 명확히 하는 것입니다.
자세한 내용은 해당 RFC를 참고하십시오!
상세 설계
호환성을 깨는 변경을 하는 절차는 다음과 같습니다(각 단계는 아래에서 더 자세히 설명합니다):
- 변경의 영향을 평가하기 위해 crater 실행을 수행하십시오.
- 해당 변경 전용의 특별 추적 이슈를 만드십시오.
- 곧바로 오류를 보고하지 마십시오. 그 대신 상위 호환성 린트 경고를 내십시오.
- 때로는 이것이 간단하지 않습니다. 과거에 우리가 사용했던 여러 기법에 대한 제안은 아래 글을 참고하십시오.
- 경고를 내는 것이 불가능한 경우:
- 오류를 보고하되, 사용자를 추적 이슈로 안내하는 구체적인 오류 메시지를 제공하도록 최선을 다하십시오
- 영향을 받는 것으로 알려진 모든 크레이트에 문제를 수정하는 PR을 제출하십시오
- 또는 최소한 해당 크레이트의 소유자에게 문제를 알리고 추적 이슈를 안내해야 합니다.
- 변경 사항이 실제 환경에서 최소 한 주기 이상 사용된 후에는 해당 경고를 오류로 전환하여 변경 사항을 안정화할 수 있습니다.
마지막으로, 플러그인에 영향을 미치는 rustc_ast 변경 사항은 일괄적으로 묶어서 처리하는 것이 일반적인 정책입니다. 이에 대해서는 아래에서 더 자세히 설명합니다.
추적 이슈
모든 호환성을 깨는 변경에는 해당 변경을 위한 전용 추적 이슈가 함께 있어야 합니다. 이 이슈의 본문에는 적용되는 변경 사항을 설명하되, 사용자가 코드를 고치기 위해 무엇을 해야 하는지에 중점을 두어야 합니다. 이슈는 이해하기 쉽고 실용적이어야 합니다. 자세한 내용은 사용자에게 RFC나 다른 이슈를 안내하는 것이 좋을 수도 있습니다. 이 이슈는 사용자가 질문이나 기타 우려 사항을 댓글로 남길 수 있는 공간의 역할도 합니다.
호환성을 깨는 변경의 추적 이슈 템플릿은 여기에서 찾을 수 있습니다. 이러한 이슈가 어떤 모습이어야 하는지에 대한 예시는 여기에서 찾을 수 있습니다.
향후 호환성 경고 발행하기
호환성을 깨는 변경을 처리하는 가장 좋은 방법은 먼저 향후 호환성 경고를 발행하는 것입니다. 이는 린트 경고의 특수한 범주입니다. 새로운 향후 호환성 경고는 다음과 같이 추가할 수 있습니다.
#![allow(unused)]
fn main() {
// 1. Define the lint in `compiler/rustc_lint/src/builtin.rs` and
// add the metadata for the future incompatibility:
declare_lint! {
pub YOUR_LINT_HERE,
Warn,
"illegal use of foo bar baz"
@future_incompatible = FutureIncompatibleInfo {
reason: fcw!(FutureReleaseError #1234) // your tracking issue here!
},
}
// 2. Add a dedicated lint pass for it.
// This step can be skipped if you emit the lint as part of an existing pass.
#[derive(Default)]
pub struct MyLintPass {
...
}
impl {Early,Late}LintPass for MyLintPass {
...
}
impl_lint_pass!(MyLintPass => [YOUR_LINT_HERE]);
// 3. emit the lint somewhere in your lint pass:
cx.emit_span_lint(
YOUR_LINT_HERE,
pat.span,
// some diagnostic struct
MyDiagnostic {
...
},
);
}
마지막으로, compiler/rustc_lint/src/lib.rs에 린트를 등록하십시오. 해당 파일에는 등록 방법을 보여 주는 예시가 이미 많이 있습니다.
유용한 기법
새로운 경고를 기존의 오래된 오류와 구분해 내기는 어려운 경우가 많습니다. 과거에 사용된 기법 중 하나는 기존 코드를 변경 없이 실행하여 그 코드가 보고했을 오류를 수집하는 것입니다. 그런 다음, 새로 내게 될 오류 중 원래 집합에 나타나지 않는 오류에 대해 경고를 발행할 수 있습니다. 또 다른 방법은 원래 코드가 실행을 마친 뒤 오류가 보고되었다면 컴파일을 중단하는 것입니다. 이렇게 하면 새 코드는 이전에 오류가 없었던 경우에만 실행된다는 것을 알 수 있습니다.
Crater와 crates.io
Crater는 변경 사항이 적용된 컴파일러로 모든 crates.io 크레이트와 여러 공개 GitHub 저장소를 컴파일하는 봇입니다. 그런 다음, 변경 사항으로 인해 더 이상 컴파일되지 않거나 새로 컴파일되기 시작한 크레이트를 담은 보고서가 생성됩니다. Crater 실행은 완료되기까지 며칠이 걸릴 수 있습니다.
영향을 평가하기 위해 항상 crater 실행을 해야 합니다. 호환성을 깨는 변경의 영향을 받는 크레이트 작성자에게 알리는 것이 예의 바르고 사려 깊은 일입니다. 문제를 고치는 PR을 제출하면 더욱 좋습니다.
곧바로 오류를 발행해도 괜찮은 경우가 있습니까?
영향이 미미하다고 판단되는 변경은 곧바로 오류를 발생시키는 단계로 넘어갈 수 있습니다. 한 가지 경험칙은 crates.io를 기준으로 확인하는 것입니다. 영향을 받는 프로젝트가 전체 10개 미만으로 발견되면(루트 오류 기준이 아님) 곧바로 오류로 전환할 수 있습니다. 이러한 경우에도 이전과 마찬가지로 “호환성을 깨는 변경” 페이지를 만들어야 하며, 오류가 사용자를 이 페이지로 안내하도록 해야 합니다. 다시 말해, 사용자가 경고 대신 오류를 받는다는 점을 제외하면 모든 것이 동일해야 합니다. 또한 영향을 받는 프로젝트에 PR을 제출해야 합니다(이상적으로는 변경을 구현하는 PR이 rustc에 반영되기 전에 제출해야 합니다).
영향이 미미하지 않다고 판단되는 경우(예: 10개가 넘는 크레이트가 영향을 받는 경우)에는 경고가 필요합니다(특정 사례에 대해 컴파일러 팀이 특별 예외를 허용하기로 합의한 경우는 제외합니다). 경고를 구현하기가 어렵다면, 영향을 받는 크레이트의 수를 줄이기 위해 변경을 반영하기 전에 크레이트를 마이그레이션하는 적극적인 전략을 세워야 합니다. 이러한 상황에 대처하는 몇 가지 기법은 다음과 같습니다.
- 문제의 하위 부분에 대해서는 경고를 발생시키고, 새로운 오류는 가능한 한 가장 작은 범위의 사례에만 적용하십시오.
- 문제를 해결하는 방법을 제안하고 사용자를 추적 이슈로 안내하는 매우 정확한 오류 메시지를 제공하도록 노력하십시오.
- 수정을 단계적으로 나누어 적용하는 것도 합리적일 수 있습니다.
- 먼저 가능한 곳에 경고를 추가하고, 그것이 반영된 후에 오류를 발생시키는 단계로 진행하십시오.
- 하위 사용자들이 사용할 수 있도록, 수정이 반영되기 전에 수정된 버전이 제공되도록 영향을 받는 크레이트의 작성자들과 협력하십시오.
안정화
변경이 이루어진 후에는 불안정 기능에 사용하는 것과 동일한 절차를 사용하여 변경을 안정화합니다.
-
새 릴리스가 나온 후에는 호환성을 깨는 변경에 해당하는 미해결 추적 이슈들을 검토하고, 그중 일부를 최종 의견 수렴 기간(FCP)에 올립니다.
-
이러한 이슈에 대한 FCP는 한 사이클 동안 진행됩니다. 사이클의 마지막 한두 주 동안 의견을 검토하고 다음과 같이 최종 결정을 내립니다.
- 오류로 전환: 변경을 하드 오류로 만들어야 합니다.
- 되돌리기: 경고를 제거하고 이전 코드가 계속 컴파일되도록 허용해야 합니다.
- 보류: 아직 결정할 수 없으므로 더 기다리거나 다른 전략을 시도합니다.
이상적으로는, 호환성을 깨는 변경이 확정되기 전에 컴파일러의 안정 브랜치에 반영되어 있어야 합니다.
린트 제거하기
“향후 경고“를 하드 오류로 만들기로 결정했다면, 해당 사용자 정의 린트를 제거하는 PR이 필요합니다. 예를 들어, overlapping_inherent_impls 호환성 린트를 제거하는 데 필요한 단계는 다음과 같습니다. 먼저 린트 이름을 대문자로 변환한 다음(OVERLAPPING_INHERENT_IMPLS), 소스에서 해당 문자열을 검색하십시오. 기본적으로 이 린트 이름이 언급된 각 위치를 변환하는 방식으로 진행합니다(컴파일러에서는 대문자 이름을 사용하고 매크로가 소문자 문자열을 자동으로 생성하므로, overlapping_inherent_impls로 검색하면 별로 찾을 수 없습니다).
참고: 이 파일들이 지금은 정확히 이대로 존재하지 않지만, 절차는 여전히 동일합니다.
린트를 제거하십시오.
가장 먼저 찾게 될 참조는 아마도 rustc_session/src/lint/builtin.rs에 있는 다음과 비슷한 린트 정의일 것입니다.
#![allow(unused)]
fn main() {
declare_lint! {
pub OVERLAPPING_INHERENT_IMPLS,
Deny, // this may also say Warning
"two overlapping inherent impls define an item with the same name were erroneously allowed",
@future_incompatible = FutureIncompatibleInfo {
reason: fcw!(FutureReleaseError #1234), // your tracking issue here!
},
}
}
이 declare_lint! 매크로는 관련 자료 구조를 생성합니다. 이 매크로를 제거하십시오. 또한 파일 뒷부분에 lint_array!의 일부로 OVERLAPPING_INHERENT_IMPLS가 언급된 곳이 있습니다. 이것도 제거하십시오.
이 린트를 제거된 린트 목록에 추가하십시오.
compiler/rustc_lint/src/lib.rs에는 “이름이 바뀌었거나 제거된 린트” 목록이 있습니다. 이 린트를 다음과 같이 목록에 추가할 수 있습니다.
#![allow(unused)]
fn main() {
store.register_removed("overlapping_inherent_impls", "converted into hard error, see #36889");
}
여기서 #36889는 해당 린트의 추적 이슈입니다.
린트를 발생시키는 곳 수정하기
마지막으로 보게 될 참조 유형은 실제로 린트 자체를 발생시키는 곳(즉, 경고가 나타나게 하는 곳)입니다. 이러한 곳은 삭제하지 않습니다. 대신 이러한 곳을 오류로 바꿔야 합니다. 이 경우 add_lint 호출은 다음과 같습니다.
#![allow(unused)]
fn main() {
self.tcx.sess.add_lint(lint::builtin::OVERLAPPING_INHERENT_IMPLS,
node_id,
self.tcx.span_of_impl(item1).unwrap(),
msg);
}
이런 용도로 node_span_lint가 쓰이는 경우도 자주 볼 수 있습니다.
이것을 오류로 바꾸어야 합니다. 어떤 경우에는 이 상황에 해당하는 오류가 이미 있을 수 있습니다. 그렇지 않은 경우에는 새 진단 코드를 할당해야 합니다. 새 진단 코드를 할당하는 방법은 여기에서 확인할 수 있습니다. 확장 설명에 이 부분에서 컴파일러 동작이 바뀌었다는 점을 언급하고, 해당 변경의 추적 이슈에 대한 참조를 포함하는 것이 좋습니다.
E0592를 코드로 채택했다고 가정해 봅시다. 그러면 위의 add_lint() 호출을 다음과 같이 바꿀 수 있습니다.
#![allow(unused)]
fn main() {
struct_span_code_err!(self.dcx(), self.tcx.span_of_impl(item1).unwrap(), E0592, msg)
.emit();
}
또는 다음과 같은 구조화된 진단을 사용하는 편이 더 좋습니다.
#![allow(unused)]
fn main() {
#[derive(Diagnostic)]
struct MyDiagnostic {
#[label]
span: Span,
...
}
}
테스트 수정하기
마지막으로 테스트 스위트를 실행하십시오. 이전에 overlapping_inherent_impls 린트를 참조하던 테스트가 몇 개 있을 것이며, 이러한 테스트는 수정해야 합니다. 일반적으로 테스트에 #[deny(overlapping_inherent_impls)]가 있었다면 그냥 제거하면 됩니다.
./x test
모두 끝났습니다!
PR을 여십시오.