Compare commits

..

2 Commits

Author SHA1 Message Date
Joe Savona
d58c07b563 [compiler] New mutability/aliasing model
Squashed, review-friendly version of the stack from https://github.com/facebook/react/pull/33488.

This is new version of our mutability and inference model, designed to replace the core algorithm for determining the sets of instructions involved in constructing a given value or set of values. The new model replaces InferReferenceEffects, InferMutableRanges (and all of its subcomponents), and parts of AnalyzeFunctions. The new model does not use per-Place effect values, but in order to make this drop-in the end _result_ of the inference adds these per-Place effects.

I'll write up a larger document on the model, first i'm doing some housekeeping to rebase the PR.
2025-06-18 09:53:47 -07:00
Joe Savona
631b1cfc74 [compiler] Remove unnecessary fixture
This is covered by iife-inline-ternary
2025-06-18 09:53:45 -07:00
400 changed files with 6513 additions and 15832 deletions

View File

@@ -622,7 +622,6 @@ module.exports = {
ScrollTimeline: 'readonly',
EventListenerOptionsOrUseCapture: 'readonly',
FocusOptions: 'readonly',
OptionalEffectTiming: 'readonly',
spyOnDev: 'readonly',
spyOnDevAndProd: 'readonly',

View File

@@ -301,12 +301,10 @@ jobs:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
- name: Install runtime dependencies
run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- name: Install compiler dependencies
run: yarn install --frozen-lockfile
working-directory: compiler
restore-keys: |
runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-and-compiler-node_modules-v6-
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- run: ./scripts/react-compiler/build-compiler.sh && ./scripts/react-compiler/link-compiler.sh
- run: yarn workspace eslint-plugin-react-hooks test

View File

@@ -85,7 +85,7 @@ jobs:
--skipTests \
--tags=${{ inputs.dist_tag }} \
--onlyPackages=${{ inputs.only_packages }} ${{ (inputs.dry && '') || '\'}}
${{ inputs.dry && '--dry' || '' }}
${{ inputs.dry && '--dry'}}
- if: '${{ inputs.skip_packages }}'
name: 'Publish all packages EXCEPT ${{ inputs.skip_packages }}'
run: |
@@ -94,19 +94,19 @@ jobs:
--skipTests \
--tags=${{ inputs.dist_tag }} \
--skipPackages=${{ inputs.skip_packages }} ${{ (inputs.dry && '') || '\'}}
${{ inputs.dry && '--dry' || '' }}
${{ inputs.dry && '--dry'}}
- if: '${{ !(inputs.skip_packages && inputs.only_packages) }}'
name: 'Publish all packages'
run: |
scripts/release/publish.js \
--ci \
--tags=${{ inputs.dist_tag }} ${{ (inputs.dry && '') || '\'}}
${{ inputs.dry && '--dry' || '' }}
${{ inputs.dry && '--dry'}}
- name: Notify Discord on failure
if: failure() && inputs.enableFailureNotification == true
uses: tsickert/discord-webhook@86dc739f3f165f16dadc5666051c367efa1692f4
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
embed-author-name: "GitHub Actions"
embed-title: '[Runtime] Publish of ${{ inputs.release_channel }}@${{ inputs.dist_tag}} release failed'
embed-title: 'Publish of $${{ inputs.release_channel }} release failed'
embed-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}/attempts/${{ github.run_attempt }}

View File

@@ -110,7 +110,7 @@ jobs:
--tags=${{ inputs.tags }} \
--publishVersion=${{ inputs.version_to_publish }} \
--onlyPackages=${{ inputs.only_packages }} ${{ (inputs.dry && '') || '\'}}
${{ inputs.dry && '--dry' || '' }}
${{ inputs.dry && '--dry'}}
- if: '${{ inputs.skip_packages }}'
name: 'Publish all packages EXCEPT ${{ inputs.skip_packages }}'
run: |
@@ -119,7 +119,7 @@ jobs:
--tags=${{ inputs.tags }} \
--publishVersion=${{ inputs.version_to_publish }} \
--skipPackages=${{ inputs.skip_packages }} ${{ (inputs.dry && '') || '\'}}
${{ inputs.dry && '--dry' || '' }}
${{ inputs.dry && '--dry'}}
- name: Archive released package for debugging
uses: actions/upload-artifact@v4
with:

View File

@@ -19,11 +19,11 @@ An Owner Stack is a string representing the components that are directly respons
* Updated `useId` to use valid CSS selectors, changing format from `:r123:` to `«r123»`. [#32001](https://github.com/facebook/react/pull/32001)
* Added a dev-only warning for null/undefined created in useEffect, useInsertionEffect, and useLayoutEffect. [#32355](https://github.com/facebook/react/pull/32355)
* Fixed a bug where dev-only methods were exported in production builds. React.act is no longer available in production builds. [#32200](https://github.com/facebook/react/pull/32200)
* Improved consistency across prod and dev to improve compatibility with Google Closure Compiler and bindings [#31808](https://github.com/facebook/react/pull/31808)
* Improved consistency across prod and dev to improve compatibility with Google Closure Complier and bindings [#31808](https://github.com/facebook/react/pull/31808)
* Improve passive effect scheduling for consistent task yielding. [#31785](https://github.com/facebook/react/pull/31785)
* Fixed asserts in React Native when passChildrenWhenCloningPersistedNodes is enabled for OffscreenComponent rendering. [#32528](https://github.com/facebook/react/pull/32528)
* Fixed component name resolution for Portal [#32640](https://github.com/facebook/react/pull/32640)
* Added support for beforetoggle and toggle events on the dialog element. [#32479](https://github.com/facebook/react/pull/32479)
* Added support for beforetoggle and toggle events on the dialog element. #32479 [#32479](https://github.com/facebook/react/pull/32479)
### React DOM
* Fixed double warning when the `href` attribute is an empty string [#31783](https://github.com/facebook/react/pull/31783)

View File

@@ -44,7 +44,6 @@ import {
PrintedCompilerPipelineValue,
} from './Output';
import {transformFromAstSync} from '@babel/core';
import {LoggerEvent} from 'babel-plugin-react-compiler/dist/Entrypoint';
function parseInput(
input: string,
@@ -144,7 +143,6 @@ const COMMON_HOOKS: Array<[string, Hook]> = [
function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
const results = new Map<string, Array<PrintedCompilerPipelineValue>>();
const error = new CompilerError();
const otherErrors: Array<CompilerErrorDetail> = [];
const upsert: (result: PrintedCompilerPipelineValue) => void = result => {
const entry = results.get(result.name);
if (Array.isArray(entry)) {
@@ -212,11 +210,7 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
},
logger: {
debugLogIRs: logIR,
logEvent: (_filename: string | null, event: LoggerEvent) => {
if (event.kind === 'CompileError') {
otherErrors.push(new CompilerErrorDetail(event.detail));
}
},
logEvent: () => {},
},
});
transformOutput = invokeCompiler(source, language, opts);
@@ -243,10 +237,6 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
);
}
}
// Only include logger errors if there weren't other errors
if (!error.hasErrors() && otherErrors.length !== 0) {
otherErrors.forEach(e => error.push(e));
}
if (error.hasErrors()) {
return [{kind: 'err', results, error: error}, language];
}

View File

@@ -72,7 +72,7 @@ export function lower(
env: Environment,
// Bindings captured from the outer function, in case lower() is called recursively (for lambdas)
bindings: Bindings | null = null,
capturedRefs: Map<t.Identifier, SourceLocation> = new Map(),
capturedRefs: Array<t.Identifier> = [],
): Result<HIRFunction, CompilerError> {
const builder = new HIRBuilder(env, {
bindings,
@@ -80,13 +80,13 @@ export function lower(
});
const context: HIRFunction['context'] = [];
for (const [ref, loc] of capturedRefs ?? []) {
for (const ref of capturedRefs ?? []) {
context.push({
kind: 'Identifier',
identifier: builder.resolveBinding(ref),
effect: Effect.Unknown,
reactive: false,
loc,
loc: ref.loc ?? GeneratedSource,
});
}
@@ -221,6 +221,7 @@ export function lower(
params,
fnType: bindings == null ? env.fnType : 'Other',
returnTypeAnnotation: null, // TODO: extract the actual return type node if present
returnType: makeType(),
returns: createTemporaryPlace(env, func.node.loc ?? GeneratedSource),
body: builder.build(),
context,
@@ -1355,83 +1356,13 @@ function lowerStatement(
return;
}
case 'WithStatement': {
builder.errors.push({
reason: `JavaScript 'with' syntax is not supported`,
description: `'with' syntax is considered deprecated and removed from JavaScript standards, consider alternatives`,
severity: ErrorSeverity.InvalidJS,
loc: stmtPath.node.loc ?? null,
suggestions: null,
});
lowerValueToTemporary(builder, {
kind: 'UnsupportedNode',
loc: stmtPath.node.loc ?? GeneratedSource,
node: stmtPath.node,
});
return;
}
case 'ClassDeclaration': {
/*
* We can in theory support nested classes, similarly to functions where we track values
* captured by the class and consider mutations of the instances to mutate the class itself
*/
builder.errors.push({
reason: `Support nested class declarations`,
severity: ErrorSeverity.Todo,
loc: stmtPath.node.loc ?? null,
suggestions: null,
});
lowerValueToTemporary(builder, {
kind: 'UnsupportedNode',
loc: stmtPath.node.loc ?? GeneratedSource,
node: stmtPath.node,
});
return;
}
case 'EnumDeclaration':
case 'TSEnumDeclaration': {
lowerValueToTemporary(builder, {
kind: 'UnsupportedNode',
loc: stmtPath.node.loc ?? GeneratedSource,
node: stmtPath.node,
});
return;
}
case 'ExportAllDeclaration':
case 'ExportDefaultDeclaration':
case 'ExportNamedDeclaration':
case 'ImportDeclaration':
case 'TSExportAssignment':
case 'TSImportEqualsDeclaration': {
builder.errors.push({
reason:
'JavaScript `import` and `export` statements may only appear at the top level of a module',
severity: ErrorSeverity.InvalidJS,
loc: stmtPath.node.loc ?? null,
suggestions: null,
});
lowerValueToTemporary(builder, {
kind: 'UnsupportedNode',
loc: stmtPath.node.loc ?? GeneratedSource,
node: stmtPath.node,
});
return;
}
case 'TSNamespaceExportDeclaration': {
builder.errors.push({
reason:
'TypeScript `namespace` statements may only appear at the top level of a module',
severity: ErrorSeverity.InvalidJS,
loc: stmtPath.node.loc ?? null,
suggestions: null,
});
lowerValueToTemporary(builder, {
kind: 'UnsupportedNode',
loc: stmtPath.node.loc ?? GeneratedSource,
node: stmtPath.node,
});
case 'TypeAlias':
case 'TSInterfaceDeclaration':
case 'TSTypeAliasDeclaration': {
// We do not preserve type annotations/syntax through transformation
return;
}
case 'ClassDeclaration':
case 'DeclareClass':
case 'DeclareExportAllDeclaration':
case 'DeclareExportDeclaration':
@@ -1442,14 +1373,31 @@ function lowerStatement(
case 'DeclareOpaqueType':
case 'DeclareTypeAlias':
case 'DeclareVariable':
case 'EnumDeclaration':
case 'ExportAllDeclaration':
case 'ExportDefaultDeclaration':
case 'ExportNamedDeclaration':
case 'ImportDeclaration':
case 'InterfaceDeclaration':
case 'OpaqueType':
case 'TSDeclareFunction':
case 'TSInterfaceDeclaration':
case 'TSEnumDeclaration':
case 'TSExportAssignment':
case 'TSImportEqualsDeclaration':
case 'TSModuleDeclaration':
case 'TSTypeAliasDeclaration':
case 'TypeAlias': {
// We do not preserve type annotations/syntax through transformation
case 'TSNamespaceExportDeclaration':
case 'WithStatement': {
builder.errors.push({
reason: `(BuildHIR::lowerStatement) Handle ${stmtPath.type} statements`,
severity: ErrorSeverity.Todo,
loc: stmtPath.node.loc ?? null,
suggestions: null,
});
lowerValueToTemporary(builder, {
kind: 'UnsupportedNode',
loc: stmtPath.node.loc ?? GeneratedSource,
node: stmtPath.node,
});
return;
}
default: {
@@ -3491,12 +3439,10 @@ function lowerFunction(
* This isn't a problem in practice because use Babel's scope analysis to
* identify the correct references.
*/
const lowering = lower(
expr,
builder.environment,
builder.bindings,
new Map([...builder.context, ...capturedContext]),
);
const lowering = lower(expr, builder.environment, builder.bindings, [
...builder.context,
...capturedContext,
]);
let loweredFunc: HIRFunction;
if (lowering.isErr()) {
lowering
@@ -3555,16 +3501,6 @@ function lowerIdentifier(
return place;
}
default: {
if (binding.kind === 'Global' && binding.name === 'eval') {
builder.errors.push({
reason: `The 'eval' function is not supported`,
description:
'Eval is an anti-pattern in JavaScript, and the code executed cannot be evaluated by React Compiler',
severity: ErrorSeverity.InvalidJS,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
}
return lowerValueToTemporary(builder, {
kind: 'LoadGlobal',
binding,
@@ -4224,11 +4160,6 @@ function captureScopes({from, to}: {from: Scope; to: Scope}): Set<Scope> {
return scopes;
}
/**
* Returns a mapping of "context" identifiers — references to free variables that
* will become part of the function expression's `context` array — along with the
* source location of their first reference within the function.
*/
function gatherCapturedContext(
fn: NodePath<
| t.FunctionExpression
@@ -4237,8 +4168,8 @@ function gatherCapturedContext(
| t.ObjectMethod
>,
componentScope: Scope,
): Map<t.Identifier, SourceLocation> {
const capturedIds = new Map<t.Identifier, SourceLocation>();
): Array<t.Identifier> {
const capturedIds = new Set<t.Identifier>();
/*
* Capture all the scopes from the parent of this function up to and including
@@ -4281,15 +4212,8 @@ function gatherCapturedContext(
// Add the base identifier binding as a dependency.
const binding = baseIdentifier.scope.getBinding(baseIdentifier.node.name);
if (
binding !== undefined &&
pureScopes.has(binding.scope) &&
!capturedIds.has(binding.identifier)
) {
capturedIds.set(
binding.identifier,
path.node.loc ?? binding.identifier.loc ?? GeneratedSource,
);
if (binding !== undefined && pureScopes.has(binding.scope)) {
capturedIds.add(binding.identifier);
}
}
@@ -4326,7 +4250,7 @@ function gatherCapturedContext(
},
});
return capturedIds;
return [...capturedIds.keys()];
}
function notNull<T>(value: T | null): value is T {

View File

@@ -246,7 +246,7 @@ export const EnvironmentConfigSchema = z.object({
/**
* Enable a new model for mutability and aliasing inference
*/
enableNewMutationAliasingModel: z.boolean().default(true),
enableNewMutationAliasingModel: z.boolean().default(false),
/**
* Enables inference of optional dependency chains. Without this flag

View File

@@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {Effect, ValueKind, ValueReason} from './HIR';
import {Effect, makeIdentifierId, ValueKind, ValueReason} from './HIR';
import {
BUILTIN_SHAPES,
BuiltInArrayId,
@@ -34,6 +34,7 @@ import {
addFunction,
addHook,
addObject,
signatureArgument,
} from './ObjectShape';
import {BuiltInType, ObjectType, PolyType} from './Types';
import {TypeConfig} from './TypeSchema';
@@ -645,35 +646,35 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
hookKind: 'useEffect',
returnValueKind: ValueKind.Frozen,
aliasing: {
receiver: '@receiver',
receiver: makeIdentifierId(0),
params: [],
rest: '@rest',
returns: '@returns',
temporaries: ['@effect'],
rest: makeIdentifierId(1),
returns: makeIdentifierId(2),
temporaries: [signatureArgument(3)],
effects: [
// Freezes the function and deps
{
kind: 'Freeze',
value: '@rest',
value: signatureArgument(1),
reason: ValueReason.Effect,
},
// Internally creates an effect object that captures the function and deps
{
kind: 'Create',
into: '@effect',
into: signatureArgument(3),
value: ValueKind.Frozen,
reason: ValueReason.KnownReturnSignature,
},
// The effect stores the function and dependencies
{
kind: 'Capture',
from: '@rest',
into: '@effect',
from: signatureArgument(1),
into: signatureArgument(3),
},
// Returns undefined
{
kind: 'Create',
into: '@returns',
into: signatureArgument(2),
value: ValueKind.Primitive,
reason: ValueReason.KnownReturnSignature,
},
@@ -905,7 +906,6 @@ export function installTypeConfig(
noAlias: typeConfig.noAlias === true,
mutableOnlyIfOperandsAreMutable:
typeConfig.mutableOnlyIfOperandsAreMutable === true,
aliasing: typeConfig.aliasing,
});
}
case 'hook': {
@@ -923,7 +923,6 @@ export function installTypeConfig(
),
returnValueKind: typeConfig.returnValueKind ?? ValueKind.Frozen,
noAlias: typeConfig.noAlias === true,
aliasing: typeConfig.aliasing,
});
}
case 'object': {

View File

@@ -279,6 +279,7 @@ export type HIRFunction = {
env: Environment;
params: Array<Place | SpreadPattern>;
returnTypeAnnotation: t.FlowType | t.TSType | null;
returnType: Type;
returns: Place;
context: Array<Place>;
effects: Array<FunctionEffect> | null;
@@ -1387,16 +1388,6 @@ export enum ValueReason {
*/
JsxCaptured = 'jsx-captured',
/**
* Argument to a hook
*/
HookCaptured = 'hook-captured',
/**
* Return value of a hook
*/
HookReturn = 'hook-return',
/**
* Passed to an effect
*/
@@ -1452,20 +1443,6 @@ export const ValueKindSchema = z.enum([
ValueKind.Context,
]);
export const ValueReasonSchema = z.enum([
ValueReason.Context,
ValueReason.Effect,
ValueReason.Global,
ValueReason.HookCaptured,
ValueReason.HookReturn,
ValueReason.JsxCaptured,
ValueReason.KnownReturnSignature,
ValueReason.Other,
ValueReason.ReactiveFunctionArgument,
ValueReason.ReducerState,
ValueReason.State,
]);
// The effect with which a value is modified.
export enum Effect {
// Default value: not allowed after lifetime inference
@@ -1769,10 +1746,6 @@ export function isUseStateType(id: Identifier): boolean {
return id.type.kind === 'Object' && id.type.shapeId === 'BuiltInUseState';
}
export function isJsxType(type: Type): boolean {
return type.kind === 'Object' && type.shapeId === 'BuiltInJsx';
}
export function isRefOrRefValue(id: Identifier): boolean {
return isUseRefType(id) || isRefValueType(id);
}

View File

@@ -106,7 +106,7 @@ export default class HIRBuilder {
#current: WipBlock;
#entry: BlockId;
#scopes: Array<Scope> = [];
#context: Map<t.Identifier, SourceLocation>;
#context: Array<t.Identifier>;
#bindings: Bindings;
#env: Environment;
#exceptionHandlerStack: Array<BlockId> = [];
@@ -121,7 +121,7 @@ export default class HIRBuilder {
return this.#env.nextIdentifierId;
}
get context(): Map<t.Identifier, SourceLocation> {
get context(): Array<t.Identifier> {
return this.#context;
}
@@ -137,13 +137,13 @@ export default class HIRBuilder {
env: Environment,
options?: {
bindings?: Bindings | null;
context?: Map<t.Identifier, SourceLocation>;
context?: Array<t.Identifier>;
entryBlockKind?: BlockKind;
},
) {
this.#env = env;
this.#bindings = options?.bindings ?? new Map();
this.#context = options?.context ?? new Map();
this.#context = options?.context ?? [];
this.#entry = makeBlockId(env.nextBlockId);
this.#current = newBlock(this.#entry, options?.entryBlockKind ?? 'block');
}

View File

@@ -107,17 +107,6 @@ export function mergeConsecutiveBlocks(fn: HIRFunction): void {
merged.merge(block.id, predecessorId);
fn.body.blocks.delete(block.id);
}
for (const [, block] of fn.body.blocks) {
for (const phi of block.phis) {
for (const [predecessorId, operand] of phi.operands) {
const mapped = merged.get(predecessorId);
if (mapped !== predecessorId) {
phi.operands.delete(predecessorId);
phi.operands.set(mapped, operand);
}
}
}
}
markPredecessors(fn.body);
for (const [, {terminal}] of fn.body.blocks) {
if (terminalHasFallthrough(terminal)) {

View File

@@ -6,18 +6,14 @@
*/
import {CompilerError} from '../CompilerError';
import {AliasingEffect, AliasingSignature} from '../Inference/AliasingEffects';
import {assertExhaustive} from '../Utils/utils';
import {AliasingSignature} from '../Inference/AliasingEffects';
import {
Effect,
GeneratedSource,
Hole,
makeDeclarationId,
makeIdentifierId,
makeInstructionId,
Place,
SourceLocation,
SpreadPattern,
ValueKind,
ValueReason,
} from './HIR';
@@ -29,7 +25,6 @@ import {
PolyType,
PrimitiveType,
} from './Types';
import {AliasingEffectConfig, AliasingSignatureConfig} from './TypeSchema';
/*
* This file exports types and defaults for JavaScript object shapes. These are
@@ -58,20 +53,13 @@ function createAnonId(): string {
export function addFunction(
registry: ShapeRegistry,
properties: Iterable<[string, BuiltInType | PolyType]>,
fn: Omit<FunctionSignature, 'hookKind' | 'aliasing'> & {
aliasing?: AliasingSignatureConfig | null | undefined;
},
fn: Omit<FunctionSignature, 'hookKind'>,
id: string | null = null,
isConstructor: boolean = false,
): FunctionType {
const shapeId = id ?? createAnonId();
const aliasing =
fn.aliasing != null
? parseAliasingSignatureConfig(fn.aliasing, '<builtin>', GeneratedSource)
: null;
addShape(registry, shapeId, properties, {
...fn,
aliasing,
hookKind: null,
});
return {
@@ -89,18 +77,11 @@ export function addFunction(
*/
export function addHook(
registry: ShapeRegistry,
fn: Omit<FunctionSignature, 'aliasing'> & {
hookKind: HookKind;
aliasing?: AliasingSignatureConfig | null | undefined;
},
fn: FunctionSignature & {hookKind: HookKind},
id: string | null = null,
): FunctionType {
const shapeId = id ?? createAnonId();
const aliasing =
fn.aliasing != null
? parseAliasingSignatureConfig(fn.aliasing, '<builtin>', GeneratedSource)
: null;
addShape(registry, shapeId, [], {...fn, aliasing});
addShape(registry, shapeId, [], fn);
return {
kind: 'Function',
return: fn.returnType,
@@ -109,129 +90,6 @@ export function addHook(
};
}
function parseAliasingSignatureConfig(
typeConfig: AliasingSignatureConfig,
moduleName: string,
loc: SourceLocation,
): AliasingSignature {
const lifetimes = new Map<string, Place>();
function define(temp: string): Place {
CompilerError.invariant(!lifetimes.has(temp), {
reason: `Invalid type configuration for module`,
description: `Expected aliasing signature to have unique names for receiver, params, rest, returns, and temporaries in module '${moduleName}'`,
loc,
});
const place = signatureArgument(lifetimes.size);
lifetimes.set(temp, place);
return place;
}
function lookup(temp: string): Place {
const place = lifetimes.get(temp);
CompilerError.invariant(place != null, {
reason: `Invalid type configuration for module`,
description: `Expected aliasing signature effects to reference known names from receiver/params/rest/returns/temporaries, but '${temp}' is not a known name in '${moduleName}'`,
loc,
});
return place;
}
const receiver = define(typeConfig.receiver);
const params = typeConfig.params.map(define);
const rest = typeConfig.rest != null ? define(typeConfig.rest) : null;
const returns = define(typeConfig.returns);
const temporaries = typeConfig.temporaries.map(define);
const effects = typeConfig.effects.map(
(effect: AliasingEffectConfig): AliasingEffect => {
switch (effect.kind) {
case 'CreateFrom':
case 'Capture':
case 'Alias':
case 'Assign': {
const from = lookup(effect.from);
const into = lookup(effect.into);
return {
kind: effect.kind,
from,
into,
};
}
case 'Mutate':
case 'MutateTransitiveConditionally': {
const value = lookup(effect.value);
return {kind: effect.kind, value};
}
case 'Create': {
const into = lookup(effect.into);
return {
kind: 'Create',
into,
reason: effect.reason,
value: effect.value,
};
}
case 'Freeze': {
const value = lookup(effect.value);
return {
kind: 'Freeze',
value,
reason: effect.reason,
};
}
case 'Impure': {
const place = lookup(effect.place);
return {
kind: 'Impure',
place,
error: CompilerError.throwTodo({
reason: 'Support impure effect declarations',
loc: GeneratedSource,
}),
};
}
case 'Apply': {
const receiver = lookup(effect.receiver);
const fn = lookup(effect.function);
const args: Array<Place | SpreadPattern | Hole> = effect.args.map(
arg => {
if (typeof arg === 'string') {
return lookup(arg);
} else if (arg.kind === 'Spread') {
return {kind: 'Spread', place: lookup(arg.place)};
} else {
return arg;
}
},
);
const into = lookup(effect.into);
return {
kind: 'Apply',
receiver,
function: fn,
mutatesFunction: effect.mutatesFunction,
args,
into,
loc,
signature: null,
};
}
default: {
assertExhaustive(
effect,
`Unexpected effect kind '${(effect as any).kind}'`,
);
}
}
},
);
return {
receiver: receiver.identifier.id,
params: params.map(p => p.identifier.id),
rest: rest != null ? rest.identifier.id : null,
returns: returns.identifier.id,
temporaries,
effects,
};
}
/*
* Add an object to an existing ShapeRegistry.
*
@@ -334,7 +192,8 @@ export type FunctionSignature = {
canonicalName?: string;
aliasing?: AliasingSignature | null | undefined;
aliasing?: AliasingSignature | null;
todo_aliasing?: AliasingSignature | null;
};
/*
@@ -461,24 +320,24 @@ addObject(BUILTIN_SHAPES, BuiltInArrayId, [
calleeEffect: Effect.Store,
returnValueKind: ValueKind.Primitive,
aliasing: {
receiver: '@receiver',
receiver: makeIdentifierId(0),
params: [],
rest: '@rest',
returns: '@returns',
rest: makeIdentifierId(1),
returns: makeIdentifierId(2),
temporaries: [],
effects: [
// Push directly mutates the array itself
{kind: 'Mutate', value: '@receiver'},
{kind: 'Mutate', value: signatureArgument(0)},
// The arguments are captured into the array
{
kind: 'Capture',
from: '@rest',
into: '@receiver',
from: signatureArgument(1),
into: signatureArgument(0),
},
// Returns the new length, a primitive
{
kind: 'Create',
into: '@returns',
into: signatureArgument(2),
value: ValueKind.Primitive,
reason: ValueReason.KnownReturnSignature,
},
@@ -515,56 +374,58 @@ addObject(BUILTIN_SHAPES, BuiltInArrayId, [
noAlias: true,
mutableOnlyIfOperandsAreMutable: true,
aliasing: {
receiver: '@receiver',
params: ['@callback'],
receiver: makeIdentifierId(0),
params: [makeIdentifierId(1)],
rest: null,
returns: '@returns',
returns: makeIdentifierId(2),
temporaries: [
// Temporary representing captured items of the receiver
'@item',
signatureArgument(3),
// Temporary representing the result of the callback
'@callbackReturn',
signatureArgument(4),
/*
* Undefined `this` arg to the callback. Note the signature does not
* support passing an explicit thisArg second param
*/
'@thisArg',
signatureArgument(5),
],
effects: [
// Map creates a new mutable array
{
kind: 'Create',
into: '@returns',
into: signatureArgument(2),
value: ValueKind.Mutable,
reason: ValueReason.KnownReturnSignature,
},
// The first arg to the callback is an item extracted from the receiver array
{
kind: 'CreateFrom',
from: '@receiver',
into: '@item',
from: signatureArgument(0),
into: signatureArgument(3),
},
// The undefined this for the callback
{
kind: 'Create',
into: '@thisArg',
into: signatureArgument(5),
value: ValueKind.Primitive,
reason: ValueReason.KnownReturnSignature,
},
// calls the callback, returning the result into a temporary
{
kind: 'Apply',
receiver: '@thisArg',
args: ['@item', {kind: 'Hole'}, '@receiver'],
function: '@callback',
into: '@callbackReturn',
receiver: signatureArgument(5),
args: [signatureArgument(3), {kind: 'Hole'}, signatureArgument(0)],
function: signatureArgument(1),
into: signatureArgument(4),
signature: null,
mutatesFunction: false,
loc: GeneratedSource,
},
// captures the result of the callback into the return array
{
kind: 'Capture',
from: '@callbackReturn',
into: '@returns',
from: signatureArgument(4),
into: signatureArgument(2),
},
],
},
@@ -716,28 +577,28 @@ addObject(BUILTIN_SHAPES, BuiltInSetId, [
// returnValueKind is technically dependent on the ValueKind of the set itself
returnValueKind: ValueKind.Mutable,
aliasing: {
receiver: '@receiver',
receiver: makeIdentifierId(0),
params: [],
rest: '@rest',
returns: '@returns',
rest: makeIdentifierId(1),
returns: makeIdentifierId(2),
temporaries: [],
effects: [
// Set.add returns the receiver Set
{
kind: 'Assign',
from: '@receiver',
into: '@returns',
from: signatureArgument(0),
into: signatureArgument(2),
},
// Set.add mutates the set itself
{
kind: 'Mutate',
value: '@receiver',
value: signatureArgument(0),
},
// Captures the rest params into the set
{
kind: 'Capture',
from: '@rest',
into: '@receiver',
from: signatureArgument(1),
into: signatureArgument(0),
},
],
},
@@ -1441,34 +1302,6 @@ export const DefaultNonmutatingHook = addHook(
calleeEffect: Effect.Read,
hookKind: 'Custom',
returnValueKind: ValueKind.Frozen,
aliasing: {
receiver: '@receiver',
params: [],
rest: '@rest',
returns: '@returns',
temporaries: [],
effects: [
// Freeze the arguments
{
kind: 'Freeze',
value: '@rest',
reason: ValueReason.HookCaptured,
},
// Returns a frozen value
{
kind: 'Create',
into: '@returns',
value: ValueKind.Frozen,
reason: ValueReason.HookReturn,
},
// May alias any arguments into the return
{
kind: 'Alias',
from: '@rest',
into: '@returns',
},
],
},
},
'DefaultNonmutatingHook',
);

View File

@@ -5,6 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import generate from '@babel/generator';
import {CompilerError} from '../CompilerError';
import {printReactiveScopeSummary} from '../ReactiveScopes/PrintReactiveFunction';
import DisjointSet from '../Utils/DisjointSet';
@@ -53,8 +54,6 @@ export function printFunction(fn: HIRFunction): string {
let definition = '';
if (fn.id !== null) {
definition += fn.id;
} else {
definition += '<<anonymous>>';
}
if (fn.params.length !== 0) {
definition +=
@@ -72,8 +71,10 @@ export function printFunction(fn: HIRFunction): string {
} else {
definition += '()';
}
definition += `: ${printPlace(fn.returns)}`;
output.push(definition);
if (definition.length !== 0) {
output.push(definition);
}
output.push(`: ${printType(fn.returnType)} @ ${printPlace(fn.returns)}`);
output.push(...fn.directives);
output.push(printHIR(fn.body));
return output.join('\n');
@@ -465,7 +466,7 @@ export function printInstructionValue(instrValue: ReactiveValue): string {
break;
}
case 'UnsupportedNode': {
value = `UnsupportedNode ${instrValue.node.type}`;
value = `UnsupportedNode(${generate(instrValue.node).code})`;
break;
}
case 'LoadLocal': {
@@ -714,7 +715,7 @@ export function printInstructionValue(instrValue: ReactiveValue): string {
break;
}
case 'FinishMemoize': {
value = `FinishMemoize decl=${printPlace(instrValue.decl)}${instrValue.pruned ? ' pruned' : ''}`;
value = `FinishMemoize decl=${printPlace(instrValue.decl)}`;
break;
}
default: {

View File

@@ -8,12 +8,7 @@
import {isValidIdentifier} from '@babel/types';
import {z} from 'zod';
import {Effect, ValueKind} from '..';
import {
EffectSchema,
ValueKindSchema,
ValueReason,
ValueReasonSchema,
} from './HIR';
import {EffectSchema, ValueKindSchema} from './HIR';
export type ObjectPropertiesConfig = {[key: string]: TypeConfig};
export const ObjectPropertiesSchema: z.ZodType<ObjectPropertiesConfig> = z
@@ -36,194 +31,6 @@ export const ObjectTypeSchema: z.ZodType<ObjectTypeConfig> = z.object({
properties: ObjectPropertiesSchema.nullable(),
});
export const LifetimeIdSchema = z.string().refine(id => id.startsWith('@'), {
message: "Placeholder names must start with '@'",
});
export type FreezeEffectConfig = {
kind: 'Freeze';
value: string;
reason: ValueReason;
};
export const FreezeEffectSchema: z.ZodType<FreezeEffectConfig> = z.object({
kind: z.literal('Freeze'),
value: LifetimeIdSchema,
reason: ValueReasonSchema,
});
export type MutateEffectConfig = {
kind: 'Mutate';
value: string;
};
export const MutateEffectSchema: z.ZodType<MutateEffectConfig> = z.object({
kind: z.literal('Mutate'),
value: LifetimeIdSchema,
});
export type MutateTransitiveConditionallyConfig = {
kind: 'MutateTransitiveConditionally';
value: string;
};
export const MutateTransitiveConditionallySchema: z.ZodType<MutateTransitiveConditionallyConfig> =
z.object({
kind: z.literal('MutateTransitiveConditionally'),
value: LifetimeIdSchema,
});
export type CreateEffectConfig = {
kind: 'Create';
into: string;
value: ValueKind;
reason: ValueReason;
};
export const CreateEffectSchema: z.ZodType<CreateEffectConfig> = z.object({
kind: z.literal('Create'),
into: LifetimeIdSchema,
value: ValueKindSchema,
reason: ValueReasonSchema,
});
export type AssignEffectConfig = {
kind: 'Assign';
from: string;
into: string;
};
export const AssignEffectSchema: z.ZodType<AssignEffectConfig> = z.object({
kind: z.literal('Assign'),
from: LifetimeIdSchema,
into: LifetimeIdSchema,
});
export type AliasEffectConfig = {
kind: 'Alias';
from: string;
into: string;
};
export const AliasEffectSchema: z.ZodType<AliasEffectConfig> = z.object({
kind: z.literal('Alias'),
from: LifetimeIdSchema,
into: LifetimeIdSchema,
});
export type CaptureEffectConfig = {
kind: 'Capture';
from: string;
into: string;
};
export const CaptureEffectSchema: z.ZodType<CaptureEffectConfig> = z.object({
kind: z.literal('Capture'),
from: LifetimeIdSchema,
into: LifetimeIdSchema,
});
export type CreateFromEffectConfig = {
kind: 'CreateFrom';
from: string;
into: string;
};
export const CreateFromEffectSchema: z.ZodType<CreateFromEffectConfig> =
z.object({
kind: z.literal('CreateFrom'),
from: LifetimeIdSchema,
into: LifetimeIdSchema,
});
export type ApplyArgConfig =
| string
| {kind: 'Spread'; place: string}
| {kind: 'Hole'};
export const ApplyArgSchema: z.ZodType<ApplyArgConfig> = z.union([
LifetimeIdSchema,
z.object({
kind: z.literal('Spread'),
place: LifetimeIdSchema,
}),
z.object({
kind: z.literal('Hole'),
}),
]);
export type ApplyEffectConfig = {
kind: 'Apply';
receiver: string;
function: string;
mutatesFunction: boolean;
args: Array<ApplyArgConfig>;
into: string;
};
export const ApplyEffectSchema: z.ZodType<ApplyEffectConfig> = z.object({
kind: z.literal('Apply'),
receiver: LifetimeIdSchema,
function: LifetimeIdSchema,
mutatesFunction: z.boolean(),
args: z.array(ApplyArgSchema),
into: LifetimeIdSchema,
});
export type ImpureEffectConfig = {
kind: 'Impure';
place: string;
};
export const ImpureEffectSchema: z.ZodType<ImpureEffectConfig> = z.object({
kind: z.literal('Impure'),
place: LifetimeIdSchema,
});
export type AliasingEffectConfig =
| FreezeEffectConfig
| CreateEffectConfig
| CreateFromEffectConfig
| AssignEffectConfig
| AliasEffectConfig
| CaptureEffectConfig
| ImpureEffectConfig
| MutateEffectConfig
| MutateTransitiveConditionallyConfig
| ApplyEffectConfig;
export const AliasingEffectSchema: z.ZodType<AliasingEffectConfig> = z.union([
FreezeEffectSchema,
CreateEffectSchema,
CreateFromEffectSchema,
AssignEffectSchema,
AliasEffectSchema,
CaptureEffectSchema,
ImpureEffectSchema,
MutateEffectSchema,
MutateTransitiveConditionallySchema,
ApplyEffectSchema,
]);
export type AliasingSignatureConfig = {
receiver: string;
params: Array<string>;
rest: string | null;
returns: string;
effects: Array<AliasingEffectConfig>;
temporaries: Array<string>;
};
export const AliasingSignatureSchema: z.ZodType<AliasingSignatureConfig> =
z.object({
receiver: LifetimeIdSchema,
params: z.array(LifetimeIdSchema),
rest: LifetimeIdSchema.nullable(),
returns: LifetimeIdSchema,
effects: z.array(AliasingEffectSchema),
temporaries: z.array(LifetimeIdSchema),
});
export type FunctionTypeConfig = {
kind: 'function';
positionalParams: Array<Effect>;
@@ -235,7 +42,6 @@ export type FunctionTypeConfig = {
mutableOnlyIfOperandsAreMutable?: boolean | null | undefined;
impure?: boolean | null | undefined;
canonicalName?: string | null | undefined;
aliasing?: AliasingSignatureConfig | null | undefined;
};
export const FunctionTypeSchema: z.ZodType<FunctionTypeConfig> = z.object({
kind: z.literal('function'),
@@ -248,7 +54,6 @@ export const FunctionTypeSchema: z.ZodType<FunctionTypeConfig> = z.object({
mutableOnlyIfOperandsAreMutable: z.boolean().nullable().optional(),
impure: z.boolean().nullable().optional(),
canonicalName: z.string().nullable().optional(),
aliasing: AliasingSignatureSchema.nullable().optional(),
});
export type HookTypeConfig = {
@@ -258,7 +63,6 @@ export type HookTypeConfig = {
returnType: TypeConfig;
returnValueKind?: ValueKind | null | undefined;
noAlias?: boolean | null | undefined;
aliasing?: AliasingSignatureConfig | null | undefined;
};
export const HookTypeSchema: z.ZodType<HookTypeConfig> = z.object({
kind: z.literal('hook'),
@@ -267,7 +71,6 @@ export const HookTypeSchema: z.ZodType<HookTypeConfig> = z.object({
returnType: z.lazy(() => TypeSchema),
returnValueKind: ValueKindSchema.nullable().optional(),
noAlias: z.boolean().nullable().optional(),
aliasing: AliasingSignatureSchema.nullable().optional(),
});
export type BuiltInTypeConfig =

View File

@@ -8,7 +8,6 @@
import {CompilerErrorDetailOptions} from '../CompilerError';
import {
FunctionExpression,
GeneratedSource,
Hole,
IdentifierId,
ObjectMethod,
@@ -19,7 +18,6 @@ import {
ValueReason,
} from '../HIR';
import {FunctionSignature} from '../HIR/ObjectShape';
import {printSourceLocation} from '../HIR/PrintHIR';
/**
* `AliasingEffect` describes a set of "effects" that an instruction/terminal has on one or
@@ -202,19 +200,10 @@ export function hashEffect(effect: AliasingEffect): string {
return [effect.kind, effect.value.identifier.id, effect.reason].join(':');
}
case 'Impure':
case 'Render': {
return [effect.kind, effect.place.identifier.id].join(':');
}
case 'Render':
case 'MutateFrozen':
case 'MutateGlobal': {
return [
effect.kind,
effect.place.identifier.id,
effect.error.severity,
effect.error.reason,
effect.error.description,
printSourceLocation(effect.error.loc ?? GeneratedSource),
].join(':');
return [effect.kind, effect.place.identifier.id].join(':');
}
case 'Mutate':
case 'MutateConditionally':

View File

@@ -22,6 +22,7 @@ import {inferMutableRanges} from './InferMutableRanges';
import inferReferenceEffects from './InferReferenceEffects';
import {assertExhaustive} from '../Utils/utils';
import {inferMutationAliasingEffects} from './InferMutationAliasingEffects';
import {inferMutationAliasingFunctionEffects} from './InferMutationAliasingFunctionEffects';
import {inferMutationAliasingRanges} from './InferMutationAliasingRanges';
export default function analyseFunctions(func: HIRFunction): void {
@@ -41,16 +42,8 @@ export default function analyseFunctions(func: HIRFunction): void {
* Reset mutable range for outer inferReferenceEffects
*/
for (const operand of instr.value.loweredFunc.func.context) {
/**
* NOTE: inferReactiveScopeVariables makes identifiers in the scope
* point to the *same* mutableRange instance. Resetting start/end
* here is insufficient, because a later mutation of the range
* for any one identifier could affect the range for other identifiers.
*/
operand.identifier.mutableRange = {
start: makeInstructionId(0),
end: makeInstructionId(0),
};
operand.identifier.mutableRange.start = makeInstructionId(0);
operand.identifier.mutableRange.end = makeInstructionId(0);
operand.identifier.scope = null;
}
break;
@@ -67,12 +60,19 @@ function lowerWithMutationAliasing(fn: HIRFunction): void {
analyseFunctions(fn);
inferMutationAliasingEffects(fn, {isFunctionExpression: true});
deadCodeElimination(fn);
const functionEffects = inferMutationAliasingRanges(fn, {
isFunctionExpression: true,
}).unwrap();
inferMutationAliasingRanges(fn, {isFunctionExpression: true});
rewriteInstructionKindsBasedOnReassignment(fn);
inferReactiveScopeVariables(fn);
fn.aliasingEffects = functionEffects;
const effects = inferMutationAliasingFunctionEffects(fn);
fn.env.logger?.debugLogIRs?.({
kind: 'hir',
name: 'AnalyseFunction (inner)',
value: fn,
});
if (effects != null) {
fn.aliasingEffects ??= [];
fn.aliasingEffects?.push(...effects);
}
/**
* Phase 2: populate the Effect of each context variable to use in inferring
@@ -80,7 +80,7 @@ function lowerWithMutationAliasing(fn: HIRFunction): void {
* effects to decide if the function may be mutable or not.
*/
const capturedOrMutated = new Set<IdentifierId>();
for (const effect of functionEffects) {
for (const effect of effects ?? []) {
switch (effect.kind) {
case 'Assign':
case 'Alias':
@@ -132,12 +132,6 @@ function lowerWithMutationAliasing(fn: HIRFunction): void {
operand.effect = Effect.Read;
}
}
fn.env.logger?.debugLogIRs?.({
kind: 'hir',
name: 'AnalyseFunction (inner)',
value: fn,
});
}
function lower(func: HIRFunction): void {

View File

@@ -341,10 +341,6 @@ export function getWriteErrorReason(abstractValue: AbstractValue): string {
return "Mutating a value returned from 'useReducer()', which should not be mutated. Use the dispatch function to update instead";
} else if (abstractValue.reason.has(ValueReason.Effect)) {
return 'Updating a value used previously in an effect function or as an effect dependency is not allowed. Consider moving the mutation before calling useEffect()';
} else if (abstractValue.reason.has(ValueReason.HookCaptured)) {
return 'Updating a value previously passed as an argument to a hook is not allowed. Consider moving the mutation before calling the hook';
} else if (abstractValue.reason.has(ValueReason.HookReturn)) {
return 'Updating a value returned from a hook is not allowed. Consider moving the mutation into the hook where the value is constructed';
} else {
return 'This mutates a variable that React considers immutable';
}

View File

@@ -38,7 +38,6 @@ import {
import {
eachInstructionValueLValue,
eachInstructionValueOperand,
eachTerminalOperand,
eachTerminalSuccessor,
} from '../HIR/visitors';
import {Ok, Result} from '../Utils/Result';
@@ -50,7 +49,6 @@ import {
} from './InferReferenceEffects';
import {
assertExhaustive,
getOrInsertDefault,
getOrInsertWith,
Set_isSuperset,
} from '../Utils/utils';
@@ -193,15 +191,16 @@ export function inferMutationAliasingEffects(
hoistedContextDeclarations,
);
let iterationCount = 0;
let count = 0;
while (queuedStates.size !== 0) {
iterationCount++;
if (iterationCount > 100) {
CompilerError.invariant(false, {
reason: `[InferMutationAliasingEffects] Potential infinite loop`,
description: `A value, temporary place, or effect was not cached properly`,
loc: fn.loc,
});
count++;
if (count > 1000) {
console.log(
'oops infinite loop',
fn.id,
typeof fn.loc !== 'symbol' ? fn.loc?.filename : null,
);
throw new Error('infinite loop');
}
for (const [blockId, block] of fn.body.blocks) {
const incomingState = queuedStates.get(blockId);
@@ -222,19 +221,8 @@ export function inferMutationAliasingEffects(
return Ok(undefined);
}
function findHoistedContextDeclarations(
fn: HIRFunction,
): Map<DeclarationId, Place | null> {
const hoisted = new Map<DeclarationId, Place | null>();
function visit(place: Place): void {
if (
hoisted.has(place.identifier.declarationId) &&
hoisted.get(place.identifier.declarationId) == null
) {
// If this is the first load of the value, store the location
hoisted.set(place.identifier.declarationId, place);
}
}
function findHoistedContextDeclarations(fn: HIRFunction): Set<DeclarationId> {
const hoisted = new Set<DeclarationId>();
for (const block of fn.body.blocks.values()) {
for (const instr of block.instructions) {
if (instr.value.kind === 'DeclareContext') {
@@ -244,17 +232,10 @@ function findHoistedContextDeclarations(
kind == InstructionKind.HoistedFunction ||
kind == InstructionKind.HoistedLet
) {
hoisted.set(instr.value.lvalue.place.identifier.declarationId, null);
}
} else {
for (const operand of eachInstructionValueOperand(instr.value)) {
visit(operand);
hoisted.add(instr.value.lvalue.place.identifier.declarationId);
}
}
}
for (const operand of eachTerminalOperand(block.terminal)) {
visit(operand);
}
}
return hoisted;
}
@@ -264,40 +245,21 @@ class Context {
instructionSignatureCache: Map<Instruction, InstructionSignature> = new Map();
effectInstructionValueCache: Map<AliasingEffect, InstructionValue> =
new Map();
applySignatureCache: Map<
AliasingSignature,
Map<AliasingEffect, Array<AliasingEffect> | null>
> = new Map();
catchHandlers: Map<BlockId, Place> = new Map();
functionSignatureCache: Map<FunctionExpression, AliasingSignature> =
new Map();
isFuctionExpression: boolean;
fn: HIRFunction;
hoistedContextDeclarations: Map<DeclarationId, Place | null>;
hoistedContextDeclarations: Set<DeclarationId>;
constructor(
isFunctionExpression: boolean,
fn: HIRFunction,
hoistedContextDeclarations: Map<DeclarationId, Place | null>,
hoistedContextDeclarations: Set<DeclarationId>,
) {
this.isFuctionExpression = isFunctionExpression;
this.fn = fn;
this.hoistedContextDeclarations = hoistedContextDeclarations;
}
cacheApplySignature(
signature: AliasingSignature,
effect: Extract<AliasingEffect, {kind: 'Apply'}>,
f: () => Array<AliasingEffect> | null,
): Array<AliasingEffect> | null {
const inner = getOrInsertDefault(
this.applySignatureCache,
signature,
new Map(),
);
return getOrInsertWith(inner, effect, f);
}
internEffect(effect: AliasingEffect): AliasingEffect {
const hash = hashEffect(effect);
let interned = this.internedEffects.get(hash);
@@ -352,11 +314,6 @@ function inferBlock(
} else if (terminal.kind === 'maybe-throw') {
const handlerParam = context.catchHandlers.get(terminal.handler);
if (handlerParam != null) {
CompilerError.invariant(state.kind(handlerParam) != null, {
reason:
'Expected catch binding to be intialized with a DeclareLocal Catch instruction',
loc: terminal.loc,
});
const effects: Array<AliasingEffect> = [];
for (const instr of block.instructions) {
if (
@@ -376,13 +333,11 @@ function inferBlock(
state.appendAlias(handlerParam, instr.lvalue);
const kind = state.kind(instr.lvalue).kind;
if (kind === ValueKind.Mutable || kind == ValueKind.Context) {
effects.push(
context.internEffect({
kind: 'Alias',
from: instr.lvalue,
into: handlerParam,
}),
);
effects.push({
kind: 'Alias',
from: instr.lvalue,
into: handlerParam,
});
}
}
}
@@ -391,11 +346,11 @@ function inferBlock(
} else if (terminal.kind === 'return') {
if (!context.isFuctionExpression) {
terminal.effects = [
context.internEffect({
{
kind: 'Freeze',
value: terminal.value,
reason: ValueReason.JsxCaptured,
}),
},
];
}
}
@@ -471,14 +426,14 @@ function applySignature(
* Track which values we've already aliased once, so that we can switch to
* appendAlias() for subsequent aliases into the same value
*/
const initialized = new Set<IdentifierId>();
const aliased = new Set<IdentifierId>();
if (DEBUG) {
console.log(printInstruction(instruction));
}
for (const effect of signature.effects) {
applyEffect(context, state, effect, initialized, effects);
applyEffect(context, state, effect, aliased, effects);
}
if (DEBUG) {
console.log(
@@ -503,7 +458,7 @@ function applyEffect(
context: Context,
state: InferenceState,
_effect: AliasingEffect,
initialized: Set<IdentifierId>,
aliased: Set<IdentifierId>,
effects: Array<AliasingEffect>,
): void {
const effect = context.internEffect(_effect);
@@ -519,13 +474,6 @@ function applyEffect(
break;
}
case 'Create': {
CompilerError.invariant(!initialized.has(effect.into.identifier.id), {
reason: `Cannot re-initialize variable within an instruction`,
description: `Re-initialized ${printPlace(effect.into)} in ${printAliasingEffect(effect)}`,
loc: effect.into.loc,
});
initialized.add(effect.into.identifier.id);
let value = context.effectInstructionValueCache.get(effect);
if (value == null) {
value = {
@@ -540,7 +488,6 @@ function applyEffect(
reason: new Set([effect.reason]),
});
state.define(effect.into, value);
effects.push(effect);
break;
}
case 'ImmutableCapture': {
@@ -558,13 +505,6 @@ function applyEffect(
break;
}
case 'CreateFrom': {
CompilerError.invariant(!initialized.has(effect.into.identifier.id), {
reason: `Cannot re-initialize variable within an instruction`,
description: `Re-initialized ${printPlace(effect.into)} in ${printAliasingEffect(effect)}`,
loc: effect.into.loc,
});
initialized.add(effect.into.identifier.id);
const fromValue = state.kind(effect.from);
let value = context.effectInstructionValueCache.get(effect);
if (value == null) {
@@ -583,48 +523,29 @@ function applyEffect(
switch (fromValue.kind) {
case ValueKind.Primitive:
case ValueKind.Global: {
effects.push({
kind: 'Create',
value: fromValue.kind,
into: effect.into,
reason: [...fromValue.reason][0] ?? ValueReason.Other,
});
// no need to track this data flow
break;
}
case ValueKind.Frozen: {
effects.push({
kind: 'Create',
value: fromValue.kind,
kind: 'ImmutableCapture',
from: effect.from,
into: effect.into,
reason: [...fromValue.reason][0] ?? ValueReason.Other,
});
applyEffect(
context,
state,
{
kind: 'ImmutableCapture',
from: effect.from,
into: effect.into,
},
initialized,
effects,
);
break;
}
default: {
effects.push(effect);
effects.push({
// OK: recording information flow
kind: 'CreateFrom', // prev Alias
from: effect.from,
into: effect.into,
});
}
}
break;
}
case 'CreateFunction': {
CompilerError.invariant(!initialized.has(effect.into.identifier.id), {
reason: `Cannot re-initialize variable within an instruction`,
description: `Re-initialized ${printPlace(effect.into)} in ${printAliasingEffect(effect)}`,
loc: effect.into.loc,
});
initialized.add(effect.into.identifier.id);
effects.push(effect);
/**
* We consider the function mutable if it has any mutable context variables or
@@ -681,7 +602,7 @@ function applyEffect(
from: capture,
into: effect.into,
},
initialized,
aliased,
effects,
);
}
@@ -689,14 +610,6 @@ function applyEffect(
}
case 'Alias':
case 'Capture': {
CompilerError.invariant(
effect.kind === 'Capture' || initialized.has(effect.into.identifier.id),
{
reason: `Expected destination value to already be initialized within this instruction for Alias effect`,
description: `Destination ${printPlace(effect.into)} is not initialized in this instruction`,
loc: effect.into.loc,
},
);
/*
* Capture describes potential information flow: storing a pointer to one value
* within another. If the destination is not mutable, or the source value has
@@ -726,17 +639,11 @@ function applyEffect(
}
case ValueKind.Frozen: {
isMutableReferenceType = false;
applyEffect(
context,
state,
{
kind: 'ImmutableCapture',
from: effect.from,
into: effect.into,
},
initialized,
effects,
);
effects.push({
kind: 'ImmutableCapture',
from: effect.from,
into: effect.into,
});
break;
}
default: {
@@ -750,13 +657,6 @@ function applyEffect(
break;
}
case 'Assign': {
CompilerError.invariant(!initialized.has(effect.into.identifier.id), {
reason: `Cannot re-initialize variable within an instruction`,
description: `Re-initialized ${printPlace(effect.into)} in ${printAliasingEffect(effect)}`,
loc: effect.into.loc,
});
initialized.add(effect.into.identifier.id);
/*
* Alias represents potential pointer aliasing. If the type is a global,
* a primitive (copy-on-write semantics) then we can prune the effect
@@ -765,17 +665,11 @@ function applyEffect(
const fromKind = fromValue.kind;
switch (fromKind) {
case ValueKind.Frozen: {
applyEffect(
context,
state,
{
kind: 'ImmutableCapture',
from: effect.from,
into: effect.into,
},
initialized,
effects,
);
effects.push({
kind: 'ImmutableCapture',
from: effect.from,
into: effect.into,
});
let value = context.effectInstructionValueCache.get(effect);
if (value == null) {
value = {
@@ -811,7 +705,12 @@ function applyEffect(
break;
}
default: {
state.assign(effect.into, effect.from);
if (aliased.has(effect.into.identifier.id)) {
state.appendAlias(effect.into, effect.from);
} else {
aliased.add(effect.into.identifier.id);
state.alias(effect.into, effect.from);
}
effects.push(effect);
break;
}
@@ -822,78 +721,70 @@ function applyEffect(
const functionValues = state.values(effect.function);
if (
functionValues.length === 1 &&
functionValues[0].kind === 'FunctionExpression' &&
functionValues[0].loweredFunc.func.aliasingEffects != null
functionValues[0].kind === 'FunctionExpression'
) {
/*
* We're calling a locally declared function, we already know it's effects!
* We just have to substitute in the args for the params
*/
const functionExpr = functionValues[0];
let signature = context.functionSignatureCache.get(functionExpr);
if (signature == null) {
signature = buildSignatureFromFunctionExpression(
state.env,
functionExpr,
);
context.functionSignatureCache.set(functionExpr, signature);
}
const signature = buildSignatureFromFunctionExpression(
state.env,
functionValues[0],
);
if (DEBUG) {
console.log(
`constructed alias signature:\n${printAliasingSignature(signature)}`,
);
}
const signatureEffects = context.cacheApplySignature(
const signatureEffects = computeEffectsForSignature(
state.env,
signature,
effect,
() =>
computeEffectsForSignature(
state.env,
signature,
effect.into,
effect.receiver,
effect.args,
functionExpr.loweredFunc.func.context,
effect.loc,
),
effect.into,
effect.receiver,
effect.args,
functionValues[0].loweredFunc.func.context,
effect.loc,
);
if (signatureEffects != null) {
if (DEBUG) {
console.log('apply function expression effects');
}
applyEffect(
context,
state,
{kind: 'MutateTransitiveConditionally', value: effect.function},
initialized,
aliased,
effects,
);
for (const signatureEffect of signatureEffects) {
applyEffect(context, state, signatureEffect, initialized, effects);
applyEffect(context, state, signatureEffect, aliased, effects);
}
break;
}
}
let signatureEffects = null;
if (effect.signature?.aliasing != null) {
const signature = effect.signature.aliasing;
signatureEffects = context.cacheApplySignature(
effect.signature.aliasing,
effect,
() =>
computeEffectsForSignature(
const signatureEffects =
effect.signature?.aliasing != null
? computeEffectsForSignature(
state.env,
signature,
effect.signature.aliasing,
effect.into,
effect.receiver,
effect.args,
[],
effect.loc,
),
);
}
)
: null;
if (signatureEffects != null) {
if (DEBUG) {
console.log('apply aliasing signature effects');
}
for (const signatureEffect of signatureEffects) {
applyEffect(context, state, signatureEffect, initialized, effects);
applyEffect(context, state, signatureEffect, aliased, effects);
}
} else if (effect.signature != null) {
if (DEBUG) {
console.log('apply legacy signature effects');
}
const legacyEffects = computeEffectsForLegacySignature(
state,
effect.signature,
@@ -903,9 +794,12 @@ function applyEffect(
effect.loc,
);
for (const legacyEffect of legacyEffects) {
applyEffect(context, state, legacyEffect, initialized, effects);
applyEffect(context, state, legacyEffect, aliased, effects);
}
} else {
if (DEBUG) {
console.log('default effects');
}
applyEffect(
context,
state,
@@ -915,7 +809,7 @@ function applyEffect(
value: ValueKind.Mutable,
reason: ValueReason.Other,
},
initialized,
aliased,
effects,
);
/*
@@ -938,21 +832,21 @@ function applyEffect(
kind: 'MutateTransitiveConditionally',
value: operand,
},
initialized,
aliased,
effects,
);
}
const mutateIterator =
arg.kind === 'Spread' ? conditionallyMutateIterator(operand) : null;
if (mutateIterator) {
applyEffect(context, state, mutateIterator, initialized, effects);
applyEffect(context, state, mutateIterator, aliased, effects);
}
applyEffect(
context,
state,
// OK: recording information flow
{kind: 'Alias', from: operand, into: effect.into},
initialized,
aliased,
effects,
);
for (const otherArg of [
@@ -980,7 +874,7 @@ function applyEffect(
from: operand,
into: other,
},
initialized,
aliased,
effects,
);
}
@@ -1007,89 +901,27 @@ function applyEffect(
console.log(prettyFormat(state.debugAbstractValue(value)));
}
if (
mutationKind === 'mutate-frozen' &&
context.hoistedContextDeclarations.has(
effect.value.identifier.declarationId,
)
) {
const description =
effect.value.identifier.name !== null &&
effect.value.identifier.name.kind === 'named'
? `Variable \`${effect.value.identifier.name.value}\` is accessed before it is declared`
: null;
const hoistedAccess = context.hoistedContextDeclarations.get(
effect.value.identifier.declarationId,
);
if (hoistedAccess != null && hoistedAccess.loc != effect.value.loc) {
applyEffect(
context,
state,
{
kind: 'MutateFrozen',
place: effect.value,
error: {
severity: ErrorSeverity.InvalidReact,
reason: `This variable is accessed before it is declared, which may prevent it from updating as the assigned value changes over time`,
description,
loc: hoistedAccess.loc,
suggestions: null,
},
},
initialized,
effects,
);
}
applyEffect(
context,
state,
{
kind: 'MutateFrozen',
place: effect.value,
error: {
severity: ErrorSeverity.InvalidReact,
reason: `This variable is accessed before it is declared, which prevents the earlier access from updating when this value changes over time`,
description,
loc: effect.value.loc,
suggestions: null,
},
},
initialized,
effects,
);
} else {
const reason = getWriteErrorReason({
kind: value.kind,
reason: value.reason,
context: new Set(),
});
const description =
effect.value.identifier.name !== null &&
effect.value.identifier.name.kind === 'named'
? `Found mutation of \`${effect.value.identifier.name.value}\``
: null;
applyEffect(
context,
state,
{
kind:
value.kind === ValueKind.Frozen
? 'MutateFrozen'
: 'MutateGlobal',
place: effect.value,
error: {
severity: ErrorSeverity.InvalidReact,
reason,
description,
loc: effect.value.loc,
suggestions: null,
},
},
initialized,
effects,
);
}
const reason = getWriteErrorReason({
kind: value.kind,
reason: value.reason,
context: new Set(),
});
effects.push({
kind:
value.kind === ValueKind.Frozen ? 'MutateFrozen' : 'MutateGlobal',
place: effect.value,
error: {
severity: ErrorSeverity.InvalidReact,
reason,
description:
effect.value.identifier.name !== null &&
effect.value.identifier.name.kind === 'named'
? `Found mutation of \`${effect.value.identifier.name.value}\``
: null,
loc: effect.value.loc,
suggestions: null,
},
});
}
break;
}
@@ -1193,7 +1025,7 @@ class InferenceState {
}
// Updates the value at @param place to point to the same value as @param value.
assign(place: Place, value: Place): void {
alias(place: Place, value: Place): void {
const values = this.#variables.get(value.identifier.id);
CompilerError.invariant(values != null, {
reason: `[InferMutationAliasingEffects] Expected value for identifier to be initialized`,
@@ -1271,6 +1103,9 @@ class InferenceState {
kind: ValueKind.Frozen,
reason: new Set([reason]),
});
if (DEBUG) {
console.log(`freeze value: ${printInstructionValue(value)} ${reason}`);
}
if (
value.kind === 'FunctionExpression' &&
(this.env.config.enablePreserveExistingMemoizationGuarantees ||
@@ -2124,15 +1959,28 @@ function computeEffectsForLegacySignature(
break;
}
case Effect.ConditionallyMutateIterator: {
const mutateIterator = conditionallyMutateIterator(place);
if (mutateIterator != null) {
effects.push(mutateIterator);
if (
isArrayType(place.identifier) ||
isSetType(place.identifier) ||
isMapType(place.identifier)
) {
effects.push({
kind: 'Capture',
from: place,
into: lvalue,
});
} else {
effects.push({
kind: 'Capture',
from: place,
into: lvalue,
});
captures.push(place);
effects.push({
kind: 'MutateTransitiveConditionally',
value: place,
});
}
effects.push({
kind: 'Capture',
from: place,
into: lvalue,
});
break;
}
case Effect.Freeze: {
@@ -2308,10 +2156,20 @@ function computeEffectsForSignature(
// Too many args and there is no rest param to hold them
(args.length > signature.params.length && signature.rest == null)
) {
if (DEBUG) {
if (signature.params.length > args.length) {
console.log(
`not enough args: ${args.length} args for ${signature.params.length} params`,
);
} else {
console.log(
`too many args: ${args.length} args for ${signature.params.length} params, with no rest param`,
);
}
}
return null;
}
// Build substitutions
const mutableSpreads = new Set<IdentifierId>();
const substitutions: Map<IdentifierId, Array<Place>> = new Map();
substitutions.set(signature.receiver, [receiver]);
substitutions.set(signature.returns, [lvalue]);
@@ -2322,17 +2180,13 @@ function computeEffectsForSignature(
continue;
} else if (params == null || i >= params.length || arg.kind === 'Spread') {
if (signature.rest == null) {
if (DEBUG) {
console.log(`no rest value to hold param`);
}
return null;
}
const place = arg.kind === 'Identifier' ? arg : arg.place;
getOrInsertWith(substitutions, signature.rest, () => []).push(place);
if (arg.kind === 'Spread') {
const mutateIterator = conditionallyMutateIterator(arg.place);
if (mutateIterator != null) {
mutableSpreads.add(arg.place.identifier.id);
}
}
} else {
const param = params[i];
substitutions.set(param, [arg]);
@@ -2404,12 +2258,6 @@ function computeEffectsForSignature(
case 'Freeze': {
const values = substitutions.get(effect.value.identifier.id) ?? [];
for (const value of values) {
if (mutableSpreads.has(value.identifier.id)) {
CompilerError.throwTodo({
reason: 'Support spread syntax for hook arguments',
loc: value.loc,
});
}
effects.push({kind: 'Freeze', value, reason: effect.reason});
}
break;
@@ -2429,14 +2277,23 @@ function computeEffectsForSignature(
case 'Apply': {
const applyReceiver = substitutions.get(effect.receiver.identifier.id);
if (applyReceiver == null || applyReceiver.length !== 1) {
if (DEBUG) {
console.log(`too many substitutions for receiver`);
}
return null;
}
const applyFunction = substitutions.get(effect.function.identifier.id);
if (applyFunction == null || applyFunction.length !== 1) {
if (DEBUG) {
console.log(`too many substitutions for function`);
}
return null;
}
const applyInto = substitutions.get(effect.into.identifier.id);
if (applyInto == null || applyInto.length !== 1) {
if (DEBUG) {
console.log(`too many substitutions for into`);
}
return null;
}
const applyArgs: Array<Place | SpreadPattern | Hole> = [];
@@ -2446,12 +2303,18 @@ function computeEffectsForSignature(
} else if (arg.kind === 'Identifier') {
const applyArg = substitutions.get(arg.identifier.id);
if (applyArg == null || applyArg.length !== 1) {
if (DEBUG) {
console.log(`too many substitutions for arg`);
}
return null;
}
applyArgs.push(applyArg[0]);
} else {
const applyArg = substitutions.get(arg.place.identifier.id);
if (applyArg == null || applyArg.length !== 1) {
if (DEBUG) {
console.log(`too many substitutions for arg`);
}
return null;
}
applyArgs.push({kind: 'Spread', place: applyArg[0]});

View File

@@ -0,0 +1,206 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {HIRFunction, IdentifierId, Place, ValueKind, ValueReason} from '../HIR';
import {getOrInsertDefault} from '../Utils/utils';
import {AliasingEffect} from './AliasingEffects';
/**
* This function tracks data flow within an inner function expression in order to
* compute a set of data-flow aliasing effects describing data flow between the function's
* params, context variables, and return value.
*
* For example, consider the following function expression:
*
* ```
* (x) => { return [x, y] }
* ```
*
* This function captures both param `x` and context variable `y` into the return value.
* Unlike our previous inference which counted this as a mutation of x and y, we want to
* build a signature for the function that describes the data flow. We would infer
* `Capture x -> return, Capture y -> return` effects for this function.
*
* This function *also* propagates more ambient-style effects (MutateFrozen, MutateGlobal, Impure, Render)
* from instructions within the function up to the function itself.
*/
export function inferMutationAliasingFunctionEffects(
fn: HIRFunction,
): Array<AliasingEffect> | null {
const effects: Array<AliasingEffect> = [];
/**
* Map used to identify tracked variables: params, context vars, return value
* This is used to detect mutation/capturing/aliasing of params/context vars
*/
const tracked = new Map<IdentifierId, Place>();
tracked.set(fn.returns.identifier.id, fn.returns);
for (const operand of [...fn.context, ...fn.params]) {
const place = operand.kind === 'Identifier' ? operand : operand.place;
tracked.set(place.identifier.id, place);
}
/**
* Track capturing/aliasing of context vars and params into each other and into the return.
* We don't need to track locals and intermediate values, since we're only concerned with effects
* as they relate to arguments visible outside the function.
*
* For each aliased identifier we track capture/alias/createfrom and then merge this with how
* the value is used. Eg capturing an alias => capture. See joinEffects() helper.
*/
type AliasedIdentifier = {
kind: AliasingKind;
place: Place;
};
const dataFlow = new Map<IdentifierId, Array<AliasedIdentifier>>();
/*
* Check for aliasing of tracked values. Also joins the effects of how the value is
* used (@param kind) with the aliasing type of each value
*/
function lookup(
place: Place,
kind: AliasedIdentifier['kind'],
): Array<AliasedIdentifier> | null {
if (tracked.has(place.identifier.id)) {
return [{kind, place}];
}
return (
dataFlow.get(place.identifier.id)?.map(aliased => ({
kind: joinEffects(aliased.kind, kind),
place: aliased.place,
})) ?? null
);
}
// todo: fixpoint
for (const block of fn.body.blocks.values()) {
for (const phi of block.phis) {
const operands: Array<AliasedIdentifier> = [];
for (const operand of phi.operands.values()) {
const inputs = lookup(operand, 'Alias');
if (inputs != null) {
operands.push(...inputs);
}
}
if (operands.length !== 0) {
dataFlow.set(phi.place.identifier.id, operands);
}
}
for (const instr of block.instructions) {
if (instr.effects == null) continue;
for (const effect of instr.effects) {
if (
effect.kind === 'Assign' ||
effect.kind === 'Capture' ||
effect.kind === 'Alias' ||
effect.kind === 'CreateFrom'
) {
const from = lookup(effect.from, effect.kind);
if (from == null) {
continue;
}
const into = lookup(effect.into, 'Alias');
if (into == null) {
getOrInsertDefault(dataFlow, effect.into.identifier.id, []).push(
...from,
);
} else {
for (const aliased of into) {
getOrInsertDefault(
dataFlow,
aliased.place.identifier.id,
[],
).push(...from);
}
}
} else if (
effect.kind === 'Create' ||
effect.kind === 'CreateFunction'
) {
getOrInsertDefault(dataFlow, effect.into.identifier.id, [
{kind: 'Alias', place: effect.into},
]);
} else if (
effect.kind === 'MutateFrozen' ||
effect.kind === 'MutateGlobal' ||
effect.kind === 'Impure' ||
effect.kind === 'Render'
) {
effects.push(effect);
}
}
}
if (block.terminal.kind === 'return') {
const from = lookup(block.terminal.value, 'Alias');
if (from != null) {
getOrInsertDefault(dataFlow, fn.returns.identifier.id, []).push(
...from,
);
}
}
}
// Create aliasing effects based on observed data flow
let hasReturn = false;
for (const [into, from] of dataFlow) {
const input = tracked.get(into);
if (input == null) {
continue;
}
for (const aliased of from) {
if (
aliased.place.identifier.id === input.identifier.id ||
!tracked.has(aliased.place.identifier.id)
) {
continue;
}
const effect = {kind: aliased.kind, from: aliased.place, into: input};
effects.push(effect);
if (
into === fn.returns.identifier.id &&
(aliased.kind === 'Assign' || aliased.kind === 'CreateFrom')
) {
hasReturn = true;
}
}
}
// TODO: more precise return effect inference
if (!hasReturn) {
effects.unshift({
kind: 'Create',
into: fn.returns,
value:
fn.returnType.kind === 'Primitive'
? ValueKind.Primitive
: ValueKind.Mutable,
reason: ValueReason.KnownReturnSignature,
});
}
return effects;
}
export enum MutationKind {
None = 0,
Conditional = 1,
Definite = 2,
}
type AliasingKind = 'Alias' | 'Capture' | 'CreateFrom' | 'Assign';
function joinEffects(
effect1: AliasingKind,
effect2: AliasingKind,
): AliasingKind {
if (effect1 === 'Capture' || effect2 === 'Capture') {
return 'Capture';
} else if (effect1 === 'Assign' || effect2 === 'Assign') {
return 'Assign';
} else {
return 'Alias';
}
}

View File

@@ -5,6 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import prettyFormat from 'pretty-format';
import {CompilerError, SourceLocation} from '..';
import {
BlockId,
@@ -13,12 +14,8 @@ import {
Identifier,
IdentifierId,
InstructionId,
isJsxType,
makeInstructionId,
ValueKind,
ValueReason,
Place,
isPrimitiveType,
} from '../HIR/HIR';
import {
eachInstructionLValue,
@@ -26,58 +23,43 @@ import {
eachTerminalOperand,
} from '../HIR/visitors';
import {assertExhaustive, getOrInsertWith} from '../Utils/utils';
import {Err, Ok, Result} from '../Utils/Result';
import {AliasingEffect} from './AliasingEffects';
import {printFunction} from '../HIR';
import {printIdentifier, printPlace} from '../HIR/PrintHIR';
import {MutationKind} from './InferMutationAliasingFunctionEffects';
import {Result} from '../Utils/Result';
const DEBUG = false;
const VERBOSE = false;
/**
* This pass builds an abstract model of the heap and interprets the effects of the
* given function in order to determine the following:
* - The mutable ranges of all identifiers in the function
* - The externally-visible effects of the function, such as mutations of params and
* context-vars, aliasing between params/context-vars/return-value, and impure side
* effects.
* - The legacy `Effect` to store on each Place.
*
* This pass builds a data flow graph using the effects, tracking an abstract notion
* of "when" each effect occurs relative to the others. It then walks each mutation
* effect against the graph, updating the range of each node that would be reachable
* at the "time" that the effect occurred.
* Infers mutable ranges for all values in the program, using previously inferred
* mutation/aliasing effects. This pass builds a data flow graph using the effects,
* tracking an abstract notion of "when" each effect occurs relative to the others.
* It then walks each mutation effect against the graph, updating the range of each
* node that would be reachable at the "time" that the effect occurred.
*
* This pass also validates against invalid effects: any function that is reachable
* by being called, or via a Render effect, is validated against mutating globals
* or calling impure code.
*
* Note that this function also populates the outer function's aliasing effects with
* any mutations that apply to its params or context variables.
*
* ## Example
* A function expression such as the following:
* any mutations that apply to its params or context variables. For example, a
* function expression such as the following:
*
* ```
* (x) => { x.y = true }
* ```
*
* Would populate a `Mutate x` aliasing effect on the outer function.
*
* ## Returned Function Effects
*
* The function returns (if successful) a list of externally-visible effects.
* This is determined by simulating a conditional, transitive mutation against
* each param, context variable, and return value in turn, and seeing which other
* such values are affected. If they're affected, they must be captured, so we
* record a Capture.
*
* The only tricky bit is the return value, which could _alias_ (or even assign)
* one or more of the params/context-vars rather than just capturing. So we have
* to do a bit more tracking for returns.
*/
export function inferMutationAliasingRanges(
fn: HIRFunction,
{isFunctionExpression}: {isFunctionExpression: boolean},
): Result<Array<AliasingEffect>, CompilerError> {
// The set of externally-visible effects
const functionEffects: Array<AliasingEffect> = [];
): Result<void, CompilerError> {
if (VERBOSE) {
console.log();
console.log(printFunction(fn));
}
/**
* Part 1: Infer mutable ranges for values. We build an abstract model of
* values, the alias/capture edges between them, and the set of mutations.
@@ -133,6 +115,20 @@ export function inferMutationAliasingRanges(
seenBlocks.add(block.id);
for (const instr of block.instructions) {
if (
instr.value.kind === 'FunctionExpression' ||
instr.value.kind === 'ObjectMethod'
) {
state.create(instr.lvalue, {
kind: 'Function',
function: instr.value.loweredFunc.func,
});
} else {
for (const lvalue of eachInstructionLValue(instr)) {
state.create(lvalue, {kind: 'Object'});
}
}
if (instr.effects == null) continue;
for (const effect of instr.effects) {
if (effect.kind === 'Create') {
@@ -145,15 +141,6 @@ export function inferMutationAliasingRanges(
} else if (effect.kind === 'CreateFrom') {
state.createFrom(index++, effect.from, effect.into);
} else if (effect.kind === 'Assign') {
/**
* TODO: Invariant that the node is not initialized yet
*
* InferFunctionExpressionAliasingEffectSignatures currently infers
* Assign effects in some places that should be Alias, leading to
* Assign effects that reinitialize a value. The end result appears to
* be fine, but we should fix that inference pass so that we add the
* invariant here.
*/
if (!state.nodes.has(effect.into.identifier)) {
state.create(effect.into, {kind: 'Object'});
}
@@ -196,10 +183,8 @@ export function inferMutationAliasingRanges(
effect.kind === 'Impure'
) {
errors.push(effect.error);
functionEffects.push(effect);
} else if (effect.kind === 'Render') {
renders.push({index: index++, place: effect.place});
functionEffects.push(effect);
}
}
}
@@ -231,6 +216,10 @@ export function inferMutationAliasingRanges(
}
}
if (VERBOSE) {
console.log(state.debug());
console.log(pretty(mutations));
}
for (const mutation of mutations) {
state.mutate(
mutation.index,
@@ -245,6 +234,10 @@ export function inferMutationAliasingRanges(
for (const render of renders) {
state.render(render.index, render.place.identifier, errors);
}
if (DEBUG) {
console.log(pretty([...state.nodes.keys()]));
}
fn.aliasingEffects ??= [];
for (const param of [...fn.context, ...fn.params]) {
const place = param.kind === 'Identifier' ? param : param.place;
const node = state.nodes.get(place.identifier);
@@ -255,13 +248,13 @@ export function inferMutationAliasingRanges(
if (node.local != null) {
if (node.local.kind === MutationKind.Conditional) {
mutated = true;
functionEffects.push({
fn.aliasingEffects.push({
kind: 'MutateConditionally',
value: {...place, loc: node.local.loc},
});
} else if (node.local.kind === MutationKind.Definite) {
mutated = true;
functionEffects.push({
fn.aliasingEffects.push({
kind: 'Mutate',
value: {...place, loc: node.local.loc},
});
@@ -270,13 +263,13 @@ export function inferMutationAliasingRanges(
if (node.transitive != null) {
if (node.transitive.kind === MutationKind.Conditional) {
mutated = true;
functionEffects.push({
fn.aliasingEffects.push({
kind: 'MutateTransitiveConditionally',
value: {...place, loc: node.transitive.loc},
});
} else if (node.transitive.kind === MutationKind.Definite) {
mutated = true;
functionEffects.push({
fn.aliasingEffects.push({
kind: 'MutateTransitive',
value: {...place, loc: node.transitive.loc},
});
@@ -465,82 +458,10 @@ export function inferMutationAliasingRanges(
}
}
/**
* Part 3
* Finish populating the externally visible effects. Above we bubble-up the side effects
* (MutateFrozen/MutableGlobal/Impure/Render) as well as mutations of context variables.
* Here we populate an effect to create the return value as well as populating alias/capture
* effects for how data flows between the params, context vars, and return.
*/
const returns = fn.returns.identifier;
functionEffects.push({
kind: 'Create',
into: fn.returns,
value: isPrimitiveType(returns)
? ValueKind.Primitive
: isJsxType(returns.type)
? ValueKind.Frozen
: ValueKind.Mutable,
reason: ValueReason.KnownReturnSignature,
});
/**
* Determine precise data-flow effects by simulating transitive mutations of the params/
* captures and seeing what other params/context variables are affected. Anything that
* would be transitively mutated needs a capture relationship.
*/
const tracked: Array<Place> = [];
const ignoredErrors = new CompilerError();
for (const param of [...fn.params, ...fn.context, fn.returns]) {
const place = param.kind === 'Identifier' ? param : param.place;
tracked.push(place);
if (VERBOSE) {
console.log(printFunction(fn));
}
for (const into of tracked) {
const mutationIndex = index++;
state.mutate(
mutationIndex,
into.identifier,
null,
true,
MutationKind.Conditional,
into.loc,
ignoredErrors,
);
for (const from of tracked) {
if (
from.identifier.id === into.identifier.id ||
from.identifier.id === fn.returns.identifier.id
) {
continue;
}
const fromNode = state.nodes.get(from.identifier);
CompilerError.invariant(fromNode != null, {
reason: `Expected a node to exist for all parameters and context variables`,
loc: into.loc,
});
if (fromNode.lastMutated === mutationIndex) {
if (into.identifier.id === fn.returns.identifier.id) {
// The return value could be any of the params/context variables
functionEffects.push({
kind: 'Alias',
from,
into,
});
} else {
// Otherwise params/context-vars can only capture each other
functionEffects.push({
kind: 'Capture',
from,
into,
});
}
}
}
}
if (errors.hasErrors() && !isFunctionExpression) {
return Err(errors);
}
return Ok(functionEffects);
return errors.asResult();
}
function appendFunctionErrors(errors: CompilerError, fn: HIRFunction): void {
@@ -556,12 +477,6 @@ function appendFunctionErrors(errors: CompilerError, fn: HIRFunction): void {
}
}
export enum MutationKind {
None = 0,
Conditional = 1,
Definite = 2,
}
type Node = {
id: Identifier;
createdFrom: Map<Identifier, number>;
@@ -570,7 +485,6 @@ type Node = {
edges: Array<{index: number; node: Identifier; kind: 'capture' | 'alias'}>;
transitive: {kind: MutationKind; loc: SourceLocation} | null;
local: {kind: MutationKind; loc: SourceLocation} | null;
lastMutated: number;
value:
| {kind: 'Object'}
| {kind: 'Phi'}
@@ -588,7 +502,6 @@ class AliasingState {
edges: [],
transitive: null,
local: null,
lastMutated: 0,
value,
});
}
@@ -598,6 +511,11 @@ class AliasingState {
const fromNode = this.nodes.get(from.identifier);
const toNode = this.nodes.get(into.identifier);
if (fromNode == null || toNode == null) {
if (VERBOSE) {
console.log(
`skip: createFrom ${printPlace(from)}${!!fromNode} -> ${printPlace(into)}${!!toNode}`,
);
}
return;
}
fromNode.edges.push({index, node: into.identifier, kind: 'alias'});
@@ -610,6 +528,11 @@ class AliasingState {
const fromNode = this.nodes.get(from.identifier);
const toNode = this.nodes.get(into.identifier);
if (fromNode == null || toNode == null) {
if (VERBOSE) {
console.log(
`skip: capture ${printPlace(from)}${!!fromNode} -> ${printPlace(into)}${!!toNode}`,
);
}
return;
}
fromNode.edges.push({index, node: into.identifier, kind: 'capture'});
@@ -622,6 +545,11 @@ class AliasingState {
const fromNode = this.nodes.get(from.identifier);
const toNode = this.nodes.get(into.identifier);
if (fromNode == null || toNode == null) {
if (VERBOSE) {
console.log(
`skip: assign ${printPlace(from)}${!!fromNode} -> ${printPlace(into)}${!!toNode}`,
);
}
return;
}
fromNode.edges.push({index, node: into.identifier, kind: 'alias'});
@@ -670,13 +598,17 @@ class AliasingState {
mutate(
index: number,
start: Identifier,
// Null is used for simulated mutations
end: InstructionId | null,
end: InstructionId,
transitive: boolean,
kind: MutationKind,
loc: SourceLocation,
errors: CompilerError,
): void {
if (DEBUG) {
console.log(
`mutate ix=${index} start=$${start.id} end=[${end}]${transitive ? ' transitive' : ''} kind=${kind}`,
);
}
const seen = new Set<Identifier>();
const queue: Array<{
place: Identifier;
@@ -691,14 +623,21 @@ class AliasingState {
seen.add(current);
const node = this.nodes.get(current);
if (node == null) {
if (DEBUG) {
console.log(
`no node! ${printIdentifier(start)} for identifier ${printIdentifier(current)}`,
);
}
continue;
}
node.lastMutated = Math.max(node.lastMutated, index);
if (end != null) {
node.id.mutableRange.end = makeInstructionId(
Math.max(node.id.mutableRange.end, end),
if (DEBUG) {
console.log(
` mutate $${node.id.id} transitive=${transitive} direction=${direction}`,
);
}
node.id.mutableRange.end = makeInstructionId(
Math.max(node.id.mutableRange.end, end),
);
if (
node.value.kind === 'Function' &&
node.transitive == null &&
@@ -762,5 +701,37 @@ class AliasingState {
}
}
}
if (DEBUG) {
const nodes = new Map();
for (const id of seen) {
const node = this.nodes.get(id);
nodes.set(id.id, node);
}
console.log(pretty(nodes));
}
}
debug(): string {
return pretty(this.nodes);
}
}
export function pretty(v: any): string {
return prettyFormat(v, {
plugins: [
{
test: v =>
v !== null && typeof v === 'object' && v.kind === 'Identifier',
serialize: v => printPlace(v),
},
{
test: v =>
v !== null &&
typeof v === 'object' &&
typeof v.declarationId === 'number',
serialize: v =>
`${printIdentifier(v)}:${v.mutableRange.start}:${v.mutableRange.end}`,
},
],
});
}

View File

@@ -11,7 +11,6 @@ import {
Environment,
FunctionExpression,
GeneratedSource,
GotoTerminal,
GotoVariant,
HIRFunction,
IdentifierId,
@@ -20,7 +19,6 @@ import {
Place,
isStatementBlockKind,
makeInstructionId,
mergeConsecutiveBlocks,
promoteTemporary,
reversePostorderBlocks,
} from '../HIR';
@@ -75,10 +73,6 @@ import {retainWhere} from '../Utils/utils';
* - All return statements in the original function expression are replaced with a
* StoreLocal to the temporary we allocated before plus a Goto to the fallthrough
* block (code following the CallExpression).
*
* Note that if the inliined function has only one return, we avoid the labeled block
* and fully inline the code. The original return is replaced with an assignmen to the
* IIFE's call expression lvalue.
*/
export function inlineImmediatelyInvokedFunctionExpressions(
fn: HIRFunction,
@@ -152,75 +146,37 @@ export function inlineImmediatelyInvokedFunctionExpressions(
*/
block.instructions.length = ii;
if (hasSingleExitReturnTerminal(body.loweredFunc.func)) {
block.terminal = {
kind: 'goto',
block: body.loweredFunc.func.body.entry,
id: block.terminal.id,
loc: block.terminal.loc,
variant: GotoVariant.Break,
} as GotoTerminal;
for (const block of body.loweredFunc.func.body.blocks.values()) {
if (block.terminal.kind === 'return') {
block.instructions.push({
id: makeInstructionId(0),
loc: block.terminal.loc,
lvalue: instr.lvalue,
value: {
kind: 'LoadLocal',
loc: block.terminal.loc,
place: block.terminal.value,
},
effects: null,
});
block.terminal = {
kind: 'goto',
block: continuationBlockId,
id: block.terminal.id,
loc: block.terminal.loc,
variant: GotoVariant.Break,
} as GotoTerminal;
}
}
for (const [id, block] of body.loweredFunc.func.body.blocks) {
block.preds.clear();
fn.body.blocks.set(id, block);
}
} else {
/*
* To account for multiple returns within the lambda, we treat the lambda
* as if it were a single labeled statement, and replace all returns with gotos
* to the label fallthrough.
*/
const newTerminal: LabelTerminal = {
block: body.loweredFunc.func.body.entry,
id: makeInstructionId(0),
kind: 'label',
fallthrough: continuationBlockId,
loc: block.terminal.loc,
};
block.terminal = newTerminal;
/*
* To account for complex control flow within the lambda, we treat the lambda
* as if it were a single labeled statement, and replace all returns with gotos
* to the label fallthrough.
*/
const newTerminal: LabelTerminal = {
block: body.loweredFunc.func.body.entry,
id: makeInstructionId(0),
kind: 'label',
fallthrough: continuationBlockId,
loc: block.terminal.loc,
};
block.terminal = newTerminal;
// We store the result in the IIFE temporary
const result = instr.lvalue;
// We store the result in the IIFE temporary
const result = instr.lvalue;
// Declare the IIFE temporary
declareTemporary(fn.env, block, result);
// Declare the IIFE temporary
declareTemporary(fn.env, block, result);
// Promote the temporary with a name as we require this to persist
if (result.identifier.name == null) {
promoteTemporary(result.identifier);
}
// Promote the temporary with a name as we require this to persist
promoteTemporary(result.identifier);
/*
* Rewrite blocks from the lambda to replace any `return` with a
* store to the result and `goto` the continuation block
*/
for (const [id, block] of body.loweredFunc.func.body.blocks) {
block.preds.clear();
rewriteBlock(fn.env, block, continuationBlockId, result);
fn.body.blocks.set(id, block);
}
/*
* Rewrite blocks from the lambda to replace any `return` with a
* store to the result and `goto` the continuation block
*/
for (const [id, block] of body.loweredFunc.func.body.blocks) {
block.preds.clear();
rewriteBlock(fn.env, block, continuationBlockId, result);
fn.body.blocks.set(id, block);
}
/*
@@ -243,7 +199,7 @@ export function inlineImmediatelyInvokedFunctionExpressions(
if (inlinedFunctions.size !== 0) {
// Remove instructions that define lambdas which we inlined
for (const block of fn.body.blocks.values()) {
for (const [, block] of fn.body.blocks) {
retainWhere(
block.instructions,
instr => !inlinedFunctions.has(instr.lvalue.identifier.id),
@@ -257,25 +213,9 @@ export function inlineImmediatelyInvokedFunctionExpressions(
reversePostorderBlocks(fn.body);
markInstructionIds(fn.body);
markPredecessors(fn.body);
mergeConsecutiveBlocks(fn);
}
}
/**
* Returns true if the function has a single exit terminal (throw/return) which is a return
*/
function hasSingleExitReturnTerminal(fn: HIRFunction): boolean {
let hasReturn = false;
let exitCount = 0;
for (const [, block] of fn.body.blocks) {
if (block.terminal.kind === 'return' || block.terminal.kind === 'throw') {
hasReturn ||= block.terminal.kind === 'return';
exitCount++;
}
}
return exitCount === 1 && hasReturn;
}
/*
* Rewrites the block so that all `return` terminals are replaced:
* * Add a StoreLocal <returnValue> = <terminal.value>

View File

@@ -1,544 +0,0 @@
# The Mutability & Aliasing Model
This document describes the new (as of June 2025) mutability and aliasing model powering React Compiler. The mutability and aliasing system is a conceptual subcomponent whose primary role is to determine minimal sets of values that mutate together, and the range of instructions over which those mutations occur. These minimal sets of values that mutate together, and the corresponding instructions doing those mutations, are ultimately grouped into reactive scopes, which then translate into memoization blocks in the output (after substantial additional processing described in the comments of those passes).
To build an intuition, consider the following example:
```js
function Component() {
// a is created and mutated over the course of these two instructions:
const a = {};
mutate(a);
// b and c are created and mutated together — mutate might modify b via c
const b = {};
const c = {b};
mutate(c);
// does not modify a/b/c
return <Foo a={a} c={c} />
}
```
The goal of mutability and aliasing inference is to understand the set of instructions that create/modify a, b, and c.
In code, the mutability and aliasing model is compromised of the following phases:
* `InferMutationAliasingEffects`. Infers a set of mutation and aliasing effects for each instruction. The approach is to generate a set of candidate effects based purely on the semantics of each instruction and the types of the operands, then use abstract interpretation to determine the actual effects (or errros) that would apply. For example, an instruction that by default has a Capture effect might downgrade to an ImmutableCapture effect if the value is known to be frozen.
* `InferMutationAliasingRanges`. Infers a mutable range (start:end instruction ids) for each value in the program, and annotates each Place with its effect type for usage in later passes. This builds a graph of data flow through the program over time in order to understand which mutations effect which values.
* `InferReactiveScopeVariables`. Given the per-Place effects, determines disjoint sets of values that mutate together and assigns all identifiers in each set to a unique scope, and updates the range to include the ranges of all constituent values.
Finally, `AnalyzeFunctions` needs to understand the mutation and aliasing semantics of nested FunctionExpression and ObjectMethod values. `AnalyzeFunctions` calls `InferFunctionExpressionAliasingEffectsSignature` to determine the publicly observable set of mutation/aliasing effects for nested functions.
## Mutation and Aliasing Effects
The inference model is based on a set of "effects" that describe subtle aspects of mutation, aliasing, and other changes to the state of values over time
### Creation Effects
#### Create
```js
{
kind: 'Create';
into: Place;
value: ValueKind;
reason: ValueReason;
}
```
Describes the creation of a new value with the given kind, and reason for having that kind. For example, `x = 10` might have an effect like `Create x = ValueKind.Primitive [ValueReason.Other]`.
#### CreateFunction
```js
{
kind: 'CreateFunction';
captures: Array<Place>;
function: FunctionExpression | ObjectMethod;
into: Place;
}
```
Describes the creation of new function value, capturing the given set of mutable values. CreateFunction is used to specifically track function types so that we can precisely model calls to those functions with `Apply`.
#### Apply
```js
{
kind: 'Apply';
receiver: Place;
function: Place; // same as receiver for function calls
mutatesFunction: boolean; // indicates if this is a type that we consdier to mutate the function itself by default
args: Array<Place | SpreadPattern | Hole>;
into: Place; // where result is stored
signature: FunctionSignature | null;
}
```
Describes the potential creation of a value by calling a function. This models `new`, function calls, and method calls. The inference algorithm uses the most precise signature it can determine:
* If the function is a locally created function expression, we use a signature inferred from the behavior of that function to interpret the effects of calling it with the given arguments.
* Else if the function has a known aliasing signature (new style precise effects signature), we apply the arguments to that signature to get a precise set of effects.
* Else if the function has a legacy style signature (with per-param effects) we convert the legacy per-Place effects into aliasing effects (described in this doc) and apply those.
* Else fall back to inferring a generic set of effects.
The generic fallback is to assume:
- The return value may alias any of the arguments (Alias param -> return)
- Any arguments *may* be transitively mutated (MutateTransitiveConditionally param)
- Any argument may be captured into any other argument (Capture paramN -> paramM for all N,M where N != M)
### Aliasing Effects
These effects describe data-flow only, separately from mutation or other state-changing semantics.
#### Assign
```js
{
kind: 'Assign';
from: Place;
into: Place;
}
```
Describes an `x = y` assignment, where the receiving (into) value is overwritten with a new (from) value. After this effect, any previous assignments/aliases to the receiving value are dropped. Note that `Alias` initializes the receiving value.
> TODO: InferMutationAliasingRanges may not fully reset aliases on encountering this effect
#### Alias
```js
{
kind: 'Alias';
from: Place;
into: Place;
}
```
Describes that an assignment _may_ occur, but that the possible assignment is non-exclusive. The canonical use-case for `Alias` is a function that may return more than one of its arguments, such as `(x, y, z) => x ? y : z`. Here, the result of this function may be `y` or `z`, but neither one overwrites the other. Note that `Alias` does _not_ initialize the receiving value: it should always be paired with an effect to create the receiving value.
#### Capture
```js
{
kind: 'Capture';
from: Place;
into: Place;
}
```
Describes that a reference to one variable (from) is stored within another value (into). Examples include:
- An array expression captures the items of the array (`array = [capturedValue]`)
- Array.prototype.push captures the pushed values into the array (`array.push(capturedValue)`)
- Property assignment captures the value onto the object (`object.property = capturedValue`)
#### CreateFrom
```js
{
kind: 'CreateFrom';
from: Place;
into: Place;
}
```
This is somewhat the inverse of `Capture`. The `CreateFrom` effect describes that a variable is initialized by extracting _part_ of another value, without taking a direct alias to the full other value. Examples include:
- Indexing into an array (`createdFrom = array[0]`)
- Reading an object property (`createdFrom = object.property`)
- Getting a Map key (`createdFrom = map.get(key)`)
#### ImmutableCapture
Describes immutable data flow from one value to another. This is not currently used for anything, but is intended to eventually power a more sophisticated escape analysis.
### State-Changing Effects
The following effects describe state changes to specific values, not data flow. In many cases, JavaScript semantics will involve a combination of both data-flow effects *and* state-change effects. For example, `object.property = value` has data flow (`Capture object <- value`) and mutation (`Mutate object`).
#### Freeze
```js
{
kind: 'Freeze',
// The reference being frozen
value: Place;
// The reason the value is frozen (passed to a hook, passed to jsx, etc)
reason: ValueReason;
}
```
Once a reference to a value has been passed to React, that value is generally not safe to mutate further. This is not a strictly required property of React, but is a natural consequence of making components and hooks composable without leaking implementation details. Concretely, once a value has been passed as a JSX prop, passed as argument to a hook, or returned from a hook, it must be assumed that the other "side" — receiver of the prop/argument/return value — will use that value as an input to an effect or memoization unit. Mutating that value (instead of creating a new value) will fail to cause the consuming computation to update:
```js
// INVALID DO NOT DO THIS
function Component(props) {
const array = useArray(props.value);
// OOPS! this value is memoized, the array won't get re-created
// when `props.value` changes, so we might just keep pushing new
// values to the same array on every render!
array.push(props.otherValue);
}
function useArray(a) {
return useMemo(() => [a], [a]);
}
```
The **Freeze** effect accepts a variable reference and a reason that the value is being frozen. Note: _freeze only applies to the reference, not the underlying value_. Our inference is conservative, and assumes that there may still be other references to the same underlying value which are mutated later. For example:
```js
const x = {};
const y = [];
x.y = y;
freeze(y); // y _reference_ is frozen
x.y.push(props.value); // but y is still considered mutable bc of this
```
#### Mutate (and MutateConditionally)
```js
{
kind: 'Mutate';
value: Place;
}
```
Mutate indicates that a value is mutated, without modifying any of the values that it may transitively have captured. Canonical examples include:
- Pushing an item onto an array modifies the array, but does not modify any items stored _within_ the array (unless the array has a reference to itself!)
- Assigning a value to an object property modifies the object, but not any values stored in the object's other properties.
This helps explain the distinction between Assign/Alias and Capture: Mutate only affects assign/alias but not captures.
`MutateConditionally` is an alternative in which the mutation _may_ happen depending on the type of the value. The conditional variant is not generally used and included for completeness.
#### MutateTransitiveConditionally (and MutateTransitive)
`MutateTransitiveConditionally` represents an operation that may mutate _any_ aspect of a value, including reaching arbitrarily deep into nested values to mutate them. This is the default semantic for unknown functions — we have no idea what they do, so we assume that they are idempotent but may mutate any aspect of the mutable values that are passed to them.
There is also `MutateTransitive` for completeness, but this is not generally used.
### Side Effects
Finally, there are a few effects that describe error, or potential error, conditions:
- `MutateFrozen` is always an error, because it indicates known mutation of a value that should not be mutated.
- `MutateGlobal` indicates known mutation of a global value, which is not safe during render. This effect is an error if reachable during render, but allowed if only reachable via an event handler or useEffect.
- `Impure` indicates calling some other logic that is impure/side-effecting. This is an error if reachable during render, but allowed if only reachable via an event handler or useEffect.
- TODO: we could probably merge this and MutateGlobal
- `Render` indicates a value that is not mutated, but is known to be called during render. It's used for a few particular places like JSX tags and JSX children, which we assume are accessed during render (while other props may be event handlers etc). This helps to detect more MutateGlobal/Impure effects and reject more invalid programs.
## Rules
### Mutation of Alias Mutates the Source Value
```
Alias a <- b
Mutate a
=>
Mutate b
```
Example:
```js
const a = maybeIdentity(b); // Alias a <- b
a.property = value; // a could be b, so this mutates b
```
### Mutation of Assignment Mutates the Source Value
```
Assign a <- b
Mutate a
=>
Mutate b
```
Example:
```js
const a = b;
a.property = value // a _is_ b, this mutates b
```
### Mutation of CreateFrom Mutates the Source Value
```
CreateFrom a <- b
Mutate a
=>
Mutate b
```
Example:
```js
const a = b[index];
a.property = value // the contents of b are transitively mutated
```
### Mutation of Capture Does *Not* Mutate the Source Value
```
Capture a <- b
Mutate a
!=>
~Mutate b~
```
Example:
```js
const a = {};
a.b = b;
a.property = value; // mutates a, not b
```
### Mutation of Source Affects Alias, Assignment, CreateFrom, and Capture
```
Alias a <- b OR Assign a <- b OR CreateFrom a <- b OR Capture a <- b
Mutate b
=>
Mutate a
```
A derived value changes when it's source value is mutated.
Example:
```js
const x = {};
const y = [x];
x.y = true; // this changes the value within `y` ie mutates y
```
### TransitiveMutation of Alias, Assignment, CreateFrom, or Capture Mutates the Source
```
Alias a <- b OR Assign a <- b OR CreateFrom a <- b OR Capture a <- b
MutateTransitive a
=>
MutateTransitive b
```
Remember, the intuition for a transitive mutation is that it's something that could traverse arbitrarily deep into an object and mutate whatever it finds. Imagine something that recurses into every nested object/array and sets `.field = value`. Given a function `mutate()` that does this, then:
```js
const a = b; // assign
mutate(a); // clearly can transitively mutate b
const a = maybeIdentity(b); // alias
mutate(a); // clearly can transitively mutate b
const a = b[index]; // createfrom
mutate(a); // clearly can transitively mutate b
const a = {};
a.b = b; // capture
mutate(a); // can transitively mutate b
```
### Freeze Does Not Freeze the Value
Freeze does not freeze the value itself:
```
Create x
Assign y <- x OR Alias y <- x OR CreateFrom y <- x OR Capture y <- x
Freeze y
!=>
~Freeze x~
```
This means that subsequent mutations of the original value are valid:
```
Create x
Assign y <- x OR Alias y <- x OR CreateFrom y <- x OR Capture y <- x
Freeze y
Mutate x
=>
Mutate x (mutation is ok)
```
As well as mutations through other assignments/aliases/captures/createfroms of the original value:
```
Create x
Assign y <- x OR Alias y <- x OR CreateFrom y <- x OR Capture y <- x
Freeze y
Alias z <- x OR Capture z <- x OR CreateFrom z <- x OR Assign z <- x
Mutate z
=>
Mutate x (mutation is ok)
```
### Freeze Freezes The Reference
Although freeze doesn't freeze the value, it does affect the reference. The reference cannot be used to mutate.
Conditional mutations of the reference are no-ops:
```
Create x
Assign y <- x OR Alias y <- x OR CreateFrom y <- x OR Capture y <- x
Freeze y
MutateConditional y
=>
(no mutation)
```
And known mutations of the reference are errors:
```
Create x
Assign y <- x OR Alias y <- x OR CreateFrom y <- x OR Capture y <- x
Freeze y
MutateConditional y
=>
MutateFrozen y error=...
```
### Corollary: Transitivity of Assign/Alias/CreateFrom/Capture
A key part of the inference model is inferring a signature for function expressions. The signature is a minimal set of effects that describes the publicly observable behavior of the function. This can include "global" effects like side effects (MutateGlobal/Impure) as well as mutations/aliasing of parameters and free variables.
In order to determine the aliasing of params and free variables into each other and/or the return value, we may encounter chains of assign, alias, createfrom, and capture effects. For example:
```js
const f = (x) => {
const y = [x]; // capture y <- x
const z = y[0]; // createfrom z <- y
return z; // assign return <- z
}
// <Effect> return <- x
```
In this example we can see that there should be some effect on `f` that tracks the flow of data from `x` into the return value. The key constraint is preserving the semantics around how local/transitive mutations of the destination would affect the source.
#### Each of the effects is transitive with itself
```
Assign b <- a
Assign c <- b
=>
Assign c <- a
```
```
Alias b <- a
Alias c <- b
=>
Alias c <- a
```
```
CreateFrom b <- a
CreateFrom c <- b
=>
CreateFrom c <- a
```
```
Capture b <- a
Capture c <- b
=>
Capture c <- a
```
#### Alias > Assign
```
Assign b <- a
Alias c <- b
=>
Alias c <- a
```
```
Alias b <- a
Assign c <- b
=>
Alias c <- a
```
### CreateFrom > Assign/Alias
Intuition:
```
CreateFrom b <- a
Alias c <- b OR Assign c <- b
=>
CreateFrom c <- a
```
```
Alias b <- a OR Assign b <- a
CreateFrom c <- b
=>
CreateFrom c <- a
```
### Capture > Assign/Alias
Intuition: capturing means that a local mutation of the destination will not affect the source, so we preserve the capture.
```
Capture b <- a
Alias c <- b OR Assign c <- b
=>
Capture c <- a
```
```
Alias b <- a OR Assign b <- a
Capture c <- b
=>
Capture c <- a
```
### Capture And CreateFrom
Intuition: these effects are inverses of each other (capturing into an object, extracting from an object). The result is based on the order of operations:
Capture then CreatFrom is equivalent to Alias: we have to assume that the result _is_ the original value and that a local mutation of the result could mutate the original.
```js
const b = [a]; // capture
const c = b[0]; // createfrom
mutate(c); // this clearly can mutate a, so the result must be one of Assign/Alias/CreateFrom
```
We use Alias as the return type because the mutability kind of the result is not derived from the source value (there's a fresh object in between due to the capture), so the full set of effects in practice would be a Create+Alias.
```
Capture b <- a
CreateFrom c <- b
=>
Alias c <- a
```
Meanwhile the opposite direction preserves the capture, because the result is not the same as the source:
```js
const b = a[0]; // createfrom
const c = [b]; // capture
mutate(c); // does not mutate a, so the result must be Capture
```
```
CreateFrom b <- a
Capture c <- b
=>
Capture c <- a
```

View File

@@ -25,6 +25,7 @@ import {
makeBlockId,
makeInstructionId,
makePropertyLiteral,
makeType,
markInstructionIds,
promoteTemporary,
reversePostorderBlocks,
@@ -252,6 +253,7 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
env,
params: [obj],
returnTypeAnnotation: null,
returnType: makeType(),
returns: createTemporaryPlace(env, GeneratedSource),
context: [],
effects: null,

View File

@@ -21,6 +21,7 @@ import {
makeBlockId,
makeIdentifierName,
makeInstructionId,
makeType,
ObjectProperty,
Place,
promoteTemporary,
@@ -367,6 +368,7 @@ function emitOutlinedFn(
env,
params: [propsObj],
returnTypeAnnotation: null,
returnType: makeType(),
returns: createTemporaryPlace(env, GeneratedSource),
context: [],
effects: null,

View File

@@ -349,9 +349,11 @@ function codegenReactiveFunction(
fn: ReactiveFunction,
): Result<CodegenFunction, CompilerError> {
for (const param of fn.params) {
const place = param.kind === 'Identifier' ? param : param.place;
cx.temp.set(place.identifier.declarationId, null);
cx.declare(place.identifier);
if (param.kind === 'Identifier') {
cx.temp.set(param.identifier.declarationId, null);
} else {
cx.temp.set(param.place.identifier.declarationId, null);
}
}
const params = fn.params.map(param => convertParameter(param));
@@ -1181,7 +1183,7 @@ function codegenTerminal(
? codegenPlaceToExpression(cx, case_.test)
: null;
const block = codegenBlock(cx, case_.block!);
return t.switchCase(test, block.body.length === 0 ? [] : [block]);
return t.switchCase(test, [block]);
}),
);
}

View File

@@ -79,10 +79,6 @@ export function extractScopeDeclarationsFromDestructuring(
fn: ReactiveFunction,
): void {
const state = new State(fn.env);
for (const param of fn.params) {
const place = param.kind === 'Identifier' ? param : param.place;
state.declared.add(place.identifier.declarationId);
}
visitReactiveFunction(fn, new Visitor(), state);
}

View File

@@ -829,14 +829,12 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor<
};
}
case 'UnsupportedNode': {
const lvalues = [];
if (lvalue !== null) {
lvalues.push({place: lvalue, level: MemoizationLevel.Never});
}
return {
lvalues,
rvalues: [],
};
CompilerError.invariant(false, {
reason: `Unexpected unsupported node`,
description: null,
loc: value.loc,
suggestions: null,
});
}
default: {
assertExhaustive(
@@ -1066,29 +1064,12 @@ class PruneScopesTransform extends ReactiveFunctionTransform<
const value = instruction.value;
if (value.kind === 'StoreLocal' && value.lvalue.kind === 'Reassign') {
// Complex cases of useMemo inlining result in a temporary that is reassigned
const ids = getOrInsertDefault(
this.reassignments,
value.lvalue.place.identifier.declarationId,
new Set(),
);
ids.add(value.value.identifier);
} else if (
value.kind === 'LoadLocal' &&
value.place.identifier.scope != null &&
instruction.lvalue != null &&
instruction.lvalue.identifier.scope == null
) {
/*
* Simpler cases result in a direct assignment to the original lvalue, with a
* LoadLocal
*/
const ids = getOrInsertDefault(
this.reassignments,
instruction.lvalue.identifier.declarationId,
new Set(),
);
ids.add(value.place.identifier);
} else if (value.kind === 'FinishMemoize') {
let decls;
if (value.decl.identifier.scope == null) {

View File

@@ -90,8 +90,7 @@ function apply(func: HIRFunction, unifier: Unifier): void {
}
}
}
const returns = func.returns.identifier;
returns.type = unifier.get(returns.type);
func.returnType = unifier.get(func.returnType);
}
type TypeEquation = {
@@ -144,12 +143,12 @@ function* generate(
}
}
if (returnTypes.length > 1) {
yield equation(func.returns.identifier.type, {
yield equation(func.returnType, {
kind: 'Phi',
operands: returnTypes,
});
} else if (returnTypes.length === 1) {
yield equation(func.returns.identifier.type, returnTypes[0]!);
yield equation(func.returnType, returnTypes[0]!);
}
}
@@ -408,7 +407,7 @@ function* generateInstructionTypes(
yield equation(left, {
kind: 'Function',
shapeId: BuiltInFunctionId,
return: value.loweredFunc.func.returns.identifier.type,
return: value.loweredFunc.func.returnType,
isConstructor: false,
});
break;

View File

@@ -445,13 +445,11 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
*/
this.recordTemporaries(instruction, state);
const value = instruction.value;
// Track reassignments from inlining of manual memo
if (
value.kind === 'StoreLocal' &&
value.lvalue.kind === 'Reassign' &&
state.manualMemoState != null
) {
// Complex cases of inlining end up with a temporary that is reassigned
const ids = getOrInsertDefault(
state.manualMemoState.reassignments,
value.lvalue.place.identifier.declarationId,
@@ -459,21 +457,6 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
);
ids.add(value.value.identifier);
}
if (
value.kind === 'LoadLocal' &&
value.place.identifier.scope != null &&
instruction.lvalue != null &&
instruction.lvalue.identifier.scope == null &&
state.manualMemoState != null
) {
// Simpler cases of inlining assign to the original IIFE lvalue
const ids = getOrInsertDefault(
state.manualMemoState.reassignments,
instruction.lvalue.identifier.declarationId,
new Set(),
);
ids.add(value.place.identifier);
}
if (value.kind === 'StartMemoize') {
let depsFromSource: Array<ManualMemoDependency> | null = null;
if (value.deps != null) {

View File

@@ -175,14 +175,21 @@ import {
* and mutability.
*/
function Component(t0) {
const $ = _c(2);
const $ = _c(4);
const { prop } = t0;
let t1;
if ($[0] !== prop) {
const obj = shallowCopy(prop);
const aliasedObj = identity(obj);
const id = [obj.id];
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);

View File

@@ -26,16 +26,20 @@ import { c as _c } from "react/compiler-runtime";
import { getNull } from "shared-runtime";
function Component(props) {
const $ = _c(2);
const $ = _c(3);
let t0;
let items;
if ($[0] !== props.a) {
items = getNull() ?? [];
t0 = getNull() ?? [];
items = t0;
items.push(props.a);
$[0] = props.a;
$[1] = items;
$[2] = t0;
} else {
items = $[1];
t0 = $[2];
}
return items;
}

View File

@@ -52,13 +52,15 @@ function Component(t0) {
}
const onClick = t1;
let t2;
let t3;
if ($[2] !== onClick) {
t2 = <div onClick={onClick}>{someGlobal.value}</div>;
t3 = <div onClick={onClick}>{someGlobal.value}</div>;
$[2] = onClick;
$[3] = t2;
$[3] = t3;
} else {
t2 = $[3];
t3 = $[3];
}
t2 = t3;
return t2;
}

View File

@@ -30,46 +30,50 @@ function Component(props) {
const $ = _c(4);
const [x] = useState(0);
let t0;
if ($[0] !== x) {
t0 = calculateExpensiveNumber(x);
$[0] = x;
$[1] = t0;
} else {
t0 = $[1];
}
const expensiveNumber = t0;
let t1;
if ($[2] !== expensiveNumber) {
t1 = <div>{expensiveNumber}</div>;
$[2] = expensiveNumber;
$[3] = t1;
if ($[0] !== x) {
t1 = calculateExpensiveNumber(x);
$[0] = x;
$[1] = t1;
} else {
t1 = $[3];
t1 = $[1];
}
return t1;
t0 = t1;
const expensiveNumber = t0;
let t2;
if ($[2] !== expensiveNumber) {
t2 = <div>{expensiveNumber}</div>;
$[2] = expensiveNumber;
$[3] = t2;
} else {
t2 = $[3];
}
return t2;
}
function Component2(props) {
const $ = _c(4);
const [x] = useState(0);
let t0;
if ($[0] !== x) {
t0 = calculateExpensiveNumber(x);
$[0] = x;
$[1] = t0;
} else {
t0 = $[1];
}
const expensiveNumber = t0;
let t1;
if ($[2] !== expensiveNumber) {
t1 = <div>{expensiveNumber}</div>;
$[2] = expensiveNumber;
$[3] = t1;
if ($[0] !== x) {
t1 = calculateExpensiveNumber(x);
$[0] = x;
$[1] = t1;
} else {
t1 = $[3];
t1 = $[1];
}
return t1;
t0 = t1;
const expensiveNumber = t0;
let t2;
if ($[2] !== expensiveNumber) {
t2 = <div>{expensiveNumber}</div>;
$[2] = expensiveNumber;
$[3] = t2;
} else {
t2 = $[3];
}
return t2;
}
```

View File

@@ -32,46 +32,50 @@ function Component(props) {
const $ = _c(4);
const [x] = useState(0);
let t0;
if ($[0] !== x) {
t0 = calculateExpensiveNumber(x);
$[0] = x;
$[1] = t0;
} else {
t0 = $[1];
}
const expensiveNumber = t0;
let t1;
if ($[2] !== expensiveNumber) {
t1 = <div>{expensiveNumber}</div>;
$[2] = expensiveNumber;
$[3] = t1;
if ($[0] !== x) {
t1 = calculateExpensiveNumber(x);
$[0] = x;
$[1] = t1;
} else {
t1 = $[3];
t1 = $[1];
}
return t1;
t0 = t1;
const expensiveNumber = t0;
let t2;
if ($[2] !== expensiveNumber) {
t2 = <div>{expensiveNumber}</div>;
$[2] = expensiveNumber;
$[3] = t2;
} else {
t2 = $[3];
}
return t2;
}
function Component2(props) {
const $ = _c(4);
const [x] = useState(0);
let t0;
if ($[0] !== x) {
t0 = calculateExpensiveNumber(x);
$[0] = x;
$[1] = t0;
} else {
t0 = $[1];
}
const expensiveNumber = t0;
let t1;
if ($[2] !== expensiveNumber) {
t1 = <div>{expensiveNumber}</div>;
$[2] = expensiveNumber;
$[3] = t1;
if ($[0] !== x) {
t1 = calculateExpensiveNumber(x);
$[0] = x;
$[1] = t1;
} else {
t1 = $[3];
t1 = $[1];
}
return t1;
t0 = t1;
const expensiveNumber = t0;
let t2;
if ($[2] !== expensiveNumber) {
t2 = <div>{expensiveNumber}</div>;
$[2] = expensiveNumber;
$[3] = t2;
} else {
t2 = $[3];
}
return t2;
}
```

View File

@@ -30,23 +30,25 @@ function Component(props) {
const $ = _c(4);
const [x] = React.useState(0);
let t0;
if ($[0] !== x) {
t0 = calculateExpensiveNumber(x);
$[0] = x;
$[1] = t0;
} else {
t0 = $[1];
}
const expensiveNumber = t0;
let t1;
if ($[2] !== expensiveNumber) {
t1 = <div>{expensiveNumber}</div>;
$[2] = expensiveNumber;
$[3] = t1;
if ($[0] !== x) {
t1 = calculateExpensiveNumber(x);
$[0] = x;
$[1] = t1;
} else {
t1 = $[3];
t1 = $[1];
}
return t1;
t0 = t1;
const expensiveNumber = t0;
let t2;
if ($[2] !== expensiveNumber) {
t2 = <div>{expensiveNumber}</div>;
$[2] = expensiveNumber;
$[3] = t2;
} else {
t2 = $[3];
}
return t2;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -36,28 +36,30 @@ function Component(props) {
const $ = _c(4);
const [x] = React.useState(0);
let t0;
if ($[0] !== x) {
t0 = calculateExpensiveNumber(x);
$[0] = x;
$[1] = t0;
} else {
t0 = $[1];
}
const expensiveNumber = t0;
let t1;
if ($[0] !== x) {
t1 = calculateExpensiveNumber(x);
$[0] = x;
$[1] = t1;
} else {
t1 = $[1];
}
t0 = t1;
const expensiveNumber = t0;
let t2;
if ($[2] !== expensiveNumber) {
t1 = (
t2 = (
<div>
{expensiveNumber}
{`${someImport}`}
</div>
);
$[2] = expensiveNumber;
$[3] = t1;
$[3] = t2;
} else {
t1 = $[3];
t2 = $[3];
}
return t1;
return t2;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -36,22 +36,26 @@ import { useMemo } from "react";
function Component(props) {
const $ = _c(2);
let t0;
let t1;
if ($[0] !== props.value) {
t0 = { value: props.value };
t1 = { value: props.value };
$[0] = props.value;
$[1] = t0;
$[1] = t1;
} else {
t0 = $[1];
t1 = $[1];
}
const handlers = t0;
const handlers = t1;
bb0: switch (props.test) {
case true: {
console.log(handlers.value);
break bb0;
}
default:
default: {
}
}
const outerHandlers = handlers;
t0 = handlers;
const outerHandlers = t0;
return outerHandlers;
}

View File

@@ -25,25 +25,17 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function bar(a) {
const $ = _c(4);
let t0;
if ($[0] !== a) {
t0 = [a];
$[0] = a;
$[1] = t0;
} else {
t0 = $[1];
}
const x = t0;
const $ = _c(2);
let y;
if ($[2] !== x[0][1]) {
if ($[0] !== a) {
const x = [a];
y = {};
y = x[0][1];
$[2] = x[0][1];
$[3] = y;
$[0] = a;
$[1] = y;
} else {
y = $[3];
y = $[1];
}
return y;
}

View File

@@ -29,29 +29,20 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function bar(a, b) {
const $ = _c(6);
let t0;
if ($[0] !== a || $[1] !== b) {
t0 = [a, b];
$[0] = a;
$[1] = b;
$[2] = t0;
} else {
t0 = $[2];
}
const x = t0;
const $ = _c(3);
let y;
if ($[3] !== x[0][1] || $[4] !== x[1][0]) {
if ($[0] !== a || $[1] !== b) {
const x = [a, b];
y = {};
let t = {};
y = x[0][1];
t = x[1][0];
$[3] = x[0][1];
$[4] = x[1][0];
$[5] = y;
$[0] = a;
$[1] = b;
$[2] = y;
} else {
y = $[5];
y = $[2];
}
return y;
}

View File

@@ -25,25 +25,17 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function bar(a) {
const $ = _c(4);
let t0;
if ($[0] !== a) {
t0 = [a];
$[0] = a;
$[1] = t0;
} else {
t0 = $[1];
}
const x = t0;
const $ = _c(2);
let y;
if ($[2] !== x[0].a[1]) {
if ($[0] !== a) {
const x = [a];
y = {};
y = x[0].a[1];
$[2] = x[0].a[1];
$[3] = y;
$[0] = a;
$[1] = y;
} else {
y = $[3];
y = $[1];
}
return y;
}

View File

@@ -24,25 +24,17 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function bar(a) {
const $ = _c(4);
let t0;
if ($[0] !== a) {
t0 = [a];
$[0] = a;
$[1] = t0;
} else {
t0 = $[1];
}
const x = t0;
const $ = _c(2);
let y;
if ($[2] !== x[0]) {
if ($[0] !== a) {
const x = [a];
y = {};
y = x[0];
$[2] = x[0];
$[3] = y;
$[0] = a;
$[1] = y;
} else {
y = $[3];
y = $[1];
}
return y;
}

View File

@@ -37,9 +37,11 @@ function useTest() {
const t1 = (w = 42);
const t2 = w;
let t3;
w = 999;
t0 = makeArray(t1, t2, 2);
t3 = 2;
t0 = makeArray(t1, t2, t3);
$[0] = t0;
} else {
t0 = $[0];

View File

@@ -37,9 +37,11 @@ function useTest() {
const t1 = (w.x = 42);
const t2 = w.x;
let t3;
w.x = 999;
t0 = makeArray(t1, t2, 2);
t3 = 2;
t0 = makeArray(t1, t2, t3);
$[0] = t0;
} else {
t0 = $[0];

View File

@@ -32,9 +32,11 @@ function useTest() {
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
const t1 = print(1);
let t2;
print(2);
t0 = makeArray(t1, 2);
t2 = 2;
t0 = makeArray(t1, t2);
$[0] = t0;
} else {
t0 = $[0];

View File

@@ -29,33 +29,37 @@ function useHook(t0) {
const $ = _c(7);
const { a, b } = t0;
let t1;
if ($[0] !== a) {
t1 = identity({ a });
$[0] = a;
$[1] = t1;
} else {
t1 = $[1];
}
const valA = t1;
let t2;
if ($[2] !== b) {
t2 = identity([b]);
$[2] = b;
$[3] = t2;
if ($[0] !== a) {
t2 = identity({ a });
$[0] = a;
$[1] = t2;
} else {
t2 = $[3];
t2 = $[1];
}
const valB = t2;
t1 = t2;
const valA = t1;
let t3;
let t4;
if ($[2] !== b) {
t4 = identity([b]);
$[2] = b;
$[3] = t4;
} else {
t4 = $[3];
}
t3 = t4;
const valB = t3;
let t5;
if ($[4] !== valA || $[5] !== valB) {
t3 = [valA, valB];
t5 = [valA, valB];
$[4] = valA;
$[5] = valB;
$[6] = t3;
$[6] = t5;
} else {
t3 = $[6];
t5 = $[6];
}
return t3;
return t5;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -34,8 +34,10 @@ function Component(props) {
let Component;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
Component = Stringify;
let t0;
Component = Component;
t0 = Component;
Component = t0;
$[0] = Component;
} else {
Component = $[0];

View File

@@ -28,18 +28,20 @@ import { c as _c } from "react/compiler-runtime";
function Foo() {
const $ = _c(1);
let t0;
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = function a(t1) {
const x_0 = t1 === undefined ? _temp : t1;
return (function b(t2) {
const y_0 = t2 === undefined ? [] : t2;
t1 = function a(t2) {
const x_0 = t2 === undefined ? _temp : t2;
return (function b(t3) {
const y_0 = t3 === undefined ? [] : t3;
return [x_0, y_0];
})();
};
$[0] = t0;
$[0] = t1;
} else {
t0 = $[0];
t1 = $[0];
}
t0 = t1;
return t0;
}
function _temp() {}

View File

@@ -67,7 +67,8 @@ function Component(props) {
case "b": {
break bb1;
}
case "c":
case "c": {
}
default: {
x = 6;
}

View File

@@ -28,6 +28,7 @@ import * as React from "react";
function Component(props) {
const $ = _c(2);
let t0;
let x;
if ($[0] !== props.value) {
x = [];
@@ -37,7 +38,8 @@ function Component(props) {
} else {
x = $[1];
}
const x_0 = x;
t0 = x;
const x_0 = t0;
return x_0;
}

View File

@@ -32,7 +32,7 @@ export const FIXTURE_ENTRYPOINT = {
11 | });
12 |
> 13 | x.value += count;
| ^ InvalidReact: Updating a value previously passed as an argument to a hook is not allowed. Consider moving the mutation before calling the hook (13:13)
| ^ InvalidReact: This mutates a variable that React considers immutable (13:13)
14 | return <Stringify x={x} cb={cb} />;
15 | }
16 |

View File

@@ -32,7 +32,7 @@ export const FIXTURE_ENTRYPOINT = {
11 | });
12 |
> 13 | x.value += count;
| ^ InvalidReact: Updating a value previously passed as an argument to a hook is not allowed. Consider moving the mutation before calling the hook (13:13)
| ^ InvalidReact: This mutates a variable that React considers immutable (13:13)
14 | return <Stringify x={x} cb={cb} />;
15 | }
16 |

View File

@@ -1,24 +0,0 @@
## Input
```javascript
function Component(props) {
eval('props.x = true');
return <div />;
}
```
## Error
```
1 | function Component(props) {
> 2 | eval('props.x = true');
| ^^^^ InvalidJS: The 'eval' function is not supported. Eval is an anti-pattern in JavaScript, and the code executed cannot be evaluated by React Compiler (2:2)
3 | return <div />;
4 | }
5 |
```

View File

@@ -1,4 +0,0 @@
function Component(props) {
eval('props.x = true');
return <div />;
}

View File

@@ -38,15 +38,13 @@ export const FIXTURE_ENTRYPOINT = {
## Error
```
17 | * $2 = Function context=setState
18 | */
> 19 | useEffect(() => setState(2), []);
| ^^^^^^^^ InvalidReact: This variable is accessed before it is declared, which may prevent it from updating as the assigned value changes over time. Variable `setState` is accessed before it is declared (19:19)
InvalidReact: This variable is accessed before it is declared, which prevents the earlier access from updating when this value changes over time. Variable `setState` is accessed before it is declared (21:21)
19 | useEffect(() => setState(2), []);
20 |
21 | const [state, setState] = useState(0);
> 21 | const [state, setState] = useState(0);
| ^^^^^^^^ InvalidReact: Updating a value used previously in an effect function or as an effect dependency is not allowed. Consider moving the mutation before calling useEffect(). Found mutation of `setState` (21:21)
22 | return <Stringify state={state} />;
23 | }
24 |
```

View File

@@ -20,7 +20,7 @@ function Component() {
2 |
3 | function Component() {
> 4 | const date = Date.now();
| ^^^^^^^^^^ InvalidReact: Calling an impure function can produce unstable results. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). `Date.now` is an impure function whose results may change on every call (4:4)
| ^^^^^^^^ InvalidReact: Calling an impure function can produce unstable results. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). `Date.now` is an impure function whose results may change on every call (4:4)
InvalidReact: Calling an impure function can produce unstable results. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). `performance.now` is an impure function whose results may change on every call (5:5)

View File

@@ -27,7 +27,7 @@ function SomeComponent() {
9 | return (
10 | <Button
> 11 | onPress={() => (sharedVal.value = Math.random())}
| ^^^^^^^^^ InvalidReact: Updating a value returned from a hook is not allowed. Consider moving the mutation into the hook where the value is constructed. Found mutation of `sharedVal` (11:11)
| ^^^^^^^^^ InvalidReact: Mutating a value returned from a function whose return value should not be mutated. Found mutation of `sharedVal` (11:11)
12 | title="Randomize"
13 | />
14 | );

View File

@@ -16,8 +16,6 @@ function useHook(a, b) {
1 | function useHook(a, b) {
> 2 | b.test = 1;
| ^ InvalidReact: Mutating component props or hook arguments is not allowed. Consider using a local variable instead (2:2)
InvalidReact: Mutating component props or hook arguments is not allowed. Consider using a local variable instead (3:3)
3 | a.test = 2;
4 | }
5 |

View File

@@ -21,8 +21,6 @@ function Component(props) {
4 | foo(() => {
> 5 | x.a = 10;
| ^ InvalidReact: Writing to a variable defined outside a component or hook is not allowed. Consider using an effect (5:5)
InvalidReact: Writing to a variable defined outside a component or hook is not allowed. Consider using an effect (6:6)
6 | x.a = 20;
7 | });
8 | }

View File

@@ -21,8 +21,6 @@ function Component() {
3 | // Cannot assign to globals
> 4 | someUnknownGlobal = true;
| ^^^^^^^^^^^^^^^^^ InvalidReact: Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render) (4:4)
InvalidReact: Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render) (5:5)
5 | moduleLocal = true;
6 | };
7 | foo();

View File

@@ -18,8 +18,6 @@ function Component() {
2 | // Cannot assign to globals
> 3 | someUnknownGlobal = true;
| ^^^^^^^^^^^^^^^^^ InvalidReact: Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render) (3:3)
InvalidReact: Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render) (4:4)
4 | moduleLocal = true;
5 | }
6 |

View File

@@ -84,7 +84,7 @@ let moduleLocal = false;
> 3 | var x = [];
| ^^^^^^^^^^^ Todo: (BuildHIR::lowerStatement) Handle var kinds in VariableDeclaration (3:3)
Todo: Support nested class declarations (5:10)
Todo: (BuildHIR::lowerStatement) Handle ClassDeclaration statements (5:10)
Todo: (BuildHIR::lowerStatement) Handle non-variable initialization in ForStatement (20:22)

View File

@@ -22,7 +22,7 @@ function Component(props) {
7 | return hasErrors;
8 | }
> 9 | return hasErrors();
| ^^^^^^^^^ Invariant: [InferMutationAliasingEffects] Expected value kind to be initialized. <unknown> hasErrors_0$15:TFunction (9:9)
| ^^^^^^^^^ Invariant: [hoisting] Expected value for identifier to be initialized. hasErrors_0$15 (9:9)
10 | }
11 |
```

View File

@@ -34,13 +34,13 @@ export const FIXTURE_ENTRYPOINT = {
## Error
```
11 |
12 | function foo() {
> 13 | return bar();
| ^^^ Todo: [PruneHoistedContexts] Rewrite hoisted function references (13:13)
13 | return bar();
14 | }
15 | function bar() {
> 15 | function bar() {
| ^^^ Todo: [PruneHoistedContexts] Rewrite hoisted function references (15:15)
16 | return 42;
17 | }
18 |
```

View File

@@ -42,32 +42,34 @@ function Component(props) {
const c1 = __c;
const $c = c1;
let t0;
if ($[0] !== $c) {
t0 = [$c];
$[0] = $c;
$[1] = t0;
} else {
t0 = $[1];
}
const array = t0;
let t1;
if ($[2] !== state) {
t1 = [state];
$[2] = state;
$[3] = t1;
if ($[0] !== $c) {
t1 = [$c];
$[0] = $c;
$[1] = t1;
} else {
t1 = $[3];
t1 = $[1];
}
t0 = t1;
const array = t0;
let t2;
if ($[4] !== array || $[5] !== t1) {
t2 = <ValidateMemoization inputs={t1} output={array} />;
$[4] = array;
$[5] = t1;
$[6] = t2;
if ($[2] !== state) {
t2 = [state];
$[2] = state;
$[3] = t2;
} else {
t2 = $[6];
t2 = $[3];
}
return t2;
let t3;
if ($[4] !== array || $[5] !== t2) {
t3 = <ValidateMemoization inputs={t2} output={array} />;
$[4] = array;
$[5] = t2;
$[6] = t3;
} else {
t3 = $[6];
}
return t3;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -63,21 +63,23 @@ function Component() {
unsafeUpdateConst();
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = [{ pretendConst }];
$[0] = t0;
} else {
t0 = $[0];
}
const value = t0;
let t1;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
t1 = <ValidateMemoization inputs={[]} output={value} />;
$[1] = t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = [{ pretendConst }];
$[0] = t1;
} else {
t1 = $[1];
t1 = $[0];
}
return t1;
t0 = t1;
const value = t0;
let t2;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
t2 = <ValidateMemoization inputs={[]} output={value} />;
$[1] = t2;
} else {
t2 = $[1];
}
return t2;
}
function _temp() {
unsafeResetConst();

View File

@@ -74,21 +74,23 @@ function Component() {
unsafeUpdateConst();
let t0;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
t0 = [{ pretendConst }];
$[1] = t0;
} else {
t0 = $[1];
}
const value = t0;
let t1;
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
t1 = <ValidateMemoization inputs={[pretendConst]} output={value} />;
$[2] = t1;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
t1 = [{ pretendConst }];
$[1] = t1;
} else {
t1 = $[2];
t1 = $[1];
}
return t1;
t0 = t1;
const value = t0;
let t2;
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
t2 = <ValidateMemoization inputs={[pretendConst]} output={value} />;
$[2] = t2;
} else {
t2 = $[2];
}
return t2;
}
function _temp() {
unsafeResetConst();

View File

@@ -38,34 +38,36 @@ function Component(props) {
$[0] = "20945b0193e529df490847c66111b38d7b02485d5b53d0829ff3b23af87b105c";
}
const [state] = useState(0);
const t0 = state * 2;
let t1;
if ($[1] !== t0) {
t1 = [t0];
$[1] = t0;
$[2] = t1;
} else {
t1 = $[2];
}
const doubled = t1;
let t0;
const t1 = state * 2;
let t2;
if ($[3] !== state) {
t2 = [state];
$[3] = state;
$[4] = t2;
if ($[1] !== t1) {
t2 = [t1];
$[1] = t1;
$[2] = t2;
} else {
t2 = $[4];
t2 = $[2];
}
t0 = t2;
const doubled = t0;
let t3;
if ($[5] !== doubled || $[6] !== t2) {
t3 = <ValidateMemoization inputs={t2} output={doubled} />;
$[5] = doubled;
$[6] = t2;
$[7] = t3;
if ($[3] !== state) {
t3 = [state];
$[3] = state;
$[4] = t3;
} else {
t3 = $[7];
t3 = $[4];
}
return t3;
let t4;
if ($[5] !== doubled || $[6] !== t3) {
t4 = <ValidateMemoization inputs={t3} output={doubled} />;
$[5] = doubled;
$[6] = t3;
$[7] = t4;
} else {
t4 = $[7];
}
return t4;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -40,34 +40,36 @@ function Component(t0) {
const $ = _c(7);
const { data } = t0;
let t1;
let t2;
if ($[0] !== data.name) {
t1 = fbt._("{name}", [fbt._param("name", data.name ?? "")], {
t2 = fbt._("{name}", [fbt._param("name", data.name ?? "")], {
hk: "csQUH",
});
$[0] = data.name;
$[1] = t1;
$[1] = t2;
} else {
t1 = $[1];
t2 = $[1];
}
t1 = t2;
const el = t1;
let t2;
if ($[2] !== data.name) {
t2 = [data.name];
$[2] = data.name;
$[3] = t2;
} else {
t2 = $[3];
}
let t3;
if ($[4] !== el || $[5] !== t2) {
t3 = <ValidateMemoization inputs={t2} output={el} />;
$[4] = el;
$[5] = t2;
$[6] = t3;
if ($[2] !== data.name) {
t3 = [data.name];
$[2] = data.name;
$[3] = t3;
} else {
t3 = $[6];
t3 = $[3];
}
return t3;
let t4;
if ($[4] !== el || $[5] !== t3) {
t4 = <ValidateMemoization inputs={t3} output={el} />;
$[4] = el;
$[5] = t3;
$[6] = t4;
} else {
t4 = $[6];
}
return t4;
}
const props1 = { data: { name: "Mike" } };

View File

@@ -1,60 +0,0 @@
## Input
```javascript
// @flow
function Component(props) {
enum Bool {
True = 'true',
False = 'false',
}
let bool: Bool = Bool.False;
if (props.value) {
bool = Bool.True;
}
return <div>{bool}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: true}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
function Component(props) {
const $ = _c(2);
enum Bool {
True = "true",
False = "false",
}
let bool = Bool.False;
if (props.value) {
bool = Bool.True;
}
let t0;
if ($[0] !== bool) {
t0 = <div>{bool}</div>;
$[0] = bool;
$[1] = t0;
} else {
t0 = $[1];
}
return t0;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ value: true }],
};
```
### Eval output
(kind: exception) Bool is not defined

View File

@@ -1,18 +0,0 @@
// @flow
function Component(props) {
enum Bool {
True = 'true',
False = 'false',
}
let bool: Bool = Bool.False;
if (props.value) {
bool = Bool.True;
}
return <div>{bool}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: true}],
};

View File

@@ -47,14 +47,17 @@ function Component(t0) {
const $ = _c(19);
const { a, b } = t0;
let t1;
let t2;
if ($[0] !== a) {
t1 = [a];
t2 = [a];
$[0] = a;
$[1] = t1;
$[1] = t2;
} else {
t1 = $[1];
t2 = $[1];
}
t1 = t2;
const x = t1;
let t3;
let items;
if ($[2] !== b || $[3] !== x) {
items = [b];
@@ -67,57 +70,59 @@ function Component(t0) {
} else {
items = $[4];
}
const y = items;
let t2;
if ($[5] !== a) {
t2 = [a];
$[5] = a;
$[6] = t2;
} else {
t2 = $[6];
}
let t3;
if ($[7] !== t2 || $[8] !== x) {
t3 = <ValidateMemoization inputs={t2} output={x} />;
$[7] = t2;
$[8] = x;
$[9] = t3;
} else {
t3 = $[9];
}
t3 = items;
const y = t3;
let t4;
if ($[10] !== b || $[11] !== x) {
t4 = [x, b];
$[10] = b;
$[11] = x;
$[12] = t4;
if ($[5] !== a) {
t4 = [a];
$[5] = a;
$[6] = t4;
} else {
t4 = $[12];
t4 = $[6];
}
let t5;
if ($[13] !== t4 || $[14] !== y) {
t5 = <ValidateMemoization inputs={t4} output={y} />;
$[13] = t4;
$[14] = y;
$[15] = t5;
if ($[7] !== t4 || $[8] !== x) {
t5 = <ValidateMemoization inputs={t4} output={x} />;
$[7] = t4;
$[8] = x;
$[9] = t5;
} else {
t5 = $[15];
t5 = $[9];
}
let t6;
if ($[16] !== t3 || $[17] !== t5) {
t6 = (
if ($[10] !== b || $[11] !== x) {
t6 = [x, b];
$[10] = b;
$[11] = x;
$[12] = t6;
} else {
t6 = $[12];
}
let t7;
if ($[13] !== t6 || $[14] !== y) {
t7 = <ValidateMemoization inputs={t6} output={y} />;
$[13] = t6;
$[14] = y;
$[15] = t7;
} else {
t7 = $[15];
}
let t8;
if ($[16] !== t5 || $[17] !== t7) {
t8 = (
<>
{t3}
{t5}
{t7}
</>
);
$[16] = t3;
$[17] = t5;
$[18] = t6;
$[16] = t5;
$[17] = t7;
$[18] = t8;
} else {
t6 = $[18];
t8 = $[18];
}
return t6;
return t8;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -34,6 +34,7 @@ import { ValidateMemoization } from "shared-runtime";
function Component(props) {
const $ = _c(7);
let t0;
let a;
if ($[0] !== props.name) {
a = [];
@@ -47,25 +48,26 @@ function Component(props) {
} else {
a = $[1];
}
const a_0 = a;
let t0;
if ($[2] !== props.name) {
t0 = [props.name];
$[2] = props.name;
$[3] = t0;
} else {
t0 = $[3];
}
t0 = a;
const a_0 = t0;
let t1;
if ($[4] !== a_0 || $[5] !== t0) {
t1 = <ValidateMemoization inputs={t0} output={a_0} />;
$[4] = a_0;
$[5] = t0;
$[6] = t1;
if ($[2] !== props.name) {
t1 = [props.name];
$[2] = props.name;
$[3] = t1;
} else {
t1 = $[6];
t1 = $[3];
}
return t1;
let t2;
if ($[4] !== a_0 || $[5] !== t1) {
t2 = <ValidateMemoization inputs={t1} output={a_0} />;
$[4] = a_0;
$[5] = t1;
$[6] = t2;
} else {
t2 = $[6];
}
return t2;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -46,7 +46,7 @@ const React$useMemo = React.useMemo;
const Internal$Reassigned$useHook = useHook;
function Component() {
const $ = _c(7);
const $ = _c(8);
const [state] = React$useState(0);
const object = Internal$Reassigned$useHook();
let t0;
@@ -59,30 +59,34 @@ function Component() {
}
const json = t0;
let t1;
if ($[2] !== state) {
const doubledArray = makeArray(state);
t1 = doubledArray.join("");
$[2] = state;
$[3] = t1;
} else {
t1 = $[3];
}
let t2;
if ($[4] !== json || $[5] !== t1) {
t2 = (
if ($[2] !== state) {
t1 = makeArray(state);
const doubledArray = t1;
t2 = doubledArray.join("");
$[2] = state;
$[3] = t2;
$[4] = t1;
} else {
t2 = $[3];
t1 = $[4];
}
let t3;
if ($[5] !== json || $[6] !== t2) {
t3 = (
<div>
{t1}
{t2}
{json}
</div>
);
$[4] = json;
$[5] = t1;
$[5] = json;
$[6] = t2;
$[7] = t3;
} else {
t2 = $[6];
t3 = $[7];
}
return t2;
return t3;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -22,16 +22,20 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function Component(props) {
const $ = _c(2);
const $ = _c(3);
let t0;
let items;
if ($[0] !== props.a) {
items = [];
t0 = [];
items = t0;
items.push(props.a);
$[0] = props.a;
$[1] = items;
$[2] = t0;
} else {
items = $[1];
t0 = $[2];
}
return items;
}

View File

@@ -48,7 +48,7 @@ export const FIXTURE_ENTRYPOINT = {
## Logs
```
{"kind":"CompileError","fnLoc":{"start":{"line":5,"column":0,"index":139},"end":{"line":12,"column":1,"index":384},"filename":"mutate-after-useeffect-optional-chain.ts"},"detail":{"reason":"Updating a value used previously in an effect function or as an effect dependency is not allowed. Consider moving the mutation before calling useEffect()","description":null,"severity":"InvalidReact","suggestions":null,"loc":{"start":{"line":10,"column":2,"index":345},"end":{"line":10,"column":5,"index":348},"filename":"mutate-after-useeffect-optional-chain.ts","identifierName":"arr"}}}
{"kind":"CompileError","fnLoc":{"start":{"line":5,"column":0,"index":139},"end":{"line":12,"column":1,"index":384},"filename":"mutate-after-useeffect-optional-chain.ts"},"detail":{"reason":"This mutates a variable that React considers immutable","description":null,"loc":{"start":{"line":10,"column":2,"index":345},"end":{"line":10,"column":5,"index":348},"filename":"mutate-after-useeffect-optional-chain.ts","identifierName":"arr"},"suggestions":null,"severity":"InvalidReact"}}
{"kind":"AutoDepsDecorations","fnLoc":{"start":{"line":9,"column":2,"index":304},"end":{"line":9,"column":39,"index":341},"filename":"mutate-after-useeffect-optional-chain.ts"},"decorations":[{"start":{"line":9,"column":24,"index":326},"end":{"line":9,"column":27,"index":329},"filename":"mutate-after-useeffect-optional-chain.ts","identifierName":"arr"}]}
{"kind":"CompileSuccess","fnLoc":{"start":{"line":5,"column":0,"index":139},"end":{"line":12,"column":1,"index":384},"filename":"mutate-after-useeffect-optional-chain.ts"},"fnName":"Component","memoSlots":0,"memoBlocks":0,"memoValues":0,"prunedMemoBlocks":0,"prunedMemoValues":0}
```

View File

@@ -47,7 +47,7 @@ export const FIXTURE_ENTRYPOINT = {
## Logs
```
{"kind":"CompileError","fnLoc":{"start":{"line":6,"column":0,"index":148},"end":{"line":11,"column":1,"index":311},"filename":"mutate-after-useeffect-ref-access.ts"},"detail":{"reason":"Mutating component props or hook arguments is not allowed. Consider using a local variable instead","description":null,"severity":"InvalidReact","suggestions":null,"loc":{"start":{"line":9,"column":2,"index":269},"end":{"line":9,"column":16,"index":283},"filename":"mutate-after-useeffect-ref-access.ts"}}}
{"kind":"CompileError","fnLoc":{"start":{"line":6,"column":0,"index":148},"end":{"line":11,"column":1,"index":311},"filename":"mutate-after-useeffect-ref-access.ts"},"detail":{"reason":"Mutating component props or hook arguments is not allowed. Consider using a local variable instead","description":null,"loc":{"start":{"line":9,"column":2,"index":269},"end":{"line":9,"column":16,"index":283},"filename":"mutate-after-useeffect-ref-access.ts"},"suggestions":null,"severity":"InvalidReact"}}
{"kind":"AutoDepsDecorations","fnLoc":{"start":{"line":8,"column":2,"index":227},"end":{"line":8,"column":40,"index":265},"filename":"mutate-after-useeffect-ref-access.ts"},"decorations":[{"start":{"line":8,"column":24,"index":249},"end":{"line":8,"column":30,"index":255},"filename":"mutate-after-useeffect-ref-access.ts","identifierName":"arrRef"}]}
{"kind":"CompileSuccess","fnLoc":{"start":{"line":6,"column":0,"index":148},"end":{"line":11,"column":1,"index":311},"filename":"mutate-after-useeffect-ref-access.ts"},"fnName":"Component","memoSlots":0,"memoBlocks":0,"memoValues":0,"prunedMemoBlocks":0,"prunedMemoValues":0}
```

View File

@@ -47,7 +47,7 @@ export const FIXTURE_ENTRYPOINT = {
## Logs
```
{"kind":"CompileError","fnLoc":{"start":{"line":4,"column":0,"index":101},"end":{"line":11,"column":1,"index":222},"filename":"mutate-after-useeffect.ts"},"detail":{"reason":"Updating a value used previously in an effect function or as an effect dependency is not allowed. Consider moving the mutation before calling useEffect()","description":null,"severity":"InvalidReact","suggestions":null,"loc":{"start":{"line":9,"column":2,"index":194},"end":{"line":9,"column":5,"index":197},"filename":"mutate-after-useeffect.ts","identifierName":"arr"}}}
{"kind":"CompileError","fnLoc":{"start":{"line":4,"column":0,"index":101},"end":{"line":11,"column":1,"index":222},"filename":"mutate-after-useeffect.ts"},"detail":{"reason":"This mutates a variable that React considers immutable","description":null,"loc":{"start":{"line":9,"column":2,"index":194},"end":{"line":9,"column":5,"index":197},"filename":"mutate-after-useeffect.ts","identifierName":"arr"},"suggestions":null,"severity":"InvalidReact"}}
{"kind":"AutoDepsDecorations","fnLoc":{"start":{"line":6,"column":2,"index":149},"end":{"line":8,"column":4,"index":190},"filename":"mutate-after-useeffect.ts"},"decorations":[{"start":{"line":7,"column":4,"index":171},"end":{"line":7,"column":7,"index":174},"filename":"mutate-after-useeffect.ts","identifierName":"arr"},{"start":{"line":7,"column":4,"index":171},"end":{"line":7,"column":7,"index":174},"filename":"mutate-after-useeffect.ts","identifierName":"arr"},{"start":{"line":7,"column":13,"index":180},"end":{"line":7,"column":16,"index":183},"filename":"mutate-after-useeffect.ts","identifierName":"foo"}]}
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":101},"end":{"line":11,"column":1,"index":222},"filename":"mutate-after-useeffect.ts"},"fnName":"Component","memoSlots":0,"memoBlocks":0,"memoValues":0,"prunedMemoBlocks":0,"prunedMemoValues":0}
```

View File

@@ -52,7 +52,7 @@ export const FIXTURE_ENTRYPOINT = {
## Logs
```
{"kind":"CompileError","fnLoc":{"start":{"line":5,"column":0,"index":163},"end":{"line":13,"column":1,"index":357},"filename":"retry-no-emit.ts"},"detail":{"reason":"Updating a value previously passed as an argument to a hook is not allowed. Consider moving the mutation before calling the hook","description":null,"severity":"InvalidReact","suggestions":null,"loc":{"start":{"line":11,"column":2,"index":320},"end":{"line":11,"column":6,"index":324},"filename":"retry-no-emit.ts","identifierName":"arr2"}}}
{"kind":"CompileError","fnLoc":{"start":{"line":5,"column":0,"index":163},"end":{"line":13,"column":1,"index":357},"filename":"retry-no-emit.ts"},"detail":{"reason":"This mutates a variable that React considers immutable","description":null,"loc":{"start":{"line":11,"column":2,"index":320},"end":{"line":11,"column":6,"index":324},"filename":"retry-no-emit.ts","identifierName":"arr2"},"suggestions":null,"severity":"InvalidReact"}}
{"kind":"AutoDepsDecorations","fnLoc":{"start":{"line":7,"column":2,"index":216},"end":{"line":7,"column":36,"index":250},"filename":"retry-no-emit.ts"},"decorations":[{"start":{"line":7,"column":31,"index":245},"end":{"line":7,"column":34,"index":248},"filename":"retry-no-emit.ts","identifierName":"arr"}]}
{"kind":"AutoDepsDecorations","fnLoc":{"start":{"line":10,"column":2,"index":274},"end":{"line":10,"column":44,"index":316},"filename":"retry-no-emit.ts"},"decorations":[{"start":{"line":10,"column":25,"index":297},"end":{"line":10,"column":29,"index":301},"filename":"retry-no-emit.ts","identifierName":"arr2"},{"start":{"line":10,"column":25,"index":297},"end":{"line":10,"column":29,"index":301},"filename":"retry-no-emit.ts","identifierName":"arr2"},{"start":{"line":10,"column":35,"index":307},"end":{"line":10,"column":42,"index":314},"filename":"retry-no-emit.ts","identifierName":"propVal"}]}
{"kind":"CompileSuccess","fnLoc":{"start":{"line":5,"column":0,"index":163},"end":{"line":13,"column":1,"index":357},"filename":"retry-no-emit.ts"},"fnName":"Foo","memoSlots":0,"memoBlocks":0,"memoValues":0,"prunedMemoBlocks":0,"prunedMemoValues":0}

View File

@@ -34,28 +34,22 @@ import { print } from "shared-runtime";
* setState types are not enough to determine to omit from deps. Must also take reactivity into account.
*/
function ReactiveRefInEffect(props) {
const $ = _c(4);
const $ = _c(2);
const [, setState1] = useRef("initial value");
const [, setState2] = useRef("initial value");
let setState;
if ($[0] !== props.foo) {
if (props.foo) {
setState = setState1;
} else {
setState = setState2;
}
$[0] = props.foo;
$[1] = setState;
if (props.foo) {
setState = setState1;
} else {
setState = $[1];
setState = setState2;
}
let t0;
if ($[2] !== setState) {
if ($[0] !== setState) {
t0 = () => print(setState);
$[2] = setState;
$[3] = t0;
$[0] = setState;
$[1] = t0;
} else {
t0 = $[3];
t0 = $[1];
}
useEffect(t0, [setState]);
}

View File

@@ -50,17 +50,19 @@ function useMakeCallback(t0) {
const [, setState] = useState(0);
let t1;
let t2;
if ($[0] !== obj.value) {
t1 = () => {
t2 = () => {
if (obj.value !== 0) {
setState(obj.value);
}
};
$[0] = obj.value;
$[1] = t1;
$[1] = t2;
} else {
t1 = $[1];
t2 = $[1];
}
t1 = t2;
const cb = t1;
useIdentity(null);

View File

@@ -26,15 +26,17 @@ import { c as _c } from "react/compiler-runtime";
function Foo() {
const $ = _c(1);
let t0;
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = function a(t1) {
const x_0 = t1 === undefined ? _temp : t1;
t1 = function a(t2) {
const x_0 = t2 === undefined ? _temp : t2;
return x_0;
};
$[0] = t0;
$[0] = t1;
} else {
t0 = $[0];
t1 = $[0];
}
t0 = t1;
return t0;
}
function _temp() {}

View File

@@ -1,215 +0,0 @@
## Input
```javascript
// @enableNewMutationAliasingModel
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"; // @enableNewMutationAliasingModel
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(2);
const { prop } = t0;
let t1;
if ($[0] !== prop) {
const obj = shallowCopy(prop);
const aliasedObj = identity(obj);
const id = [obj.id];
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>

View File

@@ -1,94 +0,0 @@
// @enableNewMutationAliasingModel
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}}],
};

View File

@@ -1,134 +0,0 @@
## Input
```javascript
// @enableNewMutationAliasingModel
import {Stringify} from 'shared-runtime';
/**
* Forked from array-map-simple.js
*
* Named lambdas (e.g. cb1) may be defined in the top scope of a function and
* used in a different lambda (getArrMap1).
*
* Here, we should try to determine if cb1 is actually called. In this case:
* - getArrMap1 is assumed to be called as it's passed to JSX
* - cb1 is not assumed to be called since it's only used as a call operand
*/
function useFoo({arr1, arr2}) {
const cb1 = e => arr1[0].value + e.value;
const getArrMap1 = () => arr1.map(cb1);
const cb2 = e => arr2[0].value + e.value;
const getArrMap2 = () => arr1.map(cb2);
return (
<Stringify
getArrMap1={getArrMap1}
getArrMap2={getArrMap2}
shouldInvokeFns={true}
/>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{arr1: [], arr2: []}],
sequentialRenders: [
{arr1: [], arr2: []},
{arr1: [], arr2: null},
{arr1: [{value: 1}, {value: 2}], arr2: [{value: -1}]},
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
import { Stringify } from "shared-runtime";
/**
* Forked from array-map-simple.js
*
* Named lambdas (e.g. cb1) may be defined in the top scope of a function and
* used in a different lambda (getArrMap1).
*
* Here, we should try to determine if cb1 is actually called. In this case:
* - getArrMap1 is assumed to be called as it's passed to JSX
* - cb1 is not assumed to be called since it's only used as a call operand
*/
function useFoo(t0) {
const $ = _c(13);
const { arr1, arr2 } = t0;
let t1;
if ($[0] !== arr1[0]) {
t1 = (e) => arr1[0].value + e.value;
$[0] = arr1[0];
$[1] = t1;
} else {
t1 = $[1];
}
const cb1 = t1;
let t2;
if ($[2] !== arr1 || $[3] !== cb1) {
t2 = () => arr1.map(cb1);
$[2] = arr1;
$[3] = cb1;
$[4] = t2;
} else {
t2 = $[4];
}
const getArrMap1 = t2;
let t3;
if ($[5] !== arr2) {
t3 = (e_0) => arr2[0].value + e_0.value;
$[5] = arr2;
$[6] = t3;
} else {
t3 = $[6];
}
const cb2 = t3;
let t4;
if ($[7] !== arr1 || $[8] !== cb2) {
t4 = () => arr1.map(cb2);
$[7] = arr1;
$[8] = cb2;
$[9] = t4;
} else {
t4 = $[9];
}
const getArrMap2 = t4;
let t5;
if ($[10] !== getArrMap1 || $[11] !== getArrMap2) {
t5 = (
<Stringify
getArrMap1={getArrMap1}
getArrMap2={getArrMap2}
shouldInvokeFns={true}
/>
);
$[10] = getArrMap1;
$[11] = getArrMap2;
$[12] = t5;
} else {
t5 = $[12];
}
return t5;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{ arr1: [], arr2: [] }],
sequentialRenders: [
{ arr1: [], arr2: [] },
{ arr1: [], arr2: null },
{ arr1: [{ value: 1 }, { value: 2 }], arr2: [{ value: -1 }] },
],
};
```
### Eval output
(kind: ok) <div>{"getArrMap1":{"kind":"Function","result":[]},"getArrMap2":{"kind":"Function","result":[]},"shouldInvokeFns":true}</div>
<div>{"getArrMap1":{"kind":"Function","result":[]},"getArrMap2":{"kind":"Function","result":[]},"shouldInvokeFns":true}</div>
<div>{"getArrMap1":{"kind":"Function","result":[2,3]},"getArrMap2":{"kind":"Function","result":[0,1]},"shouldInvokeFns":true}</div>

View File

@@ -1,36 +0,0 @@
// @enableNewMutationAliasingModel
import {Stringify} from 'shared-runtime';
/**
* Forked from array-map-simple.js
*
* Named lambdas (e.g. cb1) may be defined in the top scope of a function and
* used in a different lambda (getArrMap1).
*
* Here, we should try to determine if cb1 is actually called. In this case:
* - getArrMap1 is assumed to be called as it's passed to JSX
* - cb1 is not assumed to be called since it's only used as a call operand
*/
function useFoo({arr1, arr2}) {
const cb1 = e => arr1[0].value + e.value;
const getArrMap1 = () => arr1.map(cb1);
const cb2 = e => arr2[0].value + e.value;
const getArrMap2 = () => arr1.map(cb2);
return (
<Stringify
getArrMap1={getArrMap1}
getArrMap2={getArrMap2}
shouldInvokeFns={true}
/>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{arr1: [], arr2: []}],
sequentialRenders: [
{arr1: [], arr2: []},
{arr1: [], arr2: null},
{arr1: [{value: 1}, {value: 2}], arr2: [{value: -1}]},
],
};

View File

@@ -1,81 +0,0 @@
## Input
```javascript
import {Stringify, mutate} from 'shared-runtime';
function Component({foo, bar}) {
let x = {foo};
let y = {bar};
const f0 = function () {
let a = {y};
let b = {x};
a.y.x = b;
};
f0();
mutate(y);
return <Stringify x={y} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{foo: 2, bar: 3}],
sequentialRenders: [
{foo: 2, bar: 3},
{foo: 2, bar: 3},
{foo: 2, bar: 4},
{foo: 3, bar: 4},
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { Stringify, mutate } from "shared-runtime";
function Component(t0) {
const $ = _c(3);
const { foo, bar } = t0;
let t1;
if ($[0] !== bar || $[1] !== foo) {
const x = { foo };
const y = { bar };
const f0 = function () {
const a = { y };
const b = { x };
a.y.x = b;
};
f0();
mutate(y);
t1 = <Stringify x={y} />;
$[0] = bar;
$[1] = foo;
$[2] = t1;
} else {
t1 = $[2];
}
return t1;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ foo: 2, bar: 3 }],
sequentialRenders: [
{ foo: 2, bar: 3 },
{ foo: 2, bar: 3 },
{ foo: 2, bar: 4 },
{ foo: 3, bar: 4 },
],
};
```
### Eval output
(kind: ok) <div>{"x":{"bar":3,"x":{"x":{"foo":2}},"wat0":"joe"}}</div>
<div>{"x":{"bar":3,"x":{"x":{"foo":2}},"wat0":"joe"}}</div>
<div>{"x":{"bar":4,"x":{"x":{"foo":2}},"wat0":"joe"}}</div>
<div>{"x":{"bar":4,"x":{"x":{"foo":3}},"wat0":"joe"}}</div>

View File

@@ -1,25 +0,0 @@
import {Stringify, mutate} from 'shared-runtime';
function Component({foo, bar}) {
let x = {foo};
let y = {bar};
const f0 = function () {
let a = {y};
let b = {x};
a.y.x = b;
};
f0();
mutate(y);
return <Stringify x={y} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{foo: 2, bar: 3}],
sequentialRenders: [
{foo: 2, bar: 3},
{foo: 2, bar: 3},
{foo: 2, bar: 4},
{foo: 3, bar: 4},
],
};

View File

@@ -1,61 +0,0 @@
## Input
```javascript
// @enableNewMutationAliasingModel
function bar(a) {
let x = [a];
let y = {};
(function () {
y = x[0][1];
})();
return y;
}
export const FIXTURE_ENTRYPOINT = {
fn: bar,
params: [['val1', 'val2']],
isComponent: false,
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
function bar(a) {
const $ = _c(4);
let t0;
if ($[0] !== a) {
t0 = [a];
$[0] = a;
$[1] = t0;
} else {
t0 = $[1];
}
const x = t0;
let y;
if ($[2] !== x[0][1]) {
y = {};
y = x[0][1];
$[2] = x[0][1];
$[3] = y;
} else {
y = $[3];
}
return y;
}
export const FIXTURE_ENTRYPOINT = {
fn: bar,
params: [["val1", "val2"]],
isComponent: false,
};
```
### Eval output
(kind: ok) "val2"

View File

@@ -1,16 +0,0 @@
// @enableNewMutationAliasingModel
function bar(a) {
let x = [a];
let y = {};
(function () {
y = x[0][1];
})();
return y;
}
export const FIXTURE_ENTRYPOINT = {
fn: bar,
params: [['val1', 'val2']],
isComponent: false,
};

View File

@@ -1,71 +0,0 @@
## Input
```javascript
// @enableNewMutationAliasingModel
function bar(a, b) {
let x = [a, b];
let y = {};
let t = {};
(function () {
y = x[0][1];
t = x[1][0];
})();
return y;
}
export const FIXTURE_ENTRYPOINT = {
fn: bar,
params: [
[1, 2],
[2, 3],
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
function bar(a, b) {
const $ = _c(6);
let t0;
if ($[0] !== a || $[1] !== b) {
t0 = [a, b];
$[0] = a;
$[1] = b;
$[2] = t0;
} else {
t0 = $[2];
}
const x = t0;
let y;
if ($[3] !== x[0][1] || $[4] !== x[1][0]) {
y = {};
let t = {};
y = x[0][1];
t = x[1][0];
$[3] = x[0][1];
$[4] = x[1][0];
$[5] = y;
} else {
y = $[5];
}
return y;
}
export const FIXTURE_ENTRYPOINT = {
fn: bar,
params: [
[1, 2],
[2, 3],
],
};
```
### Eval output
(kind: ok) 2

View File

@@ -1,20 +0,0 @@
// @enableNewMutationAliasingModel
function bar(a, b) {
let x = [a, b];
let y = {};
let t = {};
(function () {
y = x[0][1];
t = x[1][0];
})();
return y;
}
export const FIXTURE_ENTRYPOINT = {
fn: bar,
params: [
[1, 2],
[2, 3],
],
};

View File

@@ -1,61 +0,0 @@
## Input
```javascript
// @enableNewMutationAliasingModel
function bar(a) {
let x = [a];
let y = {};
(function () {
y = x[0].a[1];
})();
return y;
}
export const FIXTURE_ENTRYPOINT = {
fn: bar,
params: [{a: ['val1', 'val2']}],
isComponent: false,
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
function bar(a) {
const $ = _c(4);
let t0;
if ($[0] !== a) {
t0 = [a];
$[0] = a;
$[1] = t0;
} else {
t0 = $[1];
}
const x = t0;
let y;
if ($[2] !== x[0].a[1]) {
y = {};
y = x[0].a[1];
$[2] = x[0].a[1];
$[3] = y;
} else {
y = $[3];
}
return y;
}
export const FIXTURE_ENTRYPOINT = {
fn: bar,
params: [{ a: ["val1", "val2"] }],
isComponent: false,
};
```
### Eval output
(kind: ok) "val2"

View File

@@ -1,16 +0,0 @@
// @enableNewMutationAliasingModel
function bar(a) {
let x = [a];
let y = {};
(function () {
y = x[0].a[1];
})();
return y;
}
export const FIXTURE_ENTRYPOINT = {
fn: bar,
params: [{a: ['val1', 'val2']}],
isComponent: false,
};

View File

@@ -1,59 +0,0 @@
## Input
```javascript
// @enableNewMutationAliasingModel
function bar(a) {
let x = [a];
let y = {};
(function () {
y = x[0];
})();
return y;
}
export const FIXTURE_ENTRYPOINT = {
fn: bar,
params: ['TodoAdd'],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
function bar(a) {
const $ = _c(4);
let t0;
if ($[0] !== a) {
t0 = [a];
$[0] = a;
$[1] = t0;
} else {
t0 = $[1];
}
const x = t0;
let y;
if ($[2] !== x[0]) {
y = {};
y = x[0];
$[2] = x[0];
$[3] = y;
} else {
y = $[3];
}
return y;
}
export const FIXTURE_ENTRYPOINT = {
fn: bar,
params: ["TodoAdd"],
};
```
### Eval output
(kind: ok) "TodoAdd"

View File

@@ -1,15 +0,0 @@
// @enableNewMutationAliasingModel
function bar(a) {
let x = [a];
let y = {};
(function () {
y = x[0];
})();
return y;
}
export const FIXTURE_ENTRYPOINT = {
fn: bar,
params: ['TodoAdd'],
};

View File

@@ -1,33 +0,0 @@
## Input
```javascript
// @validateNoImpureFunctionsInRender @enableNewMutationAliasingModel
function Component() {
const date = Date.now();
const now = performance.now();
const rand = Math.random();
return <Foo date={date} now={now} rand={rand} />;
}
```
## Error
```
2 |
3 | function Component() {
> 4 | const date = Date.now();
| ^^^^^^^^^^ InvalidReact: Calling an impure function can produce unstable results. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). `Date.now` is an impure function whose results may change on every call (4:4)
InvalidReact: Calling an impure function can produce unstable results. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). `performance.now` is an impure function whose results may change on every call (5:5)
InvalidReact: Calling an impure function can produce unstable results. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). `Math.random` is an impure function whose results may change on every call (6:6)
5 | const now = performance.now();
6 | const rand = Math.random();
7 | return <Foo date={date} now={now} rand={rand} />;
```

View File

@@ -1,8 +0,0 @@
// @validateNoImpureFunctionsInRender @enableNewMutationAliasingModel
function Component() {
const date = Date.now();
const now = performance.now();
const rand = Math.random();
return <Foo date={date} now={now} rand={rand} />;
}

View File

@@ -2,7 +2,6 @@
## Input
```javascript
// @enableNewMutationAliasingModel
function Component() {
let local;
@@ -42,13 +41,13 @@ function Component() {
## Error
```
4 |
5 | const reassignLocal = newValue => {
> 6 | local = newValue;
| ^^^^^ InvalidReact: Reassigning a variable after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead. Variable `local` cannot be reassigned after render (6:6)
7 | };
8 |
9 | const onClick = newValue => {
3 |
4 | const reassignLocal = newValue => {
> 5 | local = newValue;
| ^^^^^ InvalidReact: Reassigning a variable after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead. Variable `local` cannot be reassigned after render (5:5)
6 | };
7 |
8 | const onClick = newValue => {
```

View File

@@ -1,4 +1,3 @@
// @enableNewMutationAliasingModel
function Component() {
let local;

Some files were not shown because too many files have changed in this diff Show More