Skip to content

rustdoc-json: Output target feature information #139393

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
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
60 changes: 60 additions & 0 deletions src/librustdoc/json/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use std::io::{BufWriter, Write, stdout};
use std::path::PathBuf;
use std::rc::Rc;

use rustc_data_structures::fx::FxHashSet;
use rustc_hir::def_id::{DefId, DefIdSet};
use rustc_middle::ty::TyCtxt;
use rustc_session::Session;
Expand Down Expand Up @@ -123,6 +124,58 @@ impl<'tcx> JsonRenderer<'tcx> {
}
}

fn target(sess: &rustc_session::Session) -> types::Target {
// Build a set of which features are enabled on this target
let globally_enabled_features: FxHashSet<&str> =
sess.unstable_target_features.iter().map(|name| name.as_str()).collect();

// Build a map of target feature stability by feature name
use rustc_target::target_features::Stability;
let feature_stability: FxHashMap<&str, Stability> = sess
.target
.rust_target_features()
.into_iter()
.copied()
.map(|(name, stability, _)| (name, stability))
.collect();

types::Target {
triple: sess.opts.target_triple.tuple().into(),
target_features: sess
.target
.rust_target_features()
.into_iter()
.copied()
.filter(|(_, stability, _)| {
// Describe only target features which the user can toggle
stability.toggle_allowed().is_ok()
})
.map(|(name, stability, implied_features)| {
types::TargetFeature {
name: name.into(),
unstable_feature_gate: match stability {
Stability::Unstable(feature_gate) => Some(feature_gate.as_str().into()),
_ => None,
},
implies_features: implied_features
.into_iter()
.copied()
.filter(|name| {
// Imply only target features which the user can toggle
feature_stability
.get(name)
.map(|stability| stability.toggle_allowed().is_ok())
.unwrap_or(false)
})
.map(String::from)
.collect(),
globally_enabled: globally_enabled_features.contains(name),
}
})
.collect(),
}
}

impl<'tcx> FormatRenderer<'tcx> for JsonRenderer<'tcx> {
fn descr() -> &'static str {
"json"
Expand Down Expand Up @@ -248,6 +301,12 @@ impl<'tcx> FormatRenderer<'tcx> for JsonRenderer<'tcx> {
let e = ExternalCrate { crate_num: LOCAL_CRATE };
let index = (*self.index).clone().into_inner();

// Note that tcx.rust_target_features is inappropriate here because rustdoc tries to run for
// multiple targets: https://github.com./rust-lang/rust/pull/137632
//
// We want to describe a single target, so pass tcx.sess rather than tcx.
let target = target(self.tcx.sess);

