From e75fb3c224aed976cfc730dae1bbf1822f9eb65b Mon Sep 17 00:00:00 2001 From: Jim Blandy Date: Thu, 9 Nov 2023 13:21:33 -0800 Subject: [PATCH] [naga] Introduce `Literal::AbstractInt` and `AbstractFloat`. Introduce new variants of `naga::Literal`, `AbstractInt` and `AbstractFloat`, for representing WGSL abstract values. --- naga/src/back/glsl/mod.rs | 5 ++ naga/src/back/hlsl/writer.rs | 5 ++ naga/src/back/msl/writer.rs | 3 + naga/src/back/spv/writer.rs | 3 + naga/src/back/wgsl/writer.rs | 5 ++ naga/src/lib.rs | 5 ++ naga/src/proc/constant_evaluator.rs | 113 +++++++++++++++++++++++++++- naga/src/proc/mod.rs | 17 ++++- 8 files changed, 151 insertions(+), 5 deletions(-) diff --git a/naga/src/back/glsl/mod.rs b/naga/src/back/glsl/mod.rs index d08a0c02c2..e1dc906630 100644 --- a/naga/src/back/glsl/mod.rs +++ b/naga/src/back/glsl/mod.rs @@ -2451,6 +2451,11 @@ impl<'a, W: Write> Writer<'a, W> { crate::Literal::I64(_) => { return Err(Error::Custom("GLSL has no 64-bit integer type".into())); } + crate::Literal::AbstractInt(_) | crate::Literal::AbstractFloat(_) => { + return Err(Error::Custom( + "Abstract types should not appear in IR presented to backends".into(), + )); + } } } Expression::Constant(handle) => { diff --git a/naga/src/back/hlsl/writer.rs b/naga/src/back/hlsl/writer.rs index 24d54fc0e5..0dd60c6ad7 100644 --- a/naga/src/back/hlsl/writer.rs +++ b/naga/src/back/hlsl/writer.rs @@ -2040,6 +2040,11 @@ impl<'a, W: fmt::Write> super::Writer<'a, W> { crate::Literal::I32(value) => write!(self.out, "{}", value)?, crate::Literal::I64(value) => write!(self.out, "{}L", value)?, crate::Literal::Bool(value) => write!(self.out, "{}", value)?, + crate::Literal::AbstractInt(_) | crate::Literal::AbstractFloat(_) => { + return Err(Error::Custom( + "Abstract types should not appear in IR presented to backends".into(), + )); + } }, Expression::Constant(handle) => { let constant = &module.constants[handle]; diff --git a/naga/src/back/msl/writer.rs b/naga/src/back/msl/writer.rs index de226af87b..f900add71e 100644 --- a/naga/src/back/msl/writer.rs +++ b/naga/src/back/msl/writer.rs @@ -1279,6 +1279,9 @@ impl Writer { crate::Literal::Bool(value) => { write!(self.out, "{value}")?; } + crate::Literal::AbstractInt(_) | crate::Literal::AbstractFloat(_) => { + return Err(Error::Validation); + } }, crate::Expression::Constant(handle) => { let constant = &module.constants[handle]; diff --git a/naga/src/back/spv/writer.rs b/naga/src/back/spv/writer.rs index 48fc64bf24..4db86c93a7 100644 --- a/naga/src/back/spv/writer.rs +++ b/naga/src/back/spv/writer.rs @@ -1187,6 +1187,9 @@ impl Writer { } crate::Literal::Bool(true) => Instruction::constant_true(type_id, id), crate::Literal::Bool(false) => Instruction::constant_false(type_id, id), + crate::Literal::AbstractInt(_) | crate::Literal::AbstractFloat(_) => { + unreachable!("Abstract types should not appear in IR presented to backends"); + } }; instruction.to_words(&mut self.logical_layout.declarations); diff --git a/naga/src/back/wgsl/writer.rs b/naga/src/back/wgsl/writer.rs index 10da339968..a356c7e2ad 100644 --- a/naga/src/back/wgsl/writer.rs +++ b/naga/src/back/wgsl/writer.rs @@ -1099,6 +1099,11 @@ impl Writer { crate::Literal::I64(_) => { return Err(Error::Custom("unsupported i64 literal".to_string())); } + crate::Literal::AbstractInt(_) | crate::Literal::AbstractFloat(_) => { + return Err(Error::Custom( + "Abstract types should not appear in IR presented to backends".into(), + )); + } } } Expression::Constant(handle) => { diff --git a/naga/src/lib.rs b/naga/src/lib.rs index e45f463bd9..b27ebc6764 100644 --- a/naga/src/lib.rs +++ b/naga/src/lib.rs @@ -300,6 +300,9 @@ use serde::Serialize; /// Width of a boolean type, in bytes. pub const BOOL_WIDTH: Bytes = 1; +/// Width of abstract types, in bytes. +pub const ABSTRACT_WIDTH: Bytes = 8; + /// Hash map that is faster but not resilient to DoS attacks. pub type FastHashMap = rustc_hash::FxHashMap; /// Hash set that is faster but not resilient to DoS attacks. @@ -881,6 +884,8 @@ pub enum Literal { I32(i32), I64(i64), Bool(bool), + AbstractInt(i64), + AbstractFloat(f64), } #[derive(Debug, PartialEq)] diff --git a/naga/src/proc/constant_evaluator.rs b/naga/src/proc/constant_evaluator.rs index e3c07f9e16..6901ada20c 100644 --- a/naga/src/proc/constant_evaluator.rs +++ b/naga/src/proc/constant_evaluator.rs @@ -167,6 +167,15 @@ pub enum ConstantEvaluatorError { NotImplemented(String), #[error("{0} operation overflowed")] Overflow(String), + #[error( + "the concrete type `{to_type}` cannot represent the abstract value `{value}` accurately" + )] + AutomaticConversionLossy { + value: String, + to_type: &'static str, + }, + #[error("abstract floating-point values cannot be automatically converted to integers")] + AutomaticConversionFloatToInt { to_type: &'static str }, #[error("Division by zero")] DivisionByZero, #[error("Remainder by zero")] @@ -979,6 +988,8 @@ impl<'a> ConstantEvaluator<'a> { Literal::F64(_) | Literal::I64(_) => { return Err(ConstantEvaluatorError::InvalidCastArg) } + Literal::AbstractInt(v) => i32::try_from_abstract(v)?, + Literal::AbstractFloat(v) => i32::try_from_abstract(v)?, }), Sc::U32 => Literal::U32(match literal { Literal::I32(v) => v as u32, @@ -988,6 +999,8 @@ impl<'a> ConstantEvaluator<'a> { Literal::F64(_) | Literal::I64(_) => { return Err(ConstantEvaluatorError::InvalidCastArg) } + Literal::AbstractInt(v) => u32::try_from_abstract(v)?, + Literal::AbstractFloat(v) => u32::try_from_abstract(v)?, }), Sc::F32 => Literal::F32(match literal { Literal::I32(v) => v as f32, @@ -997,21 +1010,28 @@ impl<'a> ConstantEvaluator<'a> { Literal::F64(_) | Literal::I64(_) => { return Err(ConstantEvaluatorError::InvalidCastArg) } + Literal::AbstractInt(v) => f32::try_from_abstract(v)?, + Literal::AbstractFloat(v) => f32::try_from_abstract(v)?, }), Sc::F64 => Literal::F64(match literal { Literal::I32(v) => v as f64, Literal::U32(v) => v as f64, Literal::F32(v) => v as f64, - Literal::Bool(v) => v as u32 as f64, Literal::F64(v) => v, + Literal::Bool(v) => v as u32 as f64, Literal::I64(_) => return Err(ConstantEvaluatorError::InvalidCastArg), + Literal::AbstractInt(v) => f64::try_from_abstract(v)?, + Literal::AbstractFloat(v) => f64::try_from_abstract(v)?, }), Sc::BOOL => Literal::Bool(match literal { Literal::I32(v) => v != 0, Literal::U32(v) => v != 0, Literal::F32(v) => v != 0.0, Literal::Bool(v) => v, - Literal::F64(_) | Literal::I64(_) => { + Literal::F64(_) + | Literal::I64(_) + | Literal::AbstractInt(_) + | Literal::AbstractFloat(_) => { return Err(ConstantEvaluatorError::InvalidCastArg) } }), @@ -1828,3 +1848,92 @@ mod tests { } } } + +/// Trait for conversions of abstract values to concrete types. +trait TryFromAbstract: Sized { + /// Convert an abstract literal `value` to `Self`. + /// + /// Since Naga's `AbstractInt` and `AbstractFloat` exist to support + /// WGSL, we follow WGSL's conversion rules here: + /// + /// - WGSL §6.1.2. Conversion Rank says that automatic conversions + /// to integers are either lossless or an error. + /// + /// - WGSL §14.6.4 Floating Point Conversion says that conversions + /// to floating point in constant expressions and override + /// expressions are errors if the value is out of range for the + /// destination type, but rounding is okay. + /// + /// [`AbstractInt`]: crate::Literal::AbstractInt + /// [`Float`]: crate::Literal::Float + fn try_from_abstract(value: T) -> Result; +} + +impl TryFromAbstract for i32 { + fn try_from_abstract(value: i64) -> Result { + i32::try_from(value).map_err(|_| ConstantEvaluatorError::AutomaticConversionLossy { + value: format!("{value:?}"), + to_type: "i32", + }) + } +} + +impl TryFromAbstract for u32 { + fn try_from_abstract(value: i64) -> Result { + u32::try_from(value).map_err(|_| ConstantEvaluatorError::AutomaticConversionLossy { + value: format!("{value:?}"), + to_type: "u32", + }) + } +} + +impl TryFromAbstract for f32 { + fn try_from_abstract(value: i64) -> Result { + let f = value as f32; + // The range of `i64` is roughly ±18 × 10¹⁸, whereas the range of + // `f32` is roughly ±3.4 × 10³⁸, so there's no opportunity for + // overflow here. + Ok(f) + } +} + +impl TryFromAbstract for f32 { + fn try_from_abstract(value: f64) -> Result { + let f = value as f32; + if f.is_infinite() { + return Err(ConstantEvaluatorError::AutomaticConversionLossy { + value: format!("{value:?}"), + to_type: "f32", + }); + } + Ok(f) + } +} + +impl TryFromAbstract for f64 { + fn try_from_abstract(value: i64) -> Result { + let f = value as f64; + // The range of `i64` is roughly ±18 × 10¹⁸, whereas the range of + // `f64` is roughly ±1.8 × 10³⁰⁸, so there's no opportunity for + // overflow here. + Ok(f) + } +} + +impl TryFromAbstract for f64 { + fn try_from_abstract(value: f64) -> Result { + Ok(value) + } +} + +impl TryFromAbstract for i32 { + fn try_from_abstract(_: f64) -> Result { + Err(ConstantEvaluatorError::AutomaticConversionFloatToInt { to_type: "i32" }) + } +} + +impl TryFromAbstract for u32 { + fn try_from_abstract(_: f64) -> Result { + Err(ConstantEvaluatorError::AutomaticConversionFloatToInt { to_type: "u32" }) + } +} diff --git a/naga/src/proc/mod.rs b/naga/src/proc/mod.rs index 687527049e..40a342f6ce 100644 --- a/naga/src/proc/mod.rs +++ b/naga/src/proc/mod.rs @@ -106,6 +106,14 @@ impl super::Scalar { kind: crate::ScalarKind::Bool, width: crate::BOOL_WIDTH, }; + pub const ABSTRACT_INT: Self = Self { + kind: crate::ScalarKind::AbstractInt, + width: crate::ABSTRACT_WIDTH, + }; + pub const ABSTRACT_FLOAT: Self = Self { + kind: crate::ScalarKind::AbstractFloat, + width: crate::ABSTRACT_WIDTH, + }; /// Construct a float `Scalar` with the given width. /// @@ -148,7 +156,7 @@ impl Eq for crate::Literal {} impl std::hash::Hash for crate::Literal { fn hash(&self, hasher: &mut H) { match *self { - Self::F64(v) => { + Self::F64(v) | Self::AbstractFloat(v) => { hasher.write_u8(0); v.to_bits().hash(hasher); } @@ -168,7 +176,7 @@ impl std::hash::Hash for crate::Literal { hasher.write_u8(4); v.hash(hasher); } - Self::I64(v) => { + Self::I64(v) | Self::AbstractInt(v) => { hasher.write_u8(5); v.hash(hasher); } @@ -202,7 +210,8 @@ impl crate::Literal { match *self { Self::F64(_) | Self::I64(_) => 8, Self::F32(_) | Self::U32(_) | Self::I32(_) => 4, - Self::Bool(_) => 1, + Self::Bool(_) => crate::BOOL_WIDTH, + Self::AbstractInt(_) | Self::AbstractFloat(_) => crate::ABSTRACT_WIDTH, } } pub const fn scalar(&self) -> crate::Scalar { @@ -213,6 +222,8 @@ impl crate::Literal { Self::I32(_) => crate::Scalar::I32, Self::I64(_) => crate::Scalar::I64, Self::Bool(_) => crate::Scalar::BOOL, + Self::AbstractInt(_) => crate::Scalar::ABSTRACT_INT, + Self::AbstractFloat(_) => crate::Scalar::ABSTRACT_FLOAT, } } pub const fn scalar_kind(&self) -> crate::ScalarKind {