Compare commits

..

8 Commits

Author SHA1 Message Date
Joe Savona
ebc2f6b0fd [compiler] Rename InferFunctionExprAliasingEffectsSignature 2025-06-18 15:46:12 -07:00
Joe Savona
2a5f77c261 [compiler] More readable alias signature declarations
Now that we have support for defining aliasing signatures in moduleTypeProvider, which uses string names for receiver/args/returns/etc, we can reuse that same form for builtin declarations. The declarations are written in the unparsed form and than parsed/validated when registered (in the addFunction/addHook call).

This also required flushing out configs/schemas for more effect types.
2025-06-18 15:46:12 -07:00
Joseph Savona
34179fe344 [compiler] moduleTypeProvider support for aliasing signatures (#33526)
This allows us to type things like `nullthrows()` or `identity()`
functions where the return type is polymorphic on the input.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33526).
* #33571
* #33558
* #33547
* #33543
* #33533
* #33532
* #33530
* __->__ #33526
* #33522
* #33518
2025-06-18 15:43:48 -07:00
Joseph Savona
0e7cdebb32 [compiler] Repro for case of lost precision in new inference (#33522)
In comparing compilation output of the old/new inference models I found
this case (heavily distilled into a fixture). Roughly speaking the
scenario is:

* Create a mutable object `x`
* Extract part of that object and pass it to a hook/jsx so that _part_
becomes frozen
* Mutate `x`, even indirectly.

In the old model we can still independently memoize the value from the
middle step, since we assume that part of the larger value is not
changing. In the new model, the mutation from the later step effectively
overrides the freeze effect in step 2, and considers the value to have
changed later anyway.

We've already rolled out and vetted the previous behavior, confirming
that the heuristic of "that part of the mutable object is fozen now" is
generally safe. I'll fix in a follow-up.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33522).
* #33571
* #33558
* #33547
* #33543
* #33533
* #33532
* #33530
* #33526
* __->__ #33522
* #33518
2025-06-18 15:43:33 -07:00
Joseph Savona
81d8115116 [compiler] Fix infinite loop due to uncached applied signatures (#33518)
When we apply new aliasing signatures we can generate new temporaries,
which causes the abstract memory model to not converge. The fix is to
make sure we cache the applications of these signatures.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33518).
* #33571
* #33558
* #33547
* #33543
* #33533
* #33532
* #33530
* #33526
* #33522
* __->__ #33518
2025-06-18 15:43:23 -07:00
Joseph Savona
8f4ce72f0b [commit] Improve error for hoisting violations (#33514)
The previous error for hoisting violations pointed only to the variable
declaration, but didn't show where the value was accessed before that
declaration. We now track where each hoisted variable is first accessed
and report two errors, one for the reference and one for the
declaration. When we improve our diagnostic infra to support reporting
errors at multiple locations we can merge these into a single conceptual
error.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33514).
* #33571
* #33558
* #33547
* #33543
* #33533
* #33532
* #33530
* #33526
* #33522
* #33518
* __->__ #33514
* #33573
2025-06-18 15:24:41 -07:00
Joseph Savona
7ce2a63acc [compiler] update fixtures (#33573)
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33573).
* #33571
* #33558
* #33547
* #33543
* #33533
* #33532
* #33530
* #33526
* #33522
* #33518
* #33514
* __->__ #33573
2025-06-18 15:24:30 -07:00
Joseph Savona
b067c6fe79 [compiler] Improve error message for mutating hook args/return (#33513)
The previous error message was generic, because the old style function
signature didn't support a way to specify a reason alongside a freeze
effect. This meant we could only say why a value was frozen for
instructions, but not hooks which use function signatures. By defining a
new aliasing signature for custom hooks we can specify a reason and
provide a better error message.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33513).
* #33571
* #33558
* #33547
* #33543
* #33533
* #33532
* #33530
* #33526
* #33522
* #33518
* #33514
* __->__ #33513
2025-06-18 13:04:53 -07:00
25 changed files with 1244 additions and 332 deletions

View File

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

View File

@@ -1453,6 +1453,20 @@ 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

View File

@@ -6,14 +6,18 @@
*/
import {CompilerError} from '../CompilerError';
import {AliasingSignature} from '../Inference/AliasingEffects';
import {AliasingEffect, AliasingSignature} from '../Inference/AliasingEffects';
import {assertExhaustive} from '../Utils/utils';
import {
Effect,
GeneratedSource,
Hole,
makeDeclarationId,
makeIdentifierId,
makeInstructionId,
Place,
SourceLocation,
SpreadPattern,
ValueKind,
ValueReason,
} from './HIR';
@@ -25,6 +29,7 @@ import {
PolyType,
PrimitiveType,
} from './Types';
import {AliasingEffectConfig, AliasingSignatureConfig} from './TypeSchema';
/*
* This file exports types and defaults for JavaScript object shapes. These are
@@ -53,13 +58,20 @@ function createAnonId(): string {
export function addFunction(
registry: ShapeRegistry,
properties: Iterable<[string, BuiltInType | PolyType]>,
fn: Omit<FunctionSignature, 'hookKind'>,
fn: Omit<FunctionSignature, 'hookKind' | 'aliasing'> & {
aliasing?: AliasingSignatureConfig | null | undefined;
},
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 {
@@ -77,11 +89,18 @@ export function addFunction(
*/
export function addHook(
registry: ShapeRegistry,
fn: FunctionSignature & {hookKind: HookKind},
fn: Omit<FunctionSignature, 'aliasing'> & {
hookKind: HookKind;
aliasing?: AliasingSignatureConfig | null | undefined;
},
id: string | null = null,
): FunctionType {
const shapeId = id ?? createAnonId();
addShape(registry, shapeId, [], fn);
const aliasing =
fn.aliasing != null
? parseAliasingSignatureConfig(fn.aliasing, '<builtin>', GeneratedSource)
: null;
addShape(registry, shapeId, [], {...fn, aliasing});
return {
kind: 'Function',
return: fn.returnType,
@@ -90,6 +109,129 @@ 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.
*
@@ -192,8 +334,7 @@ export type FunctionSignature = {
canonicalName?: string;
aliasing?: AliasingSignature | null;
todo_aliasing?: AliasingSignature | null;
aliasing?: AliasingSignature | null | undefined;
};
/*
@@ -320,24 +461,24 @@ addObject(BUILTIN_SHAPES, BuiltInArrayId, [
calleeEffect: Effect.Store,
returnValueKind: ValueKind.Primitive,
aliasing: {
receiver: makeIdentifierId(0),
receiver: '@receiver',
params: [],
rest: makeIdentifierId(1),
returns: makeIdentifierId(2),
rest: '@rest',
returns: '@returns',
temporaries: [],
effects: [
// Push directly mutates the array itself
{kind: 'Mutate', value: signatureArgument(0)},
{kind: 'Mutate', value: '@receiver'},
// The arguments are captured into the array
{
kind: 'Capture',
from: signatureArgument(1),
into: signatureArgument(0),
from: '@rest',
into: '@receiver',
},
// Returns the new length, a primitive
{
kind: 'Create',
into: signatureArgument(2),
into: '@returns',
value: ValueKind.Primitive,
reason: ValueReason.KnownReturnSignature,
},
@@ -374,58 +515,56 @@ addObject(BUILTIN_SHAPES, BuiltInArrayId, [
noAlias: true,
mutableOnlyIfOperandsAreMutable: true,
aliasing: {
receiver: makeIdentifierId(0),
params: [makeIdentifierId(1)],
receiver: '@receiver',
params: ['@callback'],
rest: null,
returns: makeIdentifierId(2),
returns: '@returns',
temporaries: [
// Temporary representing captured items of the receiver
signatureArgument(3),
'@item',
// Temporary representing the result of the callback
signatureArgument(4),
'@callbackReturn',
/*
* Undefined `this` arg to the callback. Note the signature does not
* support passing an explicit thisArg second param
*/
signatureArgument(5),
'@thisArg',
],
effects: [
// Map creates a new mutable array
{
kind: 'Create',
into: signatureArgument(2),
into: '@returns',
value: ValueKind.Mutable,
reason: ValueReason.KnownReturnSignature,
},
// The first arg to the callback is an item extracted from the receiver array
{
kind: 'CreateFrom',
from: signatureArgument(0),
into: signatureArgument(3),
from: '@receiver',
into: '@item',
},
// The undefined this for the callback
{
kind: 'Create',
into: signatureArgument(5),
into: '@thisArg',
value: ValueKind.Primitive,
reason: ValueReason.KnownReturnSignature,
},
// calls the callback, returning the result into a temporary
{
kind: 'Apply',
receiver: signatureArgument(5),
args: [signatureArgument(3), {kind: 'Hole'}, signatureArgument(0)],
function: signatureArgument(1),
into: signatureArgument(4),
signature: null,
receiver: '@thisArg',
args: ['@item', {kind: 'Hole'}, '@receiver'],
function: '@callback',
into: '@callbackReturn',
mutatesFunction: false,
loc: GeneratedSource,
},
// captures the result of the callback into the return array
{
kind: 'Capture',
from: signatureArgument(4),
into: signatureArgument(2),
from: '@callbackReturn',
into: '@returns',
},
],
},
@@ -577,28 +716,28 @@ addObject(BUILTIN_SHAPES, BuiltInSetId, [
// returnValueKind is technically dependent on the ValueKind of the set itself
returnValueKind: ValueKind.Mutable,
aliasing: {
receiver: makeIdentifierId(0),
receiver: '@receiver',
params: [],
rest: makeIdentifierId(1),
returns: makeIdentifierId(2),
rest: '@rest',
returns: '@returns',
temporaries: [],
effects: [
// Set.add returns the receiver Set
{
kind: 'Assign',
from: signatureArgument(0),
into: signatureArgument(2),
from: '@receiver',
into: '@returns',
},
// Set.add mutates the set itself
{
kind: 'Mutate',
value: signatureArgument(0),
value: '@receiver',
},
// Captures the rest params into the set
{
kind: 'Capture',
from: signatureArgument(1),
into: signatureArgument(0),
from: '@rest',
into: '@receiver',
},
],
},
@@ -1303,30 +1442,30 @@ export const DefaultNonmutatingHook = addHook(
hookKind: 'Custom',
returnValueKind: ValueKind.Frozen,
aliasing: {
receiver: makeIdentifierId(0),
receiver: '@receiver',
params: [],
rest: makeIdentifierId(1),
returns: makeIdentifierId(2),
rest: '@rest',
returns: '@returns',
temporaries: [],
effects: [
// Freeze the arguments
{
kind: 'Freeze',
value: signatureArgument(1),
value: '@rest',
reason: ValueReason.HookCaptured,
},
// Returns a frozen value
{
kind: 'Create',
into: signatureArgument(2),
into: '@returns',
value: ValueKind.Frozen,
reason: ValueReason.HookReturn,
},
// May alias any arguments into the return
{
kind: 'Alias',
from: signatureArgument(1),
into: signatureArgument(2),
from: '@rest',
into: '@returns',
},
],
},

View File

@@ -8,7 +8,12 @@
import {isValidIdentifier} from '@babel/types';
import {z} from 'zod';
import {Effect, ValueKind} from '..';
import {EffectSchema, ValueKindSchema} from './HIR';
import {
EffectSchema,
ValueKindSchema,
ValueReason,
ValueReasonSchema,
} from './HIR';
export type ObjectPropertiesConfig = {[key: string]: TypeConfig};
export const ObjectPropertiesSchema: z.ZodType<ObjectPropertiesConfig> = z
@@ -31,6 +36,194 @@ 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>;
@@ -42,6 +235,7 @@ 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'),
@@ -54,6 +248,7 @@ 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 = {
@@ -63,6 +258,7 @@ 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'),
@@ -71,6 +267,7 @@ 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,6 +8,7 @@
import {CompilerErrorDetailOptions} from '../CompilerError';
import {
FunctionExpression,
GeneratedSource,
Hole,
IdentifierId,
ObjectMethod,
@@ -18,6 +19,7 @@ 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
@@ -200,10 +202,19 @@ export function hashEffect(effect: AliasingEffect): string {
return [effect.kind, effect.value.identifier.id, effect.reason].join(':');
}
case 'Impure':
case 'Render':
case 'Render': {
return [effect.kind, effect.place.identifier.id].join(':');
}
case 'MutateFrozen':
case 'MutateGlobal': {
return [effect.kind, effect.place.identifier.id].join(':');
return [
effect.kind,
effect.place.identifier.id,
effect.error.severity,
effect.error.reason,
effect.error.description,
printSourceLocation(effect.error.loc ?? GeneratedSource),
].join(':');
}
case 'Mutate':
case 'MutateConditionally':

View File

@@ -22,7 +22,7 @@ import {inferMutableRanges} from './InferMutableRanges';
import inferReferenceEffects from './InferReferenceEffects';
import {assertExhaustive} from '../Utils/utils';
import {inferMutationAliasingEffects} from './InferMutationAliasingEffects';
import {inferMutationAliasingFunctionEffects} from './InferMutationAliasingFunctionEffects';
import {inferFunctionExpressionAliasingEffectsSignature} from './InferFunctionExpressionAliasingEffectsSignature';
import {inferMutationAliasingRanges} from './InferMutationAliasingRanges';
export default function analyseFunctions(func: HIRFunction): void {
@@ -71,7 +71,7 @@ function lowerWithMutationAliasing(fn: HIRFunction): void {
inferMutationAliasingRanges(fn, {isFunctionExpression: true});
rewriteInstructionKindsBasedOnReassignment(fn);
inferReactiveScopeVariables(fn);
const effects = inferMutationAliasingFunctionEffects(fn);
const effects = inferFunctionExpressionAliasingEffectsSignature(fn);
fn.env.logger?.debugLogIRs?.({
kind: 'hir',
name: 'AnalyseFunction (inner)',

View File

@@ -28,7 +28,7 @@ import {AliasingEffect} from './AliasingEffects';
* 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(
export function inferFunctionExpressionAliasingEffectsSignature(
fn: HIRFunction,
): Array<AliasingEffect> | null {
const effects: Array<AliasingEffect> = [];

View File

@@ -38,6 +38,7 @@ import {
import {
eachInstructionValueLValue,
eachInstructionValueOperand,
eachTerminalOperand,
eachTerminalSuccessor,
} from '../HIR/visitors';
import {Ok, Result} from '../Utils/Result';
@@ -49,12 +50,14 @@ import {
} from './InferReferenceEffects';
import {
assertExhaustive,
getOrInsertDefault,
getOrInsertWith,
Set_isSuperset,
} from '../Utils/utils';
import {
printAliasingEffect,
printAliasingSignature,
printFunction,
printIdentifier,
printInstruction,
printInstructionValue,
@@ -194,12 +197,15 @@ export function inferMutationAliasingEffects(
let count = 0;
while (queuedStates.size !== 0) {
count++;
if (count > 1000) {
if (count > 100) {
console.log(
'oops infinite loop',
fn.id,
typeof fn.loc !== 'symbol' ? fn.loc?.filename : null,
);
if (DEBUG) {
console.log(printFunction(fn));
}
throw new Error('infinite loop');
}
for (const [blockId, block] of fn.body.blocks) {
@@ -211,6 +217,11 @@ export function inferMutationAliasingEffects(
statesByBlock.set(blockId, incomingState);
const state = incomingState.clone();
if (DEBUG) {
console.log('*************');
console.log(`bb${block.id}`);
console.log('*************');
}
inferBlock(context, state, block);
for (const nextBlockId of eachTerminalSuccessor(block.terminal)) {
@@ -221,8 +232,19 @@ export function inferMutationAliasingEffects(
return Ok(undefined);
}
function findHoistedContextDeclarations(fn: HIRFunction): Set<DeclarationId> {
const hoisted = new Set<DeclarationId>();
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);
}
}
for (const block of fn.body.blocks.values()) {
for (const instr of block.instructions) {
if (instr.value.kind === 'DeclareContext') {
@@ -232,10 +254,17 @@ function findHoistedContextDeclarations(fn: HIRFunction): Set<DeclarationId> {
kind == InstructionKind.HoistedFunction ||
kind == InstructionKind.HoistedLet
) {
hoisted.add(instr.value.lvalue.place.identifier.declarationId);
hoisted.set(instr.value.lvalue.place.identifier.declarationId, null);
}
} else {
for (const operand of eachInstructionValueOperand(instr.value)) {
visit(operand);
}
}
}
for (const operand of eachTerminalOperand(block.terminal)) {
visit(operand);
}
}
return hoisted;
}
@@ -245,21 +274,40 @@ 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: Set<DeclarationId>;
hoistedContextDeclarations: Map<DeclarationId, Place | null>;
constructor(
isFunctionExpression: boolean,
fn: HIRFunction,
hoistedContextDeclarations: Set<DeclarationId>,
hoistedContextDeclarations: Map<DeclarationId, Place | null>,
) {
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);
@@ -333,11 +381,13 @@ function inferBlock(
state.appendAlias(handlerParam, instr.lvalue);
const kind = state.kind(instr.lvalue).kind;
if (kind === ValueKind.Mutable || kind == ValueKind.Context) {
effects.push({
kind: 'Alias',
from: instr.lvalue,
into: handlerParam,
});
effects.push(
context.internEffect({
kind: 'Alias',
from: instr.lvalue,
into: handlerParam,
}),
);
}
}
}
@@ -346,11 +396,11 @@ function inferBlock(
} else if (terminal.kind === 'return') {
if (!context.isFuctionExpression) {
terminal.effects = [
{
context.internEffect({
kind: 'Freeze',
value: terminal.value,
reason: ValueReason.JsxCaptured,
},
}),
];
}
}
@@ -527,20 +577,21 @@ function applyEffect(
break;
}
case ValueKind.Frozen: {
effects.push({
kind: 'ImmutableCapture',
from: effect.from,
into: effect.into,
});
applyEffect(
context,
state,
{
kind: 'ImmutableCapture',
from: effect.from,
into: effect.into,
},
aliased,
effects,
);
break;
}
default: {
effects.push({
// OK: recording information flow
kind: 'CreateFrom', // prev Alias
from: effect.from,
into: effect.into,
});
effects.push(effect);
}
}
break;
@@ -639,11 +690,17 @@ function applyEffect(
}
case ValueKind.Frozen: {
isMutableReferenceType = false;
effects.push({
kind: 'ImmutableCapture',
from: effect.from,
into: effect.into,
});
applyEffect(
context,
state,
{
kind: 'ImmutableCapture',
from: effect.from,
into: effect.into,
},
aliased,
effects,
);
break;
}
default: {
@@ -665,11 +722,17 @@ function applyEffect(
const fromKind = fromValue.kind;
switch (fromKind) {
case ValueKind.Frozen: {
effects.push({
kind: 'ImmutableCapture',
from: effect.from,
into: effect.into,
});
applyEffect(
context,
state,
{
kind: 'ImmutableCapture',
from: effect.from,
into: effect.into,
},
aliased,
effects,
);
let value = context.effectInstructionValueCache.get(effect);
if (value == null) {
value = {
@@ -727,23 +790,33 @@ function applyEffect(
* 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 signature = buildSignatureFromFunctionExpression(
state.env,
functionValues[0],
);
const functionExpr = functionValues[0];
let signature = context.functionSignatureCache.get(functionExpr);
if (signature == null) {
signature = buildSignatureFromFunctionExpression(
state.env,
functionExpr,
);
context.functionSignatureCache.set(functionExpr, signature);
}
if (DEBUG) {
console.log(
`constructed alias signature:\n${printAliasingSignature(signature)}`,
);
}
const signatureEffects = computeEffectsForSignature(
state.env,
const signatureEffects = context.cacheApplySignature(
signature,
effect.into,
effect.receiver,
effect.args,
functionValues[0].loweredFunc.func.context,
effect.loc,
effect,
() =>
computeEffectsForSignature(
state.env,
signature,
effect.into,
effect.receiver,
effect.args,
functionExpr.loweredFunc.func.context,
effect.loc,
),
);
if (signatureEffects != null) {
if (DEBUG) {
@@ -762,18 +835,24 @@ function applyEffect(
break;
}
}
const signatureEffects =
effect.signature?.aliasing != null
? computeEffectsForSignature(
let signatureEffects = null;
if (effect.signature?.aliasing != null) {
const signature = effect.signature.aliasing;
signatureEffects = context.cacheApplySignature(
effect.signature.aliasing,
effect,
() =>
computeEffectsForSignature(
state.env,
effect.signature.aliasing,
signature,
effect.into,
effect.receiver,
effect.args,
[],
effect.loc,
)
: null;
),
);
}
if (signatureEffects != null) {
if (DEBUG) {
console.log('apply aliasing signature effects');
@@ -901,48 +980,89 @@ function applyEffect(
console.log(prettyFormat(state.debugAbstractValue(value)));
}
let reason: string;
let description: string | null = null;
if (
mutationKind === 'mutate-frozen' &&
context.hoistedContextDeclarations.has(
effect.value.identifier.declarationId,
)
) {
reason = `This variable is accessed before it is declared, which prevents the earlier access from updating when this value changes over time`;
if (
const description =
effect.value.identifier.name !== null &&
effect.value.identifier.name.kind === 'named'
) {
description = `Move the declaration of \`${effect.value.identifier.name.value}\` to before it is first referenced`;
? `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,
},
},
aliased,
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,
},
},
aliased,
effects,
);
} else {
reason = getWriteErrorReason({
const reason = getWriteErrorReason({
kind: value.kind,
reason: value.reason,
context: new Set(),
});
if (
const description =
effect.value.identifier.name !== null &&
effect.value.identifier.name.kind === 'named'
) {
description = `Found mutation of \`${effect.value.identifier.name.value}\``;
}
? `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,
},
},
aliased,
effects,
);
}
effects.push({
kind:
value.kind === ValueKind.Frozen ? 'MutateFrozen' : 'MutateGlobal',
place: effect.value,
error: {
severity: ErrorSeverity.InvalidReact,
reason,
description,
loc: effect.value.loc,
suggestions: null,
},
});
}
break;
}

View File

@@ -25,7 +25,7 @@ import {
import {assertExhaustive, getOrInsertWith} from '../Utils/utils';
import {printFunction} from '../HIR';
import {printIdentifier, printPlace} from '../HIR/PrintHIR';
import {MutationKind} from './InferMutationAliasingFunctionEffects';
import {MutationKind} from './InferFunctionExpressionAliasingEffectsSignature';
import {Result} from '../Utils/Result';
const DEBUG = false;

View File

@@ -38,13 +38,15 @@ export const FIXTURE_ENTRYPOINT = {
## Error
```
19 | useEffect(() => setState(2), []);
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)
20 |
> 21 | const [state, setState] = useState(0);
| ^^^^^^^^ InvalidReact: This variable is accessed before it is declared, which prevents the earlier access from updating when this value changes over time. Move the declaration of `setState` to before it is first referenced (21:21)
21 | const [state, setState] = useState(0);
22 | return <Stringify state={state} />;
23 | }
24 |
```

View File

@@ -57,67 +57,62 @@ import { Stringify } from "shared-runtime";
* - cb1 is not assumed to be called since it's only used as a call operand
*/
function useFoo(t0) {
const $ = _c(14);
let arr1;
let arr2;
const $ = _c(13);
const { arr1, arr2 } = t0;
let t1;
if ($[0] !== t0) {
({ arr1, arr2 } = t0);
let t2;
if ($[4] !== arr1[0]) {
t2 = (e) => arr1[0].value + e.value;
$[4] = arr1[0];
$[5] = t2;
} else {
t2 = $[5];
}
const cb1 = t2;
t1 = () => arr1.map(cb1);
$[0] = t0;
$[1] = arr1;
$[2] = arr2;
$[3] = t1;
if ($[0] !== arr1[0]) {
t1 = (e) => arr1[0].value + e.value;
$[0] = arr1[0];
$[1] = t1;
} else {
arr1 = $[1];
arr2 = $[2];
t1 = $[3];
t1 = $[1];
}
const getArrMap1 = t1;
const cb1 = t1;
let t2;
if ($[6] !== arr2) {
t2 = (e_0) => arr2[0].value + e_0.value;
$[6] = arr2;
$[7] = t2;
if ($[2] !== arr1 || $[3] !== cb1) {
t2 = () => arr1.map(cb1);
$[2] = arr1;
$[3] = cb1;
$[4] = t2;
} else {
t2 = $[7];
t2 = $[4];
}
const cb2 = t2;
const getArrMap1 = t2;
let t3;
if ($[8] !== arr1 || $[9] !== cb2) {
t3 = () => arr1.map(cb2);
$[8] = arr1;
$[9] = cb2;
$[10] = t3;
if ($[5] !== arr2) {
t3 = (e_0) => arr2[0].value + e_0.value;
$[5] = arr2;
$[6] = t3;
} else {
t3 = $[10];
t3 = $[6];
}
const getArrMap2 = t3;
const cb2 = t3;
let t4;
if ($[11] !== getArrMap1 || $[12] !== getArrMap2) {
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}
/>
);
$[11] = getArrMap1;
$[12] = getArrMap2;
$[13] = t4;
$[10] = getArrMap1;
$[11] = getArrMap2;
$[12] = t5;
} else {
t4 = $[13];
t5 = $[12];
}
return t4;
return t5;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -58,67 +58,62 @@ import { Stringify } from "shared-runtime";
* - cb1 is not assumed to be called since it's only used as a call operand
*/
function useFoo(t0) {
const $ = _c(14);
let arr1;
let arr2;
const $ = _c(13);
const { arr1, arr2 } = t0;
let t1;
if ($[0] !== t0) {
({ arr1, arr2 } = t0);
let t2;
if ($[4] !== arr1[0]) {
t2 = (e) => arr1[0].value + e.value;
$[4] = arr1[0];
$[5] = t2;
} else {
t2 = $[5];
}
const cb1 = t2;
t1 = () => arr1.map(cb1);
$[0] = t0;
$[1] = arr1;
$[2] = arr2;
$[3] = t1;
if ($[0] !== arr1[0]) {
t1 = (e) => arr1[0].value + e.value;
$[0] = arr1[0];
$[1] = t1;
} else {
arr1 = $[1];
arr2 = $[2];
t1 = $[3];
t1 = $[1];
}
const getArrMap1 = t1;
const cb1 = t1;
let t2;
if ($[6] !== arr2) {
t2 = (e_0) => arr2[0].value + e_0.value;
$[6] = arr2;
$[7] = t2;
if ($[2] !== arr1 || $[3] !== cb1) {
t2 = () => arr1.map(cb1);
$[2] = arr1;
$[3] = cb1;
$[4] = t2;
} else {
t2 = $[7];
t2 = $[4];
}
const cb2 = t2;
const getArrMap1 = t2;
let t3;
if ($[8] !== arr1 || $[9] !== cb2) {
t3 = () => arr1.map(cb2);
$[8] = arr1;
$[9] = cb2;
$[10] = t3;
if ($[5] !== arr2) {
t3 = (e_0) => arr2[0].value + e_0.value;
$[5] = arr2;
$[6] = t3;
} else {
t3 = $[10];
t3 = $[6];
}
const getArrMap2 = t3;
const cb2 = t3;
let t4;
if ($[11] !== getArrMap1 || $[12] !== getArrMap2) {
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}
/>
);
$[11] = getArrMap1;
$[12] = getArrMap2;
$[13] = t4;
$[10] = getArrMap1;
$[11] = getArrMap2;
$[12] = t5;
} else {
t4 = $[13];
t5 = $[12];
}
return t4;
return t5;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -31,13 +31,15 @@ function Component({content, refetch}) {
## Error
```
17 | // This has to error: onRefetch needs to memoize with `content` as a
18 | // dependency, but the dependency comes later
> 19 | const {data = null} = content;
| ^^^^^^^^^^^ InvalidReact: This variable is accessed before it is declared, which prevents the earlier access from updating when this value changes over time. Move the declaration of `data` to before it is first referenced (19:19)
20 |
21 | return <Foo data={data} onSubmit={onSubmit} />;
22 | }
9 | // TDZ violation!
10 | const onRefetch = useCallback(() => {
> 11 | refetch(data);
| ^^^^ InvalidReact: This variable is accessed before it is declared, which may prevent it from updating as the assigned value changes over time. Variable `data` is accessed before it is declared (11:11)
InvalidReact: This variable is accessed before it is declared, which prevents the earlier access from updating when this value changes over time. Variable `data` is accessed before it is declared (19:19)
12 | }, [refetch]);
13 |
14 | // The context variable gets frozen here since it's passed to a hook
```

View File

@@ -0,0 +1,54 @@
## Input
```javascript
// @flow @enableNewMutationAliasingModel
import fbt from 'fbt';
component Component() {
const sections = Object.keys(items);
for (let i = 0; i < sections.length; i += 3) {
chunks.push(
sections.slice(i, i + 3).map(section => {
return <Child />;
})
);
}
return <Child />;
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import fbt from "fbt";
function Component() {
const $ = _c(1);
const sections = Object.keys(items);
for (let i = 0; i < sections.length; i = i + 3, i) {
chunks.push(sections.slice(i, i + 3).map(_temp));
}
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = <Child />;
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
function _temp(section) {
return <Child />;
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -0,0 +1,17 @@
// @flow @enableNewMutationAliasingModel
import fbt from 'fbt';
component Component() {
const sections = Object.keys(items);
for (let i = 0; i < sections.length; i += 3) {
chunks.push(
sections.slice(i, i + 3).map(section => {
return <Child />;
})
);
}
return <Child />;
}

View File

@@ -0,0 +1,53 @@
## Input
```javascript
// @flow @enableNewMutationAliasingModel
import {identity, Stringify, useFragment} from 'shared-runtime';
component Example() {
const data = useFragment();
const {a, b} = identity(data);
const el = <Stringify tooltip={b} />;
identity(a.at(0));
return <Stringify icon={el} />;
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { identity, Stringify, useFragment } from "shared-runtime";
function Example() {
const $ = _c(2);
const data = useFragment();
let t0;
if ($[0] !== data) {
const { a, b } = identity(data);
const el = <Stringify tooltip={b} />;
identity(a.at(0));
t0 = <Stringify icon={el} />;
$[0] = data;
$[1] = t0;
} else {
t0 = $[1];
}
return t0;
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -0,0 +1,15 @@
// @flow @enableNewMutationAliasingModel
import {identity, Stringify, useFragment} from 'shared-runtime';
component Example() {
const data = useFragment();
const {a, b} = identity(data);
const el = <Stringify tooltip={b} />;
identity(a.at(0));
return <Stringify icon={el} />;
}

View File

@@ -30,60 +30,47 @@ import { c as _c, useFire } from "react/compiler-runtime"; // @enableFire @enabl
import { fire } from "react";
function Component(t0) {
const $ = _c(13);
let bar;
let baz;
let foo;
if ($[0] !== t0) {
({ bar, baz } = t0);
let t1;
if ($[4] !== bar) {
t1 = () => {
console.log(bar);
};
$[4] = bar;
$[5] = t1;
} else {
t1 = $[5];
}
foo = t1;
$[0] = t0;
$[1] = bar;
$[2] = baz;
$[3] = foo;
const $ = _c(9);
const { bar, baz } = t0;
let t1;
if ($[0] !== bar) {
t1 = () => {
console.log(bar);
};
$[0] = bar;
$[1] = t1;
} else {
bar = $[1];
baz = $[2];
foo = $[3];
t1 = $[1];
}
const t1 = useFire(foo);
const t2 = useFire(baz);
let t3;
if ($[6] !== bar || $[7] !== t1 || $[8] !== t2) {
t3 = () => {
t1(bar);
const foo = t1;
const t2 = useFire(foo);
const t3 = useFire(baz);
let t4;
if ($[2] !== bar || $[3] !== t2 || $[4] !== t3) {
t4 = () => {
t2(bar);
t3(bar);
};
$[2] = bar;
$[3] = t2;
$[4] = t3;
$[5] = t4;
} else {
t4 = $[5];
}
useEffect(t4);
let t5;
if ($[6] !== bar || $[7] !== t2) {
t5 = () => {
t2(bar);
};
$[6] = bar;
$[7] = t1;
$[8] = t2;
$[9] = t3;
$[7] = t2;
$[8] = t5;
} else {
t3 = $[9];
t5 = $[8];
}
useEffect(t3);
let t4;
if ($[10] !== bar || $[11] !== t1) {
t4 = () => {
t1(bar);
};
$[10] = bar;
$[11] = t1;
$[12] = t4;
} else {
t4 = $[12];
}
useEffect(t4);
useEffect(t5);
return null;
}

View File

@@ -0,0 +1,117 @@
## Input
```javascript
// @enableNewMutationAliasingModel
import {
identity,
makeObject_Primitives,
typedIdentity,
useIdentity,
ValidateMemoization,
} from 'shared-runtime';
function Component({a, b}) {
// create a mutable value with input `a`
const x = makeObject_Primitives(a);
// freeze the value
useIdentity(x);
// known to pass-through via aliasing signature
const x2 = typedIdentity(x);
// Unknown function so we assume it conditionally mutates,
// but x2 is frozen so this downgrades to a read.
// x should *not* take b as a dependency
identity(x2, b);
return <ValidateMemoization inputs={[a]} output={x} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: 0, b: 0}],
sequentialRenders: [
{a: 0, b: 0},
{a: 1, b: 0},
{a: 1, b: 1},
{a: 0, b: 1},
{a: 0, b: 0},
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
import {
identity,
makeObject_Primitives,
typedIdentity,
useIdentity,
ValidateMemoization,
} from "shared-runtime";
function Component(t0) {
const $ = _c(7);
const { a, b } = t0;
let t1;
if ($[0] !== a) {
t1 = makeObject_Primitives(a);
$[0] = a;
$[1] = t1;
} else {
t1 = $[1];
}
const x = t1;
useIdentity(x);
const x2 = typedIdentity(x);
identity(x2, b);
let t2;
if ($[2] !== a) {
t2 = [a];
$[2] = a;
$[3] = t2;
} else {
t2 = $[3];
}
let t3;
if ($[4] !== t2 || $[5] !== x) {
t3 = <ValidateMemoization inputs={t2} output={x} />;
$[4] = t2;
$[5] = x;
$[6] = t3;
} else {
t3 = $[6];
}
return t3;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ a: 0, b: 0 }],
sequentialRenders: [
{ a: 0, b: 0 },
{ a: 1, b: 0 },
{ a: 1, b: 1 },
{ a: 0, b: 1 },
{ a: 0, b: 0 },
],
};
```
### Eval output
(kind: ok) <div>{"inputs":[0],"output":{"a":0,"b":"value1","c":true}}</div>
<div>{"inputs":[1],"output":{"a":0,"b":"value1","c":true}}</div>
<div>{"inputs":[1],"output":{"a":0,"b":"value1","c":true}}</div>
<div>{"inputs":[0],"output":{"a":0,"b":"value1","c":true}}</div>
<div>{"inputs":[0],"output":{"a":0,"b":"value1","c":true}}</div>

View File

@@ -0,0 +1,39 @@
// @enableNewMutationAliasingModel
import {
identity,
makeObject_Primitives,
typedIdentity,
useIdentity,
ValidateMemoization,
} from 'shared-runtime';
function Component({a, b}) {
// create a mutable value with input `a`
const x = makeObject_Primitives(a);
// freeze the value
useIdentity(x);
// known to pass-through via aliasing signature
const x2 = typedIdentity(x);
// Unknown function so we assume it conditionally mutates,
// but x2 is frozen so this downgrades to a read.
// x should *not* take b as a dependency
identity(x2, b);
return <ValidateMemoization inputs={[a]} output={x} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: 0, b: 0}],
sequentialRenders: [
{a: 0, b: 0},
{a: 1, b: 0},
{a: 1, b: 1},
{a: 0, b: 1},
{a: 0, b: 0},
],
};

View File

@@ -0,0 +1,112 @@
## Input
```javascript
// @enableNewMutationAliasingModel
import {
identity,
makeObject_Primitives,
typedIdentity,
useIdentity,
ValidateMemoization,
} from 'shared-runtime';
function Component({a, b}) {
// create a mutable value with input `a`
const x = makeObject_Primitives(a);
// known to pass-through via aliasing signature
const x2 = typedIdentity(x);
// Unknown function so we assume it conditionally mutates,
// and x is still mutable so
identity(x2, b);
return <ValidateMemoization inputs={[a, b]} output={x} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: 0, b: 0}],
sequentialRenders: [
{a: 0, b: 0},
{a: 1, b: 0},
{a: 1, b: 1},
{a: 0, b: 1},
{a: 0, b: 0},
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
import {
identity,
makeObject_Primitives,
typedIdentity,
useIdentity,
ValidateMemoization,
} from "shared-runtime";
function Component(t0) {
const $ = _c(9);
const { a, b } = t0;
let x;
if ($[0] !== a || $[1] !== b) {
x = makeObject_Primitives(a);
const x2 = typedIdentity(x);
identity(x2, b);
$[0] = a;
$[1] = b;
$[2] = x;
} else {
x = $[2];
}
let t1;
if ($[3] !== a || $[4] !== b) {
t1 = [a, b];
$[3] = a;
$[4] = b;
$[5] = t1;
} else {
t1 = $[5];
}
let t2;
if ($[6] !== t1 || $[7] !== x) {
t2 = <ValidateMemoization inputs={t1} output={x} />;
$[6] = t1;
$[7] = x;
$[8] = t2;
} else {
t2 = $[8];
}
return t2;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ a: 0, b: 0 }],
sequentialRenders: [
{ a: 0, b: 0 },
{ a: 1, b: 0 },
{ a: 1, b: 1 },
{ a: 0, b: 1 },
{ a: 0, b: 0 },
],
};
```
### Eval output
(kind: ok) <div>{"inputs":[0,0],"output":{"a":0,"b":"value1","c":true}}</div>
<div>{"inputs":[1,0],"output":{"a":0,"b":"value1","c":true}}</div>
<div>{"inputs":[1,1],"output":{"a":0,"b":"value1","c":true}}</div>
<div>{"inputs":[0,1],"output":{"a":0,"b":"value1","c":true}}</div>
<div>{"inputs":[0,0],"output":{"a":0,"b":"value1","c":true}}</div>

View File

@@ -0,0 +1,35 @@
// @enableNewMutationAliasingModel
import {
identity,
makeObject_Primitives,
typedIdentity,
useIdentity,
ValidateMemoization,
} from 'shared-runtime';
function Component({a, b}) {
// create a mutable value with input `a`
const x = makeObject_Primitives(a);
// known to pass-through via aliasing signature
const x2 = typedIdentity(x);
// Unknown function so we assume it conditionally mutates,
// and x is still mutable so
identity(x2, b);
return <ValidateMemoization inputs={[a, b]} output={x} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: 0, b: 0}],
sequentialRenders: [
{a: 0, b: 0},
{a: 1, b: 0},
{a: 1, b: 1},
{a: 0, b: 1},
{a: 0, b: 0},
],
};

View File

@@ -30,60 +30,47 @@ import { c as _c, useFire } from "react/compiler-runtime"; // @enableFire
import { fire } from "react";
function Component(t0) {
const $ = _c(13);
let bar;
let baz;
let foo;
if ($[0] !== t0) {
({ bar, baz } = t0);
let t1;
if ($[4] !== bar) {
t1 = () => {
console.log(bar);
};
$[4] = bar;
$[5] = t1;
} else {
t1 = $[5];
}
foo = t1;
$[0] = t0;
$[1] = bar;
$[2] = baz;
$[3] = foo;
const $ = _c(9);
const { bar, baz } = t0;
let t1;
if ($[0] !== bar) {
t1 = () => {
console.log(bar);
};
$[0] = bar;
$[1] = t1;
} else {
bar = $[1];
baz = $[2];
foo = $[3];
t1 = $[1];
}
const t1 = useFire(foo);
const t2 = useFire(baz);
let t3;
if ($[6] !== bar || $[7] !== t1 || $[8] !== t2) {
t3 = () => {
t1(bar);
const foo = t1;
const t2 = useFire(foo);
const t3 = useFire(baz);
let t4;
if ($[2] !== bar || $[3] !== t2 || $[4] !== t3) {
t4 = () => {
t2(bar);
t3(bar);
};
$[2] = bar;
$[3] = t2;
$[4] = t3;
$[5] = t4;
} else {
t4 = $[5];
}
useEffect(t4);
let t5;
if ($[6] !== bar || $[7] !== t2) {
t5 = () => {
t2(bar);
};
$[6] = bar;
$[7] = t1;
$[8] = t2;
$[9] = t3;
$[7] = t2;
$[8] = t5;
} else {
t3 = $[9];
t5 = $[8];
}
useEffect(t3);
let t4;
if ($[10] !== bar || $[11] !== t1) {
t4 = () => {
t1(bar);
};
$[10] = bar;
$[11] = t1;
$[12] = t4;
} else {
t4 = $[12];
}
useEffect(t4);
useEffect(t5);
return null;
}

View File

@@ -69,6 +69,22 @@ export function makeSharedRuntimeTypeProvider({
returnValueKind: ValueKindEnum.Mutable,
noAlias: true,
},
typedIdentity: {
kind: 'function',
positionalParams: [EffectEnum.Read],
restParam: null,
calleeEffect: EffectEnum.Read,
returnType: {kind: 'type', name: 'Any'},
returnValueKind: ValueKindEnum.Mutable,
aliasing: {
receiver: '@receiver',
params: ['@value'],
rest: null,
returns: '@return',
temporaries: [],
effects: [{kind: 'Assign', from: '@value', into: '@return'}],
},
},
},
};
} else if (moduleName === 'ReactCompilerTest') {

View File

@@ -396,4 +396,8 @@ export function typedLog(...values: Array<any>): void {
console.log(...values);
}
export function typedIdentity<T>(value: T): T {
return value;
}
export default typedLog;