main
md 169 lines 4.48 KB
Rendered Raw
1 # outlineFunctions
2
3 ## File
4 `src/Optimization/OutlineFunctions.ts`
5
6 ## Purpose
7 This pass outlines pure function expressions that have no captured context into top-level helper functions. By moving these functions outside the component, they become truly static and can be shared across renders without any memoization overhead.
8
9 A function with no captured context is completely self-contained - it only uses its parameters and globals. Such functions don't need to be recreated on each render and can be hoisted to module scope.
10
11 ## Input Invariants
12 - The `enableFunctionOutlining` feature flag must be enabled
13 - Functions must have `context.length === 0` (no captured variables)
14 - Functions must be anonymous (no `id` property)
15 - Functions must not be FBT macro operands (tracked by `fbtOperands` parameter)
16
17 ## Output Guarantees
18 - Pure function expressions are replaced with `LoadGlobal` of the outlined function
19 - Outlined functions are registered with the environment for emission
20 - The original instruction is transformed to load the global
21
22 ## Algorithm
23
24 ```typescript
25 export function outlineFunctions(
26 fn: HIRFunction,
27 fbtOperands: Set<IdentifierId>,
28 ): void {
29 for (const [, block] of fn.body.blocks) {
30 for (let i = 0; i < block.instructions.length; i++) {
31 const instr = block.instructions[i]!;
32
33 if (
34 instr.value.kind === 'FunctionExpression' &&
35 instr.value.loweredFunc.func.context.length === 0 &&
36 instr.value.loweredFunc.func.id === null &&
37 !fbtOperands.has(instr.lvalue.identifier.id)
38 ) {
39 // Outline this function
40 const outlinedId = fn.env.outlineFunction(
41 instr.value.loweredFunc.func,
42 'helper',
43 );
44
45 // Replace with LoadGlobal
46 instr.value = {
47 kind: 'LoadGlobal',
48 binding: {
49 kind: 'ModuleLocal',
50 name: outlinedId,
51 },
52 loc: instr.value.loc,
53 };
54 }
55 }
56 }
57 }
58 ```
59
60 ## Edge Cases
61
62 ### Functions with Context
63 Functions that capture variables are not outlined:
64 ```javascript
65 function Component(props) {
66 const x = props.value;
67 const fn = () => x * 2; // Captures x, not outlined
68 }
69 ```
70
71 ### Named Functions
72 Functions with explicit names are not outlined:
73 ```javascript
74 const foo = function namedFn() { ... }; // Has id, not outlined
75 ```
76
77 ### FBT Operands
78 Functions used as FBT operands cannot be outlined due to translation requirements:
79 ```javascript
80 <fbt>
81 Hello <fbt:param name="user">{() => getName()}</fbt:param>
82 </fbt>
83 // The function cannot be outlined - FBT needs it inline
84 ```
85
86 ### Arrow Functions vs Function Expressions
87 Both arrow functions and function expressions are candidates:
88 ```javascript
89 const a = () => 1; // Outlined if no context
90 const b = function() {}; // Outlined if no context
91 ```
92
93 ### Recursive Functions
94 Self-referencing functions cannot be outlined (they would have themselves in context):
95 ```javascript
96 const fib = (n) => n <= 1 ? n : fib(n-1) + fib(n-2); // References self
97 ```
98
99 ## TODOs
100 None in the source file.
101
102 ## Example
103
104 ### Fixture: `outlined-helper.js`
105
106 **Input:**
107 ```javascript
108 // @enableFunctionOutlining
109 function Component(props) {
110 return (
111 <div>
112 {props.items.map(item => (
113 <Stringify key={item.id} item={item.name} />
114 ))}
115 </div>
116 );
117 }
118 ```
119
120 **Analysis:**
121 The map callback `item => <Stringify .../>` has one captured variable: nothing from the component (only uses `item` parameter). However, it receives `item` as a parameter, not from context.
122
123 If we have a truly pure helper:
124 ```javascript
125 // @enableFunctionOutlining
126 function Component(props) {
127 const double = (x) => x * 2; // No context, pure
128 return <div>{double(props.value)}</div>;
129 }
130 ```
131
132 **After OutlineFunctions:**
133 ```
134 // Outlined to module scope:
135 function _outlined_double$1(x) {
136 return x * 2;
137 }
138
139 // In component:
140 [1] $1 = LoadGlobal _outlined_double$1 // Instead of FunctionExpression
141 [2] StoreLocal Const double = $1
142 ```
143
144 **Generated Code:**
145 ```javascript
146 function _outlined_double$1(x) {
147 return x * 2;
148 }
149
150 function Component(props) {
151 const $ = _c(2);
152 const double = _outlined_double$1; // Just a reference, no recreation
153 let t0;
154 if ($[0] !== props.value) {
155 t0 = <div>{double(props.value)}</div>;
156 $[0] = props.value;
157 $[1] = t0;
158 } else {
159 t0 = $[1];
160 }
161 return t0;
162 }
163 ```
164
165 Key observations:
166 - The pure function is hoisted to module scope
167 - The component just references the outlined function
168 - No memoization needed for the function itself
169 - Reduces runtime overhead by avoiding function recreation