naga/back/
continue_forward.rs

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