debug!("Constructing Output");
let output_crate = types::Crate {
root: self.id_from_item_default(e.def_id().into()),
Expand Down Expand Up @@ -288,6 +347,7 @@ impl<'tcx> FormatRenderer<'tcx> for JsonRenderer<'tcx> {
)
})
.collect(),
target,
format_version: types::FORMAT_VERSION,
};
if let Some(ref out_dir) = self.out_dir {
Expand Down
58 changes: 57 additions & 1 deletion src/rustdoc-json-types/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ pub type FxHashMap<K, V> = HashMap<K, V>; // re-export for use in src/librustdoc
/// This integer is incremented with every breaking change to the API,
/// and is returned along with the JSON blob as [`Crate::format_version`].
/// Consuming code should assert that this value matches the format version(s) that it supports.
pub const FORMAT_VERSION: u32 = 43;
pub const FORMAT_VERSION: u32 = 44;

/// The root of the emitted JSON blob.
///
Expand All @@ -52,11 +52,67 @@ pub struct Crate {
pub paths: HashMap<Id, ItemSummary>,
/// Maps `crate_id` of items to a crate name and html_root_url if it exists.
pub external_crates: HashMap<u32, ExternalCrate>,
/// Information about the target for which this documentation was generated
pub target: Target,
/// A single version number to be used in the future when making backwards incompatible changes
/// to the JSON output.
pub format_version: u32,
}

/// Information about a target
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Target {
/// The target triple for which this documentation was generated
pub triple: String,
/// A list of features valid for use in `#[target_feature]` attributes
/// for the target where this rustdoc JSON was generated.
pub target_features: Vec<TargetFeature>,
}

/// Information about a target feature.
///
/// Rust target features are used to influence code generation, especially around selecting
/// instructions which are not universally supported by the target architecture.
///
/// Target features are commonly enabled by the [`#[target_feature]` attribute][1] to influence code
/// generation for a particular function, and less commonly enabled by compiler options like
/// `-Ctarget-feature` or `-Ctarget-cpu`. Targets themselves automatically enable certain target
/// features by default, for example because the target's ABI specification requires saving specific
/// registers which only exist in an architectural extension.
///
/// Target features can imply other target features: for example, x86-64 `avx2` implies `avx`, and
/// aarch64 `sve2` implies `sve`, since both of these architectural extensions depend on their
/// predecessors.
///
/// Target features can be probed at compile time by [`#[cfg(target_feature)]`][2] or `cfg!(…)`
/// conditional compilation to determine whether a target feature is enabled in a particular
/// context.
///
/// [1]: https://doc.rust-lang.org/stable/reference/attributes/codegen.html#the-target_feature-attribute
/// [2]: https://doc.rust-lang.org/reference/conditional-compilation.html#target_feature
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct TargetFeature {
/// The name of this target feature.
pub name: String,
/// Other target features which are implied by this target feature, if any.
pub implies_features: Vec<String>,
Comment on lines +97 to +98
Copy link
Member

Choose a reason for hiding this comment

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

I assume:

  • order doesn't matter
  • each name may appear at most once
  • features are guaranteed to form a DAG (directed acyclic graph), here including a forest of such DAGs

If all this is true, perhaps we should note it in the doc comment. If not, we should definitely note that it isn't true :)

The first two items can be made obvious by making this set instead of a Vec, though I'm ambivalent on whether that's an improvement or not. Your call!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This all should be true, but it's not ensured by the underlying data structures. rustc_target::target_features::ImpliedFeatures is just a &'static [&'static str]. Implying nonexistent features or forming a cycle wouldn't cause problems unless that feature were enabled.

If we're providing these guarantees, I'd be inclined to have librustdoc verify and panic!() if rustc_target tries to violate them. Is adding this check (and failure mode) desirable? If not, should we document these expectations with weaker language?

Separately, I note that rustdoc-json-types does not currently use any sets, only Vecs. It uses and re-exports rustc_hash::FxHashMap for maps. Should I re-export FxHashSet too?

Copy link
Member

Choose a reason for hiding this comment

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

I'll defer to rustdoc JSON maintainers on whether librustdoc should verify the guarantees explicitly.

From my point of view, it's fine to expose either a Vec or FxHashSet here.

If the properties are expected but not able to be guaranteed, I'd also be in favor of documenting them with weaker language.

Copy link
Member

@aDotInTheVoid aDotInTheVoid Apr 14, 2025

Choose a reason for hiding this comment

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

I think this should be a Vec, as we use that in other places in rustdoc_json_types for things where the order doesn't matter (eg Module::items, Struct::impls).

But maybe we should separately think about if we want to only use Vec where the order does matter, and some set type for where it doesn't.

As for testing the validity of features, that shouldn't live in rustdoc, but in rustc_target (and done in a separate PR). What guarantees they make are up to T-compiler (and maybe T-lang??) and if their broken, it'd be much nicer to get the error from compiler tests than rustdoc tests.

Copy link
Member

Choose a reason for hiding this comment

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

I don't have strong feelings either way on Vec vs set type for places where order doesn't matter. If the expectation (from docs or from context) is that order is irrelevant, my preference would be "whatever is faster to iterate over" so probably slight preference toward a Vec overall :)

/// If this target feature is unstable, the name of the associated language feature gate.
pub unstable_feature_gate: Option<String>,
/// Whether this feature is globally enabled for this compilation session.
///
/// Target features can be globally enabled implicitly as a result of the target's definition.
/// For example, x86-64 hardware floating point ABIs require saving x87 and SSE2 registers,
/// which in turn requires globally enabling the `x87` and `sse2` target features so that the
/// generated machine code conforms to the target's ABI.
///
/// Target features can also be globally enabled explicitly as a result of compiler flags like
/// [`-Ctarget-feature`][1] or [`-Ctarget-cpu`][2].
///
/// [1]: https://doc.rust-lang.org/beta/rustc/codegen-options/index.html#target-feature
/// [2]: https://doc.rust-lang.org/beta/rustc/codegen-options/index.html#target-cpu
pub globally_enabled: bool,
Comment on lines +101 to +113
Copy link
Member

Choose a reason for hiding this comment

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

Just checking my understanding of the implication here: the set of features with globally_enabled = true lets us figure out what #[cfg]'d items may be enabled or removed in the rustdoc run that produced this file, right?

No change needed if that's the case, I just wanted to make sure there's no additional hidden meaning behind globally or session.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

🤔 I chose globally to indicate that this feature is enabled "everywhere", as opposed to "here in this fn", but it seems strictly more correct to say "everywhere in this session". I'm not sure whether this distinction is important. Are there circumstances where different parts of the same build can have different -Ctarget-feature or -Ctarget-cpu flags?

Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure either — this is also why I was asking :)

I'm not familiar with the specific definition of a "session" either so it might be good to try to work out more precise language, together with the rustdoc JSON maintainers.

I'd be perfectly happy with a TODO on that more precise language though — what this PR has is certainly a large improvement over the status quo and would be valuable to have already.

Copy link
Member

Choose a reason for hiding this comment

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

Maybe something along the lines of "in this compilation"/"in this rustdoc invocation". I think saying "session" is precise, but it isn't accessible, as it's not used in a user facing way.

(I agree it'd be fine to clear this up later)

}

/// Metadata of a crate, either the same crate on which `rustdoc` was invoked, or its dependency.
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ExternalCrate {
Expand Down
3 changes: 3 additions & 0 deletions src/tools/compiletest/src/directive-list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ const KNOWN_DIRECTIVE_NAMES: &[&str] = &[
"only-32bit",
"only-64bit",
"only-aarch64",
"only-aarch64-apple-darwin",
"only-aarch64-unknown-linux-gnu",
"only-apple",
"only-arm",
Expand All @@ -189,6 +190,7 @@ const KNOWN_DIRECTIVE_NAMES: &[&str] = &[
"only-gnu",
"only-i686-pc-windows-gnu",
"only-i686-pc-windows-msvc",
"only-i686-unknown-linux-gnu",
"only-ios",
"only-linux",
"only-loongarch64",
Expand Down Expand Up @@ -220,6 +222,7 @@ const KNOWN_DIRECTIVE_NAMES: &[&str] = &[
"only-windows-msvc",
"only-x86",
"only-x86_64",
"only-x86_64-apple-darwin",
"only-x86_64-fortanix-unknown-sgx",
"only-x86_64-pc-windows-gnu",
"only-x86_64-pc-windows-msvc",
Expand Down
2 changes: 1 addition & 1 deletion src/tools/jsondocck/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ static LINE_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
r#"
//@\s+
(?P<negated>!?)
(?P<cmd>[A-Za-z]+(?:-[A-Za-z]+)*)
(?P<cmd>[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*)
(?P<args>.*)$
"#,
)
Expand Down
4 changes: 4 additions & 0 deletions src/tools/jsondoclint/src/validator/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ fn errors_on_missing_links() {
)]),
paths: FxHashMap::default(),
external_crates: FxHashMap::default(),
target: rustdoc_json_types::Target { triple: "".to_string(), target_features: vec![] },
format_version: rustdoc_json_types::FORMAT_VERSION,
};

Expand Down Expand Up @@ -112,6 +113,7 @@ fn errors_on_local_in_paths_and_not_index() {
},
)]),
external_crates: FxHashMap::default(),
target: rustdoc_json_types::Target { triple: "".to_string(), target_features: vec![] },
format_version: rustdoc_json_types::FORMAT_VERSION,
};

