| 1 | # optimizePropsMethodCalls |
| 2 | |
| 3 | ## File |
| 4 | `src/Optimization/OptimizePropsMethodCalls.ts` |
| 5 | |
| 6 | ## Purpose |
| 7 | This pass converts method calls on the props object to regular function calls. Method calls like `props.onClick()` are transformed to `const t0 = props.onClick; t0()`. This normalization enables better analysis and optimization by the compiler. |
| 8 | |
| 9 | The transformation is important because method calls have different semantics than regular calls - the receiver (`props`) would normally be passed as `this` to the method. For React props, methods are typically just callback functions where `this` binding doesn't matter, so converting them to regular calls is safe and enables better memoization. |
| 10 | |
| 11 | ## Input Invariants |
| 12 | - The function has been through type inference |
| 13 | - Props parameters are typed as `TObject<BuiltInProps>` |
| 14 | |
| 15 | ## Output Guarantees |
| 16 | - All `MethodCall` instructions where the receiver has props type are converted to `CallExpression` |
| 17 | - The method property becomes the callee of the call |
| 18 | - Arguments are preserved exactly |
| 19 | |
| 20 | ## Algorithm |
| 21 | |
| 22 | ```typescript |
| 23 | export function optimizePropsMethodCalls(fn: HIRFunction): void { |
| 24 | for (const [, block] of fn.body.blocks) { |
| 25 | for (let i = 0; i < block.instructions.length; i++) { |
| 26 | const instr = block.instructions[i]!; |
| 27 | |
| 28 | if ( |
| 29 | instr.value.kind === 'MethodCall' && |
| 30 | isPropsType(instr.value.receiver.identifier) |
| 31 | ) { |
| 32 | // Transform: props.onClick(arg) |
| 33 | // To: const t0 = props.onClick; t0(arg) |
| 34 | instr.value = { |
| 35 | kind: 'CallExpression', |
| 36 | callee: instr.value.property, // The method becomes the callee |
| 37 | args: instr.value.args, |
| 38 | loc: instr.value.loc, |
| 39 | }; |
| 40 | } |
| 41 | } |
| 42 | } |
| 43 | } |
| 44 | |
| 45 | function isPropsType(identifier: Identifier): boolean { |
| 46 | return ( |
| 47 | identifier.type.kind === 'Object' && |
| 48 | identifier.type.shapeId === BuiltInPropsId |
| 49 | ); |
| 50 | } |
| 51 | ``` |
| 52 | |
| 53 | ## Edge Cases |
| 54 | |
| 55 | ### Non-Props Method Calls |
| 56 | Method calls on non-props objects are left unchanged: |
| 57 | ```javascript |
| 58 | // Unchanged - array.map is not on props |
| 59 | array.map(x => x * 2) |
| 60 | |
| 61 | // Unchanged - obj is not props |
| 62 | obj.method() |
| 63 | ``` |
| 64 | |
| 65 | ### Props Type Detection |
| 66 | The pass uses type information to identify props: |
| 67 | ```javascript |
| 68 | function Component(props) { |
| 69 | // props has type TObject<BuiltInProps> |
| 70 | props.onClick(); // Transformed |
| 71 | } |
| 72 | |
| 73 | function Regular(obj) { |
| 74 | // obj has unknown type |
| 75 | obj.onClick(); // Not transformed |
| 76 | } |
| 77 | ``` |
| 78 | |
| 79 | ### Nested Props Access |
| 80 | Only direct method calls on props are transformed: |
| 81 | ```javascript |
| 82 | props.onClick(); // Transformed |
| 83 | props.nested.onClick(); // Not transformed (receiver is props.nested, not props) |
| 84 | ``` |
| 85 | |
| 86 | ### Arrow Function Callbacks |
| 87 | Works with any method on props: |
| 88 | ```javascript |
| 89 | props.onChange(value); // Transformed |
| 90 | props.onSubmit(data); // Transformed |
| 91 | props.validate(input); // Transformed |
| 92 | ``` |
| 93 | |
| 94 | ## TODOs |
| 95 | None in the source file. |
| 96 | |
| 97 | ## Example |
| 98 | |
| 99 | ### Fixture: Using props method |
| 100 | |
| 101 | **Input:** |
| 102 | ```javascript |
| 103 | function Component(props) { |
| 104 | return <button onClick={() => props.onClick()} />; |
| 105 | } |
| 106 | ``` |
| 107 | |
| 108 | **Before OptimizePropsMethodCalls:** |
| 109 | ``` |
| 110 | [1] $5 = Function @context[props$1] ... |
| 111 | <<anonymous>>(): |
| 112 | [1] $2 = LoadLocal props$1 |
| 113 | [2] $3 = PropertyLoad $2.onClick |
| 114 | [3] $4 = MethodCall $2.$3() // Method call on props |
| 115 | [4] Return Void |
| 116 | ``` |
| 117 | |
| 118 | **After OptimizePropsMethodCalls:** |
| 119 | ``` |
| 120 | [1] $5 = Function @context[props$1] ... |
| 121 | <<anonymous>>(): |
| 122 | [1] $2 = LoadLocal props$1 |
| 123 | [2] $3 = PropertyLoad $2.onClick |
| 124 | [3] $4 = Call $3() // Now a regular call |
| 125 | [4] Return Void |
| 126 | ``` |
| 127 | |
| 128 | Key observations: |
| 129 | - `MethodCall $2.$3()` becomes `Call $3()` |
| 130 | - The property load (`$3 = PropertyLoad $2.onClick`) is preserved |
| 131 | - The receiver (`$2`) is no longer part of the call |
| 132 | - This enables the compiler to analyze `onClick` as a regular function |