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}