[compiler] Track locations for dependencies (#35794)
Tracks locations for reactive scope dependencies, both on the deps and portions of the path. The immediate need for this is a non-public experiment where we're exploring type-directed compilation, and sometimes look up the types of expressions by location. We need to preserve locations accurately for that to work, including the locations of the deps. ## Test Plan Locations for dependencies are not easy to test: i manually spot-checked the new fixture to ensure that the deps look right. This is fine as best-effort since it doesn't impact any of our core compilation logic, i may fix forward if there are issues and will think about how to test.
Joseph Savona committed
Feb 17, 2026 at 14:06 UTC
4ac47537dda5f370dffa185a2e26f8301bf73d45
11 files changed
+194
-24
compiler/packages/babel-plugin-react-compiler/src/HIR/CollectHoistablePropertyLoads.ts
+29
-13
@@ -31,6 +31,7 @@ import {
31
PropertyLiteral,
32
ReactiveScopeDependency,
33
ScopeId,
34
+ SourceLocation,
35
TInstruction,
36
} from './HIR';
37
@@ -244,6 +245,7 @@ class PropertyPathRegistry {
245
getOrCreateIdentifier(
246
identifier: Identifier,
247
reactive: boolean,
248
+ loc: SourceLocation,
249
): PropertyPathNode {
250
/**
251
* Reads from a statically scoped variable are always safe in JS,
@@ -260,6 +262,7 @@ class PropertyPathRegistry {
262
identifier,
263
reactive,
264
path: [],
265
+ loc,
266
},
267
hasOptional: false,
268
parent: null,
@@ -290,6 +293,7 @@ class PropertyPathRegistry {
293
identifier: parent.fullPath.identifier,
294
reactive: parent.fullPath.reactive,
295
path: parent.fullPath.path.concat(entry),
296
+ loc: entry.loc,
297
},
298
hasOptional: parent.hasOptional || entry.optional,
299
};
@@ -304,7 +308,7 @@ class PropertyPathRegistry {
308
* so all subpaths of a PropertyLoad should already exist
309
* (e.g. a.b is added before a.b.c),
310
*/
307
- let currNode = this.getOrCreateIdentifier(n.identifier, n.reactive);
311
+ let currNode = this.getOrCreateIdentifier(n.identifier, n.reactive, n.loc);
312
if (n.path.length === 0) {
313
return currNode;
314
}
@@ -323,20 +327,21 @@ class PropertyPathRegistry {
327
}
328
329
function getMaybeNonNullInInstruction(
326
- instr: InstructionValue,
330
+ value: InstructionValue,
331
context: CollectHoistablePropertyLoadsContext,
332
): PropertyPathNode | null {
333
let path: ReactiveScopeDependency | null = null;
330
- if (instr.kind === 'PropertyLoad') {
331
- path = context.temporaries.get(instr.object.identifier.id) ?? {
332
- identifier: instr.object.identifier,
333
- reactive: instr.object.reactive,
334
+ if (value.kind === 'PropertyLoad') {
335
+ path = context.temporaries.get(value.object.identifier.id) ?? {
336
+ identifier: value.object.identifier,
337
+ reactive: value.object.reactive,
338
path: [],
339
+ loc: value.loc,
340
};
336
- } else if (instr.kind === 'Destructure') {
337
- path = context.temporaries.get(instr.value.identifier.id) ?? null;
338
- } else if (instr.kind === 'ComputedLoad') {
339
- path = context.temporaries.get(instr.object.identifier.id) ?? null;
341
+ } else if (value.kind === 'Destructure') {
342
+ path = context.temporaries.get(value.value.identifier.id) ?? null;
343
+ } else if (value.kind === 'ComputedLoad') {
344
+ path = context.temporaries.get(value.object.identifier.id) ?? null;
345
}
346
return path != null ? context.registry.getOrCreateProperty(path) : null;
347
}
@@ -393,7 +398,11 @@ function collectNonNullsInBlocks(
398
) {
399
const identifier = fn.params[0].identifier;
400
knownNonNullIdentifiers.add(
396
- context.registry.getOrCreateIdentifier(identifier, true),
401
+ context.registry.getOrCreateIdentifier(
402
+ identifier,
403
+ true,
404
+ fn.params[0].loc,
405
+ ),
406
);
407
}
408
const nodes = new Map<
@@ -468,6 +477,7 @@ function collectNonNullsInBlocks(
477
identifier: dep.root.value.identifier,
478
path: dep.path.slice(0, i),
479
reactive: dep.root.value.reactive,
480
+ loc: dep.loc,
481
});
482
assumedNonNullObjects.add(depNode);
483
}
@@ -654,17 +664,23 @@ function reduceMaybeOptionalChains(
664
changed = false;
665
666
for (const original of optionalChainNodes) {
657
- let {identifier, path: origPath, reactive} = original.fullPath;
667
+ let {
668
+ identifier,
669
+ path: origPath,
670
+ reactive,
671
+ loc: origLoc,
672
+ } = original.fullPath;
673
let currNode: PropertyPathNode = registry.getOrCreateIdentifier(
674
identifier,
675
reactive,
676
+ origLoc,
677
);
678
for (let i = 0; i < origPath.length; i++) {
679
const entry = origPath[i];
680
// If the base is known to be non-null, replace with a non-optional load
681
const nextEntry: DependencyPathEntry =
682
entry.optional && nodes.has(currNode)
667
- ? {property: entry.property, optional: false}
683
+ ? {property: entry.property, optional: false, loc: entry.loc}
684
: entry;
685
currNode = PropertyPathRegistry.getOrCreatePropertyEntry(
686
currNode,
compiler/packages/babel-plugin-react-compiler/src/HIR/CollectOptionalChainDependencies.ts
+12
-3
@@ -5,7 +5,7 @@
5
* LICENSE file in the root directory of this source tree.
6
*/
7
8
-import {CompilerError} from '..';
8
+import {CompilerError, SourceLocation} from '..';
9
import {assertNonNull} from './CollectHoistablePropertyLoads';
10
import {
11
BlockId,
@@ -169,6 +169,7 @@ function matchOptionalTestBlock(
169
propertyId: IdentifierId;
170
storeLocalInstr: Instruction;
171
consequentGoto: BlockId;
172
+ propertyLoadLoc: SourceLocation;
173
} | null {
174
const consequentBlock = assertNonNull(blocks.get(terminal.consequent));
175
if (
@@ -221,6 +222,7 @@ function matchOptionalTestBlock(
222
propertyId: propertyLoad.lvalue.identifier.id,
223
storeLocalInstr,
224
consequentGoto: consequentBlock.terminal.block,
225
+ propertyLoadLoc: propertyLoad.loc,
226
};
227
}
228
return null;
@@ -275,7 +277,11 @@ function traverseOptionalBlock(
277
instrVal.kind === 'PropertyLoad' &&
278
instrVal.object.identifier.id === prevInstr.lvalue.identifier.id
279
) {
278
- path.push({property: instrVal.property, optional: false});
280
+ path.push({
281
+ property: instrVal.property,
282
+ optional: false,
283
+ loc: instrVal.loc,
284
+ });
285
} else {
286
return null;
287
}
@@ -292,6 +298,7 @@ function traverseOptionalBlock(
298
identifier: maybeTest.instructions[0].value.place.identifier,
299
reactive: maybeTest.instructions[0].value.place.reactive,
300
path,
301
+ loc: maybeTest.instructions[0].value.place.loc,
302
};
303
test = maybeTest.terminal;
304
} else if (maybeTest.terminal.kind === 'optional') {
@@ -390,7 +397,7 @@ function traverseOptionalBlock(
397
loc: optional.terminal.loc,
398
},
399
);
393
- const load = {
400
+ const load: ReactiveScopeDependency = {
401
identifier: baseObject.identifier,
402
reactive: baseObject.reactive,
403
path: [
@@ -398,8 +405,10 @@ function traverseOptionalBlock(
405
{
406
property: matchConsequentResult.property,
407
optional: optional.terminal.optional,
408
+ loc: matchConsequentResult.propertyLoadLoc,
409
},
410
],
411
+ loc: matchConsequentResult.propertyLoadLoc,
412
};
413
context.processedInstrsInOptional.add(matchConsequentResult.storeLocalInstr);
414
context.processedInstrsInOptional.add(test);
compiler/packages/babel-plugin-react-compiler/src/HIR/DeriveMinimalDependenciesHIR.ts
+15
-3
@@ -12,6 +12,7 @@ import {
12
Identifier,
13
PropertyLiteral,
14
ReactiveScopeDependency,
15
+ SourceLocation,
16
} from '../HIR';
17
import {printIdentifier} from '../HIR/PrintHIR';
18
@@ -36,12 +37,13 @@ export class ReactiveScopeDependencyTreeHIR {
37
* duplicates when traversing the CFG.
38
*/
39
constructor(hoistableObjects: Iterable<ReactiveScopeDependency>) {
39
- for (const {path, identifier, reactive} of hoistableObjects) {
40
+ for (const {path, identifier, reactive, loc} of hoistableObjects) {
41
let currNode = ReactiveScopeDependencyTreeHIR.#getOrCreateRoot(
42
identifier,
43
reactive,
44
this.#hoistableObjects,
45
path.length > 0 && path[0].optional ? 'Optional' : 'NonNull',
46
+ loc,
47
);
48
49
for (let i = 0; i < path.length; i++) {
@@ -62,6 +64,7 @@ export class ReactiveScopeDependencyTreeHIR {
64
nextNode = {
65
properties: new Map(),
66
accessType,
67
+ loc: path[i].loc,
68
};
69
currNode.properties.set(path[i].property, nextNode);
70
}
@@ -75,6 +78,7 @@ export class ReactiveScopeDependencyTreeHIR {
78
reactive: boolean,
79
roots: Map<Identifier, TreeNode<T> & {reactive: boolean}>,
80
defaultAccessType: T,
81
+ loc: SourceLocation,
82
): TreeNode<T> {
83
// roots can always be accessed unconditionally in JS
84
let rootNode = roots.get(identifier);
@@ -84,6 +88,7 @@ export class ReactiveScopeDependencyTreeHIR {
88
properties: new Map(),
89
reactive,
90
accessType: defaultAccessType,
91
+ loc,
92
};
93
roots.set(identifier, rootNode);
94
} else {
@@ -102,12 +107,13 @@ export class ReactiveScopeDependencyTreeHIR {
107
* safe-to-evaluate subpath
108
*/
109
addDependency(dep: ReactiveScopeDependency): void {
105
- const {identifier, reactive, path} = dep;
110
+ const {identifier, reactive, path, loc} = dep;
111
let depCursor = ReactiveScopeDependencyTreeHIR.#getOrCreateRoot(
112
identifier,
113
reactive,
114
this.#deps,
115
PropertyAccessType.UnconditionalAccess,
116
+ loc,
117
);
118
/**
119
* hoistableCursor is null if depCursor is not an object we can hoist
@@ -153,6 +159,7 @@ export class ReactiveScopeDependencyTreeHIR {
159
depCursor,
160
entry.property,
161
accessType,
162
+ entry.loc,
163
);
164
} else if (
165
hoistableCursor != null &&
@@ -163,6 +170,7 @@ export class ReactiveScopeDependencyTreeHIR {
170
depCursor,
171
entry.property,
172
PropertyAccessType.UnconditionalAccess,
173
+ entry.loc,
174
);
175
} else {
176
/**
@@ -306,6 +314,7 @@ function merge(
314
type TreeNode<T extends string> = {
315
properties: Map<PropertyLiteral, TreeNode<T>>;
316
accessType: T;
317
+ loc: SourceLocation;
318
};
319
type HoistableNode = TreeNode<'Optional' | 'NonNull'>;
320
type DependencyNode = TreeNode<PropertyAccessType>;
@@ -323,7 +332,7 @@ function collectMinimalDependenciesInSubtree(
332
results: Set<ReactiveScopeDependency>,
333
): void {
334
if (isDependency(node.accessType)) {
326
- results.add({identifier: rootIdentifier, reactive, path});
335
+ results.add({identifier: rootIdentifier, reactive, path, loc: node.loc});
336
} else {
337
for (const [childName, childNode] of node.properties) {
338
collectMinimalDependenciesInSubtree(
@@ -335,6 +344,7 @@ function collectMinimalDependenciesInSubtree(
344
{
345
property: childName,
346
optional: isOptional(childNode.accessType),
347
+ loc: childNode.loc,
348
},
349
],
350
results,
@@ -362,12 +372,14 @@ function makeOrMergeProperty(
372
node: DependencyNode,
373
property: PropertyLiteral,
374
accessType: PropertyAccessType,
375
+ loc: SourceLocation,
376
): DependencyNode {
377
let child = node.properties.get(property);
378
if (child == null) {
379
child = {
380
properties: new Map(),
381
accessType,
382
+ loc,
383
};
384
node.properties.set(property, child);
385
} else {
compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts
+2
@@ -1639,6 +1639,7 @@ export function makePropertyLiteral(value: string | number): PropertyLiteral {
1639
export type DependencyPathEntry = {
1640
property: PropertyLiteral;
1641
optional: boolean;
1642
+ loc: SourceLocation;
1643
};
1644
export type DependencyPath = Array<DependencyPathEntry>;
1645
export type ReactiveScopeDependency = {
@@ -1656,6 +1657,7 @@ export type ReactiveScopeDependency = {
1657
*/
1658
reactive: boolean;
1659
path: DependencyPath;
1660
+ loc: SourceLocation;
1661
};
1662
1663
export function areEqualPaths(a: DependencyPath, b: DependencyPath): boolean {
compiler/packages/babel-plugin-react-compiler/src/HIR/PropagateScopeDependenciesHIR.ts
+17
-3
@@ -31,6 +31,7 @@ import {
31
ObjectMethod,
32
PropertyLiteral,
33
convertHoistedLValueKind,
34
+ SourceLocation,
35
} from './HIR';
36
import {
37
collectHoistablePropertyLoads,
@@ -298,6 +299,7 @@ function collectTemporariesSidemapImpl(
299
value.object,
300
value.property,
301
false,
302
+ value.loc,
303
temporaries,
304
);
305
temporaries.set(lvalue.identifier.id, property);
@@ -318,6 +320,7 @@ function collectTemporariesSidemapImpl(
320
identifier: value.place.identifier,
321
reactive: value.place.reactive,
322
path: [],
323
+ loc: value.loc,
324
});
325
}
326
} else if (
@@ -339,6 +342,7 @@ function getProperty(
342
object: Place,
343
propertyName: PropertyLiteral,
344
optional: boolean,
345
+ loc: SourceLocation,
346
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
347
): ReactiveScopeDependency {
348
/*
@@ -371,13 +375,18 @@ function getProperty(
375
property = {
376
identifier: object.identifier,
377
reactive: object.reactive,
374
- path: [{property: propertyName, optional}],
378
+ path: [{property: propertyName, optional, loc}],
379
+ loc,
380
};
381
} else {
382
property = {
383
identifier: resolvedDependency.identifier,
384
reactive: resolvedDependency.reactive,
380
- path: [...resolvedDependency.path, {property: propertyName, optional}],
385
+ path: [
386
+ ...resolvedDependency.path,
387
+ {property: propertyName, optional, loc},
388
+ ],
389
+ loc,
390
};
391
}
392
return property;
@@ -537,6 +546,7 @@ export class DependencyCollectionContext {
546
identifier: place.identifier,
547
reactive: place.reactive,
548
path: [],
549
+ loc: place.loc,
550
},
551
);
552
}
@@ -545,11 +555,13 @@ export class DependencyCollectionContext {
555
object: Place,
556
property: PropertyLiteral,
557
optional: boolean,
558
+ loc: SourceLocation,
559
): void {
560
const nextDependency = getProperty(
561
object,
562
property,
563
optional,
564
+ loc,
565
this.#temporaries,
566
);
567
this.visitDependency(nextDependency);
@@ -602,6 +614,7 @@ export class DependencyCollectionContext {
614
identifier: maybeDependency.identifier,
615
reactive: maybeDependency.reactive,
616
path: [],
617
+ loc: maybeDependency.loc,
618
};
619
}
620
if (this.#checkValidDependency(maybeDependency)) {
@@ -626,6 +639,7 @@ export class DependencyCollectionContext {
639
identifier: place.identifier,
640
reactive: place.reactive,
641
path: [],
642
+ loc: place.loc,
643
})
644
) {
645
currentScope.reassignments.add(place.identifier);
@@ -679,7 +693,7 @@ export function handleInstruction(
693
return;
694
}
695
if (value.kind === 'PropertyLoad') {
682
- context.visitProperty(value.object, value.property, false);
696
+ context.visitProperty(value.object, value.property, false, value.loc);
697
} else if (value.kind === 'StoreLocal') {
698
context.visitOperand(value.value);
699
if (value.lvalue.kind === InstructionKind.Reassign) {
compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts
+4
-1
@@ -74,7 +74,10 @@ export function collectMaybeMemoDependencies(
74
return {
75
root: object.root,
76
// TODO: determine if the access is optional
77
- path: [...object.path, {property: value.property, optional}],
77
+ path: [
78
+ ...object.path,
79
+ {property: value.property, optional, loc: value.loc},
80
+ ],
81
loc: value.loc,
82
};
83
}
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/MergeReactiveScopesThatInvalidateTogether.ts
+1
@@ -470,6 +470,7 @@ function canMergeScopes(
470
identifier: declaration.identifier,
471
reactive: true,
472
path: [],
473
+ loc: GeneratedSource,
474
})),
475
),
476
next.scope.dependencies,
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PrintReactiveFunction.ts
+2
-1
@@ -22,6 +22,7 @@ import {
22
printIdentifier,
23
printInstructionValue,
24
printPlace,
25
+ printSourceLocation,
26
printType,
27
} from '../HIR/PrintHIR';
28
import {assertExhaustive} from '../Utils/utils';
@@ -114,7 +115,7 @@ export function printDependency(dependency: ReactiveScopeDependency): string {
115
const identifier =
116
printIdentifier(dependency.identifier) +
117
printType(dependency.identifier.type);
117
- return `${identifier}${dependency.path.map(token => `${token.optional ? '?.' : '.'}${token.property}`).join('')}`;
118
+ return `${identifier}${dependency.path.map(token => `${token.optional ? '?.' : '.'}${token.property}`).join('')}_${printSourceLocation(dependency.loc)}`;
119
}
120
121
export function printReactiveInstructions(
compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateExhaustiveDependencies.ts
+1
@@ -756,6 +756,7 @@ function collectDependencies(
756
{
757
optional,
758
property: value.property,
759
+ loc: value.loc,
760
},
761
],
762
loc: value.loc,
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repeated-dependencies-more-precise.expect.md
new
+87
@@ -0,0 +1,87 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+// @flow
6
+import {Stringify} from 'shared-runtime';
7
+
8
+/**
9
+ * Example fixture demonstrating a case where we could hoist dependencies
10
+ * and reuse them across scopes. Here we extract a temporary for `item.value`
11
+ * and reference it both in the scope for `a`. Then the scope for `c` could
12
+ * use `<item-value-temp>.inner` as its dependency, avoiding reloading
13
+ * `item.value`.
14
+ */
15
+function Test({item, index}: {item: {value: {inner: any}}, index: number}) {
16
+ // These scopes have the same dependency, `item.value`, and could
17
+ // share a hoisted expression to evaluate it
18
+ const a = [];
19
+ if (index) {
20
+ a.push({value: item.value, index});
21
+ }
22
+ const b = [item.value];
23
+
24
+ // This dependency is more precise (nested property), the outer
25
+ // `item.value` portion could use a hoisted dep for `item.value
26
+ const c = [item.value.inner];
27
+ return <Stringify value={[a, b, c]} />;
28
+}
29
+
30
+```
31
+
32
+## Code
33
+
34
+```javascript
35
+import { c as _c } from "react/compiler-runtime";
36
+import { Stringify } from "shared-runtime";
37
+
38
+function Test(t0) {
39
+ const $ = _c(11);
40
+ const { item, index } = t0;
41
+ let a;
42
+ if ($[0] !== index || $[1] !== item.value) {
43
+ a = [];
44
+ if (index) {
45
+ a.push({ value: item.value, index });
46
+ }
47
+ $[0] = index;
48
+ $[1] = item.value;
49
+ $[2] = a;
50
+ } else {
51
+ a = $[2];
52
+ }
53
+ let t1;
54
+ if ($[3] !== item.value) {
55
+ t1 = [item.value];
56
+ $[3] = item.value;
57
+ $[4] = t1;
58
+ } else {
59
+ t1 = $[4];
60
+ }
61
+ const b = t1;
62
+ let t2;
63
+ if ($[5] !== item.value.inner) {
64
+ t2 = [item.value.inner];
65
+ $[5] = item.value.inner;
66
+ $[6] = t2;
67
+ } else {
68
+ t2 = $[6];
69
+ }
70
+ const c = t2;
71
+ let t3;
72
+ if ($[7] !== a || $[8] !== b || $[9] !== c) {
73
+ t3 = <Stringify value={[a, b, c]} />;
74
+ $[7] = a;
75
+ $[8] = b;
76
+ $[9] = c;
77
+ $[10] = t3;
78
+ } else {
79
+ t3 = $[10];
80
+ }
81
+ return t3;
82
+}
83
+
84
+```
85
+
86
+### Eval output
87
+(kind: exception) Fixture not implemented
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repeated-dependencies-more-precise.js
new
+24
@@ -0,0 +1,24 @@
1
+// @flow
2
+import {Stringify} from 'shared-runtime';
3
+
4
+/**
5
+ * Example fixture demonstrating a case where we could hoist dependencies
6
+ * and reuse them across scopes. Here we extract a temporary for `item.value`
7
+ * and reference it both in the scope for `a`. Then the scope for `c` could
8
+ * use `<item-value-temp>.inner` as its dependency, avoiding reloading
9
+ * `item.value`.
10
+ */
11
+function Test({item, index}: {item: {value: {inner: any}}, index: number}) {
12
+ // These scopes have the same dependency, `item.value`, and could
13
+ // share a hoisted expression to evaluate it
14
+ const a = [];
15
+ if (index) {
16
+ a.push({value: item.value, index});
17
+ }
18
+ const b = [item.value];
19
+
20
+ // This dependency is more precise (nested property), the outer
21
+ // `item.value` portion could use a hoisted dep for `item.value
22
+ const c = [item.value.inner];
23
+ return <Stringify value={[a, b, c]} />;
24
+}