onedrop_hlsl/rewrite/bare_expr.rs
1//! Pass: bare expression statements → `_ = <expr>;`
2
3use super::*;
4
5// ---------------------------------------------------------------------------
6// Pass: bare expression statements → WGSL phony assignment
7// ---------------------------------------------------------------------------
8
9/// HLSL silently discards `<expr>;` statements that aren't assignments
10/// or function calls — a typical typo for the user meaning `expr =
11/// ...` or `expr += ...`. WGSL refuses with `expected assignment or
12/// increment/decrement; found '-'` (or `*`, etc.).
13///
14/// Corpus shapes (post-bb4754a 2000-sample, seed 42):
15/// - `qrad-((rad-.5)/3)*(.2+.05*mid_att);` — 10 presets sharing one
16/// warp shader (likely a missing `=` in the original author's
17/// `qrad-=...`).
18/// - `texsize.zw*r;` — bare expression statement, 1 preset.
19/// - other `<ident> <binop> <expr>;` shapes in the long tail.
20///
21/// Wrap them as `_ = <expr>;` — WGSL's phony assignment, which has
22/// the same "evaluate-and-discard" semantics HLSL took implicitly.
23pub(crate) fn collect_bare_expr_stmt_edits(block: &Block, src: &str, edits: &mut Vec<TextEdit>) {
24 for s in &block.stmts {
25 collect_in_stmt(s, src, edits);
26 }
27}
28
29fn collect_in_stmt(s: &Stmt, src: &str, edits: &mut Vec<TextEdit>) {
30 match s {
31 Stmt::Expr(e) => {
32 if needs_phony_wrap(e, src) {
33 edits.push(TextEdit {
34 start: e.span().start,
35 end: e.span().start,
36 replacement: "_ = ".to_string(),
37 });
38 }
39 }
40 Stmt::Block(b) => collect_bare_expr_stmt_edits(b, src, edits),
41 Stmt::If(i) => {
42 collect_in_stmt(&i.then_branch, src, edits);
43 if let Some(e) = &i.else_branch {
44 collect_in_stmt(e, src, edits);
45 }
46 }
47 Stmt::While(w) => collect_in_stmt(&w.body, src, edits),
48 Stmt::For(f) => collect_in_stmt(&f.body, src, edits),
49 _ => {}
50 }
51}
52
53fn needs_phony_wrap(e: &Expr, src: &str) -> bool {
54 // Real statement forms — keep as-is. `Expr::Call` carries side
55 // effects (`textureStore`, etc.) so WGSL accepts it as a
56 // `function_call_statement`; `Expr::Assign` is an
57 // assignment-as-expression that the regex pipeline emits as
58 // `lhs = rhs;` downstream.
59 if matches!(e, Expr::Call(_) | Expr::Assign(_)) {
60 return false;
61 }
62 // `x++` / `x--`: the parser silently consumes the postfix
63 // operator (parse.rs `parse_postfix`) and leaves `e =
64 // Expr::Ident("x")`. The downstream regex `rewrite_postfix_inc_dec`
65 // then expands `x++;` → `x = x + 1;`. Prefixing `_ = ` here would
66 // produce the invalid `_ = x = x + 1;` after expansion. Detect by
67 // looking past `e.span().end` for the still-in-source postfix
68 // tokens.
69 let end = e.span().end as usize;
70 if end >= src.len() {
71 return false;
72 }
73 let tail = src[end..].trim_start();
74 if tail.starts_with("++") || tail.starts_with("--") {
75 return false;
76 }
77 true
78}
79
80#[cfg(test)]
81mod tests {
82 use crate::translate_shader;
83
84 #[test]
85 fn bare_subtraction_stmt_gets_phony_wrapped() {
86 // The dominant `qrad-((rad-.5)/3)*(.2+.05*mid_att);` shape.
87 let hlsl = r#"
88shader_body {
89 float qrad = 0.0;
90 float rad = 0.5;
91 float mid_att = 0.1;
92 qrad-((rad-.5)/3)*(.2+.05*mid_att);
93}
94"#;
95 let wgsl = translate_shader(hlsl).expect("translates");
96 // The statement must now begin with `_ = ` so naga accepts it
97 // as a phony assignment.
98 assert!(
99 wgsl.contains("_ = qrad-"),
100 "expected `_ = qrad-` in output, got:\n{wgsl}"
101 );
102 }
103
104 #[test]
105 fn bare_multiplication_stmt_gets_phony_wrapped() {
106 let hlsl = r#"
107shader_body {
108 float r = 1.0;
109 float4 texsize = float4(0.0, 0.0, 0.0, 0.0);
110 texsize.zw*r;
111}
112"#;
113 let wgsl = translate_shader(hlsl).expect("translates");
114 assert!(
115 wgsl.contains("_ = texsize.zw*r"),
116 "expected `_ = texsize.zw*r`, got:\n{wgsl}"
117 );
118 }
119
120 #[test]
121 fn function_call_stmt_is_left_alone() {
122 // `Expr::Call` keeps its bare form — it's a valid
123 // `function_call_statement` in WGSL.
124 let hlsl = r#"
125shader_body {
126 float dummy = 0.0;
127 sin(dummy);
128}
129"#;
130 let wgsl = translate_shader(hlsl).expect("translates");
131 // No `_ =` should be prepended to the `sin(dummy)` call.
132 assert!(
133 !wgsl.contains("_ = sin"),
134 "function call wrongly phony-wrapped:\n{wgsl}"
135 );
136 }
137
138 #[test]
139 fn postfix_increment_is_left_alone() {
140 // `i++;` parses as `Stmt::Expr(Ident("i"))` (the `++` is
141 // silently consumed). The regex postfix expander handles it
142 // downstream; phony-wrapping would corrupt the result.
143 let hlsl = r#"
144shader_body {
145 float i = 0.0;
146 i++;
147}
148"#;
149 let wgsl = translate_shader(hlsl).expect("translates");
150 assert!(
151 !wgsl.contains("_ = i"),
152 "postfix increment wrongly phony-wrapped:\n{wgsl}"
153 );
154 // And the regular postfix expansion still fires (the expander
155 // emits one or two spaces around `=` depending on the source
156 // formatting; normalise before asserting).
157 let collapsed: String = wgsl.split_whitespace().collect::<Vec<_>>().join(" ");
158 assert!(
159 collapsed.contains("i = i + 1"),
160 "postfix expansion missing:\n{wgsl}"
161 );
162 }
163}