mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
[compiler] Delete LoweredFunction.dependencies and hoisted instructions
LoweredFunction dependencies were exclusively used for dependency extraction (in `propagateScopeDeps`). Now that we have a `propagateScopeDepsHIR` that recursively traverses into nested functions, we can delete `dependencies` and their associated synthetic `LoadLocal`/`PropertyLoad` instructions. [Internal snapshot diff](https://www.internalfb.com/phabricator/paste/view/P1716950202) for this change shows ~.2% of files changed. I [read through ~60 of the changed files](https://www.internalfb.com/phabricator/paste/view/P1733074307) - most changes are due to better outlining (due to better DCE) - a few changes in memo inference are due to changed ordering ``` // source arr.map(() => contextVar.inner); // previous instructions $0 = LoadLocal arr $1 = $0.map // Below instructions are synthetic $2 = LoadLocal contextVar $3 = $2.inner $4 = Function deps=$3 context=contextVar { ... } ``` - a few changes are effectively bugfixes (see `aliased-nested-scope-fn-expr`)
This commit is contained in:
@@ -7,7 +7,6 @@
|
||||
|
||||
import {NodePath, Scope} from '@babel/traverse';
|
||||
import * as t from '@babel/types';
|
||||
import {Expression} from '@babel/types';
|
||||
import invariant from 'invariant';
|
||||
import {
|
||||
CompilerError,
|
||||
@@ -75,7 +74,7 @@ export function lower(
|
||||
parent: NodePath<t.Function> | null = null,
|
||||
): Result<HIRFunction, CompilerError> {
|
||||
const builder = new HIRBuilder(env, parent ?? func, bindings, capturedRefs);
|
||||
const context: Array<Place> = [];
|
||||
const context: HIRFunction['context'] = [];
|
||||
|
||||
for (const ref of capturedRefs ?? []) {
|
||||
context.push({
|
||||
@@ -3378,7 +3377,7 @@ function lowerFunction(
|
||||
>,
|
||||
): LoweredFunction | null {
|
||||
const componentScope: Scope = builder.parentFunction.scope;
|
||||
const captured = gatherCapturedDeps(builder, expr, componentScope);
|
||||
const capturedContext = gatherCapturedContext(expr, componentScope);
|
||||
|
||||
/*
|
||||
* TODO(gsn): In the future, we could only pass in the context identifiers
|
||||
@@ -3392,7 +3391,7 @@ function lowerFunction(
|
||||
expr,
|
||||
builder.environment,
|
||||
builder.bindings,
|
||||
[...builder.context, ...captured.identifiers],
|
||||
[...builder.context, ...capturedContext],
|
||||
builder.parentFunction,
|
||||
);
|
||||
let loweredFunc: HIRFunction;
|
||||
@@ -3405,7 +3404,6 @@ function lowerFunction(
|
||||
loweredFunc = lowering.unwrap();
|
||||
return {
|
||||
func: loweredFunc,
|
||||
dependencies: captured.refs,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4079,14 +4077,6 @@ function lowerAssignment(
|
||||
}
|
||||
}
|
||||
|
||||
function isValidDependency(path: NodePath<t.MemberExpression>): boolean {
|
||||
const parent: NodePath<t.Node> = path.parentPath;
|
||||
return (
|
||||
!path.node.computed &&
|
||||
!(parent.isCallExpression() && parent.get('callee') === path)
|
||||
);
|
||||
}
|
||||
|
||||
function captureScopes({from, to}: {from: Scope; to: Scope}): Set<Scope> {
|
||||
let scopes: Set<Scope> = new Set();
|
||||
while (from) {
|
||||
@@ -4101,8 +4091,7 @@ function captureScopes({from, to}: {from: Scope; to: Scope}): Set<Scope> {
|
||||
return scopes;
|
||||
}
|
||||
|
||||
function gatherCapturedDeps(
|
||||
builder: HIRBuilder,
|
||||
function gatherCapturedContext(
|
||||
fn: NodePath<
|
||||
| t.FunctionExpression
|
||||
| t.ArrowFunctionExpression
|
||||
@@ -4110,10 +4099,8 @@ function gatherCapturedDeps(
|
||||
| t.ObjectMethod
|
||||
>,
|
||||
componentScope: Scope,
|
||||
): {identifiers: Array<t.Identifier>; refs: Array<Place>} {
|
||||
const capturedIds: Map<t.Identifier, number> = new Map();
|
||||
const capturedRefs: Set<Place> = new Set();
|
||||
const seenPaths: Set<string> = new Set();
|
||||
): Array<t.Identifier> {
|
||||
const capturedIds = new Set<t.Identifier>();
|
||||
|
||||
/*
|
||||
* Capture all the scopes from the parent of this function up to and including
|
||||
@@ -4124,33 +4111,11 @@ function gatherCapturedDeps(
|
||||
to: componentScope,
|
||||
});
|
||||
|
||||
function addCapturedId(bindingIdentifier: t.Identifier): number {
|
||||
if (!capturedIds.has(bindingIdentifier)) {
|
||||
const index = capturedIds.size;
|
||||
capturedIds.set(bindingIdentifier, index);
|
||||
return index;
|
||||
} else {
|
||||
return capturedIds.get(bindingIdentifier)!;
|
||||
}
|
||||
}
|
||||
|
||||
function handleMaybeDependency(
|
||||
path:
|
||||
| NodePath<t.MemberExpression>
|
||||
| NodePath<t.Identifier>
|
||||
| NodePath<t.JSXOpeningElement>,
|
||||
path: NodePath<t.Identifier> | NodePath<t.JSXOpeningElement>,
|
||||
): void {
|
||||
// Base context variable to depend on
|
||||
let baseIdentifier: NodePath<t.Identifier> | NodePath<t.JSXIdentifier>;
|
||||
/*
|
||||
* Base expression to depend on, which (for now) may contain non side-effectful
|
||||
* member expressions
|
||||
*/
|
||||
let dependency:
|
||||
| NodePath<t.MemberExpression>
|
||||
| NodePath<t.JSXMemberExpression>
|
||||
| NodePath<t.Identifier>
|
||||
| NodePath<t.JSXIdentifier>;
|
||||
if (path.isJSXOpeningElement()) {
|
||||
const name = path.get('name');
|
||||
if (!(name.isJSXMemberExpression() || name.isJSXIdentifier())) {
|
||||
@@ -4166,115 +4131,20 @@ function gatherCapturedDeps(
|
||||
'Invalid logic in gatherCapturedDeps',
|
||||
);
|
||||
baseIdentifier = current;
|
||||
|
||||
/*
|
||||
* Get the expression to depend on, which may involve PropertyLoads
|
||||
* for member expressions
|
||||
*/
|
||||
let currentDep:
|
||||
| NodePath<t.JSXMemberExpression>
|
||||
| NodePath<t.Identifier>
|
||||
| NodePath<t.JSXIdentifier> = baseIdentifier;
|
||||
|
||||
while (true) {
|
||||
const nextDep: null | NodePath<t.Node> = currentDep.parentPath;
|
||||
if (nextDep && nextDep.isJSXMemberExpression()) {
|
||||
currentDep = nextDep;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
dependency = currentDep;
|
||||
} else if (path.isMemberExpression()) {
|
||||
// Calculate baseIdentifier
|
||||
let currentId: NodePath<Expression> = path;
|
||||
while (currentId.isMemberExpression()) {
|
||||
currentId = currentId.get('object');
|
||||
}
|
||||
if (!currentId.isIdentifier()) {
|
||||
return;
|
||||
}
|
||||
baseIdentifier = currentId;
|
||||
|
||||
/*
|
||||
* Get the expression to depend on, which may involve PropertyLoads
|
||||
* for member expressions
|
||||
*/
|
||||
let currentDep:
|
||||
| NodePath<t.MemberExpression>
|
||||
| NodePath<t.Identifier>
|
||||
| NodePath<t.JSXIdentifier> = baseIdentifier;
|
||||
|
||||
while (true) {
|
||||
const nextDep: null | NodePath<t.Node> = currentDep.parentPath;
|
||||
if (
|
||||
nextDep &&
|
||||
nextDep.isMemberExpression() &&
|
||||
isValidDependency(nextDep)
|
||||
) {
|
||||
currentDep = nextDep;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
dependency = currentDep;
|
||||
} else {
|
||||
baseIdentifier = path;
|
||||
dependency = path;
|
||||
}
|
||||
|
||||
/*
|
||||
* Skip dependency path, as we already tried to recursively add it (+ all subexpressions)
|
||||
* as a dependency.
|
||||
*/
|
||||
dependency.skip();
|
||||
path.skip();
|
||||
|
||||
// Add the base identifier binding as a dependency.
|
||||
const binding = baseIdentifier.scope.getBinding(baseIdentifier.node.name);
|
||||
if (binding === undefined || !pureScopes.has(binding.scope)) {
|
||||
return;
|
||||
}
|
||||
const idKey = String(addCapturedId(binding.identifier));
|
||||
|
||||
// Add the expression (potentially a memberexpr path) as a dependency.
|
||||
let exprKey = idKey;
|
||||
if (dependency.isMemberExpression()) {
|
||||
let pathTokens = [];
|
||||
let current: NodePath<Expression> = dependency;
|
||||
while (current.isMemberExpression()) {
|
||||
const property = current.get('property') as NodePath<t.Identifier>;
|
||||
pathTokens.push(property.node.name);
|
||||
current = current.get('object');
|
||||
}
|
||||
|
||||
exprKey += '.' + pathTokens.reverse().join('.');
|
||||
} else if (dependency.isJSXMemberExpression()) {
|
||||
let pathTokens = [];
|
||||
let current: NodePath<t.JSXMemberExpression | t.JSXIdentifier> =
|
||||
dependency;
|
||||
while (current.isJSXMemberExpression()) {
|
||||
const property = current.get('property');
|
||||
pathTokens.push(property.node.name);
|
||||
current = current.get('object');
|
||||
}
|
||||
}
|
||||
|
||||
if (!seenPaths.has(exprKey)) {
|
||||
let loweredDep: Place;
|
||||
if (dependency.isJSXIdentifier()) {
|
||||
loweredDep = lowerValueToTemporary(builder, {
|
||||
kind: 'LoadLocal',
|
||||
place: lowerIdentifier(builder, dependency),
|
||||
loc: path.node.loc ?? GeneratedSource,
|
||||
});
|
||||
} else if (dependency.isJSXMemberExpression()) {
|
||||
loweredDep = lowerJsxMemberExpression(builder, dependency);
|
||||
} else {
|
||||
loweredDep = lowerExpressionToTemporary(builder, dependency);
|
||||
}
|
||||
capturedRefs.add(loweredDep);
|
||||
seenPaths.add(exprKey);
|
||||
if (binding !== undefined && pureScopes.has(binding.scope)) {
|
||||
capturedIds.add(binding.identifier);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4305,13 +4175,13 @@ function gatherCapturedDeps(
|
||||
return;
|
||||
} else if (path.isJSXElement()) {
|
||||
handleMaybeDependency(path.get('openingElement'));
|
||||
} else if (path.isMemberExpression() || path.isIdentifier()) {
|
||||
} else if (path.isIdentifier()) {
|
||||
handleMaybeDependency(path);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return {identifiers: [...capturedIds.keys()], refs: [...capturedRefs]};
|
||||
return [...capturedIds.keys()];
|
||||
}
|
||||
|
||||
function notNull<T>(value: T | null): value is T {
|
||||
|
||||
+1
-36
@@ -131,15 +131,7 @@ function collectHoistablePropertyLoadsImpl(
|
||||
fn: HIRFunction,
|
||||
context: CollectHoistablePropertyLoadsContext,
|
||||
): ReadonlyMap<BlockId, BlockInfo> {
|
||||
const functionExpressionLoads = collectFunctionExpressionFakeLoads(fn);
|
||||
const actuallyEvaluatedTemporaries = new Map(
|
||||
[...context.temporaries].filter(([id]) => !functionExpressionLoads.has(id)),
|
||||
);
|
||||
|
||||
const nodes = collectNonNullsInBlocks(fn, {
|
||||
...context,
|
||||
temporaries: actuallyEvaluatedTemporaries,
|
||||
});
|
||||
const nodes = collectNonNullsInBlocks(fn, context);
|
||||
propagateNonNull(fn, nodes, context.registry);
|
||||
|
||||
if (DEBUG_PRINT) {
|
||||
@@ -598,30 +590,3 @@ function reduceMaybeOptionalChains(
|
||||
}
|
||||
} while (changed);
|
||||
}
|
||||
|
||||
function collectFunctionExpressionFakeLoads(
|
||||
fn: HIRFunction,
|
||||
): Set<IdentifierId> {
|
||||
const sources = new Map<IdentifierId, IdentifierId>();
|
||||
const functionExpressionReferences = new Set<IdentifierId>();
|
||||
|
||||
for (const [_, block] of fn.body.blocks) {
|
||||
for (const {lvalue, value} of block.instructions) {
|
||||
if (
|
||||
value.kind === 'FunctionExpression' ||
|
||||
value.kind === 'ObjectMethod'
|
||||
) {
|
||||
for (const reference of value.loweredFunc.dependencies) {
|
||||
let curr: IdentifierId | undefined = reference.identifier.id;
|
||||
while (curr != null) {
|
||||
functionExpressionReferences.add(curr);
|
||||
curr = sources.get(curr);
|
||||
}
|
||||
}
|
||||
} else if (value.kind === 'PropertyLoad') {
|
||||
sources.set(lvalue.identifier.id, value.object.identifier.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
return functionExpressionReferences;
|
||||
}
|
||||
|
||||
@@ -245,8 +245,6 @@ const EnvironmentConfigSchema = z.object({
|
||||
*/
|
||||
enableUseTypeAnnotations: z.boolean().default(false),
|
||||
|
||||
enableFunctionDependencyRewrite: z.boolean().default(true),
|
||||
|
||||
/**
|
||||
* Enables inference of optional dependency chains. Without this flag
|
||||
* a property chain such as `props?.items?.foo` will infer as a dep on
|
||||
|
||||
@@ -722,7 +722,6 @@ export type ObjectProperty = {
|
||||
};
|
||||
|
||||
export type LoweredFunction = {
|
||||
dependencies: Array<Place>;
|
||||
func: HIRFunction;
|
||||
};
|
||||
|
||||
|
||||
+16
-1
@@ -148,6 +148,14 @@ function collectScopeInfo(fn: HIRFunction): ScopeInfo {
|
||||
const scope = place.identifier.scope;
|
||||
if (scope != null) {
|
||||
placeScopes.set(place, scope);
|
||||
/**
|
||||
* Record both mutating and non-mutating scopes to merge scopes with
|
||||
* still-mutating values with inner scopes that alias those values
|
||||
* (see `nonmutating-capture-in-unsplittable-memo-block`)
|
||||
*
|
||||
* Note that this isn't perfect, as it also leads to merging of mutating
|
||||
* scopes with JSX single-instruction scopes (see `mutation-within-jsx`)
|
||||
*/
|
||||
if (scope.range.start !== scope.range.end) {
|
||||
getOrInsertDefault(scopeStarts, scope.range.start, new Set()).add(
|
||||
scope,
|
||||
@@ -254,7 +262,7 @@ function visitPlace(
|
||||
* of the stack to the mutated outer scope.
|
||||
*/
|
||||
const placeScope = getPlaceScope(id, place);
|
||||
if (placeScope != null && isMutable({id} as any, place)) {
|
||||
if (placeScope != null && isMutable({id}, place)) {
|
||||
const placeScopeIdx = activeScopes.indexOf(placeScope);
|
||||
if (placeScopeIdx !== -1 && placeScopeIdx !== activeScopes.length - 1) {
|
||||
joined.union([placeScope, ...activeScopes.slice(placeScopeIdx + 1)]);
|
||||
@@ -275,6 +283,13 @@ function getOverlappingReactiveScopes(
|
||||
for (const instr of block.instructions) {
|
||||
visitInstructionId(instr.id, context, state);
|
||||
for (const place of eachInstructionOperand(instr)) {
|
||||
if (
|
||||
(instr.value.kind === 'FunctionExpression' ||
|
||||
instr.value.kind === 'ObjectMethod') &&
|
||||
place.identifier.type.kind === 'Primitive'
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
visitPlace(instr.id, place, state);
|
||||
}
|
||||
for (const place of eachInstructionLValue(instr)) {
|
||||
|
||||
@@ -538,9 +538,6 @@ export function printInstructionValue(instrValue: ReactiveValue): string {
|
||||
.split('\n')
|
||||
.map(line => ` ${line}`)
|
||||
.join('\n');
|
||||
const deps = instrValue.loweredFunc.dependencies
|
||||
.map(dep => printPlace(dep))
|
||||
.join(',');
|
||||
const context = instrValue.loweredFunc.func.context
|
||||
.map(dep => printPlace(dep))
|
||||
.join(',');
|
||||
@@ -557,7 +554,7 @@ export function printInstructionValue(instrValue: ReactiveValue): string {
|
||||
})
|
||||
.join(', ') ?? '';
|
||||
const type = printType(instrValue.loweredFunc.func.returnType).trim();
|
||||
value = `${kind} ${name} @deps[${deps}] @context[${context}] @effects[${effects}]${type !== '' ? ` return${type}` : ''}:\n${fn}`;
|
||||
value = `${kind} ${name} @context[${context}] @effects[${effects}]${type !== '' ? ` return${type}` : ''}:\n${fn}`;
|
||||
break;
|
||||
}
|
||||
case 'TaggedTemplateExpression': {
|
||||
|
||||
+2
-3
@@ -738,9 +738,8 @@ function collectDependencies(
|
||||
}
|
||||
for (const instr of block.instructions) {
|
||||
if (
|
||||
fn.env.config.enableFunctionDependencyRewrite &&
|
||||
(instr.value.kind === 'FunctionExpression' ||
|
||||
instr.value.kind === 'ObjectMethod')
|
||||
instr.value.kind === 'FunctionExpression' ||
|
||||
instr.value.kind === 'ObjectMethod'
|
||||
) {
|
||||
context.declare(instr.lvalue.identifier, {
|
||||
id: instr.id,
|
||||
|
||||
@@ -193,7 +193,7 @@ export function* eachInstructionValueOperand(
|
||||
}
|
||||
case 'ObjectMethod':
|
||||
case 'FunctionExpression': {
|
||||
yield* instrValue.loweredFunc.dependencies;
|
||||
yield* instrValue.loweredFunc.func.context;
|
||||
break;
|
||||
}
|
||||
case 'TaggedTemplateExpression': {
|
||||
@@ -517,8 +517,9 @@ export function mapInstructionValueOperands(
|
||||
}
|
||||
case 'ObjectMethod':
|
||||
case 'FunctionExpression': {
|
||||
instrValue.loweredFunc.dependencies =
|
||||
instrValue.loweredFunc.dependencies.map(d => fn(d));
|
||||
instrValue.loweredFunc.func.context =
|
||||
instrValue.loweredFunc.func.context.map(d => fn(d));
|
||||
|
||||
break;
|
||||
}
|
||||
case 'TaggedTemplateExpression': {
|
||||
|
||||
@@ -10,9 +10,8 @@ import {
|
||||
Effect,
|
||||
HIRFunction,
|
||||
Identifier,
|
||||
IdentifierName,
|
||||
IdentifierId,
|
||||
LoweredFunction,
|
||||
Place,
|
||||
isRefOrRefValue,
|
||||
makeInstructionId,
|
||||
} from '../HIR';
|
||||
@@ -23,78 +22,39 @@ import {inferMutableContextVariables} from './InferMutableContextVariables';
|
||||
import {inferMutableRanges} from './InferMutableRanges';
|
||||
import inferReferenceEffects from './InferReferenceEffects';
|
||||
|
||||
type Dependency = {
|
||||
identifier: Identifier;
|
||||
path: Array<string>;
|
||||
};
|
||||
|
||||
// Helper class to track indirections such as LoadLocal and PropertyLoad.
|
||||
export class IdentifierState {
|
||||
properties: Map<Identifier, Dependency> = new Map();
|
||||
properties: Map<IdentifierId, Identifier> = new Map();
|
||||
|
||||
resolve(identifier: Identifier): Identifier {
|
||||
const resolved = this.properties.get(identifier);
|
||||
const resolved = this.properties.get(identifier.id);
|
||||
if (resolved !== undefined) {
|
||||
return resolved.identifier;
|
||||
return resolved;
|
||||
}
|
||||
return identifier;
|
||||
}
|
||||
|
||||
declareProperty(lvalue: Place, object: Place, property: string): void {
|
||||
const objectDependency = this.properties.get(object.identifier);
|
||||
let nextDependency: Dependency;
|
||||
if (objectDependency === undefined) {
|
||||
nextDependency = {identifier: object.identifier, path: [property]};
|
||||
} else {
|
||||
nextDependency = {
|
||||
identifier: objectDependency.identifier,
|
||||
path: [...objectDependency.path, property],
|
||||
};
|
||||
}
|
||||
this.properties.set(lvalue.identifier, nextDependency);
|
||||
}
|
||||
|
||||
declareTemporary(lvalue: Place, value: Place): void {
|
||||
const resolved: Dependency = this.properties.get(value.identifier) ?? {
|
||||
identifier: value.identifier,
|
||||
path: [],
|
||||
};
|
||||
this.properties.set(lvalue.identifier, resolved);
|
||||
alias(lvalue: Identifier, value: Identifier): void {
|
||||
this.properties.set(lvalue.id, this.properties.get(value.id) ?? value);
|
||||
}
|
||||
}
|
||||
|
||||
export default function analyseFunctions(func: HIRFunction): void {
|
||||
const state = new IdentifierState();
|
||||
|
||||
for (const [_, block] of func.body.blocks) {
|
||||
for (const instr of block.instructions) {
|
||||
switch (instr.value.kind) {
|
||||
case 'ObjectMethod':
|
||||
case 'FunctionExpression': {
|
||||
lower(instr.value.loweredFunc.func);
|
||||
infer(instr.value.loweredFunc, state, func.context);
|
||||
break;
|
||||
}
|
||||
case 'PropertyLoad': {
|
||||
state.declareProperty(
|
||||
instr.lvalue,
|
||||
instr.value.object,
|
||||
instr.value.property,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'ComputedLoad': {
|
||||
/*
|
||||
* The path is set to an empty string as the path doesn't really
|
||||
* matter for a computed load.
|
||||
infer(instr.value.loweredFunc);
|
||||
|
||||
/**
|
||||
* Reset mutable range for outer inferReferenceEffects
|
||||
*/
|
||||
state.declareProperty(instr.lvalue, instr.value.object, '');
|
||||
break;
|
||||
}
|
||||
case 'LoadLocal':
|
||||
case 'LoadContext': {
|
||||
if (instr.lvalue.identifier.name === null) {
|
||||
state.declareTemporary(instr.lvalue, instr.value.place);
|
||||
for (const operand of instr.value.loweredFunc.func.context) {
|
||||
operand.identifier.mutableRange.start = makeInstructionId(0);
|
||||
operand.identifier.mutableRange.end = makeInstructionId(0);
|
||||
operand.identifier.scope = null;
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -110,7 +70,6 @@ function lower(func: HIRFunction): void {
|
||||
inferMutableRanges(func);
|
||||
rewriteInstructionKindsBasedOnReassignment(func);
|
||||
inferReactiveScopeVariables(func);
|
||||
inferMutableContextVariables(func);
|
||||
func.env.logger?.debugLogIRs?.({
|
||||
kind: 'hir',
|
||||
name: 'AnalyseFunction (inner)',
|
||||
@@ -118,32 +77,16 @@ function lower(func: HIRFunction): void {
|
||||
});
|
||||
}
|
||||
|
||||
function infer(
|
||||
loweredFunc: LoweredFunction,
|
||||
state: IdentifierState,
|
||||
context: Array<Place>,
|
||||
): void {
|
||||
const mutations = new Map<string, Effect>();
|
||||
function infer(loweredFunc: LoweredFunction): void {
|
||||
const knownMutated = inferMutableContextVariables(loweredFunc.func);
|
||||
for (const operand of loweredFunc.func.context) {
|
||||
if (
|
||||
isMutatedOrReassigned(operand.identifier) &&
|
||||
operand.identifier.name !== null
|
||||
) {
|
||||
mutations.set(operand.identifier.name.value, operand.effect);
|
||||
}
|
||||
}
|
||||
|
||||
for (const dep of loweredFunc.dependencies) {
|
||||
let name: IdentifierName | null = null;
|
||||
|
||||
if (state.properties.has(dep.identifier)) {
|
||||
const receiver = state.properties.get(dep.identifier)!;
|
||||
name = receiver.identifier.name;
|
||||
} else {
|
||||
name = dep.identifier.name;
|
||||
}
|
||||
|
||||
if (isRefOrRefValue(dep.identifier)) {
|
||||
const identifier = operand.identifier;
|
||||
CompilerError.invariant(operand.effect === Effect.Unknown, {
|
||||
reason:
|
||||
'[AnalyseFunctions] Expected Function context effects to not have been set',
|
||||
loc: operand.loc,
|
||||
});
|
||||
if (isRefOrRefValue(identifier)) {
|
||||
/*
|
||||
* TODO: this is a hack to ensure we treat functions which reference refs
|
||||
* as having a capture and therefore being considered mutable. this ensures
|
||||
@@ -151,43 +94,16 @@ function infer(
|
||||
* could be called, and allows us to help ensure it isn't called during
|
||||
* render
|
||||
*/
|
||||
dep.effect = Effect.Capture;
|
||||
} else if (name !== null) {
|
||||
const effect = mutations.get(name.value);
|
||||
if (effect !== undefined) {
|
||||
dep.effect = effect === Effect.Unknown ? Effect.Capture : effect;
|
||||
}
|
||||
operand.effect = Effect.Capture;
|
||||
} else if (knownMutated.has(operand)) {
|
||||
operand.effect = Effect.Mutate;
|
||||
} else if (isMutatedOrReassigned(identifier)) {
|
||||
// Note that this also reflects if identifier is ConditionallyMutated
|
||||
operand.effect = Effect.Capture;
|
||||
} else {
|
||||
operand.effect = Effect.Read;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* This could potentially add duplicate deps to mutatedDeps in the case of
|
||||
* mutating a context ref in the child function and in this parent function.
|
||||
* It might be useful to dedupe this.
|
||||
*
|
||||
* In practice this never really matters because the Component function has no
|
||||
* context refs, so it will never have duplicate deps.
|
||||
*/
|
||||
for (const place of context) {
|
||||
CompilerError.invariant(place.identifier.name !== null, {
|
||||
reason: 'context refs should always have a name',
|
||||
description: null,
|
||||
loc: place.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
|
||||
const effect = mutations.get(place.identifier.name.value);
|
||||
if (effect !== undefined) {
|
||||
place.effect = effect === Effect.Unknown ? Effect.Capture : effect;
|
||||
loweredFunc.dependencies.push(place);
|
||||
}
|
||||
}
|
||||
|
||||
for (const operand of loweredFunc.func.context) {
|
||||
operand.identifier.mutableRange.start = makeInstructionId(0);
|
||||
operand.identifier.mutableRange.end = makeInstructionId(0);
|
||||
operand.identifier.scope = null;
|
||||
}
|
||||
}
|
||||
|
||||
function isMutatedOrReassigned(id: Identifier): boolean {
|
||||
|
||||
+7
-16
@@ -55,32 +55,21 @@ import {IdentifierState} from './AnalyseFunctions';
|
||||
* fn();
|
||||
* ```
|
||||
*/
|
||||
export function inferMutableContextVariables(fn: HIRFunction): void {
|
||||
export function inferMutableContextVariables(fn: HIRFunction): Set<Place> {
|
||||
const state = new IdentifierState();
|
||||
const knownMutatedIdentifiers = new Set<Identifier>();
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
for (const instr of block.instructions) {
|
||||
switch (instr.value.kind) {
|
||||
case 'PropertyLoad': {
|
||||
state.declareProperty(
|
||||
instr.lvalue,
|
||||
instr.value.object,
|
||||
instr.value.property,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'PropertyLoad':
|
||||
case 'ComputedLoad': {
|
||||
/*
|
||||
* The path is set to an empty string as the path doesn't really
|
||||
* matter for a computed load.
|
||||
*/
|
||||
state.declareProperty(instr.lvalue, instr.value.object, '');
|
||||
state.alias(instr.lvalue.identifier, instr.value.object.identifier);
|
||||
break;
|
||||
}
|
||||
case 'LoadLocal':
|
||||
case 'LoadContext': {
|
||||
if (instr.lvalue.identifier.name === null) {
|
||||
state.declareTemporary(instr.lvalue, instr.value.place);
|
||||
state.alias(instr.lvalue.identifier, instr.value.place.identifier);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -95,11 +84,13 @@ export function inferMutableContextVariables(fn: HIRFunction): void {
|
||||
visitOperand(state, knownMutatedIdentifiers, operand);
|
||||
}
|
||||
}
|
||||
const results = new Set<Place>();
|
||||
for (const operand of fn.context) {
|
||||
if (knownMutatedIdentifiers.has(operand.identifier)) {
|
||||
operand.effect = Effect.Mutate;
|
||||
results.add(operand);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
function visitOperand(
|
||||
|
||||
+25
-23
@@ -390,29 +390,31 @@ class InferenceState {
|
||||
|
||||
freezeValues(values: Set<InstructionValue>, reason: Set<ValueReason>): void {
|
||||
for (const value of values) {
|
||||
if (value.kind === 'DeclareContext') {
|
||||
/**
|
||||
* Avoid freezing hoisted context declarations
|
||||
* function Component() {
|
||||
* const cb = useBar(() => foo(2)); // produces a hoisted context declaration
|
||||
* const foo = useFoo(); // reassigns to the context variable
|
||||
* return <Foo cb={cb} />;
|
||||
* }
|
||||
*/
|
||||
continue;
|
||||
}
|
||||
this.#values.set(value, {
|
||||
kind: ValueKind.Frozen,
|
||||
reason,
|
||||
context: new Set(),
|
||||
});
|
||||
if (value.kind === 'FunctionExpression') {
|
||||
if (
|
||||
this.#env.config.enablePreserveExistingMemoizationGuarantees ||
|
||||
this.#env.config.enableTransitivelyFreezeFunctionExpressions
|
||||
) {
|
||||
if (value.kind === 'FunctionExpression') {
|
||||
/*
|
||||
* We want to freeze the captured values, not mark the operands
|
||||
* themselves as frozen. There could be mutations that occur
|
||||
* before the freeze we are processing, and it would be invalid
|
||||
* to overwrite those mutations as a freeze.
|
||||
*/
|
||||
for (const operand of eachInstructionValueOperand(value)) {
|
||||
const operandValues = this.#variables.get(operand.identifier.id);
|
||||
if (operandValues !== undefined) {
|
||||
this.freezeValues(operandValues, reason);
|
||||
}
|
||||
}
|
||||
if (
|
||||
value.kind === 'FunctionExpression' &&
|
||||
(this.#env.config.enablePreserveExistingMemoizationGuarantees ||
|
||||
this.#env.config.enableTransitivelyFreezeFunctionExpressions)
|
||||
) {
|
||||
for (const operand of value.loweredFunc.func.context) {
|
||||
const operandValues = this.#variables.get(operand.identifier.id);
|
||||
if (operandValues !== undefined) {
|
||||
this.freezeValues(operandValues, reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1143,17 +1145,17 @@ function inferBlock(
|
||||
case 'ObjectMethod':
|
||||
case 'FunctionExpression': {
|
||||
let hasMutableOperand = false;
|
||||
const mutableOperands: Array<Place> = [];
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
CompilerError.invariant(operand.effect !== Effect.Unknown, {
|
||||
reason: 'Expected fn effects to be populated',
|
||||
loc: operand.loc,
|
||||
});
|
||||
state.referenceAndRecordEffects(
|
||||
freezeActions,
|
||||
operand,
|
||||
operand.effect === Effect.Unknown ? Effect.Read : operand.effect,
|
||||
operand.effect,
|
||||
ValueReason.Other,
|
||||
);
|
||||
if (isMutableEffect(operand.effect, operand.loc)) {
|
||||
mutableOperands.push(operand);
|
||||
}
|
||||
hasMutableOperand ||= isMutableEffect(operand.effect, operand.loc);
|
||||
}
|
||||
/*
|
||||
|
||||
@@ -270,7 +270,6 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
|
||||
name: null,
|
||||
loweredFunc: {
|
||||
func: fn,
|
||||
dependencies: [],
|
||||
},
|
||||
type: 'ArrowFunctionExpression',
|
||||
loc: GeneratedSource,
|
||||
|
||||
@@ -24,7 +24,6 @@ export function outlineFunctions(
|
||||
}
|
||||
if (
|
||||
value.kind === 'FunctionExpression' &&
|
||||
value.loweredFunc.dependencies.length === 0 &&
|
||||
value.loweredFunc.func.context.length === 0 &&
|
||||
// TODO: handle outlining named functions
|
||||
value.loweredFunc.func.id === null &&
|
||||
|
||||
+8
@@ -379,6 +379,14 @@ export function findDisjointMutableValues(
|
||||
*/
|
||||
operand.identifier.mutableRange.start > 0
|
||||
) {
|
||||
if (
|
||||
instr.value.kind === 'FunctionExpression' ||
|
||||
instr.value.kind === 'ObjectMethod'
|
||||
) {
|
||||
if (operand.identifier.type.kind === 'Primitive') {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
operands.push(operand.identifier);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ import {
|
||||
eachTerminalOperand,
|
||||
} from '../HIR/visitors';
|
||||
|
||||
const DEBUG = false;
|
||||
|
||||
/*
|
||||
* Pass to eliminate redundant phi nodes:
|
||||
* - all operands are the same identifier, ie `x2 = phi(x1, x1, x1)`.
|
||||
@@ -141,6 +143,23 @@ export function eliminateRedundantPhi(
|
||||
* have already propagated forwards since we visit in reverse postorder.
|
||||
*/
|
||||
} while (rewrites.size > size && hasBackEdge);
|
||||
|
||||
if (DEBUG) {
|
||||
for (const [, block] of ir.blocks) {
|
||||
for (const phi of block.phis) {
|
||||
CompilerError.invariant(!rewrites.has(phi.place.identifier), {
|
||||
reason: '[EliminateRedundantPhis]: rewrite not complete',
|
||||
loc: phi.place.loc,
|
||||
});
|
||||
for (const [, operand] of phi.operands) {
|
||||
CompilerError.invariant(!rewrites.has(operand.identifier), {
|
||||
reason: '[EliminateRedundantPhis]: rewrite not complete',
|
||||
loc: phi.place.loc,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function rewritePlace(
|
||||
|
||||
@@ -301,9 +301,6 @@ function enterSSAImpl(
|
||||
entry.preds.add(blockId);
|
||||
builder.defineFunction(loweredFunc);
|
||||
builder.enter(() => {
|
||||
loweredFunc.context = loweredFunc.context.map(p =>
|
||||
builder.getPlace(p),
|
||||
);
|
||||
loweredFunc.params = loweredFunc.params.map(param => {
|
||||
if (param.kind === 'Identifier') {
|
||||
return builder.definePlace(param);
|
||||
|
||||
@@ -319,51 +319,6 @@ function visitFunctionExpressionAndPropagateFireDependencies(
|
||||
replaceFireFunctions(fnExpr.loweredFunc.func, context),
|
||||
);
|
||||
|
||||
/*
|
||||
* Make a mapping from each dependency to the corresponding LoadLocal for it so that
|
||||
* we can replace the loaded place with the generated fire function binding
|
||||
*/
|
||||
const loadLocalsToDepLoads = new Map<IdentifierId, LoadLocal>();
|
||||
for (const dep of fnExpr.loweredFunc.dependencies) {
|
||||
const loadLocal = context.getLoadLocalInstr(dep.identifier.id);
|
||||
if (loadLocal != null) {
|
||||
loadLocalsToDepLoads.set(loadLocal.place.identifier.id, loadLocal);
|
||||
}
|
||||
}
|
||||
|
||||
const replacedCallees = new Map<IdentifierId, Place>();
|
||||
for (const [
|
||||
calleeIdentifierId,
|
||||
loadedFireFunctionBindingPlace,
|
||||
] of calleesCapturedByFnExpression.entries()) {
|
||||
/*
|
||||
* Given the ids of captured fire callees, look at the deps for loads of those identifiers
|
||||
* and replace them with the new fire function binding
|
||||
*/
|
||||
const loadLocal = loadLocalsToDepLoads.get(calleeIdentifierId);
|
||||
if (loadLocal == null) {
|
||||
context.pushError({
|
||||
loc: fnExpr.loc,
|
||||
description: null,
|
||||
severity: ErrorSeverity.Invariant,
|
||||
reason:
|
||||
'[InsertFire] No loadLocal found for fire call argument for lambda',
|
||||
suggestions: null,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const oldPlaceId = loadLocal.place.identifier.id;
|
||||
loadLocal.place = {
|
||||
...loadedFireFunctionBindingPlace.fireFunctionBinding,
|
||||
};
|
||||
|
||||
replacedCallees.set(
|
||||
oldPlaceId,
|
||||
loadedFireFunctionBindingPlace.fireFunctionBinding,
|
||||
);
|
||||
}
|
||||
|
||||
// For each replaced callee, update the context of the function expression to track it
|
||||
for (
|
||||
let contextIdx = 0;
|
||||
@@ -371,9 +326,13 @@ function visitFunctionExpressionAndPropagateFireDependencies(
|
||||
contextIdx++
|
||||
) {
|
||||
const contextItem = fnExpr.loweredFunc.func.context[contextIdx];
|
||||
const replacedCallee = replacedCallees.get(contextItem.identifier.id);
|
||||
const replacedCallee = calleesCapturedByFnExpression.get(
|
||||
contextItem.identifier.id,
|
||||
);
|
||||
if (replacedCallee != null) {
|
||||
fnExpr.loweredFunc.func.context[contextIdx] = replacedCallee;
|
||||
fnExpr.loweredFunc.func.context[contextIdx] = {
|
||||
...replacedCallee.fireFunctionBinding,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableTransitivelyFreezeFunctionExpressions:false
|
||||
import {
|
||||
Stringify,
|
||||
mutate,
|
||||
identity,
|
||||
setPropertyByKey,
|
||||
shallowCopy,
|
||||
} from 'shared-runtime';
|
||||
/**
|
||||
* Function expression version of `aliased-nested-scope-truncated-dep`.
|
||||
*
|
||||
* In this fixture, the output would be invalid if propagateScopeDeps did not
|
||||
* avoid adding MemberExpression dependencies which would other evaluate during
|
||||
* the mutable ranges of their base objects.
|
||||
* This is different from `aliased-nested-scope-truncated-dep` which *does*
|
||||
* produce correct output regardless of MemberExpression dependency truncation.
|
||||
*
|
||||
* Note while other expressions evaluate inline, function expressions *always*
|
||||
* represent deferred evaluation. This means that
|
||||
* (1) it's always safe to reorder function expression creation until its
|
||||
* earliest potential invocation
|
||||
* (2) it's invalid to eagerly evaluate function expression dependencies during
|
||||
* their respective mutable ranges.
|
||||
*/
|
||||
|
||||
function Component({prop}) {
|
||||
let obj = shallowCopy(prop);
|
||||
|
||||
const aliasedObj = identity(obj);
|
||||
|
||||
// When `obj` is mutable (either directly or through aliases), taking a
|
||||
// dependency on `obj.id` is invalid as it may change before getId() is invoked
|
||||
const getId = () => obj.id;
|
||||
|
||||
mutate(aliasedObj);
|
||||
setPropertyByKey(aliasedObj, 'id', prop.id + 1);
|
||||
|
||||
// Calling getId() should return prop.id + 1, not the prev
|
||||
return <Stringify getId={getId} shouldInvokeFns={true} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{prop: {id: 1}}],
|
||||
sequentialRenders: [{prop: {id: 1}}, {prop: {id: 1}}, {prop: {id: 2}}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableTransitivelyFreezeFunctionExpressions:false
|
||||
import {
|
||||
Stringify,
|
||||
mutate,
|
||||
identity,
|
||||
setPropertyByKey,
|
||||
shallowCopy,
|
||||
} from "shared-runtime";
|
||||
/**
|
||||
* Function expression version of `aliased-nested-scope-truncated-dep`.
|
||||
*
|
||||
* In this fixture, the output would be invalid if propagateScopeDeps did not
|
||||
* avoid adding MemberExpression dependencies which would other evaluate during
|
||||
* the mutable ranges of their base objects.
|
||||
* This is different from `aliased-nested-scope-truncated-dep` which *does*
|
||||
* produce correct output regardless of MemberExpression dependency truncation.
|
||||
*
|
||||
* Note while other expressions evaluate inline, function expressions *always*
|
||||
* represent deferred evaluation. This means that
|
||||
* (1) it's always safe to reorder function expression creation until its
|
||||
* earliest potential invocation
|
||||
* (2) it's invalid to eagerly evaluate function expression dependencies during
|
||||
* their respective mutable ranges.
|
||||
*/
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(2);
|
||||
const { prop } = t0;
|
||||
let t1;
|
||||
if ($[0] !== prop) {
|
||||
const obj = shallowCopy(prop);
|
||||
|
||||
const aliasedObj = identity(obj);
|
||||
|
||||
const getId = () => obj.id;
|
||||
|
||||
mutate(aliasedObj);
|
||||
setPropertyByKey(aliasedObj, "id", prop.id + 1);
|
||||
|
||||
t1 = <Stringify getId={getId} shouldInvokeFns={true} />;
|
||||
$[0] = prop;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ prop: { id: 1 } }],
|
||||
sequentialRenders: [
|
||||
{ prop: { id: 1 } },
|
||||
{ prop: { id: 1 } },
|
||||
{ prop: { id: 2 } },
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"getId":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
|
||||
<div>{"getId":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
|
||||
<div>{"getId":{"kind":"Function","result":3},"shouldInvokeFns":true}</div>
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
// @enableTransitivelyFreezeFunctionExpressions:false
|
||||
import {
|
||||
Stringify,
|
||||
mutate,
|
||||
identity,
|
||||
setPropertyByKey,
|
||||
shallowCopy,
|
||||
} from 'shared-runtime';
|
||||
/**
|
||||
* Function expression version of `aliased-nested-scope-truncated-dep`.
|
||||
*
|
||||
* In this fixture, the output would be invalid if propagateScopeDeps did not
|
||||
* avoid adding MemberExpression dependencies which would other evaluate during
|
||||
* the mutable ranges of their base objects.
|
||||
* This is different from `aliased-nested-scope-truncated-dep` which *does*
|
||||
* produce correct output regardless of MemberExpression dependency truncation.
|
||||
*
|
||||
* Note while other expressions evaluate inline, function expressions *always*
|
||||
* represent deferred evaluation. This means that
|
||||
* (1) it's always safe to reorder function expression creation until its
|
||||
* earliest potential invocation
|
||||
* (2) it's invalid to eagerly evaluate function expression dependencies during
|
||||
* their respective mutable ranges.
|
||||
*/
|
||||
|
||||
function Component({prop}) {
|
||||
let obj = shallowCopy(prop);
|
||||
|
||||
const aliasedObj = identity(obj);
|
||||
|
||||
// When `obj` is mutable (either directly or through aliases), taking a
|
||||
// dependency on `obj.id` is invalid as it may change before getId() is invoked
|
||||
const getId = () => obj.id;
|
||||
|
||||
mutate(aliasedObj);
|
||||
setPropertyByKey(aliasedObj, 'id', prop.id + 1);
|
||||
|
||||
// Calling getId() should return prop.id + 1, not the prev
|
||||
return <Stringify getId={getId} shouldInvokeFns={true} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{prop: {id: 1}}],
|
||||
sequentialRenders: [{prop: {id: 1}}, {prop: {id: 1}}, {prop: {id: 2}}],
|
||||
};
|
||||
+221
@@ -0,0 +1,221 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {
|
||||
Stringify,
|
||||
mutate,
|
||||
identity,
|
||||
shallowCopy,
|
||||
setPropertyByKey,
|
||||
} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* This fixture is similar to `bug-aliased-capture-aliased-mutate` and
|
||||
* `nonmutating-capture-in-unsplittable-memo-block`, but with a focus on
|
||||
* dependency extraction.
|
||||
*
|
||||
* NOTE: this fixture is currently valid, but will break with optimizations:
|
||||
* - Scope and mutable-range based reordering may move the array creation
|
||||
* *after* the `mutate(aliasedObj)` call. This is invalid if mutate
|
||||
* reassigns inner properties.
|
||||
* - RecycleInto or other deeper-equality optimizations may produce invalid
|
||||
* output -- it may compare the array's contents / dependencies too early.
|
||||
* - Runtime validation for immutable values will break if `mutate` does
|
||||
* interior mutation of the value captured into the array.
|
||||
*
|
||||
* Before scope block creation, HIR looks like this:
|
||||
* //
|
||||
* // $1 is unscoped as obj's mutable range will be
|
||||
* // extended in a later pass
|
||||
* //
|
||||
* $1 = LoadLocal obj@0[0:12]
|
||||
* $2 = PropertyLoad $1.id
|
||||
* //
|
||||
* // $3 gets assigned a scope as Array is an allocating
|
||||
* // instruction, but this does *not* get extended or
|
||||
* // merged into the later mutation site.
|
||||
* // (explained in `bug-aliased-capture-aliased-mutate`)
|
||||
* //
|
||||
* $3@1 = Array[$2]
|
||||
* ...
|
||||
* $10@0 = LoadLocal shallowCopy@0[0, 12]
|
||||
* $11 = LoadGlobal mutate
|
||||
* $12 = $11($10@0[0, 12])
|
||||
*
|
||||
* When filling in scope dependencies, we find that it's incorrect to depend on
|
||||
* PropertyLoads from obj as it hasn't completed its mutable range. Following
|
||||
* the immutable / mutable-new typing system, we check the identity of obj to
|
||||
* detect whether it was newly created (and thus mutable) in this render pass.
|
||||
*
|
||||
* HIR with scopes looks like this.
|
||||
* bb0:
|
||||
* $1 = LoadLocal obj@0[0:12]
|
||||
* $2 = PropertyLoad $1.id
|
||||
* scopeTerminal deps=[obj@0] block=bb1 fallt=bb2
|
||||
* bb1:
|
||||
* $3@1 = Array[$2]
|
||||
* goto bb2
|
||||
* bb2:
|
||||
* ...
|
||||
*
|
||||
* This is surprising as deps now is entirely decoupled from temporaries used
|
||||
* by the block itself. scope @1's instructions now reference a value (1)
|
||||
* produced outside its scope range and (2) not represented in its dependencies
|
||||
*
|
||||
* The right thing to do is to ensure that all Loads from a value get assigned
|
||||
* the value's reactive scope. This also requires track mutating and aliasing
|
||||
* separately from scope range. In this example, that would correctly merge
|
||||
* the scopes of $3 with obj.
|
||||
* Runtime validation and optimizations such as ReactiveGraph-based reordering
|
||||
* require this as well.
|
||||
*
|
||||
* A tempting fix is to instead extend $3's ReactiveScope range up to include
|
||||
* $2 (the PropertyLoad). This fixes dependency deduping but not reordering
|
||||
* and mutability.
|
||||
*/
|
||||
function Component({prop}) {
|
||||
let obj = shallowCopy(prop);
|
||||
const aliasedObj = identity(obj);
|
||||
|
||||
// [obj.id] currently is assigned its own reactive scope
|
||||
const id = [obj.id];
|
||||
|
||||
// Writing to the alias may reassign to previously captured references.
|
||||
// The compiler currently produces valid output, but this breaks with
|
||||
// reordering, recycleInto, and other potential optimizations.
|
||||
mutate(aliasedObj);
|
||||
setPropertyByKey(aliasedObj, 'id', prop.id + 1);
|
||||
|
||||
return <Stringify id={id} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{prop: {id: 1}}],
|
||||
sequentialRenders: [{prop: {id: 1}}, {prop: {id: 1}}, {prop: {id: 2}}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import {
|
||||
Stringify,
|
||||
mutate,
|
||||
identity,
|
||||
shallowCopy,
|
||||
setPropertyByKey,
|
||||
} from "shared-runtime";
|
||||
|
||||
/**
|
||||
* This fixture is similar to `bug-aliased-capture-aliased-mutate` and
|
||||
* `nonmutating-capture-in-unsplittable-memo-block`, but with a focus on
|
||||
* dependency extraction.
|
||||
*
|
||||
* NOTE: this fixture is currently valid, but will break with optimizations:
|
||||
* - Scope and mutable-range based reordering may move the array creation
|
||||
* *after* the `mutate(aliasedObj)` call. This is invalid if mutate
|
||||
* reassigns inner properties.
|
||||
* - RecycleInto or other deeper-equality optimizations may produce invalid
|
||||
* output -- it may compare the array's contents / dependencies too early.
|
||||
* - Runtime validation for immutable values will break if `mutate` does
|
||||
* interior mutation of the value captured into the array.
|
||||
*
|
||||
* Before scope block creation, HIR looks like this:
|
||||
* //
|
||||
* // $1 is unscoped as obj's mutable range will be
|
||||
* // extended in a later pass
|
||||
* //
|
||||
* $1 = LoadLocal obj@0[0:12]
|
||||
* $2 = PropertyLoad $1.id
|
||||
* //
|
||||
* // $3 gets assigned a scope as Array is an allocating
|
||||
* // instruction, but this does *not* get extended or
|
||||
* // merged into the later mutation site.
|
||||
* // (explained in `bug-aliased-capture-aliased-mutate`)
|
||||
* //
|
||||
* $3@1 = Array[$2]
|
||||
* ...
|
||||
* $10@0 = LoadLocal shallowCopy@0[0, 12]
|
||||
* $11 = LoadGlobal mutate
|
||||
* $12 = $11($10@0[0, 12])
|
||||
*
|
||||
* When filling in scope dependencies, we find that it's incorrect to depend on
|
||||
* PropertyLoads from obj as it hasn't completed its mutable range. Following
|
||||
* the immutable / mutable-new typing system, we check the identity of obj to
|
||||
* detect whether it was newly created (and thus mutable) in this render pass.
|
||||
*
|
||||
* HIR with scopes looks like this.
|
||||
* bb0:
|
||||
* $1 = LoadLocal obj@0[0:12]
|
||||
* $2 = PropertyLoad $1.id
|
||||
* scopeTerminal deps=[obj@0] block=bb1 fallt=bb2
|
||||
* bb1:
|
||||
* $3@1 = Array[$2]
|
||||
* goto bb2
|
||||
* bb2:
|
||||
* ...
|
||||
*
|
||||
* This is surprising as deps now is entirely decoupled from temporaries used
|
||||
* by the block itself. scope @1's instructions now reference a value (1)
|
||||
* produced outside its scope range and (2) not represented in its dependencies
|
||||
*
|
||||
* The right thing to do is to ensure that all Loads from a value get assigned
|
||||
* the value's reactive scope. This also requires track mutating and aliasing
|
||||
* separately from scope range. In this example, that would correctly merge
|
||||
* the scopes of $3 with obj.
|
||||
* Runtime validation and optimizations such as ReactiveGraph-based reordering
|
||||
* require this as well.
|
||||
*
|
||||
* A tempting fix is to instead extend $3's ReactiveScope range up to include
|
||||
* $2 (the PropertyLoad). This fixes dependency deduping but not reordering
|
||||
* and mutability.
|
||||
*/
|
||||
function Component(t0) {
|
||||
const $ = _c(4);
|
||||
const { prop } = t0;
|
||||
let t1;
|
||||
if ($[0] !== prop) {
|
||||
const obj = shallowCopy(prop);
|
||||
const aliasedObj = identity(obj);
|
||||
let t2;
|
||||
if ($[2] !== obj) {
|
||||
t2 = [obj.id];
|
||||
$[2] = obj;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
const id = t2;
|
||||
|
||||
mutate(aliasedObj);
|
||||
setPropertyByKey(aliasedObj, "id", prop.id + 1);
|
||||
|
||||
t1 = <Stringify id={id} />;
|
||||
$[0] = prop;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ prop: { id: 1 } }],
|
||||
sequentialRenders: [
|
||||
{ prop: { id: 1 } },
|
||||
{ prop: { id: 1 } },
|
||||
{ prop: { id: 2 } },
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"id":[1]}</div>
|
||||
<div>{"id":[1]}</div>
|
||||
<div>{"id":[2]}</div>
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
import {
|
||||
Stringify,
|
||||
mutate,
|
||||
identity,
|
||||
shallowCopy,
|
||||
setPropertyByKey,
|
||||
} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* This fixture is similar to `bug-aliased-capture-aliased-mutate` and
|
||||
* `nonmutating-capture-in-unsplittable-memo-block`, but with a focus on
|
||||
* dependency extraction.
|
||||
*
|
||||
* NOTE: this fixture is currently valid, but will break with optimizations:
|
||||
* - Scope and mutable-range based reordering may move the array creation
|
||||
* *after* the `mutate(aliasedObj)` call. This is invalid if mutate
|
||||
* reassigns inner properties.
|
||||
* - RecycleInto or other deeper-equality optimizations may produce invalid
|
||||
* output -- it may compare the array's contents / dependencies too early.
|
||||
* - Runtime validation for immutable values will break if `mutate` does
|
||||
* interior mutation of the value captured into the array.
|
||||
*
|
||||
* Before scope block creation, HIR looks like this:
|
||||
* //
|
||||
* // $1 is unscoped as obj's mutable range will be
|
||||
* // extended in a later pass
|
||||
* //
|
||||
* $1 = LoadLocal obj@0[0:12]
|
||||
* $2 = PropertyLoad $1.id
|
||||
* //
|
||||
* // $3 gets assigned a scope as Array is an allocating
|
||||
* // instruction, but this does *not* get extended or
|
||||
* // merged into the later mutation site.
|
||||
* // (explained in `bug-aliased-capture-aliased-mutate`)
|
||||
* //
|
||||
* $3@1 = Array[$2]
|
||||
* ...
|
||||
* $10@0 = LoadLocal shallowCopy@0[0, 12]
|
||||
* $11 = LoadGlobal mutate
|
||||
* $12 = $11($10@0[0, 12])
|
||||
*
|
||||
* When filling in scope dependencies, we find that it's incorrect to depend on
|
||||
* PropertyLoads from obj as it hasn't completed its mutable range. Following
|
||||
* the immutable / mutable-new typing system, we check the identity of obj to
|
||||
* detect whether it was newly created (and thus mutable) in this render pass.
|
||||
*
|
||||
* HIR with scopes looks like this.
|
||||
* bb0:
|
||||
* $1 = LoadLocal obj@0[0:12]
|
||||
* $2 = PropertyLoad $1.id
|
||||
* scopeTerminal deps=[obj@0] block=bb1 fallt=bb2
|
||||
* bb1:
|
||||
* $3@1 = Array[$2]
|
||||
* goto bb2
|
||||
* bb2:
|
||||
* ...
|
||||
*
|
||||
* This is surprising as deps now is entirely decoupled from temporaries used
|
||||
* by the block itself. scope @1's instructions now reference a value (1)
|
||||
* produced outside its scope range and (2) not represented in its dependencies
|
||||
*
|
||||
* The right thing to do is to ensure that all Loads from a value get assigned
|
||||
* the value's reactive scope. This also requires track mutating and aliasing
|
||||
* separately from scope range. In this example, that would correctly merge
|
||||
* the scopes of $3 with obj.
|
||||
* Runtime validation and optimizations such as ReactiveGraph-based reordering
|
||||
* require this as well.
|
||||
*
|
||||
* A tempting fix is to instead extend $3's ReactiveScope range up to include
|
||||
* $2 (the PropertyLoad). This fixes dependency deduping but not reordering
|
||||
* and mutability.
|
||||
*/
|
||||
function Component({prop}) {
|
||||
let obj = shallowCopy(prop);
|
||||
const aliasedObj = identity(obj);
|
||||
|
||||
// [obj.id] currently is assigned its own reactive scope
|
||||
const id = [obj.id];
|
||||
|
||||
// Writing to the alias may reassign to previously captured references.
|
||||
// The compiler currently produces valid output, but this breaks with
|
||||
// reordering, recycleInto, and other potential optimizations.
|
||||
mutate(aliasedObj);
|
||||
setPropertyByKey(aliasedObj, 'id', prop.id + 1);
|
||||
|
||||
return <Stringify id={id} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{prop: {id: 1}}],
|
||||
sequentialRenders: [{prop: {id: 1}}, {prop: {id: 1}}, {prop: {id: 2}}],
|
||||
};
|
||||
+18
-22
@@ -44,48 +44,44 @@ import { c as _c } from "react/compiler-runtime"; // @validateRefAccessDuringRen
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
function Component() {
|
||||
const $ = _c(6);
|
||||
const $ = _c(5);
|
||||
const ref = useRef(null);
|
||||
const [state, setState] = useState(false);
|
||||
let t0;
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = () => {};
|
||||
|
||||
t1 = [];
|
||||
t0 = [];
|
||||
$[0] = t0;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
t1 = $[1];
|
||||
}
|
||||
useEffect(t0, t1);
|
||||
useEffect(_temp, t0);
|
||||
let t1;
|
||||
let t2;
|
||||
let t3;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = () => {
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = () => {
|
||||
setState(true);
|
||||
};
|
||||
t3 = [];
|
||||
t2 = [];
|
||||
$[1] = t1;
|
||||
$[2] = t2;
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
t2 = $[2];
|
||||
t3 = $[3];
|
||||
}
|
||||
useEffect(t2, t3);
|
||||
useEffect(t1, t2);
|
||||
|
||||
const t4 = String(state);
|
||||
let t5;
|
||||
if ($[4] !== t4) {
|
||||
t5 = <Child key={t4} ref={ref} />;
|
||||
const t3 = String(state);
|
||||
let t4;
|
||||
if ($[3] !== t3) {
|
||||
t4 = <Child key={t3} ref={ref} />;
|
||||
$[3] = t3;
|
||||
$[4] = t4;
|
||||
$[5] = t5;
|
||||
} else {
|
||||
t5 = $[5];
|
||||
t4 = $[4];
|
||||
}
|
||||
return t5;
|
||||
return t4;
|
||||
}
|
||||
function _temp() {}
|
||||
|
||||
function Child(t0) {
|
||||
const { ref } = t0;
|
||||
|
||||
-1
@@ -27,7 +27,6 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
function component(a, b) {
|
||||
const $ = _c(2);
|
||||
const y = { b };
|
||||
let z;
|
||||
if ($[0] !== a) {
|
||||
z = { a };
|
||||
|
||||
+10
-2
@@ -31,12 +31,20 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
function Component(t0) {
|
||||
const $ = _c(3);
|
||||
const $ = _c(5);
|
||||
const { a, b } = t0;
|
||||
let z;
|
||||
if ($[0] !== a || $[1] !== b) {
|
||||
z = { a };
|
||||
const y = { b };
|
||||
let t1;
|
||||
if ($[3] !== b) {
|
||||
t1 = { b };
|
||||
$[3] = b;
|
||||
$[4] = t1;
|
||||
} else {
|
||||
t1 = $[4];
|
||||
}
|
||||
const y = t1;
|
||||
const x = function () {
|
||||
z.a = 2;
|
||||
return Math.max(y.b, 0);
|
||||
|
||||
-1
@@ -34,7 +34,6 @@ function component(a) {
|
||||
const x = { a };
|
||||
y = {};
|
||||
|
||||
y;
|
||||
y = x;
|
||||
|
||||
mutate(y);
|
||||
|
||||
-1
@@ -31,7 +31,6 @@ function bar(a) {
|
||||
const x = [a];
|
||||
y = {};
|
||||
|
||||
y;
|
||||
y = x[0][1];
|
||||
$[0] = a;
|
||||
$[1] = y;
|
||||
|
||||
-2
@@ -37,8 +37,6 @@ function bar(a, b) {
|
||||
let t;
|
||||
t = {};
|
||||
|
||||
y;
|
||||
t;
|
||||
y = x[0][1];
|
||||
t = x[1][0];
|
||||
$[0] = a;
|
||||
|
||||
-1
@@ -31,7 +31,6 @@ function bar(a) {
|
||||
const x = [a];
|
||||
y = {};
|
||||
|
||||
y;
|
||||
y = x[0].a[1];
|
||||
$[0] = a;
|
||||
$[1] = y;
|
||||
|
||||
-1
@@ -30,7 +30,6 @@ function bar(a) {
|
||||
const x = [a];
|
||||
y = {};
|
||||
|
||||
y;
|
||||
y = x[0];
|
||||
$[0] = a;
|
||||
$[1] = y;
|
||||
|
||||
-1
@@ -25,7 +25,6 @@ function component(a) {
|
||||
const x = { a };
|
||||
y = 1;
|
||||
|
||||
y;
|
||||
y = x;
|
||||
|
||||
mutate(y);
|
||||
|
||||
+1
-2
@@ -38,9 +38,8 @@ function useTest() {
|
||||
|
||||
const t1 = (w = 42);
|
||||
const t2 = w;
|
||||
|
||||
w;
|
||||
let t3;
|
||||
|
||||
w = 999;
|
||||
t3 = 2;
|
||||
t0 = makeArray(t1, t2, t3);
|
||||
|
||||
+5
-2
@@ -19,10 +19,10 @@ function foo() {
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
function foo() {
|
||||
const $ = _c(1);
|
||||
|
||||
const getJSX = _temp;
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
const getJSX = () => <Child x={GLOBAL_IS_X} />;
|
||||
|
||||
t0 = getJSX();
|
||||
$[0] = t0;
|
||||
} else {
|
||||
@@ -31,6 +31,9 @@ function foo() {
|
||||
const result = t0;
|
||||
return result;
|
||||
}
|
||||
function _temp() {
|
||||
return <Child x={GLOBAL_IS_X} />;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
+4
-3
@@ -23,13 +23,14 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
```javascript
|
||||
function foo() {
|
||||
const f = () => {
|
||||
console.log(42);
|
||||
};
|
||||
const f = _temp;
|
||||
|
||||
f();
|
||||
return 42;
|
||||
}
|
||||
function _temp() {
|
||||
console.log(42);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: foo,
|
||||
|
||||
+5
-4
@@ -18,12 +18,10 @@ function Component(props) {
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
function Component(props) {
|
||||
const $ = _c(1);
|
||||
|
||||
const onEvent = _temp;
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
const onEvent = () => {
|
||||
console.log(42);
|
||||
};
|
||||
|
||||
t0 = <Foo onEvent={onEvent} />;
|
||||
$[0] = t0;
|
||||
} else {
|
||||
@@ -31,6 +29,9 @@ function Component(props) {
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
function _temp() {
|
||||
console.log(42);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
+1
-2
@@ -34,9 +34,8 @@ function Component(props) {
|
||||
let Component;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
Component = Stringify;
|
||||
|
||||
Component;
|
||||
let t0;
|
||||
|
||||
t0 = Component;
|
||||
Component = t0;
|
||||
$[0] = Component;
|
||||
|
||||
+7
-3
@@ -24,13 +24,17 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Error
|
||||
|
||||
```
|
||||
4 | }
|
||||
5 | return baz(); // OK: FuncDecls are HoistableDeclarations that have both declaration and value hoisting
|
||||
6 | function baz() {
|
||||
> 6 | function baz() {
|
||||
| ^^^^^^^^^^^^^^^^
|
||||
> 7 | return bar();
|
||||
| ^^^ Todo: Support functions with unreachable code that may contain hoisted declarations (7:7)
|
||||
8 | }
|
||||
| ^^^^^^^^^^^^^^^^^
|
||||
> 8 | }
|
||||
| ^^^^ Todo: Support functions with unreachable code that may contain hoisted declarations (6:8)
|
||||
9 | }
|
||||
10 |
|
||||
11 | export const FIXTURE_ENTRYPOINT = {
|
||||
```
|
||||
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableTransitivelyFreezeFunctionExpressions
|
||||
import {setPropertyByKey, Stringify, useIdentity} from 'shared-runtime';
|
||||
|
||||
function Foo({count}) {
|
||||
const x = {value: 0};
|
||||
/**
|
||||
* After this custom hook call, it's no longer valid to mutate x.
|
||||
*/
|
||||
const cb = useIdentity(() => {
|
||||
setPropertyByKey(x, 'value', count);
|
||||
});
|
||||
|
||||
x.value += count;
|
||||
return <Stringify x={x} cb={cb} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{count: 1}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
11 | });
|
||||
12 |
|
||||
> 13 | x.value += count;
|
||||
| ^ InvalidReact: This mutates a variable that React considers immutable (13:13)
|
||||
14 | return <Stringify x={x} cb={cb} />;
|
||||
15 | }
|
||||
16 |
|
||||
```
|
||||
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
// @enableTransitivelyFreezeFunctionExpressions
|
||||
import {setPropertyByKey, Stringify, useIdentity} from 'shared-runtime';
|
||||
|
||||
function Foo({count}) {
|
||||
const x = {value: 0};
|
||||
/**
|
||||
* After this custom hook call, it's no longer valid to mutate x.
|
||||
*/
|
||||
const cb = useIdentity(() => {
|
||||
setPropertyByKey(x, 'value', count);
|
||||
});
|
||||
|
||||
x.value += count;
|
||||
return <Stringify x={x} cb={cb} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{count: 1}],
|
||||
};
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableTransitivelyFreezeFunctionExpressions
|
||||
import {mutate, Stringify, useIdentity} from 'shared-runtime';
|
||||
|
||||
function Foo({count}) {
|
||||
const x = {value: 0};
|
||||
/**
|
||||
* After this custom hook call, it's no longer valid to mutate x.
|
||||
*/
|
||||
const cb = useIdentity(() => {
|
||||
x.value++;
|
||||
});
|
||||
|
||||
x.value += count;
|
||||
return <Stringify x={x} cb={cb} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{count: 1}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
11 | });
|
||||
12 |
|
||||
> 13 | x.value += count;
|
||||
| ^ InvalidReact: This mutates a variable that React considers immutable (13:13)
|
||||
14 | return <Stringify x={x} cb={cb} />;
|
||||
15 | }
|
||||
16 |
|
||||
```
|
||||
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
// @enableTransitivelyFreezeFunctionExpressions
|
||||
import {mutate, Stringify, useIdentity} from 'shared-runtime';
|
||||
|
||||
function Foo({count}) {
|
||||
const x = {value: 0};
|
||||
/**
|
||||
* After this custom hook call, it's no longer valid to mutate x.
|
||||
*/
|
||||
const cb = useIdentity(() => {
|
||||
x.value++;
|
||||
});
|
||||
|
||||
x.value += count;
|
||||
return <Stringify x={x} cb={cb} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{count: 1}],
|
||||
};
|
||||
+1
-1
@@ -22,7 +22,7 @@ function Component(props) {
|
||||
7 | return hasErrors;
|
||||
8 | }
|
||||
> 9 | return hasErrors();
|
||||
| ^^^^^^^^^ Invariant: [hoisting] Expected value for identifier to be initialized. hasErrors_0$16 (9:9)
|
||||
| ^^^^^^^^^ Invariant: [hoisting] Expected value for identifier to be initialized. hasErrors_0$14 (9:9)
|
||||
10 | }
|
||||
11 |
|
||||
```
|
||||
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validatePreserveExistingMemoizationGuarantees
|
||||
function useFoo() {
|
||||
const onClick = response => {
|
||||
setState(DISABLED_FORM);
|
||||
};
|
||||
|
||||
const [state, setState] = useState();
|
||||
const handleLogout = useCallback(() => {
|
||||
setState(DISABLED_FORM);
|
||||
}, [setState]);
|
||||
const getComponent = () => {
|
||||
return <ColumnItem onPress={() => handleLogout()} />;
|
||||
};
|
||||
|
||||
// this `getComponent` call should not be inferred as mutating setState
|
||||
return [getComponent(), onClick]; // pass onClick to avoid dce
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees
|
||||
function useFoo() {
|
||||
const $ = _c(9);
|
||||
const onClick = (response) => {
|
||||
setState(DISABLED_FORM);
|
||||
};
|
||||
|
||||
const [, t0] = useState();
|
||||
const setState = t0;
|
||||
let t1;
|
||||
if ($[0] !== setState) {
|
||||
t1 = () => {
|
||||
setState(DISABLED_FORM);
|
||||
};
|
||||
$[0] = setState;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
setState;
|
||||
const handleLogout = t1;
|
||||
let t2;
|
||||
if ($[2] !== handleLogout) {
|
||||
t2 = () => <ColumnItem onPress={() => handleLogout()} />;
|
||||
$[2] = handleLogout;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
const getComponent = t2;
|
||||
let t3;
|
||||
if ($[4] !== getComponent) {
|
||||
t3 = getComponent();
|
||||
$[4] = getComponent;
|
||||
$[5] = t3;
|
||||
} else {
|
||||
t3 = $[5];
|
||||
}
|
||||
let t4;
|
||||
if ($[6] !== onClick || $[7] !== t3) {
|
||||
t4 = [t3, onClick];
|
||||
$[6] = onClick;
|
||||
$[7] = t3;
|
||||
$[8] = t4;
|
||||
} else {
|
||||
t4 = $[8];
|
||||
}
|
||||
return t4;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
// @validatePreserveExistingMemoizationGuarantees
|
||||
function useFoo() {
|
||||
const onClick = response => {
|
||||
setState(DISABLED_FORM);
|
||||
};
|
||||
|
||||
const [state, setState] = useState();
|
||||
const handleLogout = useCallback(() => {
|
||||
setState(DISABLED_FORM);
|
||||
}, [setState]);
|
||||
const getComponent = () => {
|
||||
return <ColumnItem onPress={() => handleLogout()} />;
|
||||
};
|
||||
|
||||
// this `getComponent` call should not be inferred as mutating setState
|
||||
return [getComponent(), onClick]; // pass onClick to avoid dce
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {useEffect, useState} from 'react';
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
function Foo() {
|
||||
/**
|
||||
* Previously, this lowered to
|
||||
* $1 = LoadContext capture setState
|
||||
* $2 = FunctionExpression deps=$1 context=setState
|
||||
* [[ at this point, we freeze the `LoadContext setState` instruction, but it will never be referenced again ]]
|
||||
*
|
||||
* Now, this function expression directly references `setState`, which freezes
|
||||
* the source `DeclareContext HoistedConst setState`. Freezing source identifiers
|
||||
* (instead of the one level removed `LoadContext`) is more semantically correct
|
||||
* for everything *other* than hoisted context declarations.
|
||||
*
|
||||
* $2 = Function context=setState
|
||||
*/
|
||||
useEffect(() => setState(2), []);
|
||||
|
||||
const [state, setState] = useState(0);
|
||||
return <Stringify state={state} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{}],
|
||||
sequentialRenders: [{}, {}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Stringify } from "shared-runtime";
|
||||
|
||||
function Foo() {
|
||||
const $ = _c(3);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = [];
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
useEffect(() => setState(2), t0);
|
||||
|
||||
const [state, t1] = useState(0);
|
||||
const setState = t1;
|
||||
let t2;
|
||||
if ($[1] !== state) {
|
||||
t2 = <Stringify state={state} />;
|
||||
$[1] = state;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{}],
|
||||
sequentialRenders: [{}, {}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"state":2}</div>
|
||||
<div>{"state":2}</div>
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import {useEffect, useState} from 'react';
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
function Foo() {
|
||||
/**
|
||||
* Previously, this lowered to
|
||||
* $1 = LoadContext capture setState
|
||||
* $2 = FunctionExpression deps=$1 context=setState
|
||||
* [[ at this point, we freeze the `LoadContext setState` instruction, but it will never be referenced again ]]
|
||||
*
|
||||
* Now, this function expression directly references `setState`, which freezes
|
||||
* the source `DeclareContext HoistedConst setState`. Freezing source identifiers
|
||||
* (instead of the one level removed `LoadContext`) is more semantically correct
|
||||
* for everything *other* than hoisted context declarations.
|
||||
*
|
||||
* $2 = Function context=setState
|
||||
*/
|
||||
useEffect(() => setState(2), []);
|
||||
|
||||
const [state, setState] = useState(0);
|
||||
return <Stringify state={state} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{}],
|
||||
sequentialRenders: [{}, {}],
|
||||
};
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {useIdentity, Stringify, identity} from 'shared-runtime';
|
||||
|
||||
function Foo({val1}) {
|
||||
// `x={inner: val1}` should be able to be memoized
|
||||
const x = {inner: val1};
|
||||
|
||||
// Any references to `x` after this hook call should be read-only
|
||||
const cb = useIdentity(() => x.inner);
|
||||
|
||||
// With enableTransitivelyFreezeFunctionExpressions, it's invalid
|
||||
// to write to `x` after it's been frozen.
|
||||
// TODO: runtime validation for DX
|
||||
const copy = identity(x);
|
||||
return <Stringify copy={copy} cb={cb} shouldInvokeFns={true} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{val1: 1}],
|
||||
sequentialRenders: [{val1: 1}, {val1: 1}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { useIdentity, Stringify, identity } from "shared-runtime";
|
||||
|
||||
function Foo(t0) {
|
||||
const $ = _c(9);
|
||||
const { val1 } = t0;
|
||||
let t1;
|
||||
if ($[0] !== val1) {
|
||||
t1 = { inner: val1 };
|
||||
$[0] = val1;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const x = t1;
|
||||
let t2;
|
||||
if ($[2] !== x.inner) {
|
||||
t2 = () => x.inner;
|
||||
$[2] = x.inner;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
const cb = useIdentity(t2);
|
||||
let t3;
|
||||
if ($[4] !== x) {
|
||||
t3 = identity(x);
|
||||
$[4] = x;
|
||||
$[5] = t3;
|
||||
} else {
|
||||
t3 = $[5];
|
||||
}
|
||||
const copy = t3;
|
||||
let t4;
|
||||
if ($[6] !== cb || $[7] !== copy) {
|
||||
t4 = <Stringify copy={copy} cb={cb} shouldInvokeFns={true} />;
|
||||
$[6] = cb;
|
||||
$[7] = copy;
|
||||
$[8] = t4;
|
||||
} else {
|
||||
t4 = $[8];
|
||||
}
|
||||
return t4;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{ val1: 1 }],
|
||||
sequentialRenders: [{ val1: 1 }, { val1: 1 }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"copy":{"inner":1},"cb":{"kind":"Function","result":1},"shouldInvokeFns":true}</div>
|
||||
<div>{"copy":{"inner":1},"cb":{"kind":"Function","result":1},"shouldInvokeFns":true}</div>
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import {useIdentity, Stringify, identity} from 'shared-runtime';
|
||||
|
||||
function Foo({val1}) {
|
||||
// `x={inner: val1}` should be able to be memoized
|
||||
const x = {inner: val1};
|
||||
|
||||
// Any references to `x` after this hook call should be read-only
|
||||
const cb = useIdentity(() => x.inner);
|
||||
|
||||
// With enableTransitivelyFreezeFunctionExpressions, it's invalid
|
||||
// to write to `x` after it's been frozen.
|
||||
// TODO: runtime validation for DX
|
||||
const copy = identity(x);
|
||||
return <Stringify copy={copy} cb={cb} shouldInvokeFns={true} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{val1: 1}],
|
||||
sequentialRenders: [{val1: 1}, {val1: 1}],
|
||||
};
|
||||
+5
-2
@@ -25,10 +25,10 @@ import { c as _c } from "react/compiler-runtime";
|
||||
import { Stringify } from "shared-runtime";
|
||||
function useFoo() {
|
||||
const $ = _c(1);
|
||||
|
||||
const callback = _temp;
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
const callback = () => <Stringify value={4} />;
|
||||
|
||||
t0 = callback();
|
||||
$[0] = t0;
|
||||
} else {
|
||||
@@ -36,6 +36,9 @@ function useFoo() {
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
function _temp() {
|
||||
return <Stringify value={4} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
|
||||
+5
-2
@@ -25,10 +25,10 @@ import { c as _c } from "react/compiler-runtime";
|
||||
import * as SharedRuntime from "shared-runtime";
|
||||
function useFoo() {
|
||||
const $ = _c(1);
|
||||
|
||||
const callback = _temp;
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
const callback = () => <SharedRuntime.Text value={4} />;
|
||||
|
||||
t0 = callback();
|
||||
$[0] = t0;
|
||||
} else {
|
||||
@@ -36,6 +36,9 @@ function useFoo() {
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
function _temp() {
|
||||
return <SharedRuntime.Text value={4} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
|
||||
-1
@@ -26,7 +26,6 @@ function f(a) {
|
||||
const $ = _c(4);
|
||||
let x;
|
||||
if ($[0] !== a) {
|
||||
x;
|
||||
x = { a };
|
||||
$[0] = a;
|
||||
$[1] = x;
|
||||
|
||||
-1
@@ -27,7 +27,6 @@ function f(a) {
|
||||
const $ = _c(2);
|
||||
let x;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
x;
|
||||
x = {};
|
||||
$[0] = x;
|
||||
} else {
|
||||
|
||||
+4
-1
@@ -31,7 +31,7 @@ function Component(props) {
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = (e) => {
|
||||
setX((currentX) => currentX + null);
|
||||
setX(_temp);
|
||||
};
|
||||
$[0] = t0;
|
||||
} else {
|
||||
@@ -48,6 +48,9 @@ function Component(props) {
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
function _temp(currentX) {
|
||||
return currentX + null;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
|
||||
-1
@@ -35,7 +35,6 @@ function useFoo(arr1, arr2) {
|
||||
if ($[0] !== arr1 || $[1] !== arr2) {
|
||||
const x = [arr1];
|
||||
|
||||
y;
|
||||
(y = x.concat(arr2)), y;
|
||||
$[0] = arr1;
|
||||
$[1] = arr2;
|
||||
|
||||
+12
-17
@@ -22,26 +22,21 @@ function Component() {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
function Component() {
|
||||
const $ = _c(1);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = () => {
|
||||
while (bar()) {
|
||||
if (baz) {
|
||||
bar();
|
||||
}
|
||||
}
|
||||
return () => 4;
|
||||
};
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
const get4 = t0;
|
||||
const get4 = _temp2;
|
||||
return get4;
|
||||
}
|
||||
function _temp2() {
|
||||
while (bar()) {
|
||||
if (baz) {
|
||||
bar();
|
||||
}
|
||||
}
|
||||
return _temp;
|
||||
}
|
||||
function _temp() {
|
||||
return 4;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
-1
@@ -85,7 +85,6 @@ function Inner(props) {
|
||||
input = use(FooContext);
|
||||
}
|
||||
|
||||
input;
|
||||
input;
|
||||
let t0;
|
||||
let t1;
|
||||
|
||||
Reference in New Issue
Block a user