Expand Down Expand Up @@ -216,6 +218,7 @@ fn errors_on_missing_path() {
ItemSummary { crate_id: 0, path: vec!["foo".to_owned()], kind: ItemKind::Module },
)]),
external_crates: FxHashMap::default(),
target: rustdoc_json_types::Target { triple: "".to_string(), target_features: vec![] },
format_version: rustdoc_json_types::FORMAT_VERSION,
};

Expand Down Expand Up @@ -259,6 +262,7 @@ fn checks_local_crate_id_is_correct() {
)]),
paths: FxHashMap::default(),
external_crates: FxHashMap::default(),
target: rustdoc_json_types::Target { triple: "".to_string(), target_features: vec![] },
format_version: FORMAT_VERSION,
};
check(&krate, &[]);
Expand Down
14 changes: 14 additions & 0 deletions tests/rustdoc-json/targets/aarch64_apple_darwin.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//@ only-aarch64-apple-darwin

//@ is "$.target.triple" \"aarch64-apple-darwin\"
//@ is "$.target.target_features[?(@.name=='vh')].globally_enabled" true
//@ is "$.target.target_features[?(@.name=='sve')].globally_enabled" false
//@ has "$.target.target_features[?(@.name=='sve2')].implies_features" '["sve"]'
//@ is "$.target.target_features[?(@.name=='sve2')].unstable_feature_gate" null

