about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock4
-rw-r--r--compiler/rustc_error_messages/src/lib.rs11
-rw-r--r--compiler/rustc_errors/src/lib.rs4
-rw-r--r--compiler/rustc_macros/Cargo.toml4
-rw-r--r--compiler/rustc_macros/src/diagnostics/fluent.rs254
-rw-r--r--compiler/rustc_macros/src/diagnostics/mod.rs10
-rw-r--r--compiler/rustc_macros/src/lib.rs59
-rw-r--r--compiler/rustc_typeck/src/errors.rs18
-rw-r--r--library/core/src/alloc/layout.rs2
-rw-r--r--library/core/src/mem/valid_align.rs9
-rw-r--r--library/core/src/ptr/non_null.rs7
-rw-r--r--library/std/src/ffi/mod.rs2
-rw-r--r--src/bootstrap/dist.rs23
-rw-r--r--src/bootstrap/install.rs27
-rw-r--r--src/test/ui-fulldeps/fluent-messages/duplicate-a.ftl1
-rw-r--r--src/test/ui-fulldeps/fluent-messages/duplicate-b.ftl1
-rw-r--r--src/test/ui-fulldeps/fluent-messages/missing-message.ftl1
-rw-r--r--src/test/ui-fulldeps/fluent-messages/test.rs60
-rw-r--r--src/test/ui-fulldeps/fluent-messages/test.stderr45
-rw-r--r--src/test/ui-fulldeps/fluent-messages/valid.ftl1
20 files changed, 494 insertions, 49 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 538e88355ef..42bf166a71c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4010,10 +4010,14 @@ dependencies = [
 name = "rustc_macros"
 version = "0.1.0"
 dependencies = [
+ "annotate-snippets",
+ "fluent-bundle",
+ "fluent-syntax",
  "proc-macro2",
  "quote",
  "syn",
  "synstructure",
+ "unic-langid",
 ]
 
 [[package]]
diff --git a/compiler/rustc_error_messages/src/lib.rs b/compiler/rustc_error_messages/src/lib.rs
index e1e0ed7222d..7faf14a2472 100644
--- a/compiler/rustc_error_messages/src/lib.rs
+++ b/compiler/rustc_error_messages/src/lib.rs
@@ -6,7 +6,7 @@
 use fluent_bundle::FluentResource;
 use fluent_syntax::parser::ParserError;
 use rustc_data_structures::sync::Lrc;
-use rustc_macros::{Decodable, Encodable};
+use rustc_macros::{fluent_messages, Decodable, Encodable};
 use rustc_span::Span;
 use std::borrow::Cow;
 use std::error::Error;
@@ -29,8 +29,13 @@ use intl_memoizer::IntlLangMemoizer;
 pub use fluent_bundle::{FluentArgs, FluentError, FluentValue};
 pub use unic_langid::{langid, LanguageIdentifier};
 
-pub static DEFAULT_LOCALE_RESOURCES: &'static [&'static str] =
-    &[include_str!("../locales/en-US/typeck.ftl"), include_str!("../locales/en-US/parser.ftl")];
+// Generates `DEFAULT_LOCALE_RESOURCES` static and `fluent_generated` module.
+fluent_messages! {
+    parser => "../locales/en-US/parser.ftl",
+    typeck => "../locales/en-US/typeck.ftl",
+}
+
+pub use fluent_generated::{self as fluent, DEFAULT_LOCALE_RESOURCES};
 
 pub type FluentBundle = fluent_bundle::bundle::FluentBundle<FluentResource, IntlLangMemoizer>;
 
diff --git a/compiler/rustc_errors/src/lib.rs b/compiler/rustc_errors/src/lib.rs
index d2f50d5df54..5b9b65da343 100644
--- a/compiler/rustc_errors/src/lib.rs
+++ b/compiler/rustc_errors/src/lib.rs
@@ -31,8 +31,8 @@ use rustc_data_structures::stable_hasher::StableHasher;
 use rustc_data_structures::sync::{self, Lock, Lrc};
 use rustc_data_structures::AtomicRef;
 pub use rustc_error_messages::{
-    fallback_fluent_bundle, fluent_bundle, DiagnosticMessage, FluentBundle, LanguageIdentifier,
-    LazyFallbackBundle, MultiSpan, SpanLabel, DEFAULT_LOCALE_RESOURCES,
+    fallback_fluent_bundle, fluent, fluent_bundle, DiagnosticMessage, FluentBundle,
+    LanguageIdentifier, LazyFallbackBundle, MultiSpan, SpanLabel, DEFAULT_LOCALE_RESOURCES,
 };
 pub use rustc_lint_defs::{pluralize, Applicability};
 use rustc_span::source_map::SourceMap;
