Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion compiler/rustc_driver_impl/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1304,7 +1304,7 @@ pub fn handle_help(matches: &getopts::Matches, args: &[String]) -> bool {
let chelp_pos = opt_help_pos("C");
let print_help = || {
// Only show unstable options in --help if we accept unstable options.
let unstable_enabled = nightly_options::is_unstable_enabled(&matches);
let unstable_enabled = nightly_options::unstable_options_enabled(&matches);

@petrochenkov petrochenkov Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is actually changing the help output, I wouldn't expect the "act-as-stable" mode to do that.

FWIW, I like the "less principled escape hatch" alternative better.
Accepting all unstable flags deviates too much from acting as stable, and we don't actually need all unstable flags for UI testing, only like 2-3 of them.

View changes since the review

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, if we are able to limit it to only for-testing ones, that seems generally better.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was also thinking about that. Maybe we could do RUSTC_BOOTSTRAP=-allow-unstable-compiler-flags=ui-testing,second-flag-that-compiletest-needs, etc.? One issue with that is that compiletest actually passes also -Zunstable-options, IIRC, which increases the blast radius of the "stable-like" behavior that we change in those tests.

let nightly_build = nightly_options::match_is_nightly_build(&matches);
usage(matches.opt_present("verbose"), unstable_enabled, nightly_build);
};
Expand Down
52 changes: 48 additions & 4 deletions compiler/rustc_feature/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,38 @@ pub struct Feature {
issue: Option<NonZero<u32>>,
}

#[derive(Clone, Copy, Debug, Hash)]
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub enum UnstableFeatures {
/// Disallow use of unstable features, as on beta/stable channels.
/// Disallow use of unstable features and unstable compiler flags.
///
/// Used by default by beta/stable channels.
Disallow,
/// Allow use of unstable features, as on nightly.
/// Allow use of unstable features and unstable compiler flags.
///
/// Used by default by the nightly channel.
Allow,
/// Allow use of unstable features and unstable compiler flags, even if it is not the default
/// behavior.
///
/// Can be used to make a beta/stable channel act like a nightly channel.
///
/// Errors are bypassed for bootstrapping. This is required any time
/// during the build that feature-related lints are set to warn or above
/// because the build turns on warnings-as-errors and uses lots of unstable
/// features. As a result, this is always required for building Rust itself.
Cheat,
/// Allow the use of selected *unstable compiler flags*, but disallow the use of
/// *unstable language features*.
///
/// **This mode is designed only for internal testing of rustc.**
///
/// Specifically, we need it when we want to test how rustc behaves on the stable channel,
/// but at the same time we need to be able to pass it some unstable `-Z` compiler flags
/// required for the testing infrastructure itself to work.
AllowUnstableCompilerFlagsOnly {
/// Unstable compiler flags that we are allowed to use, without their `-Z` prefix.
allowed_flags: Vec<String>,
},
}

impl UnstableFeatures {
Expand All @@ -70,8 +91,15 @@ impl UnstableFeatures {
let is_unstable_crate =
|var: &str| krate.is_some_and(|name| var.split(',').any(|new_krate| new_krate == name));

let allow_unstable_flags_prefix = "-allow-unstable-compiler-flags=";
if let Ok(val) = env_var_rustc_bootstrap.as_deref() {
match val {
// Act as stable, but still accept unstable compiler flags required by compiletest
val if let Some(flags) = val.strip_prefix(allow_unstable_flags_prefix) => {
return UnstableFeatures::AllowUnstableCompilerFlagsOnly {
allowed_flags: flags.split(",").map(|s| s.trim().to_string()).collect(),
};
}
val if val == "1" || is_unstable_crate(val) => return UnstableFeatures::Cheat,
// Hypnotize ourselves so that we think we are a stable compiler and thus don't
// allow any unstable features.
Expand All @@ -83,10 +111,26 @@ impl UnstableFeatures {
if disable_unstable_features { UnstableFeatures::Disallow } else { UnstableFeatures::Allow }
}

/// Are we allowed to use the given unstable `flag`?
/// If it is a `-Z<flag>`, it should be passed without the `-Z` prefix.
pub fn allow_unstable_compiler_flag(&self, flag: &str) -> bool {
match self {
UnstableFeatures::Allow | UnstableFeatures::Cheat => true,
UnstableFeatures::AllowUnstableCompilerFlagsOnly { allowed_flags }
if allowed_flags.iter().any(|allowed_flag| allowed_flag == flag) =>
{
true
}
UnstableFeatures::Disallow
| UnstableFeatures::AllowUnstableCompilerFlagsOnly { .. } => false,
}
}

pub fn is_nightly_build(&self) -> bool {
match *self {
UnstableFeatures::Allow | UnstableFeatures::Cheat => true,
UnstableFeatures::Disallow => false,
UnstableFeatures::Disallow
| UnstableFeatures::AllowUnstableCompilerFlagsOnly { .. } => false,
}
}
}
Expand Down
12 changes: 12 additions & 0 deletions compiler/rustc_feature/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,16 @@ fn rustc_bootstrap_parsing() {
// Does not support specifying any crate.
assert!(is_force_stable(Some("x")));
assert!(is_force_stable(Some("x,y,z")));

// `RUSTC_BOOTSTRAP=-allow-unstable-compiler-flags` simulates the stable toolchain, while
// allowing unstable compiler flags to be passed.
std::assert_eq!(
UnstableFeatures::from_environment_value(
None,
Ok("-allow-unstable-compiler-flags=foo,bar".to_string())
),
UnstableFeatures::AllowUnstableCompilerFlagsOnly {
allowed_flags: vec!["foo".to_string(), "bar".to_string()]
}
);
}
66 changes: 50 additions & 16 deletions compiler/rustc_session/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2411,7 +2411,7 @@ pub fn parse_crate_edition(
}
};

if !edition.is_stable() && !nightly_options::is_unstable_enabled(matches) {
if !edition.is_stable() && !nightly_options::unstable_options_enabled(matches) {

@jieyouxu jieyouxu Oct 12, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: grepping shows that there's still a reference to unstable_options_enabled around 516

View changes since the review

let msg = if !is_nightly {
format!(
"the crate requires edition {edition}, but the latest edition supported by this \
Expand All @@ -2429,10 +2429,10 @@ pub fn parse_crate_edition(
fn check_error_format_stability(
early_dcx: &EarlyDiagCtxt,
unstable_opts: &UnstableOptions,
is_nightly_build: bool,
unstable_flags_enabled: bool,
format: ErrorOutputType,
) {
if unstable_opts.unstable_options || is_nightly_build {
if unstable_opts.unstable_options || unstable_flags_enabled {
return;
}
let format = match format {
Expand Down Expand Up @@ -2792,7 +2792,7 @@ pub fn build_session_options(
check_error_format_stability(
early_dcx,
&unstable_opts,
unstable_features.is_nightly_build(),
unstable_features.allow_unstable_compiler_flag("error-format"),
error_format,
);

Expand Down Expand Up @@ -2888,15 +2888,15 @@ pub fn build_session_options(
}
}

let unstable_options_enabled = nightly_options::is_unstable_enabled(matches);
let unstable_options_enabled = nightly_options::unstable_options_enabled(matches);
if !unstable_options_enabled && cg.force_frame_pointers == FramePointer::NonLeaf {
early_dcx.early_fatal(
"`-Cforce-frame-pointers=non-leaf` or `always` also requires `-Zunstable-options` \
and a nightly compiler",
)
}

if !nightly_options::is_unstable_enabled(matches) && !unstable_opts.offload.is_empty() {
if !unstable_options_enabled && !unstable_opts.offload.is_empty() {
early_dcx.early_fatal(
"`-Zoffload=Enable` also requires `-Zunstable-options` and a nightly compiler",
)
Expand Down Expand Up @@ -2974,7 +2974,7 @@ pub fn build_session_options(
}

// Parse any `-l` flags, which link to native libraries.
let libs = parse_native_libs(early_dcx, &unstable_opts, unstable_features, matches);
let libs = parse_native_libs(early_dcx, &unstable_opts, &unstable_features, matches);

let test = matches.opt_present("test");

Expand Down Expand Up @@ -3199,17 +3199,17 @@ pub mod nightly_options {
use super::{OptionStability, RustcOptGroup};
use crate::EarlyDiagCtxt;

pub fn is_unstable_enabled(matches: &getopts::Matches) -> bool {
match_is_nightly_build(matches)
/// Are we allowed to use -Z compiler flags *and* -Zunstable-options was passed?
pub fn unstable_options_enabled(matches: &getopts::Matches) -> bool {
let unstable_features =
UnstableFeatures::from_environment(matches.opt_str("crate-name").as_deref());
unstable_features.allow_unstable_compiler_flag("unstable-options")
&& matches.opt_strs("Z").iter().any(|x| *x == "unstable-options")
}

pub fn match_is_nightly_build(matches: &getopts::Matches) -> bool {
is_nightly_build(matches.opt_str("crate-name").as_deref())
}

fn is_nightly_build(krate: Option<&str>) -> bool {
UnstableFeatures::from_environment(krate).is_nightly_build()
let krate = matches.opt_str("crate-name");
UnstableFeatures::from_environment(krate.as_deref()).is_nightly_build()
}

pub fn check_nightly_options(
Expand All @@ -3218,7 +3218,8 @@ pub mod nightly_options {
flags: &[RustcOptGroup],
) {
let has_z_unstable_option = matches.opt_strs("Z").iter().any(|x| *x == "unstable-options");
let really_allows_unstable_options = match_is_nightly_build(matches);
let unstable_features =
UnstableFeatures::from_environment(matches.opt_str("crate-name").as_deref());
let mut nightly_options_on_stable = 0;

for opt in flags.iter() {
Expand All @@ -3235,7 +3236,40 @@ pub mod nightly_options {
opt.name
));
}
if really_allows_unstable_options {

// Check if the matched values are enabled
// If we're dealing with -Z, then check all the matched values (i.e. -Zfoo, -Zbar, etc.)
// If we're dealing with other flags, check their name directly
if opt.name == "Z" {
// For `-Zui-testing -Zfoo=bar -Zfoo=baz`, this returns
// ["ui-testing", "foo=bar", "foo=baz"].
// Check that all of them are allowed
let values = matches.opt_strs(opt.name);
let disallowed_flags = values
.iter()
.map(|flag| flag.split_once("=").map(|s| s.0).unwrap_or(flag))
.filter(|flag| !unstable_features.allow_unstable_compiler_flag(flag))
.collect::<Vec<_>>();
if disallowed_flags.is_empty() {
continue;
}

// At this point, check if we're using the "allow unstable flags only" mode.
// In this case, if the user allowed only foo and bar, but then -Zbaz was also
// passed, rustc would only print that `-Z` is not allowed, which doesn't tell the
// user the actual -Z flag value that was disallowed. So we try to provide better
// diagnostics here (solely for the use-case of debugging compiletest itself).
let unstable_features =
UnstableFeatures::from_environment(matches.opt_str("crate-name").as_deref());
if matches!(
unstable_features,
UnstableFeatures::AllowUnstableCompilerFlagsOnly { .. }
) {
for flag in disallowed_flags {
let _ = early_dcx.early_err(format!("the -Z{flag} option was not allowed through `-allow-unstable-compiler-flags`"));
}
}
} else if unstable_features.allow_unstable_compiler_flag(opt.name) {
continue;
}

Expand Down
2 changes: 1 addition & 1 deletion compiler/rustc_session/src/config/native_libs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ mod tests;
pub(crate) fn parse_native_libs(
early_dcx: &EarlyDiagCtxt,
unstable_opts: &UnstableOptions,
unstable_features: UnstableFeatures,
unstable_features: &UnstableFeatures,
matches: &getopts::Matches,
) -> Vec<NativeLib> {
let cx = ParseNativeLibCx {
Expand Down
46 changes: 26 additions & 20 deletions src/doc/rustc-dev-guide/src/tests/misc.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,29 @@ This is a bootstrap/compiler implementation detail, but it can also be useful fo
even if it's actually a nightly `rustc`.
This is useful because some behaviors of the compiler (e.g. diagnostics)
can differ depending on whether the compiler is nightly or not.

Note that setting `RUSTC_BOOTSTRAP` in ui tests (e.g. via `//@ rustc-env`) is pointless since ui
tests inherit the bootstrap env var from bootstrap (so it's set to `1` by default),
and since compiletest itself passes `-Z` flags (so `-1` breaks the test).
To test stable-only behavior, you need to write a `run-make` test instead.

For `run-make`/`run-make-cargo` tests, `//@ rustc-env` is not supported.
You can do something like the following for individual `rustc` invocations.

```rust,ignore
use run_make_support::rustc;

fn main() {
rustc()
// Pretend that I am very stable
.env("RUSTC_BOOTSTRAP", "-1")
//...
.run();
}
```
- `RUSTC_BOOTSTRAP=-allow-unstable-compiler-flags=<flags>` will force a given `rustc` to pretend it
is a stable compiler, even if it's actually a nightly `rustc`.
However, the compiler will still accept the passed unstable compiler flags `<flags>` (the allowed flag
should be separated by commas).
For example, if you use `RUSTC_BOOTSTRAP=-allow-unstable-compiler-flags=foo,bar`, it will be possible to
pass `-Zfoo` and `-Zbar` to the compiler.
This is useful for internal testing of the compiler, where we need to pass
unstable flags required by compiletest, but we also want to test the compiler's
behavior on the stable channel.

### UI tests

Note that setting `RUSTC_BOOTSTRAP=-1` in ui tests (e.g. via `//@ rustc-env`) is pointless since ui
tests inherit the bootstrap env var from bootstrap (so it's set to `1` by default), and since
compiletest itself passes `-Z` flags (so `-1` breaks the test).

To test stable-only behavior, use the `act-as-stable` directive, which will set
`RUSTC_BOOTSTRAP=-allow-unstable-compiler-flags` with the flags required by compiletest for you. With this
directive, the compiler will not allow using unstable language features or unstable flags that are not set by
compiletest.

### Run-make tests

For `run-make`/`run-make-cargo` tests, `//@ act-as-stable` is not supported.
You can use the `run_make_support::stable_bare_rustc` function to create an instance of the compiler
that acts as the stable channel (this internally uses `RUSTC_BOOTSTRAP=-1`).
4 changes: 2 additions & 2 deletions src/librustdoc/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,7 @@ impl Options {
match (
output_format_s.as_ref().map(|_| output_format),
show_coverage,
nightly_options::is_unstable_enabled(matches),
nightly_options::unstable_options_enabled(matches),
) {
(None | Some(OutputFormat::CoverageJson), true, _) => {}
(_, true, _) => {
Expand Down Expand Up @@ -944,7 +944,7 @@ impl Options {
output_format,
json_unused_externs,
scrape_examples_options,
unstable_features,
unstable_features: unstable_features.clone(),
doctest_build_args,
target_modifiers: collected_options.target_modifiers,
};
Expand Down
2 changes: 1 addition & 1 deletion src/librustdoc/doctest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ pub(crate) fn run(dcx: DiagCtxtHandle<'_>, input: Input, options: RustdocOptions
lint_cap: Some(options.lint_cap.unwrap_or(lint::Forbid)),
cg: options.codegen_options.clone(),
externs: options.externs.clone(),
unstable_features: options.unstable_features,
unstable_features: options.unstable_features.clone(),
actually_rustdoc: true,
edition: options.edition,
target_triple: options.target.clone(),
Expand Down
1 change: 1 addition & 0 deletions src/tools/compiletest/src/directives.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ pub(crate) struct TestProps {
}

mod directives {
pub(crate) const ACT_AS_STABLE: &str = "act-as-stable";
pub(crate) const ERROR_PATTERN: &str = "error-pattern";
pub(crate) const REGEX_ERROR_PATTERN: &str = "regex-error-pattern";
pub(crate) const COMPILE_FLAGS: &str = "compile-flags";
Expand Down
1 change: 1 addition & 0 deletions src/tools/compiletest/src/directives/directive_names.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub(crate) static KNOWN_DIRECTIVE_NAMES_SET: LazyLock<HashSet<&str>> =
/// a best-effort approximation for diagnostics. Add new directives to this list when needed.
pub(crate) const KNOWN_DIRECTIVE_NAMES: &[&str] = &[
// tidy-alphabetical-start
"act-as-stable",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I don't love the name, because it's actually more like //@ well-actually-we-allow-unstable-compiler-flags-but-not-language-features. But obviously that's a despairingly long name...

There's candidates like

  • //@ pretend-stable (which is really the same meaning as act-as-stable)
  • //@ stable-features-only, but that loses the "pretend" intent

Maybe //@ act-as-stable is fine.

"add-minicore",
"assembly-output",
"aux-bin",
Expand Down
20 changes: 19 additions & 1 deletion src/tools/compiletest/src/directives/handlers.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
use std::collections::HashMap;
use std::sync::{Arc, LazyLock};

use crate::common::{Config, PassFailMode, TestMode};
use crate::common::{Config, PassFailMode, TestMode, TestSuite};
use crate::directives::{
DirectiveLine, NormalizeKind, NormalizeRule, TestProps, parse_and_update_aux,
parse_edition_range, split_flags,
};
use crate::errors::ErrorKind;
use crate::runtest::COMPILETEST_UI_SUITE_UNSTABLE_FLAGS;

pub(crate) static DIRECTIVE_HANDLERS_MAP: LazyLock<HashMap<&str, Handler>> =
LazyLock::new(make_directive_handlers_map);
Expand Down Expand Up @@ -64,6 +65,23 @@ fn make_directive_handlers_map() -> HashMap<&'static str, Handler> {
// these handlers, e.g. by getting rid of now-redundant name checks.

let handlers: Vec<NamedHandler> = vec![
handler(ACT_AS_STABLE, |config, ln, props| {
if ln.name == ACT_AS_STABLE {
if ![TestSuite::Ui, TestSuite::UiFullDeps].contains(&config.suite) {
panic!(
"The {ACT_AS_STABLE} directive is only usable in the ui and ui-fulldeps test suites"
);
}
// Simulate the stable toolchain, but allow unstable -Z flags passed by compiletest
props.rustc_env.push((
"RUSTC_BOOTSTRAP".to_string(),
format!(
"-allow-unstable-compiler-flags={}",
COMPILETEST_UI_SUITE_UNSTABLE_FLAGS.join(",")
),
));
}
}),
handler(ERROR_PATTERN, |config, ln, props| {
config.push_name_value_directive(ln, ERROR_PATTERN, &mut props.error_patterns, |r| r);
}),
Expand Down
Loading
Loading