Compare commits

...

2 Commits

Author SHA1 Message Date
Joe Savona
96d9825588 [compiler][wip] Allow suppressions within effects 2025-09-03 17:48:48 -07:00
Joe Savona
0fba073934 [compiler] Cleanup for @enablePreserveExistingMemoizationGuarantees
I tried turning on `@enablePreserveExistingMemoizationGuarantees` by default and cleaned up a couple small things:

* We emit freeze calls for StartMemoize deps but these had ValueReason.Other so the message wasn't great. We now treat these like other hook arguments.
* PruneNonEscapingScopes was being too aggressive in this mode and memoizing even loads of globals. Switching to MemoizationLevel.Conditional ensures we build a graph that connects through to primitive-returning function calls, but doesn't unnecessarily force memoization otherwise.
2025-09-03 17:48:48 -07:00
20 changed files with 468 additions and 86 deletions

View File

@@ -8,7 +8,7 @@
import {NodePath} from '@babel/traverse';
import * as t from '@babel/types';
import prettyFormat from 'pretty-format';
import {Logger, ProgramContext} from '.';
import {Logger, ProgramContext, SingleLineSuppressionRange} from '.';
import {
HIRFunction,
ReactiveFunction,
@@ -121,6 +121,7 @@ function run(
logger: Logger | null,
filename: string | null,
code: string | null,
suppressions: Array<SingleLineSuppressionRange>,
): CodegenFunction {
const contextIdentifiers = findContextIdentifiers(func);
const env = new Environment(
@@ -134,6 +135,7 @@ function run(
filename,
code,
programContext,
suppressions,
);
env.logger?.debugLogIRs?.({
kind: 'debug',
@@ -567,6 +569,7 @@ export function compileFn(
logger: Logger | null,
filename: string | null,
code: string | null,
singleLineSuppressions: Array<SingleLineSuppressionRange>,
): CodegenFunction {
return run(
func,
@@ -577,5 +580,6 @@ export function compileFn(
logger,
filename,
code,
singleLineSuppressions,
);
}

View File

@@ -27,8 +27,9 @@ import {
import {CompilerReactTarget, PluginOptions} from './Options';
import {compileFn} from './Pipeline';
import {
filterSuppressionsThatAffectFunction,
filterSuppressionsThatAffectNode,
findProgramSuppressions,
SingleLineSuppressionRange,
suppressionsToCompilerError,
} from './Suppression';
import {GeneratedSource} from '../HIR';
@@ -691,11 +692,17 @@ function tryCompileFunction(
* Program node itself. We need to figure out whether an eslint suppression range
* applies to this function first.
*/
const suppressionsInFunction = filterSuppressionsThatAffectFunction(
const suppressionsInFunction = filterSuppressionsThatAffectNode(
programContext.suppressions,
fn,
);
if (suppressionsInFunction.length > 0) {
const singleLineSuppressions = suppressionsInFunction.filter(
s => s.kind === 'single-line',
) as Array<SingleLineSuppressionRange>;
const multiLineSuppressions = suppressionsInFunction.filter(
s => s.kind === 'multi-line',
);
if (multiLineSuppressions.length > 0) {
return {
kind: 'error',
error: suppressionsToCompilerError(suppressionsInFunction),
@@ -714,6 +721,7 @@ function tryCompileFunction(
programContext.opts.logger,
programContext.filename,
programContext.code,
singleLineSuppressions,
),
};
} catch (err) {
@@ -752,6 +760,7 @@ function retryCompileFunction(
programContext.opts.logger,
programContext.filename,
programContext.code,
[], // ignore suppressions in the retry pipeline
);
if (!retryResult.hasFireRewrite && !retryResult.hasInferredEffect) {

View File

@@ -25,11 +25,23 @@ import {GeneratedSource} from '../HIR';
* The enable comment can be missing in the case where only a disable block is present, ie the rest
* of the file has potential React violations.
*/
export type SuppressionRange = {
disableComment: t.Comment;
enableComment: t.Comment | null;
source: SuppressionSource;
};
export type SuppressionRange =
| {
kind: 'single-line';
source: SuppressionSource;
comment: t.Comment;
}
| {
kind: 'multi-line';
source: SuppressionSource;
disableComment: t.Comment;
enableComment: t.Comment | null;
};
export type SingleLineSuppressionRange = Extract<
SuppressionRange,
{kind: 'single-line'}
>;
type SuppressionSource = 'Eslint' | 'Flow';
@@ -38,15 +50,23 @@ type SuppressionSource = 'Eslint' | 'Flow';
* 1. The suppression is within the function's body; or
* 2. The suppression wraps the function
*/
export function filterSuppressionsThatAffectFunction(
suppressionRanges: Array<SuppressionRange>,
fn: NodePath<t.Function>,
): Array<SuppressionRange> {
const suppressionsInScope: Array<SuppressionRange> = [];
const fnNode = fn.node;
export function filterSuppressionsThatAffectNode<T extends SuppressionRange>(
suppressionRanges: Array<T>,
node: NodePath,
): Array<T> {
const suppressionsInScope: Array<T> = [];
const fnNode = node.node;
for (const suppressionRange of suppressionRanges) {
const enableComment =
suppressionRange.kind === 'single-line'
? suppressionRange.comment
: suppressionRange.enableComment;
const disableComment =
suppressionRange.kind === 'single-line'
? suppressionRange.comment
: suppressionRange.disableComment;
if (
suppressionRange.disableComment.start == null ||
disableComment.start == null ||
fnNode.start == null ||
fnNode.end == null
) {
@@ -54,22 +74,20 @@ export function filterSuppressionsThatAffectFunction(
}
// The suppression is within the function
if (
suppressionRange.disableComment.start > fnNode.start &&
disableComment.start > fnNode.start &&
// If there is no matching enable, the rest of the file has potential violations
(suppressionRange.enableComment === null ||
(suppressionRange.enableComment.end != null &&
suppressionRange.enableComment.end < fnNode.end))
(enableComment === null ||
(enableComment.end != null && enableComment.end < fnNode.end))
) {
suppressionsInScope.push(suppressionRange);
}
// The suppression wraps the function
if (
suppressionRange.disableComment.start < fnNode.start &&
disableComment.start < fnNode.start &&
// If there is no matching enable, the rest of the file has potential violations
(suppressionRange.enableComment === null ||
(suppressionRange.enableComment.end != null &&
suppressionRange.enableComment.end > fnNode.end))
(enableComment === null ||
(enableComment.end != null && enableComment.end > fnNode.end))
) {
suppressionsInScope.push(suppressionRange);
}
@@ -83,9 +101,7 @@ export function findProgramSuppressions(
flowSuppressions: boolean,
): Array<SuppressionRange> {
const suppressionRanges: Array<SuppressionRange> = [];
let disableComment: t.Comment | null = null;
let enableComment: t.Comment | null = null;
let source: SuppressionSource | null = null;
let suppression: SuppressionRange | null = null;
const rulePattern = `(${ruleNames.join('|')})`;
const disableNextLinePattern = new RegExp(
@@ -107,42 +123,49 @@ export function findProgramSuppressions(
* If we're already within a CommentBlock, we should not restart the range prematurely for a
* CommentLine within the block.
*/
disableComment == null &&
suppression == null &&
disableNextLinePattern.test(comment.value)
) {
disableComment = comment;
enableComment = comment;
source = 'Eslint';
suppression = {
kind: 'single-line',
comment,
source: 'Eslint',
};
}
if (
flowSuppressions &&
disableComment == null &&
suppression == null &&
flowSuppressionPattern.test(comment.value)
) {
disableComment = comment;
enableComment = comment;
source = 'Flow';
suppression = {
kind: 'single-line',
comment,
source: 'Flow',
};
}
if (disablePattern.test(comment.value)) {
disableComment = comment;
source = 'Eslint';
suppression = {
kind: 'multi-line',
disableComment: comment,
enableComment: null,
source: 'Eslint',
};
}
if (enablePattern.test(comment.value) && source === 'Eslint') {
enableComment = comment;
if (
enablePattern.test(comment.value) &&
suppression != null &&
suppression.kind === 'multi-line' &&
suppression.source === 'Eslint'
) {
suppression.enableComment = comment;
}
if (disableComment != null && source != null) {
suppressionRanges.push({
disableComment: disableComment,
enableComment: enableComment,
source,
});
disableComment = null;
enableComment = null;
source = null;
if (suppression != null) {
suppressionRanges.push(suppression);
suppression = null;
}
}
return suppressionRanges;
@@ -157,10 +180,11 @@ export function suppressionsToCompilerError(
});
const error = new CompilerError();
for (const suppressionRange of suppressionRanges) {
if (
suppressionRange.disableComment.start == null ||
suppressionRange.disableComment.end == null
) {
const disableComment =
suppressionRange.kind === 'single-line'
? suppressionRange.comment
: suppressionRange.disableComment;
if (disableComment.start == null || disableComment.end == null) {
continue;
}
let reason, suggestion;
@@ -185,22 +209,19 @@ export function suppressionsToCompilerError(
error.pushDiagnostic(
CompilerDiagnostic.create({
reason: reason,
description: `React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. Found suppression \`${suppressionRange.disableComment.value.trim()}\``,
description: `React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. Found suppression \`${disableComment.value.trim()}\``,
severity: ErrorSeverity.InvalidReact,
category: ErrorCategory.Suppression,
suggestions: [
{
description: suggestion,
range: [
suppressionRange.disableComment.start,
suppressionRange.disableComment.end,
],
range: [disableComment.start, disableComment.end],
op: CompilerSuggestionOperation.Remove,
},
],
}).withDetail({
kind: 'error',
loc: suppressionRange.disableComment.loc ?? null,
loc: disableComment.loc ?? null,
message: 'Found React rule suppression',
}),
);

View File

@@ -51,6 +51,11 @@ import {
} from './HIR';
import HIRBuilder, {Bindings, createTemporaryPlace} from './HIRBuilder';
import {BuiltInArrayId} from './ObjectShape';
import {
filterSuppressionsThatAffectNode,
SingleLineSuppressionRange,
suppressionsToCompilerError,
} from '../Entrypoint';
/*
* *******************************************************************************************
@@ -237,6 +242,42 @@ export function lower(
null,
);
if (bindings == null) {
/**
* Any single-line suppressions which didn't get captured by a call expression
* are thrown as errors from the outermost function being compiled. This is to
* allow suppressions within function expressions that are passed to useEffect,
* eg
*
* ```
* useEffect(() => {
* console.log(foo);
* // eslint-disable-next-line react-hooks/exhaustive-deps
* }, []);
* ```
*
* Where we can't throw an error when exiting the function expression, but rather
* want that suppression to bubble up to the useEffect() call node.
*
* Whereas the following should error since it's at the top-level
*
* ```
* function Component() {
* const f = () => {
* // eslint-disable-next-line react-hooks/exhaustive-deps
* };
* }
* ```
*/
const suppressions = filterSuppressionsThatAffectNode(
env.suppressions,
func,
);
if (suppressions.length !== 0) {
throw suppressionsToCompilerError(suppressions);
}
}
return Ok({
id,
params,
@@ -1766,21 +1807,27 @@ function lowerExpression(
const memberExpr = lowerMemberExpression(builder, calleePath);
const propertyPlace = lowerValueToTemporary(builder, memberExpr.value);
const args = lowerArguments(builder, expr.get('arguments'));
const suppressions = consumeSuppressionOnNode(builder, expr);
return {
kind: 'MethodCall',
receiver: memberExpr.object,
property: {...propertyPlace},
args,
loc: exprLoc,
suppressions,
};
} else {
const callee = lowerExpressionToTemporary(builder, calleePath);
const args = lowerArguments(builder, expr.get('arguments'));
const suppressions = consumeSuppressionOnNode(builder, expr);
return {
kind: 'CallExpression',
callee,
args,
loc: exprLoc,
suppressions,
};
}
}
@@ -2956,6 +3003,7 @@ function lowerOptionalCallExpression(
builder.enterReserved(consequent, () => {
const args = lowerArguments(builder, expr.get('arguments'));
const temp = buildTemporaryPlace(builder, loc);
const suppressions = consumeSuppressionOnNode(builder, expr);
if (callee.kind === 'CallExpression') {
builder.push({
id: makeInstructionId(0),
@@ -2965,6 +3013,7 @@ function lowerOptionalCallExpression(
callee: {...callee.callee},
args,
loc,
suppressions,
},
effects: null,
loc,
@@ -2979,6 +3028,7 @@ function lowerOptionalCallExpression(
property: {...callee.property},
args,
loc,
suppressions,
},
effects: null,
loc,
@@ -4477,3 +4527,23 @@ export function lowerType(node: t.FlowType | t.TSType): Type {
}
}
}
/**
* Extracts the (single-line) suppression comments from the environment that are scoped
* to within the given `node`, removing them from the environment's suppressions list.
*
* By calling this function depth-first, we can associate suppressions with the innermost
* call expression that they effect. Unconsumed suppressions are thrown at the parent
* function boundary.
*/
function consumeSuppressionOnNode(
builder: HIRBuilder,
node: NodePath,
): Array<SingleLineSuppressionRange> {
const env = builder.environment;
const suppressions = filterSuppressionsThatAffectNode(env.suppressions, node);
env.suppressions = env.suppressions.filter(
s => suppressions.indexOf(s) === -1,
);
return suppressions;
}

View File

@@ -9,7 +9,11 @@ import * as t from '@babel/types';
import {ZodError, z} from 'zod';
import {fromZodError} from 'zod-validation-error';
import {CompilerError} from '../CompilerError';
import {Logger, ProgramContext} from '../Entrypoint';
import {
Logger,
ProgramContext,
SingleLineSuppressionRange,
} from '../Entrypoint';
import {Err, Ok, Result} from '../Utils/Result';
import {
DEFAULT_GLOBALS,
@@ -702,6 +706,7 @@ export class Environment {
hasFireRewrite: boolean;
hasInferredEffect: boolean;
inferredEffectLocations: Set<SourceLocation> = new Set();
suppressions: Array<SingleLineSuppressionRange>;
#contextIdentifiers: Set<t.Identifier>;
#hoistedIdentifiers: Set<t.Identifier>;
@@ -720,6 +725,7 @@ export class Environment {
filename: string | null,
code: string | null,
programContext: ProgramContext,
suppressions: Array<SingleLineSuppressionRange>,
) {
this.#scope = scope;
this.fnType = fnType;
@@ -733,6 +739,7 @@ export class Environment {
this.#globals = new Map(DEFAULT_GLOBALS);
this.hasFireRewrite = false;
this.hasInferredEffect = false;
this.suppressions = suppressions;
if (
config.disableMemoizationForDebugging &&

View File

@@ -15,6 +15,7 @@ import {Type, makeType} from './Types';
import {z} from 'zod';
import type {AliasingEffect} from '../Inference/AliasingEffects';
import {isReservedWord} from '../Utils/Keyword';
import {SingleLineSuppressionRange} from '../Entrypoint';
/*
* *******************************************************************************************
@@ -843,6 +844,7 @@ export type MethodCall = {
property: Place;
args: Array<Place | SpreadPattern>;
loc: SourceLocation;
suppressions?: Array<SingleLineSuppressionRange>;
};
export type CallExpression = {
@@ -851,6 +853,7 @@ export type CallExpression = {
args: Array<Place | SpreadPattern>;
loc: SourceLocation;
typeArguments?: Array<t.FlowType>;
suppressions?: Array<SingleLineSuppressionRange>;
};
export type NewExpression = {

View File

@@ -255,6 +255,7 @@ function getManualMemoizationReplacement(
*/
args: [],
loc,
suppressions: [],
};
} else {
/*

View File

@@ -2089,7 +2089,7 @@ function computeSignatureForInstruction(
effects.push({
kind: 'Freeze',
value: operand,
reason: ValueReason.Other,
reason: ValueReason.HookCaptured,
});
}
}

View File

@@ -546,7 +546,7 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor<
* memoization. Note: we may still prune primitive-producing scopes if
* they don't ultimately escape at all.
*/
const level = MemoizationLevel.Memoized;
const level = MemoizationLevel.Conditional;
return {
lvalues: lvalue !== null ? [{place: lvalue, level}] : [],
rvalues: [...eachReactiveValueOperand(value)],
@@ -701,9 +701,7 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor<
}
case 'ComputedLoad':
case 'PropertyLoad': {
const level = options.forceMemoizePrimitives
? MemoizationLevel.Memoized
: MemoizationLevel.Conditional;
const level = MemoizationLevel.Conditional;
return {
// Indirection for the inner value, memoized if the value is
lvalues: lvalue !== null ? [{place: lvalue, level}] : [],

View File

@@ -11,12 +11,15 @@ import {
ErrorCategory,
ErrorSeverity,
} from '../CompilerError';
import {SuppressionRange, suppressionsToCompilerError} from '../Entrypoint';
import {FunctionExpression, HIRFunction, IdentifierId} from '../HIR';
import {Result} from '../Utils/Result';
export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
const errors = new CompilerError();
const suppressions: Array<SuppressionRange> = [];
const useMemos = new Set<IdentifierId>();
const effects = new Set<IdentifierId>();
const react = new Set<IdentifierId>();
const functions = new Map<IdentifierId, FunctionExpression>();
for (const [, block] of fn.body.blocks) {
@@ -25,6 +28,12 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
case 'LoadGlobal': {
if (value.binding.name === 'useMemo') {
useMemos.add(lvalue.identifier.id);
} else if (
value.binding.name === 'useEffect' ||
value.binding.name === 'useLayoutEffect' ||
value.binding.name === 'useInsertionEffect'
) {
effects.add(lvalue.identifier.id);
} else if (value.binding.name === 'React') {
react.add(lvalue.identifier.id);
}
@@ -34,6 +43,12 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
if (react.has(value.object.identifier.id)) {
if (value.property === 'useMemo') {
useMemos.add(lvalue.identifier.id);
} else if (
value.property === 'useEffect' ||
value.property === 'useLayoutEffect' ||
value.property === 'useInsertionEffect'
) {
effects.add(lvalue.identifier.id);
}
}
break;
@@ -47,9 +62,18 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
// Is the function being called useMemo, with at least 1 argument?
const callee =
value.kind === 'CallExpression'
? value.callee.identifier.id
: value.property.identifier.id;
const isUseMemo = useMemos.has(callee);
? value.callee.identifier
: value.property.identifier;
const isEffect = effects.has(callee.id);
if (
!isEffect &&
value.suppressions != null &&
value.suppressions.length !== 0
) {
suppressions.push(...value.suppressions);
}
const isUseMemo = useMemos.has(callee.id);
if (!isUseMemo || value.args.length === 0) {
continue;
}
@@ -112,5 +136,9 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
}
}
}
if (suppressions.length !== 0) {
const suppressionError = suppressionsToCompilerError(suppressions);
errors.merge(suppressionError);
}
return errors.asResult();
}

View File

@@ -0,0 +1,55 @@
## Input
```javascript
function Component(props) {
useEffect(
() => {
console.log(props.value);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
return <div />;
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
function Component(props) {
const $ = _c(4);
let t0;
if ($[0] !== props.value) {
t0 = () => {
console.log(props.value);
};
$[0] = props.value;
$[1] = t0;
} else {
t0 = $[1];
}
let t1;
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
t1 = [];
$[2] = t1;
} else {
t1 = $[2];
}
useEffect(t0, t1);
let t2;
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
t2 = <div />;
$[3] = t2;
} else {
t2 = $[3];
}
return t2;
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -0,0 +1,10 @@
function Component(props) {
useEffect(
() => {
console.log(props.value);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
return <div />;
}

View File

@@ -0,0 +1,52 @@
## Input
```javascript
function Component(props) {
useEffect(() => {
console.log(props.value);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return <div />;
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
function Component(props) {
const $ = _c(4);
let t0;
if ($[0] !== props.value) {
t0 = () => {
console.log(props.value);
};
$[0] = props.value;
$[1] = t0;
} else {
t0 = $[1];
}
let t1;
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
t1 = [];
$[2] = t1;
} else {
t1 = $[2];
}
useEffect(t0, t1);
let t2;
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
t2 = <div />;
$[3] = t2;
} else {
t2 = $[3];
}
return t2;
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -0,0 +1,7 @@
function Component(props) {
useEffect(() => {
console.log(props.value);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return <div />;
}

View File

@@ -2,6 +2,7 @@
## Input
```javascript
// @compilationMode:"infer"
import {makeArray} from 'shared-runtime';
function Component() {
@@ -30,7 +31,7 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { c as _c } from "react/compiler-runtime"; // @compilationMode:"infer"
import { makeArray } from "shared-runtime";
function Component() {

View File

@@ -1,3 +1,4 @@
// @compilationMode:"infer"
import {makeArray} from 'shared-runtime';
function Component() {

View File

@@ -0,0 +1,86 @@
## Input
```javascript
// @enablePreserveExistingMemoizationGuarantees
import {fbt} from 'fbt';
function Component() {
const buttonLabel = () => {
if (!someCondition) {
return <fbt desc="My label">{'Purchase as a gift'}</fbt>;
} else if (
!iconOnly &&
showPrice &&
item?.current_gift_offer?.price?.formatted != null
) {
return (
<fbt desc="Gift button's label">
{'Gift | '}
<fbt:param name="price">
{item?.current_gift_offer?.price?.formatted}
</fbt:param>
</fbt>
);
} else if (!iconOnly && !showPrice) {
return <fbt desc="Gift button's label">{'Gift'}</fbt>;
}
};
return (
<View>
<Button text={buttonLabel()} />
</View>
);
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enablePreserveExistingMemoizationGuarantees
import { fbt } from "fbt";
function Component() {
const $ = _c(1);
const buttonLabel = _temp;
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = (
<View>
<Button text={buttonLabel()} />
</View>
);
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
function _temp() {
if (!someCondition) {
return fbt._("Purchase as a gift", null, { hk: "1gHj4g" });
} else {
if (
!iconOnly &&
showPrice &&
item?.current_gift_offer?.price?.formatted != null
) {
return fbt._(
"Gift | {price}",
[fbt._param("price", item?.current_gift_offer?.price?.formatted)],
{ hk: "3GTnGE" },
);
} else {
if (!iconOnly && !showPrice) {
return fbt._("Gift", null, { hk: "3fqfrk" });
}
}
}
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -0,0 +1,31 @@
// @enablePreserveExistingMemoizationGuarantees
import {fbt} from 'fbt';
function Component() {
const buttonLabel = () => {
if (!someCondition) {
return <fbt desc="My label">{'Purchase as a gift'}</fbt>;
} else if (
!iconOnly &&
showPrice &&
item?.current_gift_offer?.price?.formatted != null
) {
return (
<fbt desc="Gift button's label">
{'Gift | '}
<fbt:param name="price">
{item?.current_gift_offer?.price?.formatted}
</fbt:param>
</fbt>
);
} else if (!iconOnly && !showPrice) {
return <fbt desc="Gift button's label">{'Gift'}</fbt>;
}
};
return (
<View>
<Button text={buttonLabel()} />
</View>
);
}

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @enableForest
// @enablePreserveExistingMemoizationGuarantees
function Component({base, start, increment, test}) {
let value = base;
for (let i = start; i < test; i += increment) {
@@ -27,25 +27,23 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableForest
import { c as _c } from "react/compiler-runtime"; // @enablePreserveExistingMemoizationGuarantees
function Component(t0) {
const $ = _c(5);
const $ = _c(2);
const { base, start, increment, test } = t0;
let value;
if ($[0] !== base || $[1] !== increment || $[2] !== start || $[3] !== test) {
value = base;
for (let i = start; i < test; i = i + increment, i) {
value = value + i;
}
$[0] = base;
$[1] = increment;
$[2] = start;
$[3] = test;
$[4] = value;
} else {
value = $[4];
let value = base;
for (let i = start; i < test; i = i + increment, i) {
value = value + i;
}
return <div>{value}</div>;
let t1;
if ($[0] !== value) {
t1 = <div>{value}</div>;
$[0] = value;
$[1] = t1;
} else {
t1 = $[1];
}
return t1;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -1,4 +1,4 @@
// @enableForest
// @enablePreserveExistingMemoizationGuarantees
function Component({base, start, increment, test}) {
let value = base;
for (let i = start; i < test; i += increment) {