@samitouri / QOS-React-1 / commits / 4ac47537dd

[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 +}