| 1 | # validateNoImpureValuesInRender |
| 2 | |
| 3 | ## File |
| 4 | `src/Validation/ValidateNoImpureValuesInRender.ts` |
| 5 | |
| 6 | ## Purpose |
| 7 | This validation pass ensures that impure values (values derived from non-deterministic function calls) are not used in render output. Impure values can produce unstable results that update unpredictably when the component re-renders, violating React's requirement that components be pure and idempotent. |
| 8 | |
| 9 | The pass tracks values produced by impure functions (like `Date.now()`, `Math.random()`, `performance.now()`) and errors if those values flow into JSX props, component return values, or other render-time contexts. |
| 10 | |
| 11 | ## Input Invariants |
| 12 | - The function has been through effect inference |
| 13 | - Aliasing effects have been computed on instructions |
| 14 | - `Impure` effects mark values from non-deterministic sources |
| 15 | - `Render` effects mark values used in render context |
| 16 | |
| 17 | ## Validation Rules |
| 18 | The pass produces errors when: |
| 19 | |
| 20 | 1. **Impure value in render context**: A value marked with an `Impure` effect flows into a position marked with a `Render` effect |
| 21 | 2. **Impure function returns in render**: A function that returns an impure value is called during render |
| 22 | |
| 23 | Error messages produced: |
| 24 | - Category: `ImpureValues` |
| 25 | - Reason: "Cannot access impure value during render" |
| 26 | - Description: "Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render." |
| 27 | |
| 28 | The error points to two locations: |
| 29 | 1. Where the impure value is used in render (e.g., as a JSX prop) |
| 30 | 2. Where the impure value originates (e.g., the `Date.now()` call) |
| 31 | |
| 32 | ## Algorithm |
| 33 | |
| 34 | ### Phase 1: Infer Impure Values |
| 35 | The pass iterates over all instructions to build a map of which identifiers contain impure values: |
| 36 | |
| 37 | ```typescript |
| 38 | function inferImpureValues( |
| 39 | fn: HIRFunction, |
| 40 | impure: Map<IdentifierId, ImpureEffect>, |
| 41 | impureFunctions: Map<IdentifierId, ImpuritySignature>, |
| 42 | cache: FunctionCache, |
| 43 | ): ImpuritySignature |
| 44 | ``` |
| 45 | |
| 46 | The algorithm uses a fixed-point iteration that propagates impurity through data flow: |
| 47 | |
| 48 | 1. **Process phi nodes**: If any operand of a phi is impure, the phi result is impure |
| 49 | 2. **Process effects**: For each instruction's effects: |
| 50 | - `Impure` effect: Mark the destination identifier as impure |
| 51 | - `Alias/Assign/Capture/CreateFrom/ImmutableCapture`: Propagate impurity from source to destination |
| 52 | - `CreateFunction`: Recursively analyze function expressions |
| 53 | - `Apply`: When calling a function with an impurity signature, propagate impurity to call results |
| 54 | |
| 55 | 3. **Control flow sensitivity**: The pass also considers control-flow dominators to detect impure values that flow through conditional branches |
| 56 | |
| 57 | ### Phase 2: Validate Render Effects |
| 58 | After impurity inference converges, the pass validates all `Render` effects: |
| 59 | |
| 60 | ```typescript |
| 61 | function validateRenderEffect(effect: RenderEffect): void { |
| 62 | const impureEffect = impure.get(effect.place.identifier.id); |
| 63 | if (impureEffect != null) { |
| 64 | // Emit error |
| 65 | } |
| 66 | } |
| 67 | ``` |
| 68 | |
| 69 | ### Special Cases |
| 70 | - Values stored in refs (`isUseRefType`) are allowed to be impure since refs are not rendered |
| 71 | - JSX elements are excluded from impurity propagation (`isJsxType`) |
| 72 | |
| 73 | ## Edge Cases |
| 74 | |
| 75 | ### Impure Values Through Helper Functions |
| 76 | If a helper function returns an impure value and is called during render, both the call site and the original impure source are reported: |
| 77 | |
| 78 | ```javascript |
| 79 | function Component() { |
| 80 | const now = () => Date.now(); // Source of impurity |
| 81 | const render = () => { |
| 82 | return <div>{now()}</div>; // Error: impure value in render |
| 83 | }; |
| 84 | return <div>{render()}</div>; // Error: impure value in render |
| 85 | } |
| 86 | ``` |
| 87 | |
| 88 | ### Indirect Impurity Through Mutation |
| 89 | When an impure value is captured into another value through mutation, the destination becomes impure: |
| 90 | |
| 91 | ```javascript |
| 92 | function Component() { |
| 93 | const obj = {}; |
| 94 | obj.time = Date.now(); // obj becomes impure |
| 95 | return <Foo obj={obj} />; // Error |
| 96 | } |
| 97 | ``` |
| 98 | |
| 99 | ### Phi Node Propagation |
| 100 | Impurity propagates through control flow merges: |
| 101 | |
| 102 | ```javascript |
| 103 | function Component({cond}) { |
| 104 | let x; |
| 105 | if (cond) { |
| 106 | x = Date.now(); // Impure path |
| 107 | } else { |
| 108 | x = 0; // Pure path |
| 109 | } |
| 110 | return <Foo x={x} />; // Error: x may be impure |
| 111 | } |
| 112 | ``` |
| 113 | |
| 114 | ## TODOs |
| 115 | From the source file: |
| 116 | |
| 117 | ```typescript |
| 118 | /** |
| 119 | * TODO: consider propagating impurity for assignments/mutations that |
| 120 | * are controlled by an impure value. |
| 121 | * |
| 122 | * Example: This should error since we know the semantics of array.push, |
| 123 | * it's a definite Mutate and definite Capture, not maybemutate+maybecapture: |
| 124 | * |
| 125 | * let x = []; |
| 126 | * if (Date.now() < START_DATE) { |
| 127 | * x.push(1); |
| 128 | * } |
| 129 | * return <Foo x={x} /> |
| 130 | */ |
| 131 | ``` |
| 132 | |
| 133 | ## Example |
| 134 | |
| 135 | ### Fixture: `error.invalid-impure-functions-in-render.js` |
| 136 | |
| 137 | **Input:** |
| 138 | ```javascript |
| 139 | // @validateNoImpureFunctionsInRender |
| 140 | |
| 141 | function Component() { |
| 142 | const date = Date.now(); |
| 143 | const now = performance.now(); |
| 144 | const rand = Math.random(); |
| 145 | return <Foo date={date} now={now} rand={rand} />; |
| 146 | } |
| 147 | ``` |
| 148 | |
| 149 | **Error:** |
| 150 | ``` |
| 151 | Found 3 errors: |
| 152 | |
| 153 | Error: Cannot access impure value during render |
| 154 | |
| 155 | Calling an impure function can produce unstable results that update unpredictably |
| 156 | when the component happens to re-render. |
| 157 | (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). |
| 158 | |
| 159 | error.invalid-impure-functions-in-render.ts:7:20 |
| 160 | 5 | const now = performance.now(); |
| 161 | 6 | const rand = Math.random(); |
| 162 | > 7 | return <Foo date={date} now={now} rand={rand} />; |
| 163 | | ^^^^ Cannot access impure value during render |
| 164 | 8 | } |
| 165 | |
| 166 | error.invalid-impure-functions-in-render.ts:4:15 |
| 167 | 2 | |
| 168 | 3 | function Component() { |
| 169 | > 4 | const date = Date.now(); |
| 170 | | ^^^^^^^^^^ `Date.now` is an impure function. |
| 171 | 5 | const now = performance.now(); |
| 172 | |
| 173 | Error: Cannot access impure value during render |
| 174 | ... |
| 175 | ``` |
| 176 | |
| 177 | Key observations: |
| 178 | - Each impure function call (`Date.now`, `performance.now`, `Math.random`) produces a separate error |
| 179 | - The error shows both the usage location (in JSX) and the source location (the impure call) |
| 180 | - The pass is enabled via the `@validateNoImpureFunctionsInRender` pragma |