// If this breaks due to stabilization, check rustc_target::target_features for a replacement
//@ is "$.target.target_features[?(@.name=='cssc')].unstable_feature_gate" '"aarch64_unstable_target_feature"'
//@ is "$.target.target_features[?(@.name=='v9a')].unstable_feature_gate" '"aarch64_ver_target_feature"'

// Ensure we don't look like x86-64
//@ !has "$.target.target_features[?(@.name=='avx2')]"
10 changes: 10 additions & 0 deletions tests/rustdoc-json/targets/aarch64_reflects_compiler_options.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//@ only-aarch64

// If we enable SVE Bit Permute, we should see that it is enabled
//@ compile-flags: -Ctarget-feature=+sve2-bitperm
//@ is "$.target.target_features[?(@.name=='sve2-bitperm')].globally_enabled" true

// As well as its dependency chain
//@ is "$.target.target_features[?(@.name=='sve2')].globally_enabled" true
//@ is "$.target.target_features[?(@.name=='sve')].globally_enabled" true
//@ is "$.target.target_features[?(@.name=='neon')].globally_enabled" true
14 changes: 14 additions & 0 deletions tests/rustdoc-json/targets/aarch64_unknown_linux_gnu.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//@ only-aarch64-unknown-linux-gnu

//@ is "$.target.triple" \"aarch64-unknown-linux-gnu\"
//@ is "$.target.target_features[?(@.name=='neon')].globally_enabled" true
//@ is "$.target.target_features[?(@.name=='sve')].globally_enabled" false
//@ has "$.target.target_features[?(@.name=='sve2')].implies_features" '["sve"]'
//@ is "$.target.target_features[?(@.name=='sve2')].unstable_feature_gate" null

// If this breaks due to stabilization, check rustc_target::target_features for a replacement
//@ is "$.target.target_features[?(@.name=='cssc')].unstable_feature_gate" '"aarch64_unstable_target_feature"'
//@ is "$.target.target_features[?(@.name=='v9a')].unstable_feature_gate" '"aarch64_ver_target_feature"'

// Ensure we don't look like x86-64
//@ !has "$.target.target_features[?(@.name=='avx2')]"
14 changes: 14 additions & 0 deletions tests/rustdoc-json/targets/i686_pc_windows_msvc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//@ only-i686-pc-windows-msvc

//@ is "$.target.triple" \"i686-pc-windows-msvc\"
//@ is "$.target.target_features[?(@.name=='sse2')].globally_enabled" true
//@ is "$.target.target_features[?(@.name=='avx2')].globally_enabled" false
//@ has "$.target.target_features[?(@.name=='avx2')].implies_features" '["avx"]'
//@ is "$.target.target_features[?(@.name=='avx2')].unstable_feature_gate" null

// If this breaks due to stabilization, check rustc_target::target_features for a replacement
//@ is "$.target.target_features[?(@.name=='amx-tile')].unstable_feature_gate" '"x86_amx_intrinsics"'
//@ is "$.target.target_features[?(@.name=='x87')].unstable_feature_gate" '"x87_target_feature"'

// Ensure we don't look like aarch64
//@ !has "$.target.target_features[?(@.name=='sve2')]"
14 changes: 14 additions & 0 deletions tests/rustdoc-json/targets/i686_unknown_linux_gnu.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//@ only-i686-unknown-linux-gnu

//@ is "$.target.triple" \"i686-unknown-linux-gnu\"
//@ is "$.target.target_features[?(@.name=='sse2')].globally_enabled" true
//@ is "$.target.target_features[?(@.name=='avx2')].globally_enabled" false
//@ has "$.target.target_features[?(@.name=='avx2')].implies_features" '["avx"]'
//@ is "$.target.target_features[?(@.name=='avx2')].unstable_feature_gate" null

// If this breaks due to stabilization, check rustc_target::target_features for a replacement
//@ is "$.target.target_features[?(@.name=='amx-tile')].unstable_feature_gate" '"x86_amx_intrinsics"'
//@ is "$.target.target_features[?(@.name=='x87')].unstable_feature_gate" '"x87_target_feature"'

// Ensure we don't look like aarch64
//@ !has "$.target.target_features[?(@.name=='sve2')]"
14 changes: 14 additions & 0 deletions tests/rustdoc-json/targets/x86_64_apple_darwin.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//@ only-x86_64-apple-darwin

