[naga] Introduce KeywordSet and CaseInsensitiveKeywordSet. (#8136)

This commit is contained in:
Kevin Reid 2025-08-27 20:42:02 -07:00 committed by GitHub
parent ca3f7f8989
commit 1c8d769b67
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 213 additions and 125 deletions

View File

@ -106,6 +106,7 @@ This allows using precompiled shaders without manually checking which backend's
- Naga now requires that no type be larger than 1 GB. This limit may be lowered in the future; feedback on an appropriate value for the limit is welcome. By @andyleiserson in [#7950](https://github.com/gfx-rs/wgpu/pull/7950).
- If the shader source contains control characters, Naga now replaces them with U+FFFD ("replacement character") in diagnostic output. By @andyleiserson in [#8049](https://github.com/gfx-rs/wgpu/pull/8049).
- Add f16 IO polyfill on Vulkan backend to enable SHADER_F16 use without requiring `storageInputOutput16`. By @cryvosh in [#7884](https://github.com/gfx-rs/wgpu/pull/7884).
- For custom Naga backend authors: `naga::proc::Namer` now accepts reserved keywords using two new dedicated types, `proc::{KeywordSet, CaseInsensitiveKeywordSet}`. By @kpreid in [#8136](https://github.com/gfx-rs/wgpu/pull/8136).
#### DX12

View File

@ -1,7 +1,6 @@
use crate::proc::KeywordSet;
use crate::racy_lock::RacyLock;
use hashbrown::HashSet;
pub const RESERVED_KEYWORDS: &[&str] = &[
//
// GLSL 4.6 keywords, from https://github.com/KhronosGroup/OpenGL-Registry/blob/d00e11dc1a1ffba581d633f21f70202051248d5c/specs/gl/GLSLangSpec.4.60.html#L2004-L2322
@ -499,11 +498,5 @@ pub const RESERVED_KEYWORDS: &[&str] = &[
/// significant time during [`Namer::reset`](crate::proc::Namer::reset).
///
/// See <https://github.com/gfx-rs/wgpu/pull/7338> for benchmarks.
pub static RESERVED_KEYWORD_SET: RacyLock<HashSet<&'static str>> = RacyLock::new(|| {
let mut set = HashSet::default();
set.reserve(RESERVED_KEYWORDS.len());
for &word in RESERVED_KEYWORDS {
set.insert(word);
}
set
});
pub static RESERVED_KEYWORD_SET: RacyLock<KeywordSet> =
RacyLock::new(|| KeywordSet::from_iter(RESERVED_KEYWORDS));

View File

@ -663,7 +663,7 @@ impl<'a, W: Write> Writer<'a, W> {
namer.reset(
module,
&keywords::RESERVED_KEYWORD_SET,
&[],
proc::CaseInsensitiveKeywordSet::empty(),
&[
"gl_", // all GL built-in variables
"_group", // all normal bindings

View File

@ -1,7 +1,6 @@
use crate::proc::{CaseInsensitiveKeywordSet, KeywordSet};
use crate::racy_lock::RacyLock;
use hashbrown::HashSet;
// When compiling with FXC without strict mode, these keywords are actually case insensitive.
// If you compile with strict mode and specify a different casing like "Pass" instead in an identifier, FXC will give this error:
// "error X3086: alternate cases for 'pass' are deprecated in strict mode"
@ -927,17 +926,11 @@ pub const TYPES: &[&str] = &{
/// significant time during [`Namer::reset`](crate::proc::Namer::reset).
///
/// See <https://github.com/gfx-rs/wgpu/pull/7338> for benchmarks.
pub static RESERVED_SET: RacyLock<HashSet<&'static str>> = RacyLock::new(|| {
let mut set = HashSet::default();
set.reserve(RESERVED.len() + TYPES.len());
for &word in RESERVED {
set.insert(word);
}
for &word in TYPES {
set.insert(word);
}
set
});
pub static RESERVED_SET: RacyLock<KeywordSet> =
RacyLock::new(|| KeywordSet::from_iter(RESERVED.iter().chain(TYPES)));
pub static RESERVED_CASE_INSENSITIVE_SET: RacyLock<CaseInsensitiveKeywordSet> =
RacyLock::new(|| CaseInsensitiveKeywordSet::from_iter(RESERVED_CASE_INSENSITIVE));
pub const RESERVED_PREFIXES: &[&str] = &[
"__dynamic_buffer_offsets",

View File

@ -155,7 +155,7 @@ impl<'a, W: fmt::Write> super::Writer<'a, W> {
self.namer.reset(
module,
&super::keywords::RESERVED_SET,
super::keywords::RESERVED_CASE_INSENSITIVE,
&super::keywords::RESERVED_CASE_INSENSITIVE_SET,
super::keywords::RESERVED_PREFIXES,
&mut self.names,
);

View File

@ -1,7 +1,6 @@
use crate::proc::KeywordSet;
use crate::racy_lock::RacyLock;
use hashbrown::HashSet;
// MSLS - Metal Shading Language Specification:
// https://developer.apple.com/metal/Metal-Shading-Language-Specification.pdf
//
@ -364,11 +363,4 @@ pub const RESERVED: &[&str] = &[
/// significant time during [`Namer::reset`](crate::proc::Namer::reset).
///
/// See <https://github.com/gfx-rs/wgpu/pull/7338> for benchmarks.
pub static RESERVED_SET: RacyLock<HashSet<&'static str>> = RacyLock::new(|| {
let mut set = HashSet::default();
set.reserve(RESERVED.len());
for &word in RESERVED {
set.insert(word);
}
set
});
pub static RESERVED_SET: RacyLock<KeywordSet> = RacyLock::new(|| KeywordSet::from_iter(RESERVED));

View File

@ -4279,7 +4279,7 @@ impl<W: Write> Writer<W> {
self.namer.reset(
module,
&super::keywords::RESERVED_SET,
&[],
proc::CaseInsensitiveKeywordSet::empty(),
&[CLAMPED_LOD_LOAD_PREFIX],
&mut self.names,
);

View File

@ -101,7 +101,7 @@ impl<W: Write> Writer<W> {
module,
&crate::keywords::wgsl::RESERVED_SET,
// an identifier must not start with two underscore
&[],
proc::CaseInsensitiveKeywordSet::empty(),
&["__", "_naga"],
&mut self.names,
);

View File

@ -4,10 +4,9 @@ Keywords for [WGSL][wgsl] (WebGPU Shading Language).
[wgsl]: https://gpuweb.github.io/gpuweb/wgsl.html
*/
use crate::proc::KeywordSet;
use crate::racy_lock::RacyLock;
use hashbrown::HashSet;
// https://gpuweb.github.io/gpuweb/wgsl/#keyword-summary
// last sync: https://github.com/gpuweb/gpuweb/blob/39f2321f547c8f0b7f473cf1d47fba30b1691303/wgsl/index.bs
pub const RESERVED: &[&str] = &[
@ -238,11 +237,4 @@ pub const RESERVED: &[&str] = &[
/// significant time during [`Namer::reset`](crate::proc::Namer::reset).
///
/// See <https://github.com/gfx-rs/wgpu/pull/7338> for benchmarks.
pub static RESERVED_SET: RacyLock<HashSet<&'static str>> = RacyLock::new(|| {
let mut set = HashSet::default();
set.reserve(RESERVED.len());
for &word in RESERVED {
set.insert(word);
}
set
});
pub static RESERVED_SET: RacyLock<KeywordSet> = RacyLock::new(|| KeywordSet::from_iter(RESERVED));

View File

@ -0,0 +1,178 @@
use core::{fmt, hash};
use crate::racy_lock::RacyLock;
use crate::FastHashSet;
/// A case-sensitive set of strings,
/// for use with [`Namer`][crate::proc::Namer] to avoid collisions with keywords and other reserved
/// identifiers.
///
/// This is currently implemented as a hash table.
/// Future versions of Naga may change the implementation based on speed and code size
/// considerations.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct KeywordSet(FastHashSet<&'static str>);
impl KeywordSet {
/// Returns a new mutable empty set.
pub fn new() -> Self {
Self::default()
}
/// Returns a reference to the empty set.
pub fn empty() -> &'static Self {
static EMPTY: RacyLock<KeywordSet> = RacyLock::new(Default::default);
&EMPTY
}
/// Returns whether the set contains the given string.
#[inline]
pub fn contains(&self, identifier: &str) -> bool {
self.0.contains(identifier)
}
}
impl Default for &'static KeywordSet {
fn default() -> Self {
KeywordSet::empty()
}
}
impl FromIterator<&'static str> for KeywordSet {
fn from_iter<T: IntoIterator<Item = &'static str>>(iter: T) -> Self {
Self(iter.into_iter().collect())
}
}
/// Accepts double references so that `KeywordSet::from_iter(&["foo"])` works.
impl<'a> FromIterator<&'a &'static str> for KeywordSet {
fn from_iter<T: IntoIterator<Item = &'a &'static str>>(iter: T) -> Self {
Self::from_iter(iter.into_iter().copied())
}
}
impl Extend<&'static str> for KeywordSet {
#[expect(
clippy::useless_conversion,
reason = "doing .into_iter() sooner reduces distinct monomorphizations"
)]
fn extend<T: IntoIterator<Item = &'static str>>(&mut self, iter: T) {
self.0.extend(iter.into_iter())
}
}
/// Accepts double references so that `.extend(&["foo"])` works.
impl<'a> Extend<&'a &'static str> for KeywordSet {
fn extend<T: IntoIterator<Item = &'a &'static str>>(&mut self, iter: T) {
self.extend(iter.into_iter().copied())
}
}
/// A case-insensitive, ASCII-only set of strings,
/// for use with [`Namer`][crate::proc::Namer] to avoid collisions with keywords and other reserved
/// identifiers.
///
/// This is currently implemented as a hash table.
/// Future versions of Naga may change the implementation based on speed and code size
/// considerations.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct CaseInsensitiveKeywordSet(FastHashSet<AsciiUniCase<&'static str>>);
impl CaseInsensitiveKeywordSet {
/// Returns a new mutable empty set.
pub fn new() -> Self {
Self::default()
}
/// Returns a reference to the empty set.
pub fn empty() -> &'static Self {
static EMPTY: RacyLock<CaseInsensitiveKeywordSet> = RacyLock::new(Default::default);
&EMPTY
}
/// Returns whether the set contains the given string, with comparison
/// by [`str::eq_ignore_ascii_case()`].
#[inline]
pub fn contains(&self, identifier: &str) -> bool {
self.0.contains(&AsciiUniCase(identifier))
}
}
impl Default for &'static CaseInsensitiveKeywordSet {
fn default() -> Self {
CaseInsensitiveKeywordSet::empty()
}
}
impl FromIterator<&'static str> for CaseInsensitiveKeywordSet {
fn from_iter<T: IntoIterator<Item = &'static str>>(iter: T) -> Self {
Self(
iter.into_iter()
.inspect(debug_assert_ascii)
.map(AsciiUniCase)
.collect(),
)
}
}
/// Accepts double references so that `CaseInsensitiveKeywordSet::from_iter(&["foo"])` works.
impl<'a> FromIterator<&'a &'static str> for CaseInsensitiveKeywordSet {
fn from_iter<T: IntoIterator<Item = &'a &'static str>>(iter: T) -> Self {
Self::from_iter(iter.into_iter().copied())
}
}
impl Extend<&'static str> for CaseInsensitiveKeywordSet {
fn extend<T: IntoIterator<Item = &'static str>>(&mut self, iter: T) {
self.0.extend(
iter.into_iter()
.inspect(debug_assert_ascii)
.map(AsciiUniCase),
)
}
}
/// Accepts double references so that `.extend(&["foo"])` works.
impl<'a> Extend<&'a &'static str> for CaseInsensitiveKeywordSet {
fn extend<T: IntoIterator<Item = &'a &'static str>>(&mut self, iter: T) {
self.extend(iter.into_iter().copied())
}
}
/// A string wrapper type with an ascii case insensitive Eq and Hash impl
#[derive(Clone, Copy)]
struct AsciiUniCase<S: AsRef<str> + ?Sized>(S);
impl<S: ?Sized + AsRef<str>> fmt::Debug for AsciiUniCase<S> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.as_ref().fmt(f)
}
}
impl<S: AsRef<str>> PartialEq<Self> for AsciiUniCase<S> {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.0.as_ref().eq_ignore_ascii_case(other.0.as_ref())
}
}
impl<S: AsRef<str>> Eq for AsciiUniCase<S> {}
impl<S: AsRef<str>> hash::Hash for AsciiUniCase<S> {
#[inline]
fn hash<H: hash::Hasher>(&self, hasher: &mut H) {
for byte in self
.0
.as_ref()
.as_bytes()
.iter()
.map(|b| b.to_ascii_lowercase())
{
hasher.write_u8(byte);
}
}
}
fn debug_assert_ascii(s: &&'static str) {
debug_assert!(s.is_ascii(), "{s:?} not ASCII")
}