diff --git a/compiler/rustc_macros/Cargo.toml b/compiler/rustc_macros/Cargo.toml
index a9192be4d6e..25b3aadc1c5 100644
--- a/compiler/rustc_macros/Cargo.toml
+++ b/compiler/rustc_macros/Cargo.toml
@@ -7,7 +7,11 @@ edition = "2021"
 proc-macro = true
 
 [dependencies]
+annotate-snippets = "0.8.0"
+fluent-bundle = "0.15.2"
+fluent-syntax = "0.11"
 synstructure = "0.12.1"
 syn = { version = "1", features = ["full"] }
 proc-macro2 = "1"
 quote = "1"
+unic-langid = { version = "0.9.0", features = ["macros"] }
diff --git a/compiler/rustc_macros/src/diagnostics/fluent.rs b/compiler/rustc_macros/src/diagnostics/fluent.rs
new file mode 100644
index 00000000000..8523d7fa9f9
--- /dev/null
+++ b/compiler/rustc_macros/src/diagnostics/fluent.rs
@@ -0,0 +1,254 @@
+use annotate_snippets::{
+    display_list::DisplayList,
+    snippet::{Annotation, AnnotationType, Slice, Snippet, SourceAnnotation},
+};
+use fluent_bundle::{FluentBundle, FluentError, FluentResource};
+use fluent_syntax::{
+    ast::{Attribute, Entry, Identifier, Message},
+    parser::ParserError,
+};
+use proc_macro::{Diagnostic, Level, Span};
+use proc_macro2::TokenStream;
+use quote::quote;
+use std::{
+    collections::HashMap,
+    fs::File,
+    io::Read,
+    path::{Path, PathBuf},
+};
+use syn::{
+    parse::{Parse, ParseStream},
+    parse_macro_input,
+    punctuated::Punctuated,
+    token, Ident, LitStr, Result,
+};
+use unic_langid::langid;
+
+struct Resource {
+    ident: Ident,
+    #[allow(dead_code)]
+    fat_arrow_token: token::FatArrow,
+    resource: LitStr,
+}
+
+impl Parse for Resource {
+    fn parse(input: ParseStream<'_>) -> Result<Self> {
+        Ok(Resource {
+            ident: input.parse()?,
+            fat_arrow_token: input.parse()?,
+            resource: input.parse()?,
+        })
+    }
+}
+
+struct Resources(Punctuated<Resource, token::Comma>);
+
+impl Parse for Resources {
+    fn parse(input: ParseStream<'_>) -> Result<Self> {
+        let mut resources = Punctuated::new();
+        loop {
+            if input.is_empty() || input.peek(token::Brace) {
+                break;
+            }
+            let value = input.parse()?;
+            resources.push_value(value);
+            if !input.peek(token::Comma) {
+                break;
+            }
+            let punct = input.parse()?;
+            resources.push_punct(punct);
+        }
+        Ok(Resources(resources))
+    }
+}
+
+/// Helper function for returning an absolute path for macro-invocation relative file paths.
+///
+/// If the input is already absolute, then the input is returned. If the input is not absolute,
+/// then it is appended to the directory containing the source file with this macro invocation.
+fn invocation_relative_path_to_absolute(span: Span, path: &str) -> PathBuf {
+    let path = Path::new(path);
+    if path.is_absolute() {
+        path.to_path_buf()
+    } else {
+        // `/a/b/c/foo/bar.rs` contains the current macro invocation
+        let mut source_file_path = span.source_file().path();
+        // `/a/b/c/foo/`
+        source_file_path.pop();
+        // `/a/b/c/foo/../locales/en-US/example.ftl`
+        source_file_path.push(path);
+        source_file_path
+    }
+}
+
+/// See [rustc_macros::fluent_messages].
+pub(crate) fn fluent_messages(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
+    let resources = parse_macro_input!(input as Resources);
+
+    // Cannot iterate over individual messages in a bundle, so do that using the
+    // `FluentResource` instead. Construct a bundle anyway to find out if there are conflicting
+    // messages in the resources.
+    let mut bundle = FluentBundle::new(vec![langid!("en-US")]);
+
+    // Map of Fluent identifiers to the `Span` of the resource that defined them, used for better
+    // diagnostics.
+    let mut previous_defns = HashMap::new();
+
+    let mut includes = TokenStream::new();
+    let mut generated = TokenStream::new();
+    for res in resources.0 {
+        let ident_span = res.ident.span().unwrap();
+        let path_span = res.resource.span().unwrap();
+
+        let relative_ftl_path = res.resource.value();
+        let absolute_ftl_path =
+            invocation_relative_path_to_absolute(ident_span, &relative_ftl_path);
+        // As this macro also outputs an `include_str!` for this file, the macro will always be
+        // re-executed when the file changes.
+        let mut resource_file = match File::open(absolute_ftl_path) {
+            Ok(resource_file) => resource_file,
+            Err(e) => {
+                Diagnostic::spanned(path_span, Level::Error, "could not open Fluent resource")
+                    .note(e.to_string())
+                    .emit();
+                continue;
+            }
+        };
+        let mut resource_contents = String::new();
+        if let Err(e) = resource_file.read_to_string(&mut resource_contents) {
+            Diagnostic::spanned(path_span, Level::Error, "could not read Fluent resource")
+                .note(e.to_string())
+                .emit();
+            continue;
+        }
+        let resource = match FluentResource::try_new(resource_contents) {
+            Ok(resource) => resource,
+            Err((this, errs)) => {
+                Diagnostic::spanned(path_span, Level::Error, "could not parse Fluent resource")
+                    .help("see additional errors emitted")
+                    .emit();
+                for ParserError { pos, slice: _, kind } in errs {
+                    let mut err = kind.to_string();
+                    // Entirely unnecessary string modification so that the error message starts
+                    // with a lowercase as rustc errors do.
+                    err.replace_range(
+                        0..1,
+                        &err.chars().next().unwrap().to_lowercase().to_string(),
+                    );
+
+                    let line_starts: Vec<usize> = std::iter::once(0)
+                        .chain(
+                            this.source()
+                                .char_indices()
+                                .filter_map(|(i, c)| Some(i + 1).filter(|_| c == '\n')),
+                        )
+                        .collect();
+                    let line_start = line_starts
+                        .iter()
+                        .enumerate()
+                        .map(|(line, idx)| (line + 1, idx))
+                        .filter(|(_, idx)| **idx <= pos.start)
+                        .last()
+                        .unwrap()
+                        .0;
+
+                    let snippet = Snippet {
+                        title: Some(Annotation {
+                            label: Some(&err),
+                            id: None,
+                            annotation_type: AnnotationType::Error,
+                        }),
+                        footer: vec![],
+                        slices: vec![Slice {
+                            source: this.source(),
+                            line_start,
+                            origin: Some(&relative_ftl_path),
+                            fold: true,
+                            annotations: vec![SourceAnnotation {
+                                label: "",
+                                annotation_type: AnnotationType::Error,
+                                range: (pos.start, pos.end - 1),
+                            }],
+                        }],
+                        opt: Default::default(),
+                    };
+                    let dl = DisplayList::from(snippet);
+                    eprintln!("{}\n", dl);
+                }
+                continue;
+            }
+        };
+
+        let mut constants = TokenStream::new();
+        for entry in resource.entries() {
+            let span = res.ident.span();
+            if let Entry::Message(Message { id: Identifier { name }, attributes, .. }) = entry {
+                let _ = previous_defns.entry(name.to_string()).or_insert(ident_span);
+
+                // `typeck-foo-bar` => `foo_bar`
+                let snake_name = Ident::new(
+                    &name.replace(&format!("{}-", res.ident), "").replace("-", "_"),
+                    span,
+                );
+                constants.extend(quote! {
+                    pub const #snake_name: crate::DiagnosticMessage =
+                        crate::DiagnosticMessage::FluentIdentifier(
+                            std::borrow::Cow::Borrowed(#name),
+                            None
+                        );
+                });
+
+                for Attribute { id: Identifier { name: attr_name }, .. } in attributes {
+                    let attr_snake_name = attr_name.replace("-", "_");
+                    let snake_name = Ident::new(&format!("{snake_name}_{attr_snake_name}"), span);
+                    constants.extend(quote! {
+                        pub const #snake_name: crate::DiagnosticMessage =
+                            crate::DiagnosticMessage::FluentIdentifier(
+                                std::borrow::Cow::Borrowed(#name),
+                                Some(std::borrow::Cow::Borrowed(#attr_name))
+                            );
+                    });
+                }
+            }
+        }
+
+        if let Err(errs) = bundle.add_resource(resource) {
+            for e in errs {
+                match e {
+                    FluentError::Overriding { kind, id } => {
+                        Diagnostic::spanned(
+                            ident_span,
+                            Level::Error,
+                            format!("overrides existing {}: `{}`", kind, id),
+                        )
+                        .span_help(previous_defns[&id], "previously defined in this resource")
+                        .emit();
+                    }
+                    FluentError::ResolverError(_) | FluentError::ParserError(_) => unreachable!(),
+                }
+            }
+        }
+
+        includes.extend(quote! { include_str!(#relative_ftl_path), });
+
+        let ident = res.ident;
+        generated.extend(quote! {
+            pub mod #ident {
+                #constants
+            }
+        });
+    }
+
+    quote! {
+        #[allow(non_upper_case_globals)]
+        #[doc(hidden)]
+        pub mod fluent_generated {
+            pub static DEFAULT_LOCALE_RESOURCES: &'static [&'static str] = &[
+                #includes
+            ];
+
+            #generated
+        }
+    }
+    .into()
+}
diff --git a/compiler/rustc_macros/src/diagnostics/mod.rs b/compiler/rustc_macros/src/diagnostics/mod.rs
index 73ad415d6c3..69573d904d4 100644
--- a/compiler/rustc_macros/src/diagnostics/mod.rs
+++ b/compiler/rustc_macros/src/diagnostics/mod.rs
@@ -1,9 +1,11 @@
 mod diagnostic;
 mod error;
+mod fluent;
 mod subdiagnostic;
 mod utils;
 
 use diagnostic::SessionDiagnosticDerive;
+pub(crate) use fluent::fluent_messages;
 use proc_macro2::TokenStream;
 use quote::format_ident;
 use subdiagnostic::SessionSubdiagnosticDerive;
@@ -12,7 +14,7 @@ use synstructure::Structure;
 /// Implements `#[derive(SessionDiagnostic)]`, which allows for errors to be specified as a struct,
 /// independent from the actual diagnostics emitting code.
 ///
-/// ```ignore (pseudo-rust)
+/// ```ignore (rust)
 /// # extern crate rustc_errors;
 /// # use rustc_errors::Applicability;
 /// # extern crate rustc_span;
@@ -43,7 +45,7 @@ use synstructure::Structure;
 ///
 /// Then, later, to emit the error:
 ///
-/// ```ignore (pseudo-rust)
+/// ```ignore (rust)
 /// sess.emit_err(MoveOutOfBorrowError {
 ///     expected,
 ///     actual,
@@ -67,7 +69,7 @@ pub fn session_diagnostic_derive(s: Structure<'_>) -> TokenStream {
 /// suggestions to be specified as a structs or enums, independent from the actual diagnostics
 /// emitting code or diagnostic derives.
 ///
-/// ```ignore (pseudo-rust)
+/// ```ignore (rust)
 /// #[derive(SessionSubdiagnostic)]
 /// pub enum ExpectedIdentifierLabel<'tcx> {
 ///     #[label(slug = "parser-expected-identifier")]
@@ -104,7 +106,7 @@ pub fn session_diagnostic_derive(s: Structure<'_>) -> TokenStream {
 ///
 /// Then, later, to add the subdiagnostic:
 ///
-/// ```ignore (pseudo-rust)
+/// ```ignore (rust)
 /// diag.subdiagnostic(ExpectedIdentifierLabel::WithoutFound { span });
 ///
 /// diag.subdiagnostic(RawIdentifierSuggestion { span, applicability, ident });
diff --git a/compiler/rustc_macros/src/lib.rs b/compiler/rustc_macros/src/lib.rs
index 0baebdb7130..7c8e3c6d140 100644
--- a/compiler/rustc_macros/src/lib.rs
+++ b/compiler/rustc_macros/src/lib.rs
@@ -2,6 +2,7 @@
 #![feature(let_else)]
 #![feature(never_type)]
 #![feature(proc_macro_diagnostic)]
+#![feature(proc_macro_span)]
 #![allow(rustc::default_hash_types)]
 #![recursion_limit = "128"]
 
@@ -49,6 +50,64 @@ pub fn newtype_index(input: TokenStream) -> TokenStream {
     newtype::newtype(input)
 }
 
+/// Implements the `fluent_messages` macro, which performs compile-time validation of the
+/// compiler's Fluent resources (i.e. that the resources parse and don't multiply define the same
+/// messages) and generates constants that make using those messages in diagnostics more ergonomic.
+///
+/// For example, given the following invocation of the macro..
+///
+/// ```ignore (rust)
+/// fluent_messages! {
+///     typeck => "./typeck.ftl",
+/// }
+/// ```
+/// ..where `typeck.ftl` has the following contents..
+///
+/// ```fluent
+/// typeck-field-multiply-specified-in-initializer =
+///     field `{$ident}` specified more than once
+///     .label = used more than once
+///     .label-previous-use = first use of `{$ident}`
+/// ```
+/// ...then the macro parse the Fluent resource, emitting a diagnostic if it fails to do so, and
+/// will generate the following code:
+///
+/// ```ignore (rust)
+/// pub static DEFAULT_LOCALE_RESOURCES: &'static [&'static str] = &[
+///     include_str!("./typeck.ftl"),
+/// ];
+///
+/// mod fluent_generated {
+///     mod typeck {
+///         pub const field_multiply_specified_in_initializer: DiagnosticMessage =
+///             DiagnosticMessage::fluent("typeck-field-multiply-specified-in-initializer");
+///         pub const field_multiply_specified_in_initializer_label_previous_use: DiagnosticMessage =
+///             DiagnosticMessage::fluent_attr(
+///                 "typeck-field-multiply-specified-in-initializer",
+///                 "previous-use-label"
+///             );
+///     }
+/// }
+/// ```
+/// When emitting a diagnostic, the generated constants can be used as follows:
+///
+/// ```ignore (rust)
+/// let mut err = sess.struct_span_err(
+///     span,
+///     fluent::typeck::field_multiply_specified_in_initializer
+/// );
+/// err.span_default_label(span);
+/// err.span_label(
+///     previous_use_span,
+///     fluent::typeck::field_multiply_specified_in_initializer_label_previous_use
+/// );
+/// err.emit();
+/// ```
+#[proc_macro]
+pub fn fluent_messages(input: TokenStream) -> TokenStream {
+    diagnostics::fluent_messages(input)
+}
+
 decl_derive!([HashStable, attributes(stable_hasher)] => hash_stable::hash_stable_derive);
 decl_derive!(
     [HashStable_Generic, attributes(stable_hasher)] =>
diff --git a/compiler/rustc_typeck/src/errors.rs b/compiler/rustc_typeck/src/errors.rs
index cd3813ca4f5..d9c9f2920b0 100644
--- a/compiler/rustc_typeck/src/errors.rs
+++ b/compiler/rustc_typeck/src/errors.rs
@@ -1,7 +1,5 @@
 //! Errors emitted by typeck.
-use rustc_errors::{
-    error_code, Applicability, DiagnosticBuilder, DiagnosticMessage, ErrorGuaranteed,
-};
+use rustc_errors::{error_code, Applicability, DiagnosticBuilder, ErrorGuaranteed};
 use rustc_macros::{SessionDiagnostic, SessionSubdiagnostic};
 use rustc_middle::ty::Ty;
 use rustc_session::{parse::ParseSess, SessionDiagnostic};
@@ -264,10 +262,9 @@ pub struct MissingTypeParams {
 // Manual implementation of `SessionDiagnostic` to be able to call `span_to_snippet`.
 impl<'a> SessionDiagnostic<'a> for MissingTypeParams {
     fn into_diagnostic(self, sess: &'a ParseSess) -> DiagnosticBuilder<'a, ErrorGuaranteed> {
-        static SLUG: &'static str = "typeck-missing-type-params";
         let mut err = sess.span_diagnostic.struct_span_err_with_code(
             self.span,
-            DiagnosticMessage::fluent(SLUG),
+            rustc_errors::fluent::typeck::missing_type_params,
             error_code!(E0393),
         );
         err.set_arg("parameterCount", self.missing_type_params.len());
@@ -280,7 +277,7 @@ impl<'a> SessionDiagnostic<'a> for MissingTypeParams {
                 .join(", "),
         );
 
-        err.span_label(self.def_span, DiagnosticMessage::fluent_attr(SLUG, "label"));
+        err.span_label(self.def_span, rustc_errors::fluent::typeck::missing_type_params_label);
 
         let mut suggested = false;
         if let (Ok(snippet), true) = (
@@ -298,7 +295,7 @@ impl<'a> SessionDiagnostic<'a> for MissingTypeParams {
                 // least we can clue them to the correct syntax `Iterator<Type>`.
                 err.span_suggestion(
                     self.span,
-                    DiagnosticMessage::fluent_attr(SLUG, "suggestion"),
+                    rustc_errors::fluent::typeck::missing_type_params_suggestion,
                     format!("{}<{}>", snippet, self.missing_type_params.join(", ")),
                     Applicability::HasPlaceholders,
                 );
@@ -306,10 +303,13 @@ impl<'a> SessionDiagnostic<'a> for MissingTypeParams {
             }
         }
         if !suggested {
-            err.span_label(self.span, DiagnosticMessage::fluent_attr(SLUG, "no-suggestion-label"));
+            err.span_label(
+                self.span,
+                rustc_errors::fluent::typeck::missing_type_params_no_suggestion_label,
+            );
         }
 
-        err.note(DiagnosticMessage::fluent_attr(SLUG, "note"));
+        err.note(rustc_errors::fluent::typeck::missing_type_params_note);
         err
     }
 }
diff --git a/library/core/src/alloc/layout.rs b/library/core/src/alloc/layout.rs
index cf864039a23..2f378836cbb 100644
--- a/library/core/src/alloc/layout.rs
+++ b/library/core/src/alloc/layout.rs
@@ -26,7 +26,7 @@ const fn size_align<T>() -> (usize, usize) {
 /// like this are met, use specific allocators with looser
 /// requirements, or use the more lenient `Allocator` interface.)
 #[stable(feature = "alloc_layout", since = "1.28.0")]
-#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
 #[lang = "alloc_layout"]
 pub struct Layout {
     // size of the requested block of memory, measured in bytes.
diff --git a/library/core/src/mem/valid_align.rs b/library/core/src/mem/valid_align.rs
index 596a67f255a..fcfa95120df 100644
--- a/library/core/src/mem/valid_align.rs
+++ b/library/core/src/mem/valid_align.rs
@@ -1,6 +1,6 @@
 use crate::convert::TryFrom;
 use crate::num::NonZeroUsize;
-use crate::{cmp, fmt, mem, num};
+use crate::{cmp, fmt, hash, mem, num};
 
 /// A type storing a `usize` which is a power of two, and thus
 /// represents a possible alignment in the rust abstract machine.
@@ -105,6 +105,13 @@ impl cmp::PartialOrd for ValidAlign {
     }
 }
 
+impl hash::Hash for ValidAlign {
+    #[inline]
+    fn hash<H: hash::Hasher>(&self, state: &mut H) {
+        self.as_nonzero().hash(state)
+    }
+}
+
 #[cfg(target_pointer_width = "16")]
 type ValidAlignEnum = ValidAlignEnum16;
 #[cfg(target_pointer_width = "32")]
diff --git a/library/core/src/ptr/non_null.rs b/library/core/src/ptr/non_null.rs
index f0455317175..f3ef094cbcc 100644
--- a/library/core/src/ptr/non_null.rs
+++ b/library/core/src/ptr/non_null.rs
@@ -499,14 +499,15 @@ impl<T> NonNull<[T]> {
     /// # Examples
     ///
     /// ```rust
-    /// #![feature(slice_ptr_len, nonnull_slice_from_raw_parts)]
+    /// #![feature(nonnull_slice_from_raw_parts)]
     /// use std::ptr::NonNull;
     ///
     /// let slice: NonNull<[i8]> = NonNull::slice_from_raw_parts(NonNull::dangling(), 3);
     /// assert_eq!(slice.len(), 3);
     /// ```
-    #[unstable(feature = "slice_ptr_len", issue = "71146")]
-    #[rustc_const_unstable(feature = "const_slice_ptr_len", issue = "71146")]
+    #[stable(feature = "slice_ptr_len_nonnull", since = "1.63.0")]
+    #[rustc_const_stable(feature = "const_slice_ptr_len_nonnull", since = "1.63.0")]
+    #[rustc_allow_const_fn_unstable(const_slice_ptr_len)]
     #[must_use]
     #[inline]
     pub const fn len(self) -> usize {
diff --git a/library/std/src/ffi/mod.rs b/library/std/src/ffi/mod.rs
index 10983a33232..94ae97a2e40 100644
--- a/library/std/src/ffi/mod.rs
+++ b/library/std/src/ffi/mod.rs
@@ -104,7 +104,7 @@
 //! On Unix, [`OsStr`] implements the
 //! <code>std::os::unix::ffi::[OsStrExt][unix.OsStrExt]</code> trait, which
 //! augments it with two methods, [`from_bytes`] and [`as_bytes`].
-//! These do inexpensive conversions from and to UTF-8 byte slices.
+//! These do inexpensive conversions from and to byte slices.
 //!
 //! Additionally, on Unix [`OsString`] implements the
 //! <code>std::os::unix::ffi::[OsStringExt][unix.OsStringExt]</code> trait,
diff --git a/src/bootstrap/dist.rs b/src/bootstrap/dist.rs
index ae9fc1c527f..b5901ce6f74 100644
--- a/src/bootstrap/dist.rs
+++ b/src/bootstrap/dist.rs
@@ -897,29 +897,6 @@ impl Step for PlainSourceTarball {
     }
 }
 
-// We have to run a few shell scripts, which choke quite a bit on both `\`
-// characters and on `C:\` paths, so normalize both of them away.
-pub fn sanitize_sh(path: &Path) -> String {
-    let path = path.to_str().unwrap().replace("\\", "/");
-    return change_drive(unc_to_lfs(&path)).unwrap_or(path);
-
-    fn unc_to_lfs(s: &str) -> &str {
-        s.strip_prefix("//?/").unwrap_or(s)
-    }
-
-    fn change_drive(s: &str) -> Option<String> {
-        let mut ch = s.chars();
-        let drive = ch.next().unwrap_or('C');
-        if ch.next() != Some(':') {
-            return None;
-        }
-        if ch.next() != Some('/') {
-            return None;
-        }
-        Some(format!("/{}/{}", drive, &s[drive.len_utf8() + 2..]))
-    }
-}
-
 #[derive(Debug, PartialOrd, Ord, Copy, Clone, Hash, PartialEq, Eq)]
 pub struct Cargo {
     pub compiler: Compiler,
diff --git a/src/bootstrap/install.rs b/src/bootstrap/install.rs
index 08e37a16279..6e49f39ffb6 100644
--- a/src/bootstrap/install.rs
+++ b/src/bootstrap/install.rs
@@ -5,12 +5,12 @@
 
 use std::env;
 use std::fs;
-use std::path::{Component, PathBuf};
+use std::path::{Component, Path, PathBuf};
 use std::process::Command;
 
 use crate::util::t;
 
-use crate::dist::{self, sanitize_sh};
+use crate::dist;
 use crate::tarball::GeneratedTarball;
 use crate::Compiler;
 
@@ -22,6 +22,29 @@ const SHELL: &str = "bash";
 #[cfg(not(target_os = "illumos"))]
 const SHELL: &str = "sh";
 
+// We have to run a few shell scripts, which choke quite a bit on both `\`
+// characters and on `C:\` paths, so normalize both of them away.
+fn sanitize_sh(path: &Path) -> String {
+    let path = path.to_str().unwrap().replace("\\", "/");
+    return change_drive(unc_to_lfs(&path)).unwrap_or(path);
+
+    fn unc_to_lfs(s: &str) -> &str {
+        s.strip_prefix("//?/").unwrap_or(s)
+    }
+
+    fn change_drive(s: &str) -> Option<String> {
+        let mut ch = s.chars();
+        let drive = ch.next().unwrap_or('C');
+        if ch.next() != Some(':') {
+            return None;
+        }
+        if ch.next() != Some('/') {
+            return None;
+        }
+        Some(format!("/{}/{}", drive, &s[drive.len_utf8() + 2..]))
+    }
+}
+
 fn install_sh(
     builder: &Builder<'_>,
     package: &str,
diff --git a/src/test/ui-fulldeps/fluent-messages/duplicate-a.ftl b/src/test/ui-fulldeps/fluent-messages/duplicate-a.ftl
new file mode 100644
index 00000000000..fd9976b5a41
--- /dev/null
+++ b/src/test/ui-fulldeps/fluent-messages/duplicate-a.ftl
@@ -0,0 +1 @@
+key = Value
diff --git a/src/test/ui-fulldeps/fluent-messages/duplicate-b.ftl b/src/test/ui-fulldeps/fluent-messages/duplicate-b.ftl
new file mode 100644
index 00000000000..fd9976b5a41
--- /dev/null
+++ b/src/test/ui-fulldeps/fluent-messages/duplicate-b.ftl
@@ -0,0 +1 @@
+key = Value
diff --git a/src/test/ui-fulldeps/fluent-messages/missing-message.ftl b/src/test/ui-fulldeps/fluent-messages/missing-message.ftl
new file mode 100644
index 00000000000..372b1a2e453
--- /dev/null
+++ b/src/test/ui-fulldeps/fluent-messages/missing-message.ftl
@@ -0,0 +1 @@
+missing-message = 
diff --git a/src/test/ui-fulldeps/fluent-messages/test.rs b/src/test/ui-fulldeps/fluent-messages/test.rs
new file mode 100644
index 00000000000..b05d3d08ccb
--- /dev/null
+++ b/src/test/ui-fulldeps/fluent-messages/test.rs
@@ -0,0 +1,60 @@
+// normalize-stderr-test "note.*" -> "note: os-specific message"
+
+#![feature(rustc_private)]
+#![crate_type = "lib"]
+
+extern crate rustc_macros;
+use rustc_macros::fluent_messages;
+
+/// Copy of the relevant `DiagnosticMessage` variant constructed by `fluent_messages` as it
+/// expects `crate::DiagnosticMessage` to exist.
+pub enum DiagnosticMessage {
+    FluentIdentifier(std::borrow::Cow<'static, str>, Option<std::borrow::Cow<'static, str>>),
+}
+
+mod missing_absolute {
+    use super::fluent_messages;
+
+    fluent_messages! {
+        missing_absolute => "/definitely_does_not_exist.ftl",
+//~^ ERROR could not open Fluent resource
+    }
+}
+
+mod missing_relative {
+    use super::fluent_messages;
+
+    fluent_messages! {
+        missing_relative => "../definitely_does_not_exist.ftl",
+//~^ ERROR could not open Fluent resource
+    }
+}
+
+mod missing_message {
+    use super::fluent_messages;
+
+    fluent_messages! {
+        missing_message => "./missing-message.ftl",
+//~^ ERROR could not parse Fluent resource
+    }
+}
+
+mod duplicate {
+    use super::fluent_messages;
+
+    fluent_messages! {
+        a => "./duplicate-a.ftl",
+        b => "./duplicate-b.ftl",
+//~^ ERROR overrides existing message: `key`
+    }
+}
+
+mod valid {
+    use super::fluent_messages;
+
+    fluent_messages! {
+        valid => "./valid.ftl",
+    }
+
+    use self::fluent_generated::{DEFAULT_LOCALE_RESOURCES, valid::valid};
+}
diff --git a/src/test/ui-fulldeps/fluent-messages/test.stderr b/src/test/ui-fulldeps/fluent-messages/test.stderr
new file mode 100644
index 00000000000..f88d09bee6e
--- /dev/null
+++ b/src/test/ui-fulldeps/fluent-messages/test.stderr
@@ -0,0 +1,45 @@
+error: could not open Fluent resource
+  --> $DIR/test.rs:19:29
+   |
+LL |         missing_absolute => "/definitely_does_not_exist.ftl",
+   |                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+   |
+   = note: os-specific message
+
+error: could not open Fluent resource
+  --> $DIR/test.rs:28:29
+   |
+LL |         missing_relative => "../definitely_does_not_exist.ftl",
+   |                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+   |
+   = note: os-specific message
+
+error: could not parse Fluent resource
+  --> $DIR/test.rs:37:28
+   |
+LL |         missing_message => "./missing-message.ftl",
+   |                            ^^^^^^^^^^^^^^^^^^^^^^^
+   |
+   = help: see additional errors emitted
+
+error: expected a message field for "missing-message"
+ --> ./missing-message.ftl:1:1
+  |
+1 | missing-message = 
+  | ^^^^^^^^^^^^^^^^^^
+  |
+
+error: overrides existing message: `key`
+  --> $DIR/test.rs:47:9
+   |
+LL |         b => "./duplicate-b.ftl",
+   |         ^
+   |
+help: previously defined in this resource
+  --> $DIR/test.rs:46:9
+   |
+LL |         a => "./duplicate-a.ftl",
+   |         ^
+
+error: aborting due to 4 previous errors
+
diff --git a/src/test/ui-fulldeps/fluent-messages/valid.ftl b/src/test/ui-fulldeps/fluent-messages/valid.ftl
new file mode 100644
index 00000000000..0eee4a02b96
--- /dev/null
+++ b/src/test/ui-fulldeps/fluent-messages/valid.ftl
@@ -0,0 +1 @@
+valid = Valid!