//@ is "$.target.triple" \"x86_64-apple-darwin\"
//@ is "$.target.target_features[?(@.name=='sse2')].globally_enabled" true
//@ is "$.target.target_features[?(@.name=='avx2')].globally_enabled" false
//@ has "$.target.target_features[?(@.name=='avx2')].implies_features" '["avx"]'
//@ is "$.target.target_features[?(@.name=='avx2')].unstable_feature_gate" null

// If this breaks due to stabilization, check rustc_target::target_features for a replacement
//@ is "$.target.target_features[?(@.name=='amx-tile')].unstable_feature_gate" '"x86_amx_intrinsics"'
//@ is "$.target.target_features[?(@.name=='x87')].unstable_feature_gate" '"x87_target_feature"'

// Ensure we don't look like aarch64
//@ !has "$.target.target_features[?(@.name=='sve2')]"
14 changes: 14 additions & 0 deletions tests/rustdoc-json/targets/x86_64_pc_windows_gnu.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//@ only-x86_64-pc-windows-gnu

//@ is "$.target.triple" \"x86_64-pc-windows-gnu\"
//@ is "$.target.target_features[?(@.name=='sse2')].globally_enabled" true
//@ is "$.target.target_features[?(@.name=='avx2')].globally_enabled" false
//@ has "$.target.target_features[?(@.name=='avx2')].implies_features" '["avx"]'
//@ is "$.target.target_features[?(@.name=='avx2')].unstable_feature_gate" null

// If this breaks due to stabilization, check rustc_target::target_features for a replacement
//@ is "$.target.target_features[?(@.name=='amx-tile')].unstable_feature_gate" '"x86_amx_intrinsics"'
//@ is "$.target.target_features[?(@.name=='x87')].unstable_feature_gate" '"x87_target_feature"'

// Ensure we don't look like aarch64
//@ !has "$.target.target_features[?(@.name=='sve2')]"
14 changes: 14 additions & 0 deletions tests/rustdoc-json/targets/x86_64_pc_windows_msvc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//@ only-x86_64-pc-windows-msvc

//@ is "$.target.triple" \"x86_64-pc-windows-msvc\"
//@ is "$.target.target_features[?(@.name=='sse2')].globally_enabled" true
//@ is "$.target.target_features[?(@.name=='avx2')].globally_enabled" false
//@ has "$.target.target_features[?(@.name=='avx2')].implies_features" '["avx"]'
//@ is "$.target.target_features[?(@.name=='avx2')].unstable_feature_gate" null

// If this breaks due to stabilization, check rustc_target::target_features for a replacement
//@ is "$.target.target_features[?(@.name=='amx-tile')].unstable_feature_gate" '"x86_amx_intrinsics"'
//@ is "$.target.target_features[?(@.name=='x87')].unstable_feature_gate" '"x87_target_feature"'

// Ensure we don't look like aarch64
//@ !has "$.target.target_features[?(@.name=='sve2')]"
10 changes: 10 additions & 0 deletions tests/rustdoc-json/targets/x86_64_reflects_compiler_options.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//@ only-x86_64

// If we enable AVX2, we should see that it is enabled
//@ compile-flags: -Ctarget-feature=+avx2
//@ is "$.target.target_features[?(@.name=='avx2')].globally_enabled" true

// As well as its dependency chain
//@ is "$.target.target_features[?(@.name=='avx')].globally_enabled" true
//@ is "$.target.target_features[?(@.name=='sse4.2')].globally_enabled" true
//@ is "$.target.target_features[?(@.name=='sse4.1')].globally_enabled" true
14 changes: 14 additions & 0 deletions tests/rustdoc-json/targets/x86_64_unknown_linux_gnu.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//@ only-x86_64-unknown-linux-gnu

//@ is "$.target.triple" \"x86_64-unknown-linux-gnu\"
//@ is "$.target.target_features[?(@.name=='sse2')].globally_enabled" true
//@ is "$.target.target_features[?(@.name=='avx2')].globally_enabled" false
//@ has "$.target.target_features[?(@.name=='avx2')].implies_features" '["avx"]'
//@ is "$.target.target_features[?(@.name=='avx2')].unstable_feature_gate" null

// If this breaks due to stabilization, check rustc_target::target_features for a replacement
//@ is "$.target.target_features[?(@.name=='amx-tile')].unstable_feature_gate" '"x86_amx_intrinsics"'
//@ is "$.target.target_features[?(@.name=='x87')].unstable_feature_gate" '"x87_target_feature"'

// Ensure we don't look like aarch64
//@ !has "$.target.target_features[?(@.name=='sve2')]"
Loading