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}