1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311
//! Workarounds for platform bugs and limitations in switches and loops.
//!
//! In these docs, we use CamelCase links for Naga IR concepts, and ordinary
//! `code` formatting for HLSL or GLSL concepts.
//!
//! ## Avoiding `continue` within `switch`
//!
//! As described in <https://github.com/gfx-rs/wgpu/issues/4485>, the FXC HLSL
//! compiler doesn't allow `continue` statements within `switch` statements, but
//! Naga IR does. We work around this by introducing synthetic boolean local
//! variables and branches.
//!
//! Specifically:
//!
//! - We generate code for [`Continue`] statements within [`SwitchCase`]s that
//! sets an introduced `bool` local to `true` and does a `break`, jumping to
//! immediately after the generated `switch`.
//!
//! - When generating code for a [`Switch`] statement, we conservatively assume
//! it might contain such a [`Continue`] statement, so:
//!
//! - If it's the outermost such [`Switch`] within a [`Loop`], we declare the
//! `bool` local ahead of the switch, initialized to `false`. Immediately
//! after the `switch`, we check the local and do a `continue` if it's set.
//!
//! - If the [`Switch`] is nested within other [`Switch`]es, then after the
//! generated `switch`, we check the local (which we know was declared
//! before the surrounding `switch`) and do a `break` if it's set.
//!
//! - As an optimization, we only generate the check of the local if a
//! [`Continue`] statement is encountered within the [`Switch`]. This may
//! help drivers more easily identify that the `bool` is unused.
//!
//! So while we "weaken" the [`Continue`] statement by rendering it as a `break`
//! statement, we also place checks immediately at the locations to which those
//! `break` statements will jump, until we can be sure we've reached the
//! intended target of the original [`Continue`].
//!
//! In the case of nested [`Loop`] and [`Switch`] statements, there may be
//! multiple introduced `bool` locals in scope, but there's no problem knowing
//! which one to operate on. At any point, there is at most one [`Loop`]
//! statement that could be targeted by a [`Continue`] statement, so the correct
//! `bool` local to set and test is always the one introduced for the innermost
//! enclosing [`Loop`]'s outermost [`Switch`].
//!
//! # Avoiding single body `switch` statements
//!
//! As described in <https://github.com/gfx-rs/wgpu/issues/4514>, some language
//! front ends miscompile `switch` statements where all cases branch to the same
//! body. Our HLSL and GLSL backends render [`Switch`] statements with a single
//! [`SwitchCase`] as `do {} while(false);` loops.
//!
//! However, this rewriting introduces a new loop that could "capture"
//! `continue` statements in its body. To avoid doing so, we apply the
//! [`Continue`]-to-`break` transformation described above.
//!
//! [`Continue`]: crate::Statement::Continue
//! [`Loop`]: crate::Statement::Loop
//! [`Switch`]: crate::Statement::Switch
//! [`SwitchCase`]: crate::SwitchCase
use crate::proc::Namer;
use std::rc::Rc;
/// A summary of the code surrounding a statement.
enum Nesting {
/// Currently nested in at least one [`Loop`] statement.
///
/// [`Continue`] should apply to the innermost loop.
///
/// When this entry is on the top of the stack:
///
/// * When entering an inner [`Loop`] statement, push a [`Loop`][nl] state
/// onto the stack.
///
/// * When entering a nested [`Switch`] statement, push a [`Switch`][ns]
/// state onto the stack with a new variable name. Before the generated
/// `switch`, introduce a `bool` local with that name, initialized to
/// `false`.
///
/// When exiting the [`Loop`] for which this entry was pushed, pop it from
/// the stack.
///
/// [`Continue`]: crate::Statement::Continue
/// [`Loop`]: crate::Statement::Loop
/// [`Switch`]: crate::Statement::Switch
/// [ns]: Nesting::Switch
/// [nl]: Nesting::Loop
Loop,
/// Currently nested in at least one [`Switch`] that may need to forward
/// [`Continue`]s.
///
/// This includes [`Switch`]es rendered as `do {} while(false)` loops, but
/// doesn't need to include regular [`Switch`]es in backends that can
/// support `continue` within switches.
///
/// [`Continue`] should be forwarded to the innermost surrounding [`Loop`].
///
/// When this entry is on the top of the stack:
///
/// * When entering a nested [`Loop`], push a [`Loop`][nl] state onto the
/// stack.
///
/// * When entering a nested [`Switch`], push a [`Switch`][ns] state onto
/// the stack with a clone of the introduced `bool` variable's name.
///
/// * When encountering a [`Continue`] statement, render it as code to set
/// the introduced `bool` local (whose name is held in [`variable`]) to
/// `true`, and then `break`. Set [`continue_encountered`] to `true` to
/// record that the [`Switch`] contains a [`Continue`].
///
/// * When exiting this [`Switch`], pop its entry from the stack. If
/// [`continue_encountered`] is set, then we have rendered [`Continue`]
/// statements as `break` statements that jump to this point. Generate
/// code to check `variable`, and if it is `true`:
///
/// * If there is another [`Switch`][ns] left on top of the stack, set
/// its `continue_encountered` flag, and generate a `break`. (Both
/// [`Switch`][ns]es are within the same [`Loop`] and share the same
/// introduced variable, so there's no need to set another flag to
/// continue to exit the `switch`es.)
///
/// * Otherwise, `continue`.
///
/// When we exit the [`Switch`] for which this entry was pushed, pop it.
///
/// [`Continue`]: crate::Statement::Continue
/// [`Loop`]: crate::Statement::Loop
/// [`Switch`]: crate::Statement::Switch
/// [`variable`]: Nesting::Switch::variable
/// [`continue_encountered`]: Nesting::Switch::continue_encountered
/// [ns]: Nesting::Switch
/// [nl]: Nesting::Loop
Switch {
variable: Rc<String>,
/// Set if we've generated code for a [`Continue`] statement with this
/// entry on the top of the stack.
///
/// If this is still clear when we finish rendering the [`Switch`], then
/// we know we don't need to generate branch forwarding code. Omitting
/// that may make it easier for drivers to tell that the `bool` we
/// introduced ahead of the [`Switch`] is actually unused.
///
/// [`Continue`]: crate::Statement::Continue
/// [`Switch`]: crate::Statement::Switch
continue_encountered: bool,
},
}
/// A micro-IR for code a backend should generate after a [`Switch`].
///
/// [`Switch`]: crate::Statement::Switch
pub(super) enum ExitControlFlow {
None,
/// Emit `if (continue_variable) { continue; }`
Continue {
variable: Rc<String>,
},
/// Emit `if (continue_variable) { break; }`
///
/// Used after a [`Switch`] to exit from an enclosing [`Switch`].
///
/// After the enclosing switch, its associated check will consult this same
/// variable, see that it is set, and exit early.
///
/// [`Switch`]: crate::Statement::Switch
Break {
variable: Rc<String>,
},
}
/// Utility for tracking nesting of loops and switches to orchestrate forwarding
/// of continue statements inside of a switch to the enclosing loop.
///
/// See [module docs](self) for why we need this.
#[derive(Default)]
pub(super) struct ContinueCtx {
stack: Vec<Nesting>,
}
impl ContinueCtx {
/// Resets internal state.
///
/// Use this to reuse memory between writing sessions.
pub fn clear(&mut self) {
self.stack.clear();
}
/// Updates internal state to record entering a [`Loop`] statement.
///
/// [`Loop`]: crate::Statement::Loop
pub fn enter_loop(&mut self) {
self.stack.push(Nesting::Loop);
}
/// Updates internal state to record exiting a [`Loop`] statement.
///
/// [`Loop`]: crate::Statement::Loop
pub fn exit_loop(&mut self) {
if !matches!(self.stack.pop(), Some(Nesting::Loop)) {
unreachable!("ContinueCtx stack out of sync");
}
}
/// Updates internal state to record entering a [`Switch`] statement.
///
/// Return `Some(variable)` if this [`Switch`] is nested within a [`Loop`],
/// and the caller should introcue a new `bool` local variable named
/// `variable` above the `switch`, for forwarding [`Continue`] statements.
///
/// `variable` is guaranteed not to conflict with any names used by the
/// program itself.
///
/// [`Continue`]: crate::Statement::Continue
/// [`Loop`]: crate::Statement::Loop
/// [`Switch`]: crate::Statement::Switch
pub fn enter_switch(&mut self, namer: &mut Namer) -> Option<Rc<String>> {
match self.stack.last() {
// If the stack is empty, we are not in loop, so we don't need to
// forward continue statements within this `Switch`. We can leave
// the stack empty.
None => None,
Some(&Nesting::Loop { .. }) => {
let variable = Rc::new(namer.call("should_continue"));
self.stack.push(Nesting::Switch {
variable: Rc::clone(&variable),
continue_encountered: false,
});
Some(variable)
}
Some(&Nesting::Switch { ref variable, .. }) => {
self.stack.push(Nesting::Switch {
variable: Rc::clone(variable),
continue_encountered: false,
});
// We have already declared the variable before some enclosing
// `Switch`.
None
}
}
}
/// Update internal state to record leaving a [`Switch`] statement.
///
/// Return an [`ExitControlFlow`] value indicating what code should be
/// introduced after the generated `switch` to forward continues.
///
/// [`Switch`]: crate::Statement::Switch
pub fn exit_switch(&mut self) -> ExitControlFlow {
match self.stack.pop() {
// This doesn't indicate a problem: we don't start pushing entries
// for `Switch` statements unless we have an enclosing `Loop`.
None => ExitControlFlow::None,
Some(Nesting::Loop { .. }) => {
unreachable!("Unexpected loop state when exiting switch");
}
Some(Nesting::Switch {
variable,
continue_encountered: inner_continue,
}) => {
if !inner_continue {
// No `Continue` statement was encountered, so we didn't
// introduce any `break`s jumping to this point.
ExitControlFlow::None
} else if let Some(&mut Nesting::Switch {
continue_encountered: ref mut outer_continue,
..
}) = self.stack.last_mut()
{
// This is nested in another `Switch`. Propagate upwards
// that there is a continue statement present.
*outer_continue = true;
ExitControlFlow::Break { variable }
} else {
ExitControlFlow::Continue { variable }
}
}
}
}
/// Determine what to generate for a [`Continue`] statement.
///
/// If we can generate an ordinary `continue` statement, return `None`.
///
/// Otherwise, we're enclosed by a [`Switch`] that is itself enclosed by a
/// [`Loop`]. Return `Some(variable)` to indicate that the [`Continue`]
/// should be rendered as setting `variable` to `true`, and then doing a
/// `break`.
///
/// This also notes that we've encountered a [`Continue`] statement, so that
/// we can generate the right code to forward the branch following the
/// enclosing `switch`.
///
/// [`Continue`]: crate::Statement::Continue
/// [`Loop`]: crate::Statement::Loop
/// [`Switch`]: crate::Statement::Switch
pub fn continue_encountered(&mut self) -> Option<&str> {
if let Some(&mut Nesting::Switch {
ref variable,
ref mut continue_encountered,
}) = self.stack.last_mut()
{
*continue_encountered = true;
Some(variable)
} else {
None
}
}
}