Compare commits

..

2 Commits

Author SHA1 Message Date
Mofei Zhang
90131ceca5 [compiler] Add reactive flag on scope dependencies
When collecting scope dependencies, mark each dependency with `reactive: true | false`. This prepares for later PRs https://github.com/facebook/react/pull/33326 and https://github.com/facebook/react/pull/32099 which rewrite scope dependencies into instructions.

Note that some reactive objects may have non-reactive properties, but we do not currently track this.

Technically, state[0] is reactive and state[1] is not. Currently, both would be marked as reactive.
```js
const state = useState();
```
2025-05-22 15:44:36 -04:00
Mofei Zhang
64b555c5fb [compiler] Prepare HIRBuilder to be used by later passes 2025-05-22 15:44:36 -04:00
475 changed files with 2209 additions and 20153 deletions

View File

@@ -496,7 +496,6 @@ module.exports = {
'packages/react-devtools-shared/src/devtools/views/**/*.js',
'packages/react-devtools-shared/src/hook.js',
'packages/react-devtools-shared/src/backend/console.js',
'packages/react-devtools-shared/src/backend/fiber/renderer.js',
'packages/react-devtools-shared/src/backend/shared/DevToolsComponentStackFrame.js',
'packages/react-devtools-shared/src/frontend/utils/withPermissionsCheck.js',
],
@@ -505,7 +504,6 @@ module.exports = {
__IS_FIREFOX__: 'readonly',
__IS_EDGE__: 'readonly',
__IS_NATIVE__: 'readonly',
__IS_INTERNAL_MCP_BUILD__: 'readonly',
__IS_INTERNAL_VERSION__: 'readonly',
chrome: 'readonly',
},
@@ -561,7 +559,6 @@ module.exports = {
ConsoleTask: 'readonly', // TOOD: Figure out what the official name of this will be.
ReturnType: 'readonly',
AnimationFrameID: 'readonly',
WeakRef: 'readonly',
// For Flow type annotation. Only `BigInt` is valid at runtime.
bigint: 'readonly',
BigInt: 'readonly',
@@ -612,7 +609,6 @@ module.exports = {
TimeoutID: 'readonly',
WheelEventHandler: 'readonly',
FinalizationRegistry: 'readonly',
Exclude: 'readonly',
Omit: 'readonly',
Keyframe: 'readonly',
PropertyIndexedKeyframes: 'readonly',

View File

@@ -11,7 +11,6 @@ permissions: {}
jobs:
check_access:
if: ${{ github.event.pull_request.draft == false }}
runs-on: ubuntu-latest
outputs:
is_member_or_collaborator: ${{ steps.check_is_member_or_collaborator.outputs.is_member_or_collaborator }}

View File

@@ -280,35 +280,6 @@ jobs:
if: steps.node_modules.outputs.cache-hit != 'true'
- run: yarn test ${{ matrix.params }} --ci --shard=${{ matrix.shard }}
# Hardcoded to improve parallelism
test-linter:
name: Test eslint-plugin-react-hooks
needs: [runtime_compiler_node_modules_cache]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: |
yarn.lock
compiler/yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4
id: node_modules
with:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
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
# ----- BUILD -----
build_and_lint:
name: yarn build and lint

View File

@@ -11,7 +11,6 @@ permissions: {}
jobs:
check_access:
if: ${{ github.event.pull_request.draft == false }}
runs-on: ubuntu-latest
outputs:
is_member_or_collaborator: ${{ steps.check_is_member_or_collaborator.outputs.is_member_or_collaborator }}

View File

@@ -17,17 +17,6 @@ on:
description: 'Whether to notify the team on Discord when the release fails. Useful if this workflow is called from an automation.'
required: false
type: boolean
only_packages:
description: Packages to publish (space separated)
type: string
skip_packages:
description: Packages to NOT publish (space separated)
type: string
dry:
required: true
description: Dry run instead of publish?
type: boolean
default: true
secrets:
DISCORD_WEBHOOK_URL:
description: 'Discord webhook URL to notify on failure. Only required if enableFailureNotification is true.'
@@ -72,36 +61,10 @@ jobs:
if: steps.node_modules.outputs.cache-hit != 'true'
- run: yarn --cwd scripts/release install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- run: cp ./scripts/release/ci-npmrc ~/.npmrc
- run: |
GH_TOKEN=${{ secrets.GH_TOKEN }} scripts/release/prepare-release-from-ci.js --skipTests -r ${{ inputs.release_channel }} --commit=${{ inputs.commit_sha }}
- name: Check prepared files
run: ls -R build/node_modules
- if: '${{ inputs.only_packages }}'
name: 'Publish ${{ inputs.only_packages }}'
run: |
scripts/release/publish.js \
--ci \
--skipTests \
--tags=${{ inputs.dist_tag }} \
--onlyPackages=${{ inputs.only_packages }} ${{ (inputs.dry && '') || '\'}}
${{ inputs.dry && '--dry'}}
- if: '${{ inputs.skip_packages }}'
name: 'Publish all packages EXCEPT ${{ inputs.skip_packages }}'
run: |
scripts/release/publish.js \
--ci \
--skipTests \
--tags=${{ inputs.dist_tag }} \
--skipPackages=${{ inputs.skip_packages }} ${{ (inputs.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'}}
cp ./scripts/release/ci-npmrc ~/.npmrc
scripts/release/publish.js --ci --tags ${{ inputs.dist_tag }}
- name: Notify Discord on failure
if: failure() && inputs.enableFailureNotification == true
uses: tsickert/discord-webhook@86dc739f3f165f16dadc5666051c367efa1692f4

View File

@@ -5,25 +5,6 @@ on:
inputs:
prerelease_commit_sha:
required: true
only_packages:
description: Packages to publish (space separated)
type: string
skip_packages:
description: Packages to NOT publish (space separated)
type: string
dry:
required: true
description: Dry run instead of publish?
type: boolean
default: true
experimental_only:
type: boolean
description: Only publish to the experimental tag
default: false
force_notify:
description: Force a Discord notification?
type: boolean
default: false
permissions: {}
@@ -31,26 +12,8 @@ env:
TZ: /usr/share/zoneinfo/America/Los_Angeles
jobs:
notify:
if: ${{ inputs.force_notify || inputs.dry == false || inputs.dry == 'false' }}
runs-on: ubuntu-latest
steps:
- name: Discord Webhook Action
uses: tsickert/discord-webhook@86dc739f3f165f16dadc5666051c367efa1692f4
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
embed-author-name: ${{ github.event.sender.login }}
embed-author-url: ${{ github.event.sender.html_url }}
embed-author-icon-url: ${{ github.event.sender.avatar_url }}
embed-title: "⚠️ Publishing ${{ inputs.experimental_only && 'EXPERIMENTAL' || 'CANARY & EXPERIMENTAL' }} release ${{ (inputs.dry && ' (dry run)') || '' }}"
embed-description: |
```json
${{ toJson(inputs) }}
```
embed-url: https://github.com/facebook/react/actions/runs/${{ github.run_id }}
publish_prerelease_canary:
if: ${{ !inputs.experimental_only }}
name: Publish to Canary channel
uses: facebook/react/.github/workflows/runtime_prereleases.yml@main
permissions:
@@ -70,9 +33,6 @@ jobs:
# downstream consumers might still expect that tag. We can remove this
# after some time has elapsed and the change has been communicated.
dist_tag: canary,next
only_packages: ${{ inputs.only_packages }}
skip_packages: ${{ inputs.skip_packages }}
dry: ${{ inputs.dry }}
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -88,15 +48,10 @@ jobs:
# different versions of the same package, even if they use different
# dist tags.
needs: publish_prerelease_canary
# Ensures the job runs even if canary is skipped
if: always()
with:
commit_sha: ${{ inputs.prerelease_commit_sha }}
release_channel: experimental
dist_tag: experimental
only_packages: ${{ inputs.only_packages }}
skip_packages: ${{ inputs.skip_packages }}
dry: ${{ inputs.dry }}
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -22,7 +22,6 @@ jobs:
release_channel: stable
dist_tag: canary,next
enableFailureNotification: true
dry: false
secrets:
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -44,7 +43,6 @@ jobs:
release_channel: experimental
dist_tag: experimental
enableFailureNotification: true
dry: false
secrets:
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -6,10 +6,7 @@ on:
- cron: '0 * * * *'
workflow_dispatch:
permissions:
# https://github.com/actions/stale/tree/v9/?tab=readme-ov-file#recommended-permissions
issues: write
pull-requests: write
permissions: {}
env:
TZ: /usr/share/zoneinfo/America/Los_Angeles

View File

@@ -3,12 +3,13 @@
const {esNextPaths} = require('./scripts/shared/pathsByLanguageVersion');
module.exports = {
plugins: ['prettier-plugin-hermes-parser'],
bracketSpacing: false,
singleQuote: true,
bracketSameLine: true,
trailingComma: 'es5',
printWidth: 80,
parser: 'flow',
parser: 'hermes',
arrowParens: 'avoid',
overrides: [
{

View File

@@ -8,8 +8,8 @@ set -eo pipefail
HERE=$(pwd)
cd ../../packages/react-compiler-runtime && yarn --silent link && cd "$HERE"
cd ../../packages/babel-plugin-react-compiler && yarn --silent link && cd "$HERE"
cd ../../packages/react-compiler-runtime && yarn --silent link && cd $HERE
cd ../../packages/babel-plugin-react-compiler && yarn --silent link && cd $HERE
yarn --silent link babel-plugin-react-compiler
yarn --silent link react-compiler-runtime

View File

@@ -41,10 +41,6 @@ const DynamicGatingOptionsSchema = z.object({
source: z.string(),
});
export type DynamicGatingOptions = z.infer<typeof DynamicGatingOptionsSchema>;
const CustomOptOutDirectiveSchema = z
.nullable(z.array(z.string()))
.default(null);
type CustomOptOutDirective = z.infer<typeof CustomOptOutDirectiveSchema>;
export type PluginOptions = {
environment: EnvironmentConfig;
@@ -136,11 +132,6 @@ export type PluginOptions = {
*/
ignoreUseNoForget: boolean;
/**
* Unstable / do not use
*/
customOptOutDirectives: CustomOptOutDirective;
sources: Array<string> | ((filename: string) => boolean) | null;
/**
@@ -287,7 +278,6 @@ export const defaultOptions: PluginOptions = {
return filename.indexOf('node_modules') === -1;
},
enableReanimatedCheck: true,
customOptOutDirectives: null,
target: '19',
} as const;
@@ -348,21 +338,6 @@ export function parsePluginOptions(obj: unknown): PluginOptions {
}
break;
}
case 'customOptOutDirectives': {
const result = CustomOptOutDirectiveSchema.safeParse(value);
if (result.success) {
parsedOptions[key] = result.data;
} else {
CompilerError.throwInvalidConfig({
reason:
'Could not parse custom opt out directives. Update React Compiler config to fix the error',
description: `${fromZodError(result.error)}`,
loc: null,
suggestions: null,
});
}
break;
}
default: {
parsedOptions[key] = value;
}

View File

@@ -104,8 +104,6 @@ import {validateNoImpureFunctionsInRender} from '../Validation/ValidateNoImpureF
import {CompilerError} from '..';
import {validateStaticComponents} from '../Validation/ValidateStaticComponents';
import {validateNoFreezingKnownMutableFunctions} from '../Validation/ValidateNoFreezingKnownMutableFunctions';
import {inferMutationAliasingEffects} from '../Inference/InferMutationAliasingEffects';
import {inferMutationAliasingRanges} from '../Inference/InferMutationAliasingRanges';
export type CompilerPipelineValue =
| {kind: 'ast'; name: string; value: CodegenFunction}
@@ -229,27 +227,15 @@ function runWithEnvironment(
analyseFunctions(hir);
log({kind: 'hir', name: 'AnalyseFunctions', value: hir});
if (!env.config.enableNewMutationAliasingModel) {
const fnEffectErrors = inferReferenceEffects(hir);
if (env.isInferredMemoEnabled) {
if (fnEffectErrors.length > 0) {
CompilerError.throw(fnEffectErrors[0]);
}
}
log({kind: 'hir', name: 'InferReferenceEffects', value: hir});
} else {
const mutabilityAliasingErrors = inferMutationAliasingEffects(hir);
log({kind: 'hir', name: 'InferMutationAliasingEffects', value: hir});
if (env.isInferredMemoEnabled) {
if (mutabilityAliasingErrors.isErr()) {
throw mutabilityAliasingErrors.unwrapErr();
}
const fnEffectErrors = inferReferenceEffects(hir);
if (env.isInferredMemoEnabled) {
if (fnEffectErrors.length > 0) {
CompilerError.throw(fnEffectErrors[0]);
}
}
log({kind: 'hir', name: 'InferReferenceEffects', value: hir});
if (!env.config.enableNewMutationAliasingModel) {
validateLocalsNotReassignedAfterRender(hir);
}
validateLocalsNotReassignedAfterRender(hir);
// Note: Has to come after infer reference effects because "dead" code may still affect inference
deadCodeElimination(hir);
@@ -263,21 +249,8 @@ function runWithEnvironment(
pruneMaybeThrows(hir);
log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
if (!env.config.enableNewMutationAliasingModel) {
inferMutableRanges(hir);
log({kind: 'hir', name: 'InferMutableRanges', value: hir});
} else {
const mutabilityAliasingErrors = inferMutationAliasingRanges(hir, {
isFunctionExpression: false,
});
log({kind: 'hir', name: 'InferMutationAliasingRanges', value: hir});
if (env.isInferredMemoEnabled) {
if (mutabilityAliasingErrors.isErr()) {
throw mutabilityAliasingErrors.unwrapErr();
}
validateLocalsNotReassignedAfterRender(hir);
}
}
inferMutableRanges(hir);
log({kind: 'hir', name: 'InferMutableRanges', value: hir});
if (env.isInferredMemoEnabled) {
if (env.config.assertValidMutableRanges) {
@@ -304,10 +277,7 @@ function runWithEnvironment(
validateNoImpureFunctionsInRender(hir).unwrap();
}
if (
env.config.validateNoFreezingKnownMutableFunctions ||
env.config.enableNewMutationAliasingModel
) {
if (env.config.validateNoFreezingKnownMutableFunctions) {
validateNoFreezingKnownMutableFunctions(hir).unwrap();
}
}

View File

@@ -63,16 +63,7 @@ export function tryFindDirectiveEnablingMemoization(
export function findDirectiveDisablingMemoization(
directives: Array<t.Directive>,
{customOptOutDirectives}: PluginOptions,
): t.Directive | null {
if (customOptOutDirectives != null) {
return (
directives.find(
directive =>
customOptOutDirectives.indexOf(directive.value.value) !== -1,
) ?? null
);
}
return (
directives.find(directive =>
OPT_OUT_DIRECTIVES.has(directive.value.value),
@@ -403,8 +394,7 @@ export function compileProgram(
code: pass.code,
suppressions,
hasModuleScopeOptOut:
findDirectiveDisablingMemoization(program.node.directives, pass.opts) !=
null,
findDirectiveDisablingMemoization(program.node.directives) != null,
});
const queue: Array<CompileSource> = findFunctionsToCompile(
@@ -581,10 +571,7 @@ function processFn(
}
directives = {
optIn: optIn.unwrapOr(null),
optOut: findDirectiveDisablingMemoization(
fn.node.body.directives,
programContext.opts,
),
optOut: findDirectiveDisablingMemoization(fn.node.body.directives),
};
}

View File

@@ -5,14 +5,13 @@
* LICENSE file in the root directory of this source tree.
*/
import {HIRFunction, MutableRange, Place} from './HIR';
import invariant from 'invariant';
import {HIRFunction, Identifier, MutableRange} from './HIR';
import {
eachInstructionLValue,
eachInstructionOperand,
eachTerminalOperand,
} from './visitors';
import {CompilerError} from '..';
import {printPlace} from './PrintHIR';
/*
* Checks that all mutable ranges in the function are well-formed, with
@@ -21,43 +20,38 @@ import {printPlace} from './PrintHIR';
export function assertValidMutableRanges(fn: HIRFunction): void {
for (const [, block] of fn.body.blocks) {
for (const phi of block.phis) {
visit(phi.place, `phi for block bb${block.id}`);
for (const [pred, operand] of phi.operands) {
visit(operand, `phi predecessor bb${pred} for block bb${block.id}`);
visitIdentifier(phi.place.identifier);
for (const [, operand] of phi.operands) {
visitIdentifier(operand.identifier);
}
}
for (const instr of block.instructions) {
for (const operand of eachInstructionLValue(instr)) {
visit(operand, `instruction [${instr.id}]`);
visitIdentifier(operand.identifier);
}
for (const operand of eachInstructionOperand(instr)) {
visit(operand, `instruction [${instr.id}]`);
visitIdentifier(operand.identifier);
}
}
for (const operand of eachTerminalOperand(block.terminal)) {
visit(operand, `terminal [${block.terminal.id}]`);
visitIdentifier(operand.identifier);
}
}
}
function visit(place: Place, description: string): void {
validateMutableRange(place, place.identifier.mutableRange, description);
if (place.identifier.scope !== null) {
validateMutableRange(place, place.identifier.scope.range, description);
function visitIdentifier(identifier: Identifier): void {
validateMutableRange(identifier.mutableRange);
if (identifier.scope !== null) {
validateMutableRange(identifier.scope.range);
}
}
function validateMutableRange(
place: Place,
range: MutableRange,
description: string,
): void {
CompilerError.invariant(
(range.start === 0 && range.end === 0) || range.end > range.start,
{
reason: `Invalid mutable range: [${range.start}:${range.end}]`,
description: `${printPlace(place)} in ${description}`,
loc: place.loc,
},
function validateMutableRange(mutableRange: MutableRange): void {
invariant(
(mutableRange.start === 0 && mutableRange.end === 0) ||
mutableRange.end > mutableRange.start,
'Identifier scope mutableRange was invalid: [%s:%s]',
mutableRange.start,
mutableRange.end,
);
}

View File

@@ -47,7 +47,7 @@ import {
makeType,
promoteTemporary,
} from './HIR';
import HIRBuilder, {Bindings, createTemporaryPlace} from './HIRBuilder';
import HIRBuilder, {Bindings} from './HIRBuilder';
import {BuiltInArrayId} from './ObjectShape';
/*
@@ -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,
});
}
@@ -181,7 +181,6 @@ export function lower(
loc: GeneratedSource,
value: lowerExpressionToTemporary(builder, body),
id: makeInstructionId(0),
effects: null,
};
builder.terminateWithContinuation(terminal, fallthrough);
} else if (body.isBlockStatement()) {
@@ -211,7 +210,6 @@ export function lower(
loc: GeneratedSource,
}),
id: makeInstructionId(0),
effects: null,
},
null,
);
@@ -222,7 +220,6 @@ export function lower(
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,
generator: func.node.generator === true,
@@ -230,7 +227,6 @@ export function lower(
loc: func.node.loc ?? GeneratedSource,
env,
effects: null,
aliasingEffects: null,
directives,
});
}
@@ -291,7 +287,6 @@ function lowerStatement(
loc: stmt.node.loc ?? GeneratedSource,
value,
id: makeInstructionId(0),
effects: null,
};
builder.terminate(terminal, 'block');
return;
@@ -1242,7 +1237,6 @@ function lowerStatement(
kind: 'Debugger',
loc,
},
effects: null,
loc,
});
return;
@@ -1900,7 +1894,6 @@ function lowerExpression(
place: leftValue,
loc: exprLoc,
},
effects: null,
loc: exprLoc,
});
builder.terminateWithContinuation(
@@ -2836,7 +2829,6 @@ function lowerOptionalCallExpression(
args,
loc,
},
effects: null,
loc,
});
} else {
@@ -2850,7 +2842,6 @@ function lowerOptionalCallExpression(
args,
loc,
},
effects: null,
loc,
});
}
@@ -3439,12 +3430,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
@@ -3476,10 +3465,9 @@ export function lowerValueToTemporary(
const place: Place = buildTemporaryPlace(builder, value.loc);
builder.push({
id: makeInstructionId(0),
lvalue: {...place},
value: value,
effects: null,
loc: value.loc,
lvalue: {...place},
});
return place;
}
@@ -4162,11 +4150,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
@@ -4175,8 +4158,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
@@ -4219,15 +4202,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);
}
}
@@ -4264,7 +4240,7 @@ function gatherCapturedContext(
},
});
return capturedIds;
return [...capturedIds.keys()];
}
function notNull<T>(value: T | null): value is T {

View File

@@ -243,11 +243,6 @@ export const EnvironmentConfigSchema = z.object({
*/
enableUseTypeAnnotations: z.boolean().default(false),
/**
* Enable a new model for mutability and aliasing inference
*/
enableNewMutationAliasingModel: z.boolean().default(true),
/**
* Enables inference of optional dependency chains. Without this flag
* a property chain such as `props?.items?.foo` will infer as a dep on

View File

@@ -17,7 +17,6 @@ import {
BuiltInSetId,
BuiltInUseActionStateId,
BuiltInUseContextHookId,
BuiltInUseEffectEventId,
BuiltInUseEffectHookId,
BuiltInUseInsertionEffectHookId,
BuiltInUseLayoutEffectHookId,
@@ -28,7 +27,6 @@ import {
BuiltInUseTransitionId,
BuiltInWeakMapId,
BuiltInWeakSetId,
BuiltinEffectEventId,
ReanimatedSharedValueId,
ShapeRegistry,
addFunction,
@@ -644,41 +642,6 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
calleeEffect: Effect.Read,
hookKind: 'useEffect',
returnValueKind: ValueKind.Frozen,
aliasing: {
receiver: '@receiver',
params: [],
rest: '@rest',
returns: '@returns',
temporaries: ['@effect'],
effects: [
// Freezes the function and deps
{
kind: 'Freeze',
value: '@rest',
reason: ValueReason.Effect,
},
// Internally creates an effect object that captures the function and deps
{
kind: 'Create',
into: '@effect',
value: ValueKind.Frozen,
reason: ValueReason.KnownReturnSignature,
},
// The effect stores the function and dependencies
{
kind: 'Capture',
from: '@rest',
into: '@effect',
},
// Returns undefined
{
kind: 'Create',
into: '@returns',
value: ValueKind.Primitive,
reason: ValueReason.KnownReturnSignature,
},
],
},
},
BuiltInUseEffectHookId,
),
@@ -759,27 +722,6 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
BuiltInFireId,
),
],
[
'useEffectEvent',
addHook(
DEFAULT_SHAPES,
{
positionalParams: [],
restParam: Effect.Freeze,
returnType: {
kind: 'Function',
return: {kind: 'Poly'},
shapeId: BuiltinEffectEventId,
isConstructor: false,
},
calleeEffect: Effect.Read,
hookKind: 'useEffectEvent',
// Frozen because it should not mutate any locally-bound values
returnValueKind: ValueKind.Frozen,
},
BuiltInUseEffectEventId,
),
],
];
TYPED_GLOBALS.push(
@@ -905,7 +847,6 @@ export function installTypeConfig(
noAlias: typeConfig.noAlias === true,
mutableOnlyIfOperandsAreMutable:
typeConfig.mutableOnlyIfOperandsAreMutable === true,
aliasing: typeConfig.aliasing,
});
}
case 'hook': {
@@ -923,7 +864,6 @@ export function installTypeConfig(
),
returnValueKind: typeConfig.returnValueKind ?? ValueKind.Frozen,
noAlias: typeConfig.noAlias === true,
aliasing: typeConfig.aliasing,
});
}
case 'object': {

View File

@@ -13,7 +13,6 @@ import {Environment, ReactFunctionType} from './Environment';
import type {HookKind} from './ObjectShape';
import {Type, makeType} from './Types';
import {z} from 'zod';
import type {AliasingEffect} from '../Inference/AliasingEffects';
/*
* *******************************************************************************************
@@ -101,7 +100,6 @@ export type ReactiveInstruction = {
id: InstructionId;
lvalue: Place | null;
value: ReactiveValue;
effects?: Array<AliasingEffect> | null; // TODO make non-optional
loc: SourceLocation;
};
@@ -280,14 +278,12 @@ export type HIRFunction = {
params: Array<Place | SpreadPattern>;
returnTypeAnnotation: t.FlowType | t.TSType | null;
returnType: Type;
returns: Place;
context: Array<Place>;
effects: Array<FunctionEffect> | null;
body: HIR;
generator: boolean;
async: boolean;
directives: Array<string>;
aliasingEffects?: Array<AliasingEffect> | null;
};
export type FunctionEffect =
@@ -453,7 +449,6 @@ export type ReturnTerminal = {
value: Place;
id: InstructionId;
fallthrough?: never;
effects: Array<AliasingEffect> | null;
};
export type GotoTerminal = {
@@ -614,7 +609,6 @@ export type MaybeThrowTerminal = {
id: InstructionId;
loc: SourceLocation;
fallthrough?: never;
effects: Array<AliasingEffect> | null;
};
export type ReactiveScopeTerminal = {
@@ -651,14 +645,12 @@ export type Instruction = {
lvalue: Place;
value: InstructionValue;
loc: SourceLocation;
effects: Array<AliasingEffect> | null;
};
export type TInstruction<T extends InstructionValue> = {
id: InstructionId;
lvalue: Place;
value: T;
effects: Array<AliasingEffect> | null;
loc: SourceLocation;
};
@@ -1388,21 +1380,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
*/
Effect = 'effect',
/**
* Return value of a function with known frozen return value, e.g. `useState`.
*/
@@ -1453,20 +1430,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
@@ -1822,13 +1785,6 @@ export function isFireFunctionType(id: Identifier): boolean {
);
}
export function isEffectEventFunctionType(id: Identifier): boolean {
return (
id.type.kind === 'Function' &&
id.type.shapeId === 'BuiltInEffectEventFunction'
);
}
export function isStableType(id: Identifier): boolean {
return (
isSetStateType(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');
}
@@ -165,7 +165,6 @@ export default class HIRBuilder {
handler: exceptionHandler,
id: makeInstructionId(0),
loc: instruction.loc,
effects: null,
},
continuationBlock,
);

View File

@@ -12,7 +12,6 @@ import {
GeneratedSource,
HIRFunction,
Instruction,
Place,
} from './HIR';
import {markPredecessors} from './HIRBuilder';
import {terminalFallthrough, terminalHasFallthrough} from './visitors';
@@ -81,22 +80,20 @@ export function mergeConsecutiveBlocks(fn: HIRFunction): void {
suggestions: null,
});
const operand = Array.from(phi.operands.values())[0]!;
const lvalue: Place = {
kind: 'Identifier',
identifier: phi.place.identifier,
effect: Effect.ConditionallyMutate,
reactive: false,
loc: GeneratedSource,
};
const instr: Instruction = {
id: predecessor.terminal.id,
lvalue: {...lvalue},
lvalue: {
kind: 'Identifier',
identifier: phi.place.identifier,
effect: Effect.ConditionallyMutate,
reactive: false,
loc: GeneratedSource,
},
value: {
kind: 'LoadLocal',
place: {...operand},
loc: GeneratedSource,
},
effects: [{kind: 'Alias', from: {...operand}, into: {...lvalue}}],
loc: GeneratedSource,
};
predecessor.instructions.push(instr);

View File

@@ -6,30 +6,14 @@
*/
import {CompilerError} from '../CompilerError';
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';
import {Effect, ValueKind, ValueReason} from './HIR';
import {
BuiltInType,
FunctionType,
makeType,
ObjectType,
PolyType,
PrimitiveType,
} from './Types';
import {AliasingEffectConfig, AliasingSignatureConfig} from './TypeSchema';
/*
* This file exports types and defaults for JavaScript object shapes. These are
@@ -58,20 +42,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 +66,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 +79,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.
*
@@ -284,7 +131,6 @@ export type HookKind =
| 'useCallback'
| 'useTransition'
| 'useImperativeHandle'
| 'useEffectEvent'
| 'Custom';
/*
@@ -333,8 +179,6 @@ export type FunctionSignature = {
impure?: boolean;
canonicalName?: string;
aliasing?: AliasingSignature | null | undefined;
};
/*
@@ -382,8 +226,6 @@ export const BuiltInUseTransitionId = 'BuiltInUseTransition';
export const BuiltInStartTransitionId = 'BuiltInStartTransition';
export const BuiltInFireId = 'BuiltInFire';
export const BuiltInFireFunctionId = 'BuiltInFireFunction';
export const BuiltInUseEffectEventId = 'BuiltInUseEffectEvent';
export const BuiltinEffectEventId = 'BuiltInEffectEventFunction';
// See getReanimatedModuleType() in Globals.ts — this is part of supporting Reanimated's ref-like types
export const ReanimatedSharedValueId = 'ReanimatedSharedValueId';
@@ -460,30 +302,6 @@ addObject(BUILTIN_SHAPES, BuiltInArrayId, [
returnType: PRIMITIVE_TYPE,
calleeEffect: Effect.Store,
returnValueKind: ValueKind.Primitive,
aliasing: {
receiver: '@receiver',
params: [],
rest: '@rest',
returns: '@returns',
temporaries: [],
effects: [
// Push directly mutates the array itself
{kind: 'Mutate', value: '@receiver'},
// The arguments are captured into the array
{
kind: 'Capture',
from: '@rest',
into: '@receiver',
},
// Returns the new length, a primitive
{
kind: 'Create',
into: '@returns',
value: ValueKind.Primitive,
reason: ValueReason.KnownReturnSignature,
},
],
},
}),
],
[
@@ -514,60 +332,6 @@ addObject(BUILTIN_SHAPES, BuiltInArrayId, [
returnValueKind: ValueKind.Mutable,
noAlias: true,
mutableOnlyIfOperandsAreMutable: true,
aliasing: {
receiver: '@receiver',
params: ['@callback'],
rest: null,
returns: '@returns',
temporaries: [
// Temporary representing captured items of the receiver
'@item',
// Temporary representing the result of the callback
'@callbackReturn',
/*
* Undefined `this` arg to the callback. Note the signature does not
* support passing an explicit thisArg second param
*/
'@thisArg',
],
effects: [
// Map creates a new mutable array
{
kind: 'Create',
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: '@receiver',
into: '@item',
},
// The undefined this for the callback
{
kind: 'Create',
into: '@thisArg',
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',
mutatesFunction: false,
},
// captures the result of the callback into the return array
{
kind: 'Capture',
from: '@callbackReturn',
into: '@returns',
},
],
},
}),
],
[
@@ -715,32 +479,6 @@ addObject(BUILTIN_SHAPES, BuiltInSetId, [
calleeEffect: Effect.Store,
// returnValueKind is technically dependent on the ValueKind of the set itself
returnValueKind: ValueKind.Mutable,
aliasing: {
receiver: '@receiver',
params: [],
rest: '@rest',
returns: '@returns',
temporaries: [],
effects: [
// Set.add returns the receiver Set
{
kind: 'Assign',
from: '@receiver',
into: '@returns',
},
// Set.add mutates the set itself
{
kind: 'Mutate',
value: '@receiver',
},
// Captures the rest params into the set
{
kind: 'Capture',
from: '@rest',
into: '@receiver',
},
],
},
}),
],
[
@@ -1210,19 +948,6 @@ addObject(BUILTIN_SHAPES, BuiltInRefValueId, [
['*', {kind: 'Object', shapeId: BuiltInRefValueId}],
]);
addFunction(
BUILTIN_SHAPES,
[],
{
positionalParams: [],
restParam: Effect.ConditionallyMutate,
returnType: {kind: 'Poly'},
calleeEffect: Effect.ConditionallyMutate,
returnValueKind: ValueKind.Mutable,
},
BuiltinEffectEventId,
);
/**
* MixedReadOnly =
* | primitive
@@ -1441,53 +1166,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',
);
export function signatureArgument(id: number): Place {
const place: Place = {
kind: 'Identifier',
effect: Effect.Unknown,
loc: GeneratedSource,
reactive: false,
identifier: {
declarationId: makeDeclarationId(id),
id: makeIdentifierId(id),
loc: GeneratedSource,
mutableRange: {start: makeInstructionId(0), end: makeInstructionId(0)},
name: null,
scope: null,
type: makeType(),
},
};
return place;
}

View File

@@ -35,7 +35,6 @@ import type {
Type,
} from './HIR';
import {GotoVariant, InstructionKind} from './HIR';
import {AliasingEffect, AliasingSignature} from '../Inference/AliasingEffects';
export type Options = {
indent: number;
@@ -68,15 +67,13 @@ export function printFunction(fn: HIRFunction): string {
})
.join(', ') +
')';
} else {
definition += '()';
}
if (definition.length !== 0) {
output.push(definition);
}
output.push(`: ${printType(fn.returnType)} @ ${printPlace(fn.returns)}`);
output.push(...fn.directives);
output.push(printType(fn.returnType));
output.push(printHIR(fn.body));
output.push(...fn.directives);
return output.join('\n');
}
@@ -154,10 +151,7 @@ export function printMixedHIR(
export function printInstruction(instr: ReactiveInstruction): string {
const id = `[${instr.id}]`;
let value = printInstructionValue(instr.value);
if (instr.effects != null) {
value += `\n ${instr.effects.map(printAliasingEffect).join('\n ')}`;
}
const value = printInstructionValue(instr.value);
if (instr.lvalue !== null) {
return `${id} ${printPlace(instr.lvalue)} = ${value}`;
@@ -219,9 +213,6 @@ export function printTerminal(terminal: Terminal): Array<string> | string {
value = `[${terminal.id}] Return${
terminal.value != null ? ' ' + printPlace(terminal.value) : ''
}`;
if (terminal.effects != null) {
value += `\n ${terminal.effects.map(printAliasingEffect).join('\n ')}`;
}
break;
}
case 'goto': {
@@ -290,9 +281,6 @@ export function printTerminal(terminal: Terminal): Array<string> | string {
}
case 'maybe-throw': {
value = `[${terminal.id}] MaybeThrow continuation=bb${terminal.continuation} handler=bb${terminal.handler}`;
if (terminal.effects != null) {
value += `\n ${terminal.effects.map(printAliasingEffect).join('\n ')}`;
}
break;
}
case 'scope': {
@@ -567,11 +555,8 @@ export function printInstructionValue(instrValue: ReactiveValue): string {
}
})
.join(', ') ?? '';
const aliasingEffects =
instrValue.loweredFunc.func.aliasingEffects
?.map(printAliasingEffect)
?.join(', ') ?? '';
value = `${kind} ${name} @context[${context}] @effects[${effects}] @aliasingEffects=[${aliasingEffects}]\n${fn}`;
const type = printType(instrValue.loweredFunc.func.returnType).trim();
value = `${kind} ${name} @context[${context}] @effects[${effects}]${type !== '' ? ` return${type}` : ''}:\n${fn}`;
break;
}
case 'TaggedTemplateExpression': {
@@ -937,107 +922,3 @@ function getFunctionName(
return defaultValue;
}
}
export function printAliasingEffect(effect: AliasingEffect): string {
switch (effect.kind) {
case 'Assign': {
return `Assign ${printPlaceForAliasEffect(effect.into)} = ${printPlaceForAliasEffect(effect.from)}`;
}
case 'Alias': {
return `Alias ${printPlaceForAliasEffect(effect.into)} = ${printPlaceForAliasEffect(effect.from)}`;
}
case 'Capture': {
return `Capture ${printPlaceForAliasEffect(effect.into)} <- ${printPlaceForAliasEffect(effect.from)}`;
}
case 'ImmutableCapture': {
return `ImmutableCapture ${printPlaceForAliasEffect(effect.into)} <- ${printPlaceForAliasEffect(effect.from)}`;
}
case 'Create': {
return `Create ${printPlaceForAliasEffect(effect.into)} = ${effect.value}`;
}
case 'CreateFrom': {
return `Create ${printPlaceForAliasEffect(effect.into)} = kindOf(${printPlaceForAliasEffect(effect.from)})`;
}
case 'CreateFunction': {
return `Function ${printPlaceForAliasEffect(effect.into)} = Function captures=[${effect.captures.map(printPlaceForAliasEffect).join(', ')}]`;
}
case 'Apply': {
const receiverCallee =
effect.receiver.identifier.id === effect.function.identifier.id
? printPlaceForAliasEffect(effect.receiver)
: `${printPlaceForAliasEffect(effect.receiver)}.${printPlaceForAliasEffect(effect.function)}`;
const args = effect.args
.map(arg => {
if (arg.kind === 'Identifier') {
return printPlaceForAliasEffect(arg);
} else if (arg.kind === 'Hole') {
return ' ';
}
return `...${printPlaceForAliasEffect(arg.place)}`;
})
.join(', ');
let signature = '';
if (effect.signature != null) {
if (effect.signature.aliasing != null) {
signature = printAliasingSignature(effect.signature.aliasing);
} else {
signature = JSON.stringify(effect.signature, null, 2);
}
}
return `Apply ${printPlaceForAliasEffect(effect.into)} = ${receiverCallee}(${args})${signature != '' ? '\n ' : ''}${signature}`;
}
case 'Freeze': {
return `Freeze ${printPlaceForAliasEffect(effect.value)} ${effect.reason}`;
}
case 'Mutate':
case 'MutateConditionally':
case 'MutateTransitive':
case 'MutateTransitiveConditionally': {
return `${effect.kind} ${printPlaceForAliasEffect(effect.value)}`;
}
case 'MutateFrozen': {
return `MutateFrozen ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`;
}
case 'MutateGlobal': {
return `MutateGlobal ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`;
}
case 'Impure': {
return `Impure ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`;
}
case 'Render': {
return `Render ${printPlaceForAliasEffect(effect.place)}`;
}
default: {
assertExhaustive(effect, `Unexpected kind '${(effect as any).kind}'`);
}
}
}
function printPlaceForAliasEffect(place: Place): string {
return printIdentifier(place.identifier);
}
export function printAliasingSignature(signature: AliasingSignature): string {
const tokens: Array<string> = ['function '];
if (signature.temporaries.length !== 0) {
tokens.push('<');
tokens.push(
signature.temporaries.map(temp => `$${temp.identifier.id}`).join(', '),
);
tokens.push('>');
}
tokens.push('(');
tokens.push('this=$' + String(signature.receiver));
for (const param of signature.params) {
tokens.push(', $' + String(param));
}
if (signature.rest != null) {
tokens.push(`, ...$${String(signature.rest)}`);
}
tokens.push('): ');
tokens.push('$' + String(signature.returns) + ':');
for (const effect of signature.effects) {
tokens.push('\n ' + printAliasingEffect(effect));
}
return tokens.join('');
}

View File

@@ -1,285 +0,0 @@
import {
Place,
ReactiveScopeDependency,
Identifier,
makeInstructionId,
InstructionKind,
GeneratedSource,
BlockId,
makeTemporaryIdentifier,
Effect,
GotoVariant,
HIR,
} from './HIR';
import {CompilerError} from '../CompilerError';
import {Environment} from './Environment';
import HIRBuilder from './HIRBuilder';
import {lowerValueToTemporary} from './BuildHIR';
type DependencyInstructions = {
place: Place;
value: HIR;
exitBlockId: BlockId;
};
export function buildDependencyInstructions(
dep: ReactiveScopeDependency,
env: Environment,
): DependencyInstructions {
const builder = new HIRBuilder(env, {
entryBlockKind: 'value',
});
let dependencyValue: Identifier;
if (dep.path.every(path => !path.optional)) {
dependencyValue = writeNonOptionalDependency(dep, env, builder);
} else {
dependencyValue = writeOptionalDependency(dep, builder, null);
}
const exitBlockId = builder.terminate(
{
kind: 'unsupported',
loc: GeneratedSource,
id: makeInstructionId(0),
},
null,
);
return {
place: {
kind: 'Identifier',
identifier: dependencyValue,
effect: Effect.Freeze,
reactive: dep.reactive,
loc: GeneratedSource,
},
value: builder.build(),
exitBlockId,
};
}
/**
* Write instructions for a simple dependency (without optional chains)
*/
function writeNonOptionalDependency(
dep: ReactiveScopeDependency,
env: Environment,
builder: HIRBuilder,
): Identifier {
const loc = dep.identifier.loc;
let curr: Identifier = makeTemporaryIdentifier(env.nextIdentifierId, loc);
builder.push({
lvalue: {
identifier: curr,
kind: 'Identifier',
effect: Effect.Mutate,
reactive: dep.reactive,
loc,
},
value: {
kind: 'LoadLocal',
place: {
identifier: dep.identifier,
kind: 'Identifier',
effect: Effect.Freeze,
reactive: dep.reactive,
loc,
},
loc,
},
id: makeInstructionId(1),
loc: loc,
effects: null,
});
/**
* Iteratively build up dependency instructions by reading from the last written
* instruction.
*/
for (const path of dep.path) {
const next = makeTemporaryIdentifier(env.nextIdentifierId, loc);
builder.push({
lvalue: {
identifier: next,
kind: 'Identifier',
effect: Effect.Mutate,
reactive: dep.reactive,
loc,
},
value: {
kind: 'PropertyLoad',
object: {
identifier: curr,
kind: 'Identifier',
effect: Effect.Freeze,
reactive: dep.reactive,
loc,
},
property: path.property,
loc,
},
id: makeInstructionId(1),
loc: loc,
effects: null,
});
curr = next;
}
return curr;
}
/**
* Write a dependency into optional blocks.
*
* e.g. `a.b?.c.d` is written to an optional block that tests `a.b` and
* conditionally evaluates `c.d`.
*/
function writeOptionalDependency(
dep: ReactiveScopeDependency,
builder: HIRBuilder,
parentAlternate: BlockId | null,
): Identifier {
const env = builder.environment;
/**
* Reserve an identifier which will be used to store the result of this
* dependency.
*/
const dependencyValue: Place = {
kind: 'Identifier',
identifier: makeTemporaryIdentifier(env.nextIdentifierId, GeneratedSource),
effect: Effect.Mutate,
reactive: dep.reactive,
loc: GeneratedSource,
};
/**
* Reserve a block which is the fallthrough (and transitive successor) of this
* optional chain.
*/
const continuationBlock = builder.reserve(builder.currentBlockKind());
let alternate;
if (parentAlternate != null) {
alternate = parentAlternate;
} else {
/**
* If an outermost alternate block has not been reserved, write one
*
* $N = Primitive undefined
* $M = StoreLocal $OptionalResult = $N
* goto fallthrough
*/
alternate = builder.enter('value', () => {
const temp = lowerValueToTemporary(builder, {
kind: 'Primitive',
value: undefined,
loc: GeneratedSource,
});
lowerValueToTemporary(builder, {
kind: 'StoreLocal',
lvalue: {kind: InstructionKind.Const, place: {...dependencyValue}},
value: {...temp},
type: null,
loc: GeneratedSource,
});
return {
kind: 'goto',
variant: GotoVariant.Break,
block: continuationBlock.id,
id: makeInstructionId(0),
loc: GeneratedSource,
};
});
}
// Reserve the consequent block, which is the successor of the test block
const consequent = builder.reserve('value');
let testIdentifier: Identifier | null = null;
const testBlock = builder.enter('value', () => {
const testDependency = {
...dep,
path: dep.path.slice(0, dep.path.length - 1),
};
const firstOptional = dep.path.findIndex(path => path.optional);
CompilerError.invariant(firstOptional !== -1, {
reason:
'[ScopeDependencyUtils] Internal invariant broken: expected optional path',
loc: dep.identifier.loc,
description: null,
suggestions: null,
});
if (firstOptional === dep.path.length - 1) {
// Base case: the test block is simple
testIdentifier = writeNonOptionalDependency(testDependency, env, builder);
} else {
// Otherwise, the test block is a nested optional chain
testIdentifier = writeOptionalDependency(
testDependency,
builder,
alternate,
);
}
return {
kind: 'branch',
test: {
identifier: testIdentifier,
effect: Effect.Freeze,
kind: 'Identifier',
loc: GeneratedSource,
reactive: dep.reactive,
},
consequent: consequent.id,
alternate,
id: makeInstructionId(0),
loc: GeneratedSource,
fallthrough: continuationBlock.id,
};
});
builder.enterReserved(consequent, () => {
CompilerError.invariant(testIdentifier !== null, {
reason: 'Satisfy type checker',
description: null,
loc: null,
suggestions: null,
});
lowerValueToTemporary(builder, {
kind: 'StoreLocal',
lvalue: {kind: InstructionKind.Const, place: {...dependencyValue}},
value: lowerValueToTemporary(builder, {
kind: 'PropertyLoad',
object: {
identifier: testIdentifier,
kind: 'Identifier',
effect: Effect.Freeze,
reactive: dep.reactive,
loc: GeneratedSource,
},
property: dep.path.at(-1)!.property,
loc: GeneratedSource,
}),
type: null,
loc: GeneratedSource,
});
return {
kind: 'goto',
variant: GotoVariant.Break,
block: continuationBlock.id,
id: makeInstructionId(0),
loc: GeneratedSource,
};
});
builder.terminateWithContinuation(
{
kind: 'optional',
optional: dep.path.at(-1)!.optional,
test: testBlock,
fallthrough: continuationBlock.id,
id: makeInstructionId(0),
loc: GeneratedSource,
},
continuationBlock,
);
return dependencyValue.identifier;
}

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

@@ -735,7 +735,6 @@ export function mapTerminalSuccessors(
loc: terminal.loc,
value: terminal.value,
id: makeInstructionId(0),
effects: terminal.effects,
};
}
case 'throw': {
@@ -843,7 +842,6 @@ export function mapTerminalSuccessors(
handler,
id: makeInstructionId(0),
loc: terminal.loc,
effects: terminal.effects,
};
}
case 'try': {

View File

@@ -1,244 +0,0 @@
/**
* 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 {CompilerErrorDetailOptions} from '../CompilerError';
import {
FunctionExpression,
GeneratedSource,
Hole,
IdentifierId,
ObjectMethod,
Place,
SourceLocation,
SpreadPattern,
ValueKind,
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
* more values in a program. These effects include mutation of values, freezing values,
* tracking data flow between values, and other specialized cases.
*/
export type AliasingEffect =
/**
* Marks the given value and its direct aliases as frozen.
*
* Captured values are *not* considered frozen, because we cannot be sure that a previously
* captured value will still be captured at the point of the freeze.
*
* For example:
* const x = {};
* const y = [x];
* y.pop(); // y dosn't contain x anymore!
* freeze(y);
* mutate(x); // safe to mutate!
*
* The exception to this is FunctionExpressions - since it is impossible to change which
* value a function closes over[1] we can transitively freeze functions and their captures.
*
* [1] Except for `let` values that are reassigned and closed over by a function, but we
* handle this explicitly with StoreContext/LoadContext.
*/
| {kind: 'Freeze'; value: Place; reason: ValueReason}
/**
* Mutate the value and any direct aliases (not captures). Errors if the value is not mutable.
*/
| {kind: 'Mutate'; value: Place}
/**
* Mutate the value and any direct aliases (not captures), but only if the value is known mutable.
* This should be rare.
*
* TODO: this is only used for IteratorNext, but even then MutateTransitiveConditionally is more
* correct for iterators of unknown types.
*/
| {kind: 'MutateConditionally'; value: Place}
/**
* Mutate the value, any direct aliases, and any transitive captures. Errors if the value is not mutable.
*/
| {kind: 'MutateTransitive'; value: Place}
/**
* Mutates any of the value, its direct aliases, and its transitive captures that are mutable.
*/
| {kind: 'MutateTransitiveConditionally'; value: Place}
/**
* Records information flow from `from` to `into` in cases where local mutation of the destination
* will *not* mutate the source:
*
* - Capture a -> b and Mutate(b) X=> (does not imply) Mutate(a)
* - Capture a -> b and MutateTransitive(b) => (does imply) Mutate(a)
*
* Example: `array.push(item)`. Information from item is captured into array, but there is not a
* direct aliasing, and local mutations of array will not modify item.
*/
| {kind: 'Capture'; from: Place; into: Place}
/**
* Records information flow from `from` to `into` in cases where local mutation of the destination
* *will* mutate the source:
*
* - Alias a -> b and Mutate(b) => (does imply) Mutate(a)
* - Alias a -> b and MutateTransitive(b) => (does imply) Mutate(a)
*
* Example: `c = identity(a)`. We don't know what `identity()` returns so we can't use Assign.
* But we have to assume that it _could_ be returning its input, such that a local mutation of
* c could be mutating a.
*/
| {kind: 'Alias'; from: Place; into: Place}
/**
* Records direct assignment: `into = from`.
*/
| {kind: 'Assign'; from: Place; into: Place}
/**
* Creates a value of the given type at the given place
*/
| {kind: 'Create'; into: Place; value: ValueKind; reason: ValueReason}
/**
* Creates a new value with the same kind as the starting value.
*/
| {kind: 'CreateFrom'; from: Place; into: Place}
/**
* Immutable data flow, used for escape analysis. Does not influence mutable range analysis:
*/
| {kind: 'ImmutableCapture'; from: Place; into: Place}
/**
* Calls the function at the given place with the given arguments either captured or aliased,
* and captures/aliases the result into the given place.
*/
| {
kind: 'Apply';
receiver: Place;
function: Place;
mutatesFunction: boolean;
args: Array<Place | SpreadPattern | Hole>;
into: Place;
signature: FunctionSignature | null;
loc: SourceLocation;
}
/**
* Constructs a function value with the given captures. The mutability of the function
* will be determined by the mutability of the capture values when evaluated.
*/
| {
kind: 'CreateFunction';
captures: Array<Place>;
function: FunctionExpression | ObjectMethod;
into: Place;
}
/**
* Mutation of a value known to be immutable
*/
| {kind: 'MutateFrozen'; place: Place; error: CompilerErrorDetailOptions}
/**
* Mutation of a global
*/
| {
kind: 'MutateGlobal';
place: Place;
error: CompilerErrorDetailOptions;
}
/**
* Indicates a side-effect that is not safe during render
*/
| {kind: 'Impure'; place: Place; error: CompilerErrorDetailOptions}
/**
* Indicates that a given place is accessed during render. Used to distingush
* hook arguments that are known to be called immediately vs those used for
* event handlers/effects, and for JSX values known to be called during render
* (tags, children) vs those that may be events/effect (other props).
*/
| {
kind: 'Render';
place: Place;
};
export function hashEffect(effect: AliasingEffect): string {
switch (effect.kind) {
case 'Apply': {
return [
effect.kind,
effect.receiver.identifier.id,
effect.function.identifier.id,
effect.mutatesFunction,
effect.args
.map(a => {
if (a.kind === 'Hole') {
return '';
} else if (a.kind === 'Identifier') {
return a.identifier.id;
} else {
return `...${a.place.identifier.id}`;
}
})
.join(','),
effect.into.identifier.id,
].join(':');
}
case 'CreateFrom':
case 'ImmutableCapture':
case 'Assign':
case 'Alias':
case 'Capture': {
return [
effect.kind,
effect.from.identifier.id,
effect.into.identifier.id,
].join(':');
}
case 'Create': {
return [
effect.kind,
effect.into.identifier.id,
effect.value,
effect.reason,
].join(':');
}
case 'Freeze': {
return [effect.kind, effect.value.identifier.id, effect.reason].join(':');
}
case 'Impure':
case 'Render': {
return [effect.kind, effect.place.identifier.id].join(':');
}
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(':');
}
case 'Mutate':
case 'MutateConditionally':
case 'MutateTransitive':
case 'MutateTransitiveConditionally': {
return [effect.kind, effect.value.identifier.id].join(':');
}
case 'CreateFunction': {
return [
effect.kind,
effect.into.identifier.id,
// return places are a unique way to identify functions themselves
effect.function.loweredFunc.func.returns.identifier.id,
effect.captures.map(p => p.identifier.id).join(','),
].join(':');
}
}
}
export type AliasingSignature = {
receiver: IdentifierId;
params: Array<IdentifierId>;
rest: IdentifierId | null;
returns: IdentifierId;
effects: Array<AliasingEffect>;
temporaries: Array<Place>;
};

View File

@@ -10,7 +10,6 @@ import {
Effect,
HIRFunction,
Identifier,
IdentifierId,
LoweredFunction,
isRefOrRefValue,
makeInstructionId,
@@ -20,10 +19,6 @@ import {inferReactiveScopeVariables} from '../ReactiveScopes';
import {rewriteInstructionKindsBasedOnReassignment} from '../SSA';
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 {
for (const [_, block] of func.body.blocks) {
@@ -31,27 +26,15 @@ export default function analyseFunctions(func: HIRFunction): void {
switch (instr.value.kind) {
case 'ObjectMethod':
case 'FunctionExpression': {
if (!func.env.config.enableNewMutationAliasingModel) {
lower(instr.value.loweredFunc.func);
infer(instr.value.loweredFunc);
} else {
lowerWithMutationAliasing(instr.value.loweredFunc.func);
}
lower(instr.value.loweredFunc.func);
infer(instr.value.loweredFunc);
/**
* 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;
@@ -61,87 +44,6 @@ export default function analyseFunctions(func: HIRFunction): void {
}
}
function lowerWithMutationAliasing(fn: HIRFunction): void {
/**
* Phase 1: similar to lower(), but using the new mutation/aliasing inference
*/
analyseFunctions(fn);
inferMutationAliasingEffects(fn, {isFunctionExpression: true});
deadCodeElimination(fn);
inferMutationAliasingRanges(fn, {isFunctionExpression: true});
rewriteInstructionKindsBasedOnReassignment(fn);
inferReactiveScopeVariables(fn);
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
* the outer function. For example, InferMutationAliasingEffects uses context variable
* effects to decide if the function may be mutable or not.
*/
const capturedOrMutated = new Set<IdentifierId>();
for (const effect of effects ?? []) {
switch (effect.kind) {
case 'Assign':
case 'Alias':
case 'Capture':
case 'CreateFrom': {
capturedOrMutated.add(effect.from.identifier.id);
break;
}
case 'Apply': {
CompilerError.invariant(false, {
reason: `[AnalyzeFunctions] Expected Apply effects to be replaced with more precise effects`,
loc: effect.function.loc,
});
}
case 'Mutate':
case 'MutateConditionally':
case 'MutateTransitive':
case 'MutateTransitiveConditionally': {
capturedOrMutated.add(effect.value.identifier.id);
break;
}
case 'Impure':
case 'Render':
case 'MutateFrozen':
case 'MutateGlobal':
case 'CreateFunction':
case 'Create':
case 'Freeze':
case 'ImmutableCapture': {
// no-op
break;
}
default: {
assertExhaustive(
effect,
`Unexpected effect kind ${(effect as any).kind}`,
);
}
}
}
for (const operand of fn.context) {
if (
capturedOrMutated.has(operand.identifier.id) ||
operand.effect === Effect.Capture
) {
operand.effect = Effect.Capture;
} else {
operand.effect = Effect.Read;
}
}
}
function lower(func: HIRFunction): void {
analyseFunctions(func);
inferReferenceEffects(func, {isFunctionExpression: true});

View File

@@ -197,7 +197,6 @@ function makeManualMemoizationMarkers(
deps: depsList,
loc: fnExpr.loc,
},
effects: null,
loc: fnExpr.loc,
},
{
@@ -209,7 +208,6 @@ function makeManualMemoizationMarkers(
decl: {...memoDecl},
loc: fnExpr.loc,
},
effects: null,
loc: fnExpr.loc,
},
];

View File

@@ -10,6 +10,7 @@ import {CompilerError, SourceLocation} from '..';
import {
ArrayExpression,
Effect,
Environment,
FunctionExpression,
GeneratedSource,
HIRFunction,
@@ -28,10 +29,6 @@ import {
isSetStateType,
isFireFunctionType,
makeScopeId,
HIR,
BasicBlock,
BlockId,
isEffectEventFunctionType,
} from '../HIR';
import {collectHoistablePropertyLoadsInInnerFn} from '../HIR/CollectHoistablePropertyLoads';
import {collectOptionalChainSidemap} from '../HIR/CollectOptionalChainDependencies';
@@ -41,20 +38,13 @@ import {
createTemporaryPlace,
fixScopeAndIdentifierRanges,
markInstructionIds,
markPredecessors,
reversePostorderBlocks,
} from '../HIR/HIRBuilder';
import {
collectTemporariesSidemap,
DependencyCollectionContext,
handleInstruction,
} from '../HIR/PropagateScopeDependenciesHIR';
import {buildDependencyInstructions} from '../HIR/ScopeDependencyUtils';
import {
eachInstructionOperand,
eachTerminalOperand,
terminalFallthrough,
} from '../HIR/visitors';
import {eachInstructionOperand, eachTerminalOperand} from '../HIR/visitors';
import {empty} from '../Utils/Stack';
import {getOrInsertWith} from '../Utils/utils';
@@ -63,6 +53,7 @@ import {getOrInsertWith} from '../Utils/utils';
* a second argument to the useEffect call if no dependency array is provided.
*/
export function inferEffectDependencies(fn: HIRFunction): void {
let hasRewrite = false;
const fnExpressions = new Map<
IdentifierId,
TInstruction<FunctionExpression>
@@ -95,7 +86,6 @@ export function inferEffectDependencies(fn: HIRFunction): void {
* reactive(Identifier i) = Union_{reference of i}(reactive(reference))
*/
const reactiveIds = inferReactiveIdentifiers(fn);
const rewriteBlocks: Array<BasicBlock> = [];
for (const [, block] of fn.body.blocks) {
if (block.terminal.kind === 'scope') {
@@ -111,7 +101,7 @@ export function inferEffectDependencies(fn: HIRFunction): void {
);
}
}
const rewriteInstrs: Array<SpliceInfo> = [];
const rewriteInstrs = new Map<InstructionId, Array<Instruction>>();
for (const instr of block.instructions) {
const {value, lvalue} = instr;
if (value.kind === 'FunctionExpression') {
@@ -175,6 +165,7 @@ export function inferEffectDependencies(fn: HIRFunction): void {
) {
// We have a useEffect call with no deps array, so we need to infer the deps
const effectDeps: Array<Place> = [];
const newInstructions: Array<Instruction> = [];
const deps: ArrayExpression = {
kind: 'ArrayExpression',
elements: effectDeps,
@@ -205,29 +196,24 @@ export function inferEffectDependencies(fn: HIRFunction): void {
*/
const usedDeps = [];
for (const maybeDep of minimalDeps) {
for (const dep of minimalDeps) {
if (
((isUseRefType(maybeDep.identifier) ||
isSetStateType(maybeDep.identifier)) &&
!reactiveIds.has(maybeDep.identifier.id)) ||
isFireFunctionType(maybeDep.identifier) ||
isEffectEventFunctionType(maybeDep.identifier)
((isUseRefType(dep.identifier) ||
isSetStateType(dep.identifier)) &&
!reactiveIds.has(dep.identifier.id)) ||
isFireFunctionType(dep.identifier)
) {
// exclude non-reactive hook results, which will never be in a memo block
continue;
}
const dep = truncateDepAtCurrent(maybeDep);
const {place, value, exitBlockId} = buildDependencyInstructions(
const {place, instructions} = writeDependencyToInstructions(
dep,
reactiveIds.has(dep.identifier.id),
fn.env,
fnExpr.loc,
);
rewriteInstrs.push({
kind: 'block',
location: instr.id,
value,
exitBlockId: exitBlockId,
});
newInstructions.push(...instructions);
effectDeps.push(place);
usedDeps.push(dep);
}
@@ -248,34 +234,27 @@ export function inferEffectDependencies(fn: HIRFunction): void {
});
}
// Step 2: push the inferred deps array as an argument of the useEffect
rewriteInstrs.push({
kind: 'instr',
location: instr.id,
value: {
id: makeInstructionId(0),
loc: GeneratedSource,
lvalue: {...depsPlace, effect: Effect.Mutate},
value: deps,
effects: null,
},
newInstructions.push({
id: makeInstructionId(0),
loc: GeneratedSource,
lvalue: {...depsPlace, effect: Effect.Mutate},
value: deps,
});
// Step 2: push the inferred deps array as an argument of the useEffect
value.args.push({...depsPlace, effect: Effect.Freeze});
rewriteInstrs.set(instr.id, newInstructions);
fn.env.inferredEffectLocations.add(callee.loc);
} else if (loadGlobals.has(value.args[0].identifier.id)) {
// Global functions have no reactive dependencies, so we can insert an empty array
rewriteInstrs.push({
kind: 'instr',
location: instr.id,
value: {
id: makeInstructionId(0),
loc: GeneratedSource,
lvalue: {...depsPlace, effect: Effect.Mutate},
value: deps,
effects: null,
},
newInstructions.push({
id: makeInstructionId(0),
loc: GeneratedSource,
lvalue: {...depsPlace, effect: Effect.Mutate},
value: deps,
});
value.args.push({...depsPlace, effect: Effect.Freeze});
rewriteInstrs.set(instr.id, newInstructions);
fn.env.inferredEffectLocations.add(callee.loc);
}
} else if (
@@ -306,164 +285,85 @@ export function inferEffectDependencies(fn: HIRFunction): void {
}
}
}
rewriteSplices(block, rewriteInstrs, rewriteBlocks);
}
if (rewriteBlocks.length > 0) {
for (const block of rewriteBlocks) {
fn.body.blocks.set(block.id, block);
if (rewriteInstrs.size > 0) {
hasRewrite = true;
const newInstrs = [];
for (const instr of block.instructions) {
const newInstr = rewriteInstrs.get(instr.id);
if (newInstr != null) {
newInstrs.push(...newInstr, instr);
} else {
newInstrs.push(instr);
}
}
block.instructions = newInstrs;
}
/**
* Fixup the HIR to restore RPO, ensure correct predecessors, and renumber
* instructions.
*/
reversePostorderBlocks(fn.body);
markPredecessors(fn.body);
}
if (hasRewrite) {
// Renumber instructions and fix scope ranges
markInstructionIds(fn.body);
fixScopeAndIdentifierRanges(fn.body);
fn.env.hasInferredEffect = true;
}
}
function truncateDepAtCurrent(
function writeDependencyToInstructions(
dep: ReactiveScopeDependency,
): ReactiveScopeDependency {
const idx = dep.path.findIndex(path => path.property === 'current');
if (idx === -1) {
return dep;
} else {
return {...dep, path: dep.path.slice(0, idx)};
}
}
type SpliceInfo =
| {kind: 'instr'; location: InstructionId; value: Instruction}
| {
kind: 'block';
location: InstructionId;
value: HIR;
exitBlockId: BlockId;
};
function rewriteSplices(
originalBlock: BasicBlock,
splices: Array<SpliceInfo>,
rewriteBlocks: Array<BasicBlock>,
): void {
if (splices.length === 0) {
return;
}
/**
* Splice instructions or value blocks into the original block.
* --- original block ---
* bb_original
* instr1
* ...
* instr2 <-- splice location
* instr3
* ...
* <original terminal>
*
* If there is more than one block in the splice, this means that we're
* splicing in a set of value-blocks of the following structure:
* --- blocks we're splicing in ---
* bb_entry:
* instrEntry
* ...
* <splice terminal> fallthrough=bb_exit
*
* bb1(value):
* ...
*
* bb_exit:
* instrExit
* ...
* <synthetic terminal>
*
*
* --- rewritten blocks ---
* bb_original
* instr1
* ... (original instructions)
* instr2
* instrEntry
* ... (spliced instructions)
* <splice terminal> fallthrough=bb_exit
*
* bb1(value):
* ...
*
* bb_exit:
* instrExit
* ... (spliced instructions)
* instr3
* ... (original instructions)
* <original terminal>
*/
const originalInstrs = originalBlock.instructions;
let currBlock: BasicBlock = {...originalBlock, instructions: []};
rewriteBlocks.push(currBlock);
let cursor = 0;
for (const rewrite of splices) {
while (originalInstrs[cursor].id < rewrite.location) {
CompilerError.invariant(
originalInstrs[cursor].id < originalInstrs[cursor + 1].id,
{
reason:
'[InferEffectDependencies] Internal invariant broken: expected block instructions to be sorted',
loc: originalInstrs[cursor].loc,
},
);
currBlock.instructions.push(originalInstrs[cursor]);
cursor++;
reactive: boolean,
env: Environment,
loc: SourceLocation,
): {place: Place; instructions: Array<Instruction>} {
const instructions: Array<Instruction> = [];
let currValue = createTemporaryPlace(env, GeneratedSource);
currValue.reactive = reactive;
instructions.push({
id: makeInstructionId(0),
loc: GeneratedSource,
lvalue: {...currValue, effect: Effect.Mutate},
value: {
kind: 'LoadLocal',
place: {
kind: 'Identifier',
identifier: dep.identifier,
effect: Effect.Capture,
reactive,
loc: loc,
},
loc: loc,
},
});
for (const path of dep.path) {
if (path.optional) {
/**
* TODO: instead of truncating optional paths, reuse
* instructions from hoisted dependencies block(s)
*/
break;
}
CompilerError.invariant(originalInstrs[cursor].id === rewrite.location, {
reason:
'[InferEffectDependencies] Internal invariant broken: splice location not found',
loc: originalInstrs[cursor].loc,
if (path.property === 'current') {
/*
* Prune ref.current accesses. This may over-capture for non-ref values with
* a current property, but that's fine.
*/
break;
}
const nextValue = createTemporaryPlace(env, GeneratedSource);
nextValue.reactive = reactive;
instructions.push({
id: makeInstructionId(0),
loc: GeneratedSource,
lvalue: {...nextValue, effect: Effect.Mutate},
value: {
kind: 'PropertyLoad',
object: {...currValue, effect: Effect.Capture},
property: path.property,
loc: loc,
},
});
if (rewrite.kind === 'instr') {
currBlock.instructions.push(rewrite.value);
} else {
const {entry, blocks} = rewrite.value;
const entryBlock = blocks.get(entry)!;
// splice in all instructions from the entry block
currBlock.instructions.push(...entryBlock.instructions);
if (blocks.size > 1) {
/**
* We're splicing in a set of value-blocks, which means we need
* to push new blocks and update terminals.
*/
CompilerError.invariant(
terminalFallthrough(entryBlock.terminal) === rewrite.exitBlockId,
{
reason:
'[InferEffectDependencies] Internal invariant broken: expected entry block to have a fallthrough',
loc: entryBlock.terminal.loc,
},
);
const originalTerminal = currBlock.terminal;
currBlock.terminal = entryBlock.terminal;
for (const [id, block] of blocks) {
if (id === entry) {
continue;
}
if (id === rewrite.exitBlockId) {
block.terminal = originalTerminal;
currBlock = block;
}
rewriteBlocks.push(block);
}
}
}
currValue = nextValue;
}
currBlock.instructions.push(...originalInstrs.slice(cursor));
currValue.effect = Effect.Freeze;
return {place: currValue, instructions};
}
function inferReactiveIdentifiers(fn: HIRFunction): Set<IdentifierId> {

View File

@@ -324,7 +324,7 @@ function isEffectSafeOutsideRender(effect: FunctionEffect): boolean {
return effect.kind === 'GlobalMutation';
}
export function getWriteErrorReason(abstractValue: AbstractValue): string {
function getWriteErrorReason(abstractValue: AbstractValue): string {
if (abstractValue.reason.has(ValueReason.Global)) {
return 'Writing to a variable defined outside a component or hook is not allowed. Consider using an effect';
} else if (abstractValue.reason.has(ValueReason.JsxCaptured)) {
@@ -339,12 +339,6 @@ export function getWriteErrorReason(abstractValue: AbstractValue): string {
return "Mutating a value returned from 'useState()', which should not be mutated. Use the setter function to update instead";
} else if (abstractValue.reason.has(ValueReason.ReducerState)) {
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

@@ -86,7 +86,7 @@ export function inferMutableRanges(ir: HIRFunction): void {
}
}
function areEqualMaps<T, U>(a: Map<T, U>, b: Map<T, U>): boolean {
function areEqualMaps<T>(a: Map<T, T>, b: Map<T, T>): boolean {
if (a.size !== b.size) {
return false;
}

View File

@@ -1,206 +0,0 @@
/**
* 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

@@ -1,737 +0,0 @@
/**
* 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 prettyFormat from 'pretty-format';
import {CompilerError, SourceLocation} from '..';
import {
BlockId,
Effect,
HIRFunction,
Identifier,
IdentifierId,
InstructionId,
makeInstructionId,
Place,
} from '../HIR/HIR';
import {
eachInstructionLValue,
eachInstructionValueOperand,
eachTerminalOperand,
} from '../HIR/visitors';
import {assertExhaustive, getOrInsertWith} from '../Utils/utils';
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;
/**
* 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. For example, a
* function expression such as the following:
*
* ```
* (x) => { x.y = true }
* ```
*
* Would populate a `Mutate x` aliasing effect on the outer function.
*/
export function inferMutationAliasingRanges(
fn: HIRFunction,
{isFunctionExpression}: {isFunctionExpression: boolean},
): 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.
* Edges and mutations are ordered, with mutations processed against the
* abstract model only after it is fully constructed by visiting all blocks
* _and_ connecting phis. Phis are considered ordered at the time of the
* phi node.
*
* This should (may?) mean that mutations are able to see the full state
* of the graph and mark all the appropriate identifiers as mutated at
* the correct point, accounting for both backward and forward edges.
* Ie a mutation of x accounts for both values that flowed into x,
* and values that x flowed into.
*/
const state = new AliasingState();
type PendingPhiOperand = {from: Place; into: Place; index: number};
const pendingPhis = new Map<BlockId, Array<PendingPhiOperand>>();
const mutations: Array<{
index: number;
id: InstructionId;
transitive: boolean;
kind: MutationKind;
place: Place;
}> = [];
const renders: Array<{index: number; place: Place}> = [];
let index = 0;
const errors = new CompilerError();
for (const param of [...fn.params, ...fn.context, fn.returns]) {
const place = param.kind === 'Identifier' ? param : param.place;
state.create(place, {kind: 'Object'});
}
const seenBlocks = new Set<BlockId>();
for (const block of fn.body.blocks.values()) {
for (const phi of block.phis) {
state.create(phi.place, {kind: 'Phi'});
for (const [pred, operand] of phi.operands) {
if (!seenBlocks.has(pred)) {
// NOTE: annotation required to actually typecheck and not silently infer `any`
const blockPhis = getOrInsertWith<BlockId, Array<PendingPhiOperand>>(
pendingPhis,
pred,
() => [],
);
blockPhis.push({from: operand, into: phi.place, index: index++});
} else {
state.assign(index++, operand, phi.place);
}
}
}
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') {
state.create(effect.into, {kind: 'Object'});
} else if (effect.kind === 'CreateFunction') {
state.create(effect.into, {
kind: 'Function',
function: effect.function.loweredFunc.func,
});
} else if (effect.kind === 'CreateFrom') {
state.createFrom(index++, effect.from, effect.into);
} else if (effect.kind === 'Assign') {
if (!state.nodes.has(effect.into.identifier)) {
state.create(effect.into, {kind: 'Object'});
}
state.assign(index++, effect.from, effect.into);
} else if (effect.kind === 'Alias') {
state.assign(index++, effect.from, effect.into);
} else if (effect.kind === 'Capture') {
state.capture(index++, effect.from, effect.into);
} else if (
effect.kind === 'MutateTransitive' ||
effect.kind === 'MutateTransitiveConditionally'
) {
mutations.push({
index: index++,
id: instr.id,
transitive: true,
kind:
effect.kind === 'MutateTransitive'
? MutationKind.Definite
: MutationKind.Conditional,
place: effect.value,
});
} else if (
effect.kind === 'Mutate' ||
effect.kind === 'MutateConditionally'
) {
mutations.push({
index: index++,
id: instr.id,
transitive: false,
kind:
effect.kind === 'Mutate'
? MutationKind.Definite
: MutationKind.Conditional,
place: effect.value,
});
} else if (
effect.kind === 'MutateFrozen' ||
effect.kind === 'MutateGlobal' ||
effect.kind === 'Impure'
) {
errors.push(effect.error);
} else if (effect.kind === 'Render') {
renders.push({index: index++, place: effect.place});
}
}
}
const blockPhis = pendingPhis.get(block.id);
if (blockPhis != null) {
for (const {from, into, index} of blockPhis) {
state.assign(index, from, into);
}
}
if (block.terminal.kind === 'return') {
state.assign(index++, block.terminal.value, fn.returns);
}
if (
(block.terminal.kind === 'maybe-throw' ||
block.terminal.kind === 'return') &&
block.terminal.effects != null
) {
for (const effect of block.terminal.effects) {
if (effect.kind === 'Alias') {
state.assign(index++, effect.from, effect.into);
} else {
CompilerError.invariant(effect.kind === 'Freeze', {
reason: `Unexpected '${effect.kind}' effect for MaybeThrow terminal`,
loc: block.terminal.loc,
});
}
}
}
}
if (VERBOSE) {
console.log(state.debug());
console.log(pretty(mutations));
}
for (const mutation of mutations) {
state.mutate(
mutation.index,
mutation.place.identifier,
makeInstructionId(mutation.id + 1),
mutation.transitive,
mutation.kind,
mutation.place.loc,
errors,
);
}
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);
if (node == null) {
continue;
}
let mutated = false;
if (node.local != null) {
if (node.local.kind === MutationKind.Conditional) {
mutated = true;
fn.aliasingEffects.push({
kind: 'MutateConditionally',
value: {...place, loc: node.local.loc},
});
} else if (node.local.kind === MutationKind.Definite) {
mutated = true;
fn.aliasingEffects.push({
kind: 'Mutate',
value: {...place, loc: node.local.loc},
});
}
}
if (node.transitive != null) {
if (node.transitive.kind === MutationKind.Conditional) {
mutated = true;
fn.aliasingEffects.push({
kind: 'MutateTransitiveConditionally',
value: {...place, loc: node.transitive.loc},
});
} else if (node.transitive.kind === MutationKind.Definite) {
mutated = true;
fn.aliasingEffects.push({
kind: 'MutateTransitive',
value: {...place, loc: node.transitive.loc},
});
}
}
if (mutated) {
place.effect = Effect.Capture;
}
}
/**
* Part 2
* Add legacy operand-specific effects based on instruction effects and mutable ranges.
* Also fixes up operand mutable ranges, making sure that start is non-zero if the value
* is mutated (depended on by later passes like InferReactiveScopeVariables which uses this
* to filter spurious mutations of globals, which we now guard against more precisely)
*/
for (const block of fn.body.blocks.values()) {
for (const phi of block.phis) {
// TODO: we don't actually set these effects today!
phi.place.effect = Effect.Store;
const isPhiMutatedAfterCreation: boolean =
phi.place.identifier.mutableRange.end >
(block.instructions.at(0)?.id ?? block.terminal.id);
for (const operand of phi.operands.values()) {
operand.effect = isPhiMutatedAfterCreation
? Effect.Capture
: Effect.Read;
}
if (
isPhiMutatedAfterCreation &&
phi.place.identifier.mutableRange.start === 0
) {
/*
* TODO: ideally we'd construct a precise start range, but what really
* matters is that the phi's range appears mutable (end > start + 1)
* so we just set the start to the previous instruction before this block
*/
const firstInstructionIdOfBlock =
block.instructions.at(0)?.id ?? block.terminal.id;
phi.place.identifier.mutableRange.start = makeInstructionId(
firstInstructionIdOfBlock - 1,
);
}
}
for (const instr of block.instructions) {
for (const lvalue of eachInstructionLValue(instr)) {
lvalue.effect = Effect.ConditionallyMutate;
if (lvalue.identifier.mutableRange.start === 0) {
lvalue.identifier.mutableRange.start = instr.id;
}
if (lvalue.identifier.mutableRange.end === 0) {
lvalue.identifier.mutableRange.end = makeInstructionId(
Math.max(instr.id + 1, lvalue.identifier.mutableRange.end),
);
}
}
for (const operand of eachInstructionValueOperand(instr.value)) {
operand.effect = Effect.Read;
}
if (instr.effects == null) {
continue;
}
const operandEffects = new Map<IdentifierId, Effect>();
for (const effect of instr.effects) {
switch (effect.kind) {
case 'Assign':
case 'Alias':
case 'Capture':
case 'CreateFrom': {
const isMutatedOrReassigned =
effect.into.identifier.mutableRange.end > instr.id;
if (isMutatedOrReassigned) {
operandEffects.set(effect.from.identifier.id, Effect.Capture);
operandEffects.set(effect.into.identifier.id, Effect.Store);
} else {
operandEffects.set(effect.from.identifier.id, Effect.Read);
operandEffects.set(effect.into.identifier.id, Effect.Store);
}
break;
}
case 'CreateFunction':
case 'Create': {
break;
}
case 'Mutate': {
operandEffects.set(effect.value.identifier.id, Effect.Store);
break;
}
case 'Apply': {
CompilerError.invariant(false, {
reason: `[AnalyzeFunctions] Expected Apply effects to be replaced with more precise effects`,
loc: effect.function.loc,
});
}
case 'MutateTransitive':
case 'MutateConditionally':
case 'MutateTransitiveConditionally': {
operandEffects.set(
effect.value.identifier.id,
Effect.ConditionallyMutate,
);
break;
}
case 'Freeze': {
operandEffects.set(effect.value.identifier.id, Effect.Freeze);
break;
}
case 'ImmutableCapture': {
// no-op, Read is the default
break;
}
case 'Impure':
case 'Render':
case 'MutateFrozen':
case 'MutateGlobal': {
// no-op
break;
}
default: {
assertExhaustive(
effect,
`Unexpected effect kind ${(effect as any).kind}`,
);
}
}
}
for (const lvalue of eachInstructionLValue(instr)) {
const effect =
operandEffects.get(lvalue.identifier.id) ??
Effect.ConditionallyMutate;
lvalue.effect = effect;
}
for (const operand of eachInstructionValueOperand(instr.value)) {
if (
operand.identifier.mutableRange.end > instr.id &&
operand.identifier.mutableRange.start === 0
) {
operand.identifier.mutableRange.start = instr.id;
}
const effect = operandEffects.get(operand.identifier.id) ?? Effect.Read;
operand.effect = effect;
}
/**
* This case is targeted at hoisted functions like:
*
* ```
* x();
* function x() { ... }
* ```
*
* Which turns into:
*
* t0 = DeclareContext HoistedFunction x
* t1 = LoadContext x
* t2 = CallExpression t1 ( )
* t3 = FunctionExpression ...
* t4 = StoreContext Function x = t3
*
* If the function had captured mutable values, it would already have its
* range extended to include the StoreContext. But if the function doesn't
* capture any mutable values its range won't have been extended yet. We
* want to ensure that the value is memoized along with the context variable,
* not independently of it (bc of the way we do codegen for hoisted functions).
* So here we check for StoreContext rvalues and if they haven't already had
* their range extended to at least this instruction, we extend it.
*/
if (
instr.value.kind === 'StoreContext' &&
instr.value.value.identifier.mutableRange.end <= instr.id
) {
instr.value.value.identifier.mutableRange.end = makeInstructionId(
instr.id + 1,
);
}
}
if (block.terminal.kind === 'return') {
block.terminal.value.effect = isFunctionExpression
? Effect.Read
: Effect.Freeze;
} else {
for (const operand of eachTerminalOperand(block.terminal)) {
operand.effect = Effect.Read;
}
}
}
if (VERBOSE) {
console.log(printFunction(fn));
}
return errors.asResult();
}
function appendFunctionErrors(errors: CompilerError, fn: HIRFunction): void {
for (const effect of fn.aliasingEffects ?? []) {
switch (effect.kind) {
case 'Impure':
case 'MutateFrozen':
case 'MutateGlobal': {
errors.push(effect.error);
break;
}
}
}
}
type Node = {
id: Identifier;
createdFrom: Map<Identifier, number>;
captures: Map<Identifier, number>;
aliases: Map<Identifier, number>;
edges: Array<{index: number; node: Identifier; kind: 'capture' | 'alias'}>;
transitive: {kind: MutationKind; loc: SourceLocation} | null;
local: {kind: MutationKind; loc: SourceLocation} | null;
value:
| {kind: 'Object'}
| {kind: 'Phi'}
| {kind: 'Function'; function: HIRFunction};
};
class AliasingState {
nodes: Map<Identifier, Node> = new Map();
create(place: Place, value: Node['value']): void {
this.nodes.set(place.identifier, {
id: place.identifier,
createdFrom: new Map(),
captures: new Map(),
aliases: new Map(),
edges: [],
transitive: null,
local: null,
value,
});
}
createFrom(index: number, from: Place, into: Place): void {
this.create(into, {kind: 'Object'});
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'});
if (!toNode.createdFrom.has(from.identifier)) {
toNode.createdFrom.set(from.identifier, index);
}
}
capture(index: number, from: Place, into: Place): void {
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'});
if (!toNode.captures.has(from.identifier)) {
toNode.captures.set(from.identifier, index);
}
}
assign(index: number, from: Place, into: Place): void {
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'});
if (!toNode.aliases.has(from.identifier)) {
toNode.aliases.set(from.identifier, index);
}
}
render(index: number, start: Identifier, errors: CompilerError): void {
const seen = new Set<Identifier>();
const queue: Array<Identifier> = [start];
while (queue.length !== 0) {
const current = queue.pop()!;
if (seen.has(current)) {
continue;
}
seen.add(current);
const node = this.nodes.get(current);
if (node == null || node.transitive != null || node.local != null) {
continue;
}
if (node.value.kind === 'Function') {
appendFunctionErrors(errors, node.value.function);
}
for (const [alias, when] of node.createdFrom) {
if (when >= index) {
continue;
}
queue.push(alias);
}
for (const [alias, when] of node.aliases) {
if (when >= index) {
continue;
}
queue.push(alias);
}
for (const [capture, when] of node.captures) {
if (when >= index) {
continue;
}
queue.push(capture);
}
}
}
mutate(
index: number,
start: Identifier,
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;
transitive: boolean;
direction: 'backwards' | 'forwards';
}> = [{place: start, transitive, direction: 'backwards'}];
while (queue.length !== 0) {
const {place: current, transitive, direction} = queue.pop()!;
if (seen.has(current)) {
continue;
}
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;
}
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 &&
node.local == null
) {
appendFunctionErrors(errors, node.value.function);
}
if (transitive) {
if (node.transitive == null || node.transitive.kind < kind) {
node.transitive = {kind, loc};
}
} else {
if (node.local == null || node.local.kind < kind) {
node.local = {kind, loc};
}
}
/**
* all mutations affect "forward" edges by the rules:
* - Capture a -> b, mutate(a) => mutate(b)
* - Alias a -> b, mutate(a) => mutate(b)
*/
for (const edge of node.edges) {
if (edge.index >= index) {
break;
}
queue.push({place: edge.node, transitive, direction: 'forwards'});
}
for (const [alias, when] of node.createdFrom) {
if (when >= index) {
continue;
}
queue.push({place: alias, transitive: true, direction: 'backwards'});
}
if (direction === 'backwards' || node.value.kind !== 'Phi') {
/**
* all mutations affect backward alias edges by the rules:
* - Alias a -> b, mutate(b) => mutate(a)
* - Alias a -> b, mutateTransitive(b) => mutate(a)
*
* However, if we reached a phi because one of its inputs was mutated
* (and we're advancing "forwards" through that node's edges), then
* we know we've already processed the mutation at its source. The
* phi's other inputs can't be affected.
*/
for (const [alias, when] of node.aliases) {
if (when >= index) {
continue;
}
queue.push({place: alias, transitive, direction: 'backwards'});
}
}
/**
* but only transitive mutations affect captures
*/
if (transitive) {
for (const [capture, when] of node.captures) {
if (when >= index) {
continue;
}
queue.push({place: capture, transitive, direction: 'backwards'});
}
}
}
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

@@ -48,7 +48,7 @@ import {
eachTerminalOperand,
eachTerminalSuccessor,
} from '../HIR/visitors';
import {assertExhaustive, Set_isSuperset} from '../Utils/utils';
import {assertExhaustive} from '../Utils/utils';
import {
inferTerminalFunctionEffects,
inferInstructionFunctionEffects,
@@ -779,7 +779,7 @@ function inferParam(
* │ Mutable │───┘
* └──────────────────────────┘
*/
export function mergeValueKinds(a: ValueKind, b: ValueKind): ValueKind {
function mergeValues(a: ValueKind, b: ValueKind): ValueKind {
if (a === b) {
return a;
} else if (a === ValueKind.MaybeFrozen || b === ValueKind.MaybeFrozen) {
@@ -821,16 +821,28 @@ export function mergeValueKinds(a: ValueKind, b: ValueKind): ValueKind {
}
}
/**
* @returns `true` if `a` is a superset of `b`.
*/
function isSuperset<T>(a: ReadonlySet<T>, b: ReadonlySet<T>): boolean {
for (const v of b) {
if (!a.has(v)) {
return false;
}
}
return true;
}
function mergeAbstractValues(
a: AbstractValue,
b: AbstractValue,
): AbstractValue {
const kind = mergeValueKinds(a.kind, b.kind);
const kind = mergeValues(a.kind, b.kind);
if (
kind === a.kind &&
kind === b.kind &&
Set_isSuperset(a.reason, b.reason) &&
Set_isSuperset(a.context, b.context)
isSuperset(a.reason, b.reason) &&
isSuperset(a.context, b.context)
) {
return a;
}
@@ -1977,7 +1989,7 @@ function areArgumentsImmutableAndNonMutating(
return true;
}
export function getArgumentEffect(
function getArgumentEffect(
signatureEffect: Effect | null,
arg: Place | SpreadPattern,
): Effect {

View File

@@ -17,7 +17,6 @@ import {
InstructionKind,
LabelTerminal,
Place,
isStatementBlockKind,
makeInstructionId,
promoteTemporary,
reversePostorderBlocks,
@@ -91,106 +90,100 @@ export function inlineImmediatelyInvokedFunctionExpressions(
*/
const queue = Array.from(fn.body.blocks.values());
queue: for (const block of queue) {
/*
* We can't handle labels inside expressions yet, so we don't inline IIFEs if they are in an
* expression block.
*/
if (isStatementBlockKind(block.kind)) {
for (let ii = 0; ii < block.instructions.length; ii++) {
const instr = block.instructions[ii]!;
switch (instr.value.kind) {
case 'FunctionExpression': {
if (instr.lvalue.identifier.name === null) {
functions.set(instr.lvalue.identifier.id, instr.value);
}
break;
for (let ii = 0; ii < block.instructions.length; ii++) {
const instr = block.instructions[ii]!;
switch (instr.value.kind) {
case 'FunctionExpression': {
if (instr.lvalue.identifier.name === null) {
functions.set(instr.lvalue.identifier.id, instr.value);
}
case 'CallExpression': {
if (instr.value.args.length !== 0) {
// We don't support inlining when there are arguments
continue;
}
const body = functions.get(instr.value.callee.identifier.id);
if (body === undefined) {
// Not invoking a local function expression, can't inline
continue;
}
if (
body.loweredFunc.func.params.length > 0 ||
body.loweredFunc.func.async ||
body.loweredFunc.func.generator
) {
// Can't inline functions with params, or async/generator functions
continue;
}
// We know this function is used for an IIFE and can prune it later
inlinedFunctions.add(instr.value.callee.identifier.id);
// Create a new block which will contain code following the IIFE call
const continuationBlockId = fn.env.nextBlockId;
const continuationBlock: BasicBlock = {
id: continuationBlockId,
instructions: block.instructions.slice(ii + 1),
kind: block.kind,
phis: new Set(),
preds: new Set(),
terminal: block.terminal,
};
fn.body.blocks.set(continuationBlockId, continuationBlock);
/*
* Trim the original block to contain instructions up to (but not including)
* the IIFE
*/
block.instructions.length = ii;
/*
* 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;
// Declare the IIFE temporary
declareTemporary(fn.env, block, result);
// 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);
}
/*
* Ensure we visit the continuation block, since there may have been
* sequential IIFEs that need to be visited.
*/
queue.push(continuationBlock);
continue queue;
break;
}
case 'CallExpression': {
if (instr.value.args.length !== 0) {
// We don't support inlining when there are arguments
continue;
}
default: {
for (const place of eachInstructionValueOperand(instr.value)) {
// Any other use of a function expression means it isn't an IIFE
functions.delete(place.identifier.id);
}
const body = functions.get(instr.value.callee.identifier.id);
if (body === undefined) {
// Not invoking a local function expression, can't inline
continue;
}
if (
body.loweredFunc.func.params.length > 0 ||
body.loweredFunc.func.async ||
body.loweredFunc.func.generator
) {
// Can't inline functions with params, or async/generator functions
continue;
}
// We know this function is used for an IIFE and can prune it later
inlinedFunctions.add(instr.value.callee.identifier.id);
// Create a new block which will contain code following the IIFE call
const continuationBlockId = fn.env.nextBlockId;
const continuationBlock: BasicBlock = {
id: continuationBlockId,
instructions: block.instructions.slice(ii + 1),
kind: block.kind,
phis: new Set(),
preds: new Set(),
terminal: block.terminal,
};
fn.body.blocks.set(continuationBlockId, continuationBlock);
/*
* Trim the original block to contain instructions up to (but not including)
* the IIFE
*/
block.instructions.length = ii;
/*
* 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;
// Declare the IIFE temporary
declareTemporary(fn.env, block, result);
// 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);
}
/*
* Ensure we visit the continuation block, since there may have been
* sequential IIFEs that need to be visited.
*/
queue.push(continuationBlock);
continue queue;
}
default: {
for (const place of eachInstructionValueOperand(instr.value)) {
// Any other use of a function expression means it isn't an IIFE
functions.delete(place.identifier.id);
}
}
}
@@ -242,7 +235,6 @@ function rewriteBlock(
type: null,
loc: terminal.loc,
},
effects: null,
});
block.terminal = {
kind: 'goto',
@@ -271,6 +263,5 @@ function declareTemporary(
type: null,
loc: result.loc,
},
effects: null,
});
}

View File

@@ -151,7 +151,6 @@ export function inlineJsxTransform(
type: null,
loc: instr.value.loc,
},
effects: null,
loc: instr.loc,
};
currentBlockInstructions.push(varInstruction);
@@ -168,7 +167,6 @@ export function inlineJsxTransform(
},
loc: instr.value.loc,
},
effects: null,
loc: instr.loc,
};
currentBlockInstructions.push(devGlobalInstruction);
@@ -222,7 +220,6 @@ export function inlineJsxTransform(
type: null,
loc: instr.value.loc,
},
effects: null,
loc: instr.loc,
};
thenBlockInstructions.push(reassignElseInstruction);
@@ -295,7 +292,6 @@ export function inlineJsxTransform(
],
loc: instr.value.loc,
},
effects: null,
loc: instr.loc,
};
elseBlockInstructions.push(reactElementInstruction);
@@ -313,7 +309,6 @@ export function inlineJsxTransform(
type: null,
loc: instr.value.loc,
},
effects: null,
loc: instr.loc,
};
elseBlockInstructions.push(reassignConditionalInstruction);
@@ -441,7 +436,6 @@ function createSymbolProperty(
binding: {kind: 'Global', name: 'Symbol'},
loc: instr.value.loc,
},
effects: null,
loc: instr.loc,
};
nextInstructions.push(symbolInstruction);
@@ -456,7 +450,6 @@ function createSymbolProperty(
property: makePropertyLiteral('for'),
loc: instr.value.loc,
},
effects: null,
loc: instr.loc,
};
nextInstructions.push(symbolForInstruction);
@@ -470,7 +463,6 @@ function createSymbolProperty(
value: symbolName,
loc: instr.value.loc,
},
effects: null,
loc: instr.loc,
};
nextInstructions.push(symbolValueInstruction);
@@ -486,7 +478,6 @@ function createSymbolProperty(
args: [symbolValueInstruction.lvalue],
loc: instr.value.loc,
},
effects: null,
loc: instr.loc,
};
const $$typeofProperty: ObjectProperty = {
@@ -517,7 +508,6 @@ function createTagProperty(
value: componentTag.name,
loc: instr.value.loc,
},
effects: null,
loc: instr.loc,
};
tagProperty = {
@@ -644,7 +634,6 @@ function createPropsProperties(
elements: [...children],
loc: instr.value.loc,
},
effects: null,
loc: instr.loc,
};
nextInstructions.push(childrenPropInstruction);
@@ -668,7 +657,6 @@ function createPropsProperties(
value: null,
loc: instr.value.loc,
},
effects: null,
loc: instr.loc,
};
refProperty = {
@@ -690,7 +678,6 @@ function createPropsProperties(
value: null,
loc: instr.value.loc,
},
effects: null,
loc: instr.loc,
};
keyProperty = {
@@ -724,7 +711,6 @@ function createPropsProperties(
properties: props,
loc: instr.value.loc,
},
effects: null,
loc: instr.loc,
};
propsProperty = {

View File

@@ -146,7 +146,6 @@ function emitLoadLoweredContextCallee(
id: makeInstructionId(0),
loc: GeneratedSource,
lvalue: createTemporaryPlace(env, GeneratedSource),
effects: null,
value: loadGlobal,
};
}
@@ -193,7 +192,6 @@ function emitPropertyLoad(
lvalue: object,
value: loadObj,
id: makeInstructionId(0),
effects: null,
loc: GeneratedSource,
};
@@ -208,7 +206,6 @@ function emitPropertyLoad(
lvalue: element,
value: loadProp,
id: makeInstructionId(0),
effects: null,
loc: GeneratedSource,
};
return {
@@ -240,7 +237,6 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
kind: 'return',
loc: GeneratedSource,
value: arrayInstr.lvalue,
effects: null,
},
preds: new Set(),
phis: new Set(),
@@ -254,7 +250,6 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
params: [obj],
returnTypeAnnotation: null,
returnType: makeType(),
returns: createTemporaryPlace(env, GeneratedSource),
context: [],
effects: null,
body: {
@@ -283,7 +278,6 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
loc: GeneratedSource,
},
lvalue: createTemporaryPlace(env, GeneratedSource),
effects: null,
loc: GeneratedSource,
};
return fnInstr;
@@ -300,7 +294,6 @@ function emitArrayInstr(elements: Array<Place>, env: Environment): Instruction {
id: makeInstructionId(0),
value: array,
lvalue: arrayLvalue,
effects: null,
loc: GeneratedSource,
};
return arrayInstr;

View File

@@ -297,7 +297,6 @@ function emitOutlinedJsx(
},
loc: GeneratedSource,
},
effects: null,
};
promoteTemporaryJsxTag(loadJsx.lvalue.identifier);
const jsxExpr: Instruction = {
@@ -313,7 +312,6 @@ function emitOutlinedJsx(
openingLoc: GeneratedSource,
closingLoc: GeneratedSource,
},
effects: null,
};
return [loadJsx, jsxExpr];
@@ -355,7 +353,6 @@ function emitOutlinedFn(
kind: 'return',
loc: GeneratedSource,
value: instructions.at(-1)!.lvalue,
effects: null,
},
preds: new Set(),
phis: new Set(),
@@ -369,7 +366,6 @@ function emitOutlinedFn(
params: [propsObj],
returnTypeAnnotation: null,
returnType: makeType(),
returns: createTemporaryPlace(env, GeneratedSource),
context: [],
effects: null,
body: {
@@ -521,7 +517,6 @@ function emitDestructureProps(
loc: GeneratedSource,
value: propsObj,
},
effects: null,
};
return destructurePropsInstr;
}

View File

@@ -44,7 +44,7 @@ import {
getHookKind,
makeIdentifierName,
} from '../HIR/HIR';
import {printIdentifier, printInstruction, printPlace} from '../HIR/PrintHIR';
import {printIdentifier, printPlace} from '../HIR/PrintHIR';
import {eachPatternOperand} from '../HIR/visitors';
import {Err, Ok, Result} from '../Utils/Result';
import {GuardKind} from '../Utils/RuntimeDiagnosticConstants';
@@ -1310,7 +1310,7 @@ function codegenInstructionNullable(
});
CompilerError.invariant(value?.type === 'FunctionExpression', {
reason: 'Expected a function as a function declaration value',
description: `Got ${value == null ? String(value) : value.type} at ${printInstruction(instr)}`,
description: null,
loc: instr.value.loc,
suggestions: null,
});
@@ -1726,7 +1726,7 @@ function codegenInstructionValue(
}
case 'UnaryExpression': {
value = t.unaryExpression(
instrValue.operator,
instrValue.operator as 'throw', // todo
codegenPlaceToExpression(cx, instrValue.value),
);
break;
@@ -2582,16 +2582,7 @@ function codegenValue(
value: boolean | number | string | null | undefined,
): t.Expression {
if (typeof value === 'number') {
if (value < 0) {
/**
* Babel's code generator produces invalid JS for negative numbers when
* run with { compact: true }.
* See repro https://codesandbox.io/p/devbox/5d47fr
*/
return t.unaryExpression('-', t.numericLiteral(-value), false);
} else {
return t.numericLiteral(value);
}
return t.numericLiteral(value);
} else if (typeof value === 'boolean') {
return t.booleanLiteral(value);
} else if (typeof value === 'string') {

View File

@@ -436,7 +436,6 @@ function makeLoadUseFireInstruction(
value: instrValue,
lvalue: {...useFirePlace},
loc: GeneratedSource,
effects: null,
};
}
@@ -461,7 +460,6 @@ function makeLoadFireCalleeInstruction(
},
lvalue: {...loadedFireCallee},
loc: GeneratedSource,
effects: null,
};
}
@@ -485,7 +483,6 @@ function makeCallUseFireInstruction(
value: useFireCall,
lvalue: {...useFireCallResultPlace},
loc: GeneratedSource,
effects: null,
};
}
@@ -514,7 +511,6 @@ function makeStoreUseFireInstruction(
},
lvalue: fireFunctionBindingLValuePlace,
loc: GeneratedSource,
effects: null,
};
}

View File

@@ -93,21 +93,6 @@ const testComplexConfigDefaults: PartialEnvironmentConfig = {
},
],
};
function* splitPragma(
pragma: string,
): Generator<{key: string; value: string | null}> {
for (const entry of pragma.split('@')) {
const keyVal = entry.trim();
const valIdx = keyVal.indexOf(':');
if (valIdx === -1) {
yield {key: keyVal.split(' ', 1)[0], value: null};
} else {
yield {key: keyVal.slice(0, valIdx), value: keyVal.slice(valIdx + 1)};
}
}
}
/**
* For snap test fixtures and playground only.
*/
@@ -116,11 +101,19 @@ function parseConfigPragmaEnvironmentForTest(
): EnvironmentConfig {
const maybeConfig: Partial<Record<keyof EnvironmentConfig, unknown>> = {};
for (const {key, value: val} of splitPragma(pragma)) {
for (const token of pragma.split(' ')) {
if (!token.startsWith('@')) {
continue;
}
const keyVal = token.slice(1);
const valIdx = keyVal.indexOf(':');
const key = valIdx === -1 ? keyVal : keyVal.slice(0, valIdx);
const val = valIdx === -1 ? undefined : keyVal.slice(valIdx + 1);
const isSet = val === undefined || val === 'true';
if (!hasOwnProperty(EnvironmentConfigSchema.shape, key)) {
continue;
}
const isSet = val == null || val === 'true';
if (isSet && key in testComplexConfigDefaults) {
maybeConfig[key] = testComplexConfigDefaults[key];
} else if (isSet) {
@@ -183,11 +176,18 @@ export function parseConfigPragmaForTests(
compilationMode: defaults.compilationMode,
environment,
};
for (const {key, value: val} of splitPragma(pragma)) {
for (const token of pragma.split(' ')) {
if (!token.startsWith('@')) {
continue;
}
const keyVal = token.slice(1);
const idx = keyVal.indexOf(':');
const key = idx === -1 ? keyVal : keyVal.slice(0, idx);
const val = idx === -1 ? undefined : keyVal.slice(idx + 1);
if (!hasOwnProperty(defaultOptions, key)) {
continue;
}
const isSet = val == null || val === 'true';
const isSet = val === undefined || val === 'true';
if (isSet && key in testComplexPluginOptionDefaults) {
options[key] = testComplexPluginOptionDefaults[key];
} else if (isSet) {

View File

@@ -121,21 +121,6 @@ export function Set_intersect<T>(sets: Array<ReadonlySet<T>>): Set<T> {
return result;
}
/**
* @returns `true` if `a` is a superset of `b`.
*/
export function Set_isSuperset<T>(
a: ReadonlySet<T>,
b: ReadonlySet<T>,
): boolean {
for (const v of b) {
if (!a.has(v)) {
return false;
}
}
return true;
}
export function Iterable_some<T>(
iter: Iterable<T>,
pred: (item: T) => boolean,

View File

@@ -58,7 +58,8 @@ export function validateNoFreezingKnownMutableFunctions(
const effect = contextMutationEffects.get(operand.identifier.id);
if (effect != null) {
errors.push({
reason: `This argument is a function which may reassign or mutate local variables after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead`,
reason: `This argument is a function which modifies local variables when called, which can bypass memoization and cause the UI not to update`,
description: `Functions that are returned from hooks, passed as arguments to hooks, or passed as props to components may not mutate local variables`,
loc: operand.loc,
severity: ErrorSeverity.InvalidReact,
});
@@ -111,55 +112,6 @@ export function validateNoFreezingKnownMutableFunctions(
);
if (knownMutation && knownMutation.kind === 'ContextMutation') {
contextMutationEffects.set(lvalue.identifier.id, knownMutation);
} else if (
fn.env.config.enableNewMutationAliasingModel &&
value.loweredFunc.func.aliasingEffects != null
) {
const context = new Set(
value.loweredFunc.func.context.map(p => p.identifier.id),
);
effects: for (const effect of value.loweredFunc.func
.aliasingEffects) {
switch (effect.kind) {
case 'Mutate':
case 'MutateTransitive': {
const knownMutation = contextMutationEffects.get(
effect.value.identifier.id,
);
if (knownMutation != null) {
contextMutationEffects.set(
lvalue.identifier.id,
knownMutation,
);
} else if (
context.has(effect.value.identifier.id) &&
!isRefOrRefLikeMutableType(effect.value.identifier.type)
) {
contextMutationEffects.set(lvalue.identifier.id, {
kind: 'ContextMutation',
effect: Effect.Mutate,
loc: effect.value.loc,
places: new Set([effect.value]),
});
break effects;
}
break;
}
case 'MutateConditionally':
case 'MutateTransitiveConditionally': {
const knownMutation = contextMutationEffects.get(
effect.value.identifier.id,
);
if (knownMutation != null) {
contextMutationEffects.set(
lvalue.identifier.id,
knownMutation,
);
}
break;
}
}
}
}
break;
}

View File

@@ -9,7 +9,7 @@ export {validateContextVariableLValues} from './ValidateContextVariableLValues';
export {validateHooksUsage} from './ValidateHooksUsage';
export {validateMemoizedEffectDependencies} from './ValidateMemoizedEffectDependencies';
export {validateNoCapitalizedCalls} from './ValidateNoCapitalizedCalls';
export {validateNoRefAccessInRender} from './ValidateNoRefAccessInRender';
export {validateNoRefAccessInRender} from './ValidateNoRefAccesInRender';
export {validateNoSetStateInRender} from './ValidateNoSetStateInRender';
export {validatePreservedManualMemoization} from './ValidatePreservedManualMemoization';
export {validateUseMemo} from './ValidateUseMemo';

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

@@ -1,56 +0,0 @@
## Input
```javascript
import {Stringify} from 'shared-runtime';
function Repro(props) {
const MY_CONST = -2;
return <Stringify>{props.arg - MY_CONST}</Stringify>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Repro,
params: [
{
arg: 3,
},
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { Stringify } from "shared-runtime";
function Repro(props) {
const $ = _c(2);
const t0 = props.arg - -2;
let t1;
if ($[0] !== t0) {
t1 = <Stringify>{t0}</Stringify>;
$[0] = t0;
$[1] = t1;
} else {
t1 = $[1];
}
return t1;
}
export const FIXTURE_ENTRYPOINT = {
fn: Repro,
params: [
{
arg: 3,
},
],
};
```
### Eval output
(kind: ok) <div>{"children":5}</div>

View File

@@ -1,15 +0,0 @@
import {Stringify} from 'shared-runtime';
function Repro(props) {
const MY_CONST = -2;
return <Stringify>{props.arg - MY_CONST}</Stringify>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Repro,
params: [
{
arg: 3,
},
],
};

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel:false
// @flow @enableTransitivelyFreezeFunctionExpressions:false
import {arrayPush, setPropertyByKey, Stringify} from 'shared-runtime';
/**

View File

@@ -1,4 +1,4 @@
// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel:false
// @flow @enableTransitivelyFreezeFunctionExpressions:false
import {arrayPush, setPropertyByKey, Stringify} from 'shared-runtime';
/**

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel:false
// @flow @enableTransitivelyFreezeFunctionExpressions:false
import {setPropertyByKey, Stringify} from 'shared-runtime';
/**

View File

@@ -1,4 +1,4 @@
// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel:false
// @flow @enableTransitivelyFreezeFunctionExpressions:false
import {setPropertyByKey, Stringify} from 'shared-runtime';
/**

View File

@@ -2,7 +2,6 @@
## Input
```javascript
// @enableNewMutationAliasingModel:false
import {makeArray, mutate} from 'shared-runtime';
/**
@@ -57,7 +56,7 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:false
import { c as _c } from "react/compiler-runtime";
import { makeArray, mutate } from "shared-runtime";
/**

View File

@@ -1,4 +1,3 @@
// @enableNewMutationAliasingModel:false
import {makeArray, mutate} from 'shared-runtime';
/**

View File

@@ -2,7 +2,6 @@
## Input
```javascript
// @enableNewMutationAliasingModel:false
import {CONST_TRUE, Stringify, mutate, useIdentity} from 'shared-runtime';
/**
@@ -39,7 +38,7 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:false
import { c as _c } from "react/compiler-runtime";
import { CONST_TRUE, Stringify, mutate, useIdentity } from "shared-runtime";
/**

View File

@@ -1,4 +1,3 @@
// @enableNewMutationAliasingModel:false
import {CONST_TRUE, Stringify, mutate, useIdentity} from 'shared-runtime';
/**

View File

@@ -2,7 +2,6 @@
## Input
```javascript
// @enableNewMutationAliasingModel:false
import {identity, mutate} from 'shared-runtime';
/**
@@ -40,7 +39,7 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:false
import { c as _c } from "react/compiler-runtime";
import { identity, mutate } from "shared-runtime";
/**

View File

@@ -1,4 +1,3 @@
// @enableNewMutationAliasingModel:false
import {identity, mutate} from 'shared-runtime';
/**

View File

@@ -1,132 +0,0 @@
## Input
```javascript
import {useRef, useEffect} from 'react';
/**
* The postfix increment operator should return the value before incrementing.
* ```js
* const id = count.current; // 0
* count.current = count.current + 1; // 1
* return id;
* ```
* The bug is that we currently increment the value before the expression is evaluated.
* This bug does not trigger when the incremented value is a plain primitive.
*
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok) {"count":{"current":0},"updateCountPostfix":"[[ function params=0 ]]","updateCountPrefix":"[[ function params=0 ]]"}
* logs: ['id = 0','count = 1']
* Forget:
* (kind: ok) {"count":{"current":0},"updateCountPostfix":"[[ function params=0 ]]","updateCountPrefix":"[[ function params=0 ]]"}
* logs: ['id = 1','count = 1']
*/
function useFoo() {
const count = useRef(0);
const updateCountPostfix = () => {
const id = count.current++;
return id;
};
const updateCountPrefix = () => {
const id = ++count.current;
return id;
};
useEffect(() => {
const id = updateCountPostfix();
console.log(`id = ${id}`);
console.log(`count = ${count.current}`);
}, []);
return {count, updateCountPostfix, updateCountPrefix};
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { useRef, useEffect } from "react";
/**
* The postfix increment operator should return the value before incrementing.
* ```js
* const id = count.current; // 0
* count.current = count.current + 1; // 1
* return id;
* ```
* The bug is that we currently increment the value before the expression is evaluated.
* This bug does not trigger when the incremented value is a plain primitive.
*
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok) {"count":{"current":0},"updateCountPostfix":"[[ function params=0 ]]","updateCountPrefix":"[[ function params=0 ]]"}
* logs: ['id = 0','count = 1']
* Forget:
* (kind: ok) {"count":{"current":0},"updateCountPostfix":"[[ function params=0 ]]","updateCountPrefix":"[[ function params=0 ]]"}
* logs: ['id = 1','count = 1']
*/
function useFoo() {
const $ = _c(5);
const count = useRef(0);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = () => {
count.current = count.current + 1;
const id = count.current;
return id;
};
$[0] = t0;
} else {
t0 = $[0];
}
const updateCountPostfix = t0;
let t1;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
t1 = () => {
const id_0 = (count.current = count.current + 1);
return id_0;
};
$[1] = t1;
} else {
t1 = $[1];
}
const updateCountPrefix = t1;
let t2;
let t3;
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
t2 = () => {
const id_1 = updateCountPostfix();
console.log(`id = ${id_1}`);
console.log(`count = ${count.current}`);
};
t3 = [];
$[2] = t2;
$[3] = t3;
} else {
t2 = $[2];
t3 = $[3];
}
useEffect(t2, t3);
let t4;
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
t4 = { count, updateCountPostfix, updateCountPrefix };
$[4] = t4;
} else {
t4 = $[4];
}
return t4;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [],
};
```

View File

@@ -1,42 +0,0 @@
import {useRef, useEffect} from 'react';
/**
* The postfix increment operator should return the value before incrementing.
* ```js
* const id = count.current; // 0
* count.current = count.current + 1; // 1
* return id;
* ```
* The bug is that we currently increment the value before the expression is evaluated.
* This bug does not trigger when the incremented value is a plain primitive.
*
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok) {"count":{"current":0},"updateCountPostfix":"[[ function params=0 ]]","updateCountPrefix":"[[ function params=0 ]]"}
* logs: ['id = 0','count = 1']
* Forget:
* (kind: ok) {"count":{"current":0},"updateCountPostfix":"[[ function params=0 ]]","updateCountPrefix":"[[ function params=0 ]]"}
* logs: ['id = 1','count = 1']
*/
function useFoo() {
const count = useRef(0);
const updateCountPostfix = () => {
const id = count.current++;
return id;
};
const updateCountPrefix = () => {
const id = ++count.current;
return id;
};
useEffect(() => {
const id = updateCountPostfix();
console.log(`id = ${id}`);
console.log(`count = ${count.current}`);
}, []);
return {count, updateCountPostfix, updateCountPrefix};
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [],
};

View File

@@ -1,138 +0,0 @@
## Input
```javascript
// @enableNewMutationAliasingModel:false
import {ValidateMemoization} from 'shared-runtime';
const Codes = {
en: {name: 'English'},
ja: {name: 'Japanese'},
ko: {name: 'Korean'},
zh: {name: 'Chinese'},
};
function Component(a) {
let keys;
if (a) {
keys = Object.keys(Codes);
} else {
return null;
}
const options = keys.map(code => {
const country = Codes[code];
return {
name: country.name,
code,
};
});
return (
<>
<ValidateMemoization inputs={[]} output={keys} onlyCheckCompiled={true} />
<ValidateMemoization
inputs={[]}
output={options}
onlyCheckCompiled={true}
/>
</>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: false}],
sequentialRenders: [
{a: false},
{a: true},
{a: true},
{a: false},
{a: true},
{a: false},
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:false
import { ValidateMemoization } from "shared-runtime";
const Codes = {
en: { name: "English" },
ja: { name: "Japanese" },
ko: { name: "Korean" },
zh: { name: "Chinese" },
};
function Component(a) {
const $ = _c(4);
let keys;
if (a) {
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = Object.keys(Codes);
$[0] = t0;
} else {
t0 = $[0];
}
keys = t0;
} else {
return null;
}
let t0;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
t0 = keys.map(_temp);
$[1] = t0;
} else {
t0 = $[1];
}
const options = t0;
let t1;
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
t1 = (
<ValidateMemoization inputs={[]} output={keys} onlyCheckCompiled={true} />
);
$[2] = t1;
} else {
t1 = $[2];
}
let t2;
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
t2 = (
<>
{t1}
<ValidateMemoization
inputs={[]}
output={options}
onlyCheckCompiled={true}
/>
</>
);
$[3] = t2;
} else {
t2 = $[3];
}
return t2;
}
function _temp(code) {
const country = Codes[code];
return { name: country.name, code };
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ a: false }],
sequentialRenders: [
{ a: false },
{ a: true },
{ a: true },
{ a: false },
{ a: true },
{ a: false },
],
};
```

View File

@@ -1,48 +0,0 @@
// @enableNewMutationAliasingModel:false
import {ValidateMemoization} from 'shared-runtime';
const Codes = {
en: {name: 'English'},
ja: {name: 'Japanese'},
ko: {name: 'Korean'},
zh: {name: 'Chinese'},
};
function Component(a) {
let keys;
if (a) {
keys = Object.keys(Codes);
} else {
return null;
}
const options = keys.map(code => {
const country = Codes[code];
return {
name: country.name,
code,
};
});
return (
<>
<ValidateMemoization inputs={[]} output={keys} onlyCheckCompiled={true} />
<ValidateMemoization
inputs={[]}
output={options}
onlyCheckCompiled={true}
/>
</>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: false}],
sequentialRenders: [
{a: false},
{a: true},
{a: true},
{a: false},
{a: true},
{a: false},
],
};

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

@@ -1,35 +0,0 @@
## Input
```javascript
// @customOptOutDirectives:["use todo memo"]
function Component() {
'use todo memo';
return <div>hello world!</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [],
};
```
## Code
```javascript
// @customOptOutDirectives:["use todo memo"]
function Component() {
"use todo memo";
return <div>hello world!</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [],
};
```
### Eval output
(kind: ok) <div>hello world!</div>

View File

@@ -1,10 +0,0 @@
// @customOptOutDirectives:["use todo memo"]
function Component() {
'use todo memo';
return <div>hello world!</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [],
};

View File

@@ -2,7 +2,6 @@
## Input
```javascript
// @enableNewMutationAliasingModel:false
function Component() {
const foo = () => {
someGlobal = true;
@@ -16,13 +15,13 @@ function Component() {
## Error
```
2 | function Component() {
3 | const foo = () => {
> 4 | someGlobal = 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)
5 | };
6 | return <div {...foo} />;
7 | }
1 | function Component() {
2 | const foo = () => {
> 3 | someGlobal = 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)
4 | };
5 | return <div {...foo} />;
6 | }
```

View File

@@ -1,4 +1,3 @@
// @enableNewMutationAliasingModel:false
function Component() {
const foo = () => {
someGlobal = true;

View File

@@ -1,58 +0,0 @@
## Input
```javascript
// @validateNoFreezingKnownMutableFunctions @enableNewMutationAliasingModel:false
import {useCallback, useEffect, useRef} from 'react';
import {useHook} from 'shared-runtime';
function Component() {
const params = useHook();
const update = useCallback(
partialParams => {
const nextParams = {
...params,
...partialParams,
};
nextParams.param = 'value';
console.log(nextParams);
},
[params]
);
const ref = useRef(null);
useEffect(() => {
if (ref.current === null) {
update();
}
}, [update]);
return 'ok';
}
```
## Error
```
18 | );
19 | const ref = useRef(null);
> 20 | useEffect(() => {
| ^^^^^^^
> 21 | if (ref.current === null) {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 22 | update();
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 23 | }
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 24 | }, [update]);
| ^^^^ InvalidReact: This argument is a function which may reassign or mutate local variables after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead (20:24)
InvalidReact: The function modifies a local variable here (14:14)
25 |
26 | return 'ok';
27 | }
```

View File

@@ -1,27 +0,0 @@
// @validateNoFreezingKnownMutableFunctions @enableNewMutationAliasingModel:false
import {useCallback, useEffect, useRef} from 'react';
import {useHook} from 'shared-runtime';
function Component() {
const params = useHook();
const update = useCallback(
partialParams => {
const nextParams = {
...params,
...partialParams,
};
nextParams.param = 'value';
console.log(nextParams);
},
[params]
);
const ref = useRef(null);
useEffect(() => {
if (ref.current === null) {
update();
}
}, [update]);
return 'ok';
}

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

@@ -24,7 +24,7 @@ function useFoo() {
> 6 | cache.set('key', 'value');
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 7 | });
| ^^^^ InvalidReact: This argument is a function which may reassign or mutate local variables after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead (5:7)
| ^^^^ InvalidReact: This argument is a function which modifies local variables when called, which can bypass memoization and cause the UI not to update. Functions that are returned from hooks, passed as arguments to hooks, or passed as props to components may not mutate local variables (5:7)
InvalidReact: The function modifies a local variable here (6:6)
8 | }

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

@@ -1,62 +0,0 @@
## Input
```javascript
// @enableNewMutationAliasingModel
import {Stringify, useIdentity} from 'shared-runtime';
function Component({prop1, prop2}) {
'use memo';
const data = useIdentity(
new Map([
[0, 'value0'],
[1, 'value1'],
])
);
let i = 0;
const items = [];
items.push(
<Stringify
key={i}
onClick={() => data.get(i) + prop1}
shouldInvokeFns={true}
/>
);
i = i + 1;
items.push(
<Stringify
key={i}
onClick={() => data.get(i) + prop2}
shouldInvokeFns={true}
/>
);
return <>{items}</>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{prop1: 'prop1', prop2: 'prop2'}],
sequentialRenders: [
{prop1: 'prop1', prop2: 'prop2'},
{prop1: 'prop1', prop2: 'prop2'},
{prop1: 'changed', prop2: 'prop2'},
],
};
```
## Error
```
20 | />
21 | );
> 22 | i = i + 1;
| ^ InvalidReact: Updating a value used previously in JSX is not allowed. Consider moving the mutation before the JSX. Found mutation of `i` (22:22)
23 | items.push(
24 | <Stringify
25 | key={i}
```

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

@@ -20,7 +20,7 @@ function Component() {
5 | cache.set('key', 'value');
6 | };
> 7 | return <Foo fn={fn} />;
| ^^ InvalidReact: This argument is a function which may reassign or mutate local variables after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead (7:7)
| ^^ InvalidReact: This argument is a function which modifies local variables when called, which can bypass memoization and cause the UI not to update. Functions that are returned from hooks, passed as arguments to hooks, or passed as props to components may not mutate local variables (7:7)
InvalidReact: The function modifies a local variable here (5:5)
8 | }

View File

@@ -26,7 +26,7 @@ function useFoo() {
> 8 | cache.set('key', 'value');
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 9 | };
| ^^^^ InvalidReact: This argument is a function which may reassign or mutate local variables after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead (7:9)
| ^^^^ InvalidReact: This argument is a function which modifies local variables when called, which can bypass memoization and cause the UI not to update. Functions that are returned from hooks, passed as arguments to hooks, or passed as props to components may not mutate local variables (7:9)
InvalidReact: The function modifies a local variable here (8:8)
10 | }

View File

@@ -1,92 +0,0 @@
## Input
```javascript
// @flow @enableNewMutationAliasingModel
/**
* This hook returns a function that when called with an input object,
* will return the result of mapping that input with the supplied map
* function. Results are cached, so if the same input is passed again,
* the same output object will be returned.
*
* Note that this technically violates the rules of React and is unsafe:
* hooks must return immutable objects and be pure, and a function which
* captures and mutates a value when called is inherently not pure.
*
* However, in this case it is technically safe _if_ the mapping function
* is pure *and* the resulting objects are never modified. This is because
* the function only caches: the result of `returnedFunction(someInput)`
* strictly depends on `returnedFunction` and `someInput`, and cannot
* otherwise change over time.
*/
hook useMemoMap<TInput: interface {}, TOutput>(
map: TInput => TOutput
): TInput => TOutput {
return useMemo(() => {
// The original issue is that `cache` was not memoized together with the returned
// function. This was because neither appears to ever be mutated — the function
// is known to mutate `cache` but the function isn't called.
//
// The fix is to detect cases like this — functions that are mutable but not called -
// and ensure that their mutable captures are aliased together into the same scope.
const cache = new WeakMap<TInput, TOutput>();
return input => {
let output = cache.get(input);
if (output == null) {
output = map(input);
cache.set(input, output);
}
return output;
};
}, [map]);
}
```
## Error
```
19 | map: TInput => TOutput
20 | ): TInput => TOutput {
> 21 | return useMemo(() => {
| ^^^^^^^^^^^^^^^
> 22 | // The original issue is that `cache` was not memoized together with the returned
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 23 | // function. This was because neither appears to ever be mutated — the function
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 24 | // is known to mutate `cache` but the function isn't called.
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 25 | //
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 26 | // The fix is to detect cases like this — functions that are mutable but not called -
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 27 | // and ensure that their mutable captures are aliased together into the same scope.
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 28 | const cache = new WeakMap<TInput, TOutput>();
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 29 | return input => {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 30 | let output = cache.get(input);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 31 | if (output == null) {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 32 | output = map(input);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 33 | cache.set(input, output);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 34 | }
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 35 | return output;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 36 | };
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 37 | }, [map]);
| ^^^^^^^^^^^^ InvalidReact: This argument is a function which may reassign or mutate local variables after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead (21:37)
InvalidReact: The function modifies a local variable here (33:33)
38 | }
39 |
```

View File

@@ -18,7 +18,7 @@ function Component(props) {
a.property = true;
b.push(false);
};
return <div onClick={f} />;
return <div onClick={f()} />;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -14,7 +14,7 @@ function Component(props) {
a.property = true;
b.push(false);
};
return <div onClick={f} />;
return <div onClick={f()} />;
}
export const FIXTURE_ENTRYPOINT = {

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

@@ -2,7 +2,6 @@
## Input
```javascript
// @enableNewMutationAliasingModel:false
function Foo() {
const x = () => {
window.href = 'foo';
@@ -22,13 +21,13 @@ export const FIXTURE_ENTRYPOINT = {
## Error
```
2 | function Foo() {
3 | const x = () => {
> 4 | window.href = 'foo';
| ^^^^^^ InvalidReact: Writing to a variable defined outside a component or hook is not allowed. Consider using an effect (4:4)
5 | };
6 | const y = {x};
7 | return <Bar y={y} />;
1 | function Foo() {
2 | const x = () => {
> 3 | window.href = 'foo';
| ^^^^^^ InvalidReact: Writing to a variable defined outside a component or hook is not allowed. Consider using an effect (3:3)
4 | };
5 | const y = {x};
6 | return <Bar y={y} />;
```

View File

@@ -1,4 +1,3 @@
// @enableNewMutationAliasingModel:false
function Foo() {
const x = () => {
window.href = 'foo';

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

@@ -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$14 (9:9)
10 | }
11 |
```

View File

@@ -0,0 +1,32 @@
## Input
```javascript
function Component(props) {
return (
useMemo(() => {
return [props.value];
}) || []
);
}
```
## Error
```
1 | function Component(props) {
2 | return (
> 3 | useMemo(() => {
| ^^^^^^^^^^^^^^^
> 4 | return [props.value];
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 5 | }) || []
| ^^^^^^^^^^^^^ Todo: Support labeled statements combined with value blocks (conditional, logical, optional chaining, etc) (3:5)
6 | );
7 | }
8 |
```

View File

@@ -0,0 +1,7 @@
function Component(props) {
return (
useMemo(() => {
return [props.value];
}) || []
);
}

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

@@ -2,7 +2,6 @@
## Input
```javascript
// @enableNewMutationAliasingModel
import {useEffect, useState} from 'react';
import {Stringify} from 'shared-runtime';
@@ -34,19 +33,45 @@ export const FIXTURE_ENTRYPOINT = {
```
## Code
## Error
```javascript
import { c as _c } from "react/compiler-runtime";
import { useEffect, useState } from "react";
import { Stringify } from "shared-runtime";
function Foo() {
const $ = _c(3);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = [];
$[0] = t0;
} else {
t0 = $[0];
}
useEffect(() => setState(2), t0);
const [state, t1] = useState(0);
const setState = t1;
let t2;
if ($[1] !== state) {
t2 = <Stringify state={state} />;
$[1] = state;
$[2] = t2;
} else {
t2 = $[2];
}
return t2;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{}],
sequentialRenders: [{}, {}],
};
```
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);
22 | return <Stringify state={state} />;
```
### Eval output
(kind: ok) <div>{"state":2}</div>
<div>{"state":2}</div>

View File

@@ -1,4 +1,3 @@
// @enableNewMutationAliasingModel
import {useEffect, useState} from 'react';
import {Stringify} from 'shared-runtime';

View File

@@ -1,40 +0,0 @@
## Input
```javascript
function Component(props) {
const x = props.foo
? 1
: (() => {
throw new Error('Did not receive 1');
})();
return items;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{foo: true}],
};
```
## Code
```javascript
function Component(props) {
props.foo ? 1 : _temp();
return items;
}
function _temp() {
throw new Error("Did not receive 1");
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ foo: true }],
};
```
### Eval output
(kind: exception) items is not defined

View File

@@ -1,13 +0,0 @@
function Component(props) {
const x = props.foo
? 1
: (() => {
throw new Error('Did not receive 1');
})();
return items;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{foo: true}],
};

View File

@@ -1,58 +0,0 @@
## Input
```javascript
// @inferEffectDependencies @panicThreshold:"none" @loggerTestOnly
import {useEffect} from 'react';
import {print} from 'shared-runtime';
function Component({foo}) {
const arr = [];
// Taking either arr[0].value or arr as a dependency is reasonable
// as long as developers know what to expect.
useEffect(() => print(arr[0]?.value));
arr.push({value: foo});
return arr;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{foo: 1}],
};
```
## Code
```javascript
// @inferEffectDependencies @panicThreshold:"none" @loggerTestOnly
import { useEffect } from "react";
import { print } from "shared-runtime";
function Component(t0) {
const { foo } = t0;
const arr = [];
useEffect(() => print(arr[0]?.value), [arr[0]?.value]);
arr.push({ value: foo });
return arr;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ foo: 1 }],
};
```
## 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":"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}
```
### Eval output
(kind: ok) [{"value":1}]
logs: [1]

View File

@@ -1,17 +0,0 @@
// @inferEffectDependencies @panicThreshold:"none" @loggerTestOnly
import {useEffect} from 'react';
import {print} from 'shared-runtime';
function Component({foo}) {
const arr = [];
// Taking either arr[0].value or arr as a dependency is reasonable
// as long as developers know what to expect.
useEffect(() => print(arr[0]?.value));
arr.push({value: foo});
return arr;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{foo: 1}],
};

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