View File

@ -5,6 +5,7 @@
mod constant_evaluator;
mod emitter;
pub mod index;
mod keyword_set;
mod layouter;
mod namer;
mod overloads;
@ -17,6 +18,7 @@ pub use constant_evaluator::{
};
pub use emitter::Emitter;
pub use index::{BoundsCheckPolicies, BoundsCheckPolicy, IndexableLength, IndexableLengthError};
pub use keyword_set::{CaseInsensitiveKeywordSet, KeywordSet};
pub use layouter::{Alignment, LayoutError, LayoutErrorInner, Layouter, TypeLayout};
pub use namer::{EntryPointIndex, ExternalTextureNameKey, NameKey, Namer};
pub use overloads::{Conclusion, MissingSpecialType, OverloadSet, Rule};

View File

@ -1,16 +1,15 @@
use alloc::{
borrow::Cow,
boxed::Box,
format,
string::{String, ToString},
vec::Vec,
};
use core::hash::{Hash, Hasher};
use hashbrown::HashSet;
use once_cell::race::OnceBox;
use crate::{arena::Handle, FastHashMap, FastHashSet};
use crate::{
arena::Handle,
proc::{keyword_set::CaseInsensitiveKeywordSet, KeywordSet},
FastHashMap,
};
pub type EntryPointIndex = u16;
const SEPARATOR: char = '_';
@ -86,27 +85,15 @@ pub enum NameKey {
/// This processor assigns names to all the things in a module
/// that may need identifiers in a textual backend.
#[derive(Default)]
pub struct Namer {
/// The last numeric suffix used for each base name. Zero means "no suffix".
unique: FastHashMap<String, u32>,
keywords: &'static HashSet<&'static str>,
keywords_case_insensitive: FastHashSet<AsciiUniCase<&'static str>>,
keywords: &'static KeywordSet,
keywords_case_insensitive: &'static CaseInsensitiveKeywordSet,
reserved_prefixes: Vec<&'static str>,
}
impl Default for Namer {
fn default() -> Self {
static DEFAULT_KEYWORDS: OnceBox<HashSet<&'static str>> = OnceBox::new();
Self {
unique: Default::default(),
keywords: DEFAULT_KEYWORDS.get_or_init(|| Box::new(HashSet::default())),
keywords_case_insensitive: Default::default(),
reserved_prefixes: Default::default(),
}
}
}
impl Namer {
/// Return a form of `string` suitable for use as the base of an identifier.
///
@ -191,13 +178,11 @@ impl Namer {
let mut suffixed = base.to_string();
if base.ends_with(char::is_numeric)
|| self.keywords.contains(base.as_ref())
|| self
.keywords_case_insensitive
.contains(&AsciiUniCase(base.as_ref()))
|| self.keywords_case_insensitive.contains(base.as_ref())
{
suffixed.push(SEPARATOR);
}
debug_assert!(!self.keywords.contains::<str>(&suffixed));
debug_assert!(!self.keywords.contains(&suffixed));
// `self.unique` wants to own its keys. This allocates only if we haven't
// already done so earlier.
self.unique.insert(base.into_owned(), 0);
@ -228,8 +213,8 @@ impl Namer {
pub fn reset(
&mut self,
module: &crate::Module,
reserved_keywords: &'static HashSet<&'static str>,
reserved_keywords_case_insensitive: &[&'static str],
reserved_keywords: &'static KeywordSet,
reserved_keywords_case_insensitive: &'static CaseInsensitiveKeywordSet,
reserved_prefixes: &[&'static str],
output: &mut FastHashMap<NameKey, String>,
) {
@ -238,16 +223,7 @@ impl Namer {
self.unique.clear();
self.keywords = reserved_keywords;
debug_assert!(reserved_keywords_case_insensitive
.iter()
.all(|s| s.is_ascii()));
self.keywords_case_insensitive.clear();
self.keywords_case_insensitive.extend(
reserved_keywords_case_insensitive
.iter()
.map(|string| AsciiUniCase(*string)),
);
self.keywords_case_insensitive = reserved_keywords_case_insensitive;
// Choose fallback names for anonymous entry point return types.
let mut entrypoint_type_fallbacks = FastHashMap::default();
@ -384,33 +360,6 @@ impl Namer {
}
}
/// A string wrapper type with an ascii case insensitive Eq and Hash impl
struct AsciiUniCase<S: AsRef<str> + ?Sized>(S);
impl<S: AsRef<str>> PartialEq<Self> for AsciiUniCase<S> {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.0.as_ref().eq_ignore_ascii_case(other.0.as_ref())
}
}
impl<S: AsRef<str>> Eq for AsciiUniCase<S> {}
impl<S: AsRef<str>> Hash for AsciiUniCase<S> {
#[inline]
fn hash<H: Hasher>(&self, hasher: &mut H) {
for byte in self
.0
.as_ref()
.as_bytes()
.iter()
.map(|b| b.to_ascii_lowercase())
{
hasher.write_u8(byte);
}
}
}
#[test]
fn test() {
let mut namer = Namer::default();

View File

@ -1,11 +1,3 @@
#![cfg_attr(
not(any(glsl_out, hlsl_out, msl_out, feature = "wgsl-in", wgsl_out)),
expect(
dead_code,
reason = "RacyLock is only required for the above configurations"
)
)]
use alloc::boxed::Box;
use once_cell::race::OnceBox;
@ -25,17 +17,13 @@ impl<T: 'static> RacyLock<T> {
init,
}
}
/// Loads the internal value, initializing it if required.
pub fn get(&self) -> &T {
self.inner.get_or_init(|| Box::new((self.init)()))
}
}
impl<T: 'static> core::ops::Deref for RacyLock<T> {
type Target = T;
/// Loads the internal value, initializing it if required.
fn deref(&self) -> &Self::Target {
self.get()
self.inner.get_or_init(|| Box::new((self.init)()))
}
}