Compare commits

..

1 Commits

Author SHA1 Message Date
Joe Savona
2824f88468 [compiler] Cleanup: consistent tryRecord() wrapping and error recording 2026-02-23 16:07:44 -08:00
32 changed files with 877 additions and 1025 deletions

View File

@@ -252,7 +252,6 @@ export type LoggerEvent =
| CompileErrorEvent
| CompileDiagnosticEvent
| CompileSkipEvent
| CompileUnexpectedThrowEvent
| PipelineErrorEvent
| TimingEvent;
@@ -287,11 +286,6 @@ export type PipelineErrorEvent = {
fnLoc: t.SourceLocation | null;
data: string;
};
export type CompileUnexpectedThrowEvent = {
kind: 'CompileUnexpectedThrow';
fnLoc: t.SourceLocation | null;
data: string;
};
export type TimingEvent = {
kind: 'Timing';
measurement: PerformanceMeasure;

View File

@@ -13,6 +13,7 @@ import {CompilerError} from '../CompilerError';
import {Err, Ok, Result} from '../Utils/Result';
import {
HIRFunction,
IdentifierId,
ReactiveFunction,
assertConsistentIdentifiers,
assertTerminalPredsExist,
@@ -160,8 +161,12 @@ function runWithEnvironment(
pruneMaybeThrows(hir);
log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
validateContextVariableLValues(hir);
validateUseMemo(hir);
env.tryRecord(() => {
validateContextVariableLValues(hir);
});
env.tryRecord(() => {
validateUseMemo(hir);
});
if (env.enableDropManualMemoization) {
dropManualMemoization(hir);
@@ -197,10 +202,14 @@ function runWithEnvironment(
if (env.enableValidations) {
if (env.config.validateHooksUsage) {
validateHooksUsage(hir);
env.tryRecord(() => {
validateHooksUsage(hir);
});
}
if (env.config.validateNoCapitalizedCalls) {
validateNoCapitalizedCalls(hir);
env.tryRecord(() => {
validateNoCapitalizedCalls(hir);
});
}
}
@@ -210,7 +219,9 @@ function runWithEnvironment(
analyseFunctions(hir);
log({kind: 'hir', name: 'AnalyseFunctions', value: hir});
inferMutationAliasingEffects(hir);
env.tryRecord(() => {
inferMutationAliasingEffects(hir);
});
log({kind: 'hir', name: 'InferMutationAliasingEffects', value: hir});
if (env.outputMode === 'ssr') {
@@ -224,23 +235,31 @@ function runWithEnvironment(
pruneMaybeThrows(hir);
log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
inferMutationAliasingRanges(hir, {
isFunctionExpression: false,
env.tryRecord(() => {
inferMutationAliasingRanges(hir, {
isFunctionExpression: false,
});
});
log({kind: 'hir', name: 'InferMutationAliasingRanges', value: hir});
if (env.enableValidations) {
validateLocalsNotReassignedAfterRender(hir);
env.tryRecord(() => {
validateLocalsNotReassignedAfterRender(hir);
});
if (env.config.assertValidMutableRanges) {
assertValidMutableRanges(hir);
}
if (env.config.validateRefAccessDuringRender) {
validateNoRefAccessInRender(hir);
env.tryRecord(() => {
validateNoRefAccessInRender(hir);
});
}
if (env.config.validateNoSetStateInRender) {
validateNoSetStateInRender(hir);
env.tryRecord(() => {
validateNoSetStateInRender(hir);
});
}
if (
@@ -249,7 +268,9 @@ function runWithEnvironment(
) {
env.logErrors(validateNoDerivedComputationsInEffects_exp(hir));
} else if (env.config.validateNoDerivedComputationsInEffects) {
validateNoDerivedComputationsInEffects(hir);
env.tryRecord(() => {
validateNoDerivedComputationsInEffects(hir);
});
}
if (env.config.validateNoSetStateInEffects && env.outputMode === 'lint') {
@@ -260,7 +281,9 @@ function runWithEnvironment(
env.logErrors(validateNoJSXInTryStatement(hir));
}
validateNoFreezingKnownMutableFunctions(hir);
env.tryRecord(() => {
validateNoFreezingKnownMutableFunctions(hir);
});
}
inferReactivePlaces(hir);
@@ -272,7 +295,9 @@ function runWithEnvironment(
env.config.validateExhaustiveEffectDependencies
) {
// NOTE: this relies on reactivity inference running first
validateExhaustiveDependencies(hir);
env.tryRecord(() => {
validateExhaustiveDependencies(hir);
});
}
}
@@ -301,7 +326,8 @@ function runWithEnvironment(
log({kind: 'hir', name: 'InferReactiveScopeVariables', value: hir});
}
const fbtOperands = memoizeFbtAndMacroOperandsInSameScope(hir);
let fbtOperands: Set<IdentifierId> = new Set();
fbtOperands = memoizeFbtAndMacroOperandsInSameScope(hir);
log({
kind: 'hir',
name: 'MemoizeFbtAndMacroOperandsInSameScope',
@@ -386,7 +412,6 @@ function runWithEnvironment(
});
assertTerminalSuccessorsExist(hir);
assertTerminalPredsExist(hir);
propagateScopeDependenciesHIR(hir);
log({
kind: 'hir',
@@ -394,7 +419,8 @@ function runWithEnvironment(
value: hir,
});
const reactiveFunction = buildReactiveFunction(hir);
let reactiveFunction!: ReactiveFunction;
reactiveFunction = buildReactiveFunction(hir);
log({
kind: 'reactive',
name: 'BuildReactiveFunction',
@@ -481,7 +507,8 @@ function runWithEnvironment(
value: reactiveFunction,
});
const uniqueIdentifiers = renameVariables(reactiveFunction);
let uniqueIdentifiers: Set<string> = new Set();
uniqueIdentifiers = renameVariables(reactiveFunction);
log({
kind: 'reactive',
name: 'RenameVariables',
@@ -499,7 +526,9 @@ function runWithEnvironment(
env.config.enablePreserveExistingMemoizationGuarantees ||
env.config.validatePreserveExistingMemoizationGuarantees
) {
validatePreservedManualMemoization(reactiveFunction);
env.tryRecord(() => {
validatePreservedManualMemoization(reactiveFunction);
});
}
const ast = codegenFunction(reactiveFunction, {
@@ -512,7 +541,9 @@ function runWithEnvironment(
}
if (env.config.validateSourceLocations) {
validateSourceLocations(func, ast, env);
env.tryRecord(() => {
validateSourceLocations(func, ast, env);
});
}
/**

View File

@@ -713,20 +713,6 @@ function tryCompileFunction(
return {kind: 'error', error: result.unwrapErr()};
}
} catch (err) {
/**
* A pass incorrectly threw instead of recording the error.
* Log for detection in development.
*/
if (
err instanceof CompilerError &&
err.details.every(detail => detail.category !== ErrorCategory.Invariant)
) {
programContext.logEvent({
kind: 'CompileUnexpectedThrow',
fnLoc: fn.node.loc ?? null,
data: err.toString(),
});
}
return {kind: 'error', error: err};
}
}

View File

@@ -310,13 +310,16 @@ function traverseOptionalBlock(
* - a optional base block with a separate nested optional-chain (e.g. a(c?.d)?.d)
*/
const testBlock = context.blocks.get(maybeTest.terminal.fallthrough)!;
/**
* Fallthrough of the inner optional should be a block with no
* instructions, terminating with Test($<temporary written to from
* StoreLocal>)
*/
if (testBlock.terminal.kind !== 'branch') {
return null;
if (testBlock!.terminal.kind !== 'branch') {
/**
* Fallthrough of the inner optional should be a block with no
* instructions, terminating with Test($<temporary written to from
* StoreLocal>)
*/
CompilerError.throwTodo({
reason: `Unexpected terminal kind \`${testBlock.terminal.kind}\` for optional fallthrough block`,
loc: maybeTest.terminal.loc,
});
}
/**
* Recurse into inner optional blocks to collect inner optional-chain

View File

@@ -759,6 +759,29 @@ export class Environment {
return this.#errors;
}
/**
* Wraps a callback in try/catch: if the callback throws a CompilerError
* that is NOT an invariant, the error is recorded and execution continues.
* Non-CompilerError exceptions and invariants are re-thrown.
*/
tryRecord(fn: () => void): void {
try {
fn();
} catch (err) {
if (err instanceof CompilerError) {
// Check if any detail is an invariant — if so, re-throw
for (const detail of err.details) {
if (detail.category === ErrorCategory.Invariant) {
throw err;
}
}
this.recordErrors(err);
} else {
throw err;
}
}
}
isContextIdentifier(node: t.Identifier): boolean {
return this.#contextIdentifiers.has(node);
}

View File

@@ -7,12 +7,7 @@
import {Binding, NodePath} from '@babel/traverse';
import * as t from '@babel/types';
import {
CompilerError,
CompilerDiagnostic,
CompilerErrorDetail,
ErrorCategory,
} from '../CompilerError';
import {CompilerError, ErrorCategory} from '../CompilerError';
import {Environment} from './Environment';
import {
BasicBlock,
@@ -115,6 +110,7 @@ export default class HIRBuilder {
#bindings: Bindings;
#env: Environment;
#exceptionHandlerStack: Array<BlockId> = [];
errors: CompilerError = new CompilerError();
/**
* Traversal context: counts the number of `fbt` tag parents
* of the current babel node.
@@ -152,10 +148,6 @@ export default class HIRBuilder {
this.#current = newBlock(this.#entry, options?.entryBlockKind ?? 'block');
}
recordError(error: CompilerDiagnostic | CompilerErrorDetail): void {
this.#env.recordError(error);
}
currentBlockKind(): BlockKind {
return this.#current.kind;
}
@@ -316,28 +308,34 @@ export default class HIRBuilder {
resolveBinding(node: t.Identifier): Identifier {
if (node.name === 'fbt') {
this.recordError(
new CompilerErrorDetail({
category: ErrorCategory.Todo,
reason: 'Support local variables named `fbt`',
description:
'Local variables named `fbt` may conflict with the fbt plugin and are not yet supported',
loc: node.loc ?? GeneratedSource,
suggestions: null,
}),
);
CompilerError.throwDiagnostic({
category: ErrorCategory.Todo,
reason: 'Support local variables named `fbt`',
description:
'Local variables named `fbt` may conflict with the fbt plugin and are not yet supported',
details: [
{
kind: 'error',
message: 'Rename to avoid conflict with fbt plugin',
loc: node.loc ?? GeneratedSource,
},
],
});
}
if (node.name === 'this') {
this.recordError(
new CompilerErrorDetail({
category: ErrorCategory.UnsupportedSyntax,
reason: '`this` is not supported syntax',
description:
'React Compiler does not support compiling functions that use `this`',
loc: node.loc ?? GeneratedSource,
suggestions: null,
}),
);
CompilerError.throwDiagnostic({
category: ErrorCategory.UnsupportedSyntax,
reason: '`this` is not supported syntax',
description:
'React Compiler does not support compiling functions that use `this`',
details: [
{
kind: 'error',
message: '`this` was used here',
loc: node.loc ?? GeneratedSource,
},
],
});
}
const originalName = node.name;
let name = originalName;
@@ -383,15 +381,13 @@ export default class HIRBuilder {
instr => instr.value.kind === 'FunctionExpression',
)
) {
this.recordError(
new CompilerErrorDetail({
reason: `Support functions with unreachable code that may contain hoisted declarations`,
loc: block.instructions[0]?.loc ?? block.terminal.loc,
description: null,
suggestions: null,
category: ErrorCategory.Todo,
}),
);
this.errors.push({
reason: `Support functions with unreachable code that may contain hoisted declarations`,
loc: block.instructions[0]?.loc ?? block.terminal.loc,
description: null,
suggestions: null,
category: ErrorCategory.Todo,
});
}
}
ir.blocks = rpoBlocks;

View File

@@ -293,7 +293,7 @@ function extractManualMemoizationArgs(
instr: TInstruction<CallExpression> | TInstruction<MethodCall>,
kind: 'useCallback' | 'useMemo',
sidemap: IdentifierSidemap,
env: Environment,
errors: CompilerError,
): {
fnPlace: Place;
depsList: Array<ManualMemoDependency> | null;
@@ -303,7 +303,7 @@ function extractManualMemoizationArgs(
Place | SpreadPattern | undefined
>;
if (fnPlace == null || fnPlace.kind !== 'Identifier') {
env.recordError(
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
reason: `Expected a callback function to be passed to ${kind}`,
@@ -335,7 +335,7 @@ function extractManualMemoizationArgs(
? sidemap.maybeDepsLists.get(depsListPlace.identifier.id)
: null;
if (maybeDepsList == null) {
env.recordError(
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
reason: `Expected the dependency list for ${kind} to be an array literal`,
@@ -354,7 +354,7 @@ function extractManualMemoizationArgs(
for (const dep of maybeDepsList.deps) {
const maybeDep = sidemap.maybeDeps.get(dep.identifier.id);
if (maybeDep == null) {
env.recordError(
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
reason: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
@@ -389,6 +389,7 @@ function extractManualMemoizationArgs(
* is only used for memoizing values and not for running arbitrary side effects.
*/
export function dropManualMemoization(func: HIRFunction): void {
const errors = new CompilerError();
const isValidationEnabled =
func.env.config.validatePreserveExistingMemoizationGuarantees ||
func.env.config.validateNoSetStateInRender ||
@@ -435,7 +436,7 @@ export function dropManualMemoization(func: HIRFunction): void {
instr as TInstruction<CallExpression> | TInstruction<MethodCall>,
manualMemo.kind,
sidemap,
func.env,
errors,
);
if (memoDetails == null) {
@@ -463,7 +464,7 @@ export function dropManualMemoization(func: HIRFunction): void {
* is rare and likely sketchy.
*/
if (!sidemap.functions.has(fnPlace.identifier.id)) {
func.env.recordError(
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
reason: `Expected the first argument to be an inline function expression`,
@@ -548,6 +549,10 @@ export function dropManualMemoization(func: HIRFunction): void {
markInstructionIds(func.body);
}
}
if (errors.hasAnyErrors()) {
func.env.recordErrors(errors);
}
}
function findOptionalPlaces(fn: HIRFunction): Set<IdentifierId> {

View File

@@ -20,7 +20,6 @@ import {
Place,
isPrimitiveType,
} from '../HIR/HIR';
import {Environment} from '../HIR/Environment';
import {
eachInstructionLValue,
eachInstructionValueOperand,
@@ -108,7 +107,7 @@ export function inferMutationAliasingRanges(
let index = 0;
const shouldRecordErrors = !isFunctionExpression && fn.env.enableValidations;
const errors = new CompilerError();
for (const param of [...fn.params, ...fn.context, fn.returns]) {
const place = param.kind === 'Identifier' ? param : param.place;
@@ -201,9 +200,7 @@ export function inferMutationAliasingRanges(
effect.kind === 'MutateGlobal' ||
effect.kind === 'Impure'
) {
if (shouldRecordErrors) {
fn.env.recordError(effect.error);
}
errors.pushDiagnostic(effect.error);
functionEffects.push(effect);
} else if (effect.kind === 'Render') {
renders.push({index: index++, place: effect.place});
@@ -248,15 +245,11 @@ export function inferMutationAliasingRanges(
mutation.kind,
mutation.place.loc,
mutation.reason,
shouldRecordErrors ? fn.env : null,
errors,
);
}
for (const render of renders) {
state.render(
render.index,
render.place.identifier,
shouldRecordErrors ? fn.env : null,
);
state.render(render.index, render.place.identifier, errors);
}
for (const param of [...fn.context, ...fn.params]) {
const place = param.kind === 'Identifier' ? param : param.place;
@@ -505,6 +498,7 @@ export function inferMutationAliasingRanges(
* would be transitively mutated needs a capture relationship.
*/
const tracked: Array<Place> = [];
const ignoredErrors = new CompilerError();
for (const param of [...fn.params, ...fn.context, fn.returns]) {
const place = param.kind === 'Identifier' ? param : param.place;
tracked.push(place);
@@ -519,7 +513,7 @@ export function inferMutationAliasingRanges(
MutationKind.Conditional,
into.loc,
null,
null,
ignoredErrors,
);
for (const from of tracked) {
if (
@@ -553,17 +547,23 @@ export function inferMutationAliasingRanges(
}
}
if (
errors.hasAnyErrors() &&
!isFunctionExpression &&
fn.env.enableValidations
) {
fn.env.recordErrors(errors);
}
return functionEffects;
}
function appendFunctionErrors(env: Environment | null, fn: HIRFunction): void {
if (env == null) return;
function appendFunctionErrors(errors: CompilerError, fn: HIRFunction): void {
for (const effect of fn.aliasingEffects ?? []) {
switch (effect.kind) {
case 'Impure':
case 'MutateFrozen':
case 'MutateGlobal': {
env.recordError(effect.error);
errors.pushDiagnostic(effect.error);
break;
}
}
@@ -664,7 +664,7 @@ class AliasingState {
}
}
render(index: number, start: Identifier, env: Environment | null): void {
render(index: number, start: Identifier, errors: CompilerError): void {
const seen = new Set<Identifier>();
const queue: Array<Identifier> = [start];
while (queue.length !== 0) {
@@ -678,7 +678,7 @@ class AliasingState {
continue;
}
if (node.value.kind === 'Function') {
appendFunctionErrors(env, node.value.function);
appendFunctionErrors(errors, node.value.function);
}
for (const [alias, when] of node.createdFrom) {
if (when >= index) {
@@ -710,7 +710,7 @@ class AliasingState {
startKind: MutationKind,
loc: SourceLocation,
reason: MutationReason | null,
env: Environment | null,
errors: CompilerError,
): void {
const seen = new Map<Identifier, MutationKind>();
const queue: Array<{
@@ -742,7 +742,7 @@ class AliasingState {
node.transitive == null &&
node.local == null
) {
appendFunctionErrors(env, node.value.function);
appendFunctionErrors(errors, node.value.function);
}
if (transitive) {
if (node.transitive == null || node.transitive.kind < kind) {

View File

@@ -1007,10 +1007,11 @@ class Driver {
const test = this.visitValueBlock(testBlockId, loc);
const testBlock = this.cx.ir.blocks.get(test.block)!;
if (testBlock.terminal.kind !== 'branch') {
CompilerError.invariant(false, {
reason: `Expected a branch terminal for ${terminalKind} test block`,
description: `Got \`${testBlock.terminal.kind}\``,
CompilerError.throwTodo({
reason: `Unexpected terminal kind \`${testBlock.terminal.kind}\` for ${terminalKind} test block`,
description: null,
loc: testBlock.terminal.loc,
suggestions: null,
});
}
return {

View File

@@ -13,11 +13,7 @@ import {
pruneUnusedLabels,
renameVariables,
} from '.';
import {
CompilerError,
CompilerErrorDetail,
ErrorCategory,
} from '../CompilerError';
import {CompilerError, ErrorCategory} from '../CompilerError';
import {Environment, ExternalFunction} from '../HIR';
import {
ArrayPattern,
@@ -351,6 +347,10 @@ function codegenReactiveFunction(
}
}
if (cx.errors.hasAnyErrors()) {
fn.env.recordErrors(cx.errors);
}
const countMemoBlockVisitor = new CountMemoBlockVisitor(fn.env);
visitReactiveFunction(fn, countMemoBlockVisitor, undefined);
@@ -420,6 +420,7 @@ class Context {
*/
#declarations: Set<DeclarationId> = new Set();
temp: Temporaries;
errors: CompilerError = new CompilerError();
objectMethods: Map<IdentifierId, ObjectMethod> = new Map();
uniqueIdentifiers: Set<string>;
fbtOperands: Set<IdentifierId>;
@@ -438,10 +439,6 @@ class Context {
this.fbtOperands = fbtOperands;
this.temp = temporaries !== null ? new Map(temporaries) : new Map();
}
recordError(error: CompilerErrorDetail): void {
this.env.recordError(error);
}
get nextCacheIndex(): number {
return this.#nextCacheIndex++;
}
@@ -778,15 +775,12 @@ function codegenTerminal(
loc: terminal.init.loc,
});
if (terminal.init.instructions.length !== 2) {
cx.recordError(
new CompilerErrorDetail({
reason: 'Support non-trivial for..in inits',
category: ErrorCategory.Todo,
loc: terminal.init.loc,
suggestions: null,
}),
);
return t.emptyStatement();
CompilerError.throwTodo({
reason: 'Support non-trivial for..in inits',
description: null,
loc: terminal.init.loc,
suggestions: null,
});
}
const iterableCollection = terminal.init.instructions[0];
const iterableItem = terminal.init.instructions[1];
@@ -801,15 +795,12 @@ function codegenTerminal(
break;
}
case 'StoreContext': {
cx.recordError(
new CompilerErrorDetail({
reason: 'Support non-trivial for..in inits',
category: ErrorCategory.Todo,
loc: terminal.init.loc,
suggestions: null,
}),
);
return t.emptyStatement();
CompilerError.throwTodo({
reason: 'Support non-trivial for..in inits',
description: null,
loc: terminal.init.loc,
suggestions: null,
});
}
default:
CompilerError.invariant(false, {
@@ -879,15 +870,12 @@ function codegenTerminal(
loc: terminal.test.loc,
});
if (terminal.test.instructions.length !== 2) {
cx.recordError(
new CompilerErrorDetail({
reason: 'Support non-trivial for..of inits',
category: ErrorCategory.Todo,
loc: terminal.init.loc,
suggestions: null,
}),
);
return t.emptyStatement();
CompilerError.throwTodo({
reason: 'Support non-trivial for..of inits',
description: null,
loc: terminal.init.loc,
suggestions: null,
});
}
const iterableItem = terminal.test.instructions[1];
let lval: t.LVal;
@@ -901,15 +889,12 @@ function codegenTerminal(
break;
}
case 'StoreContext': {
cx.recordError(
new CompilerErrorDetail({
reason: 'Support non-trivial for..of inits',
category: ErrorCategory.Todo,
loc: terminal.init.loc,
suggestions: null,
}),
);
return t.emptyStatement();
CompilerError.throwTodo({
reason: 'Support non-trivial for..of inits',
description: null,
loc: terminal.init.loc,
suggestions: null,
});
}
default:
CompilerError.invariant(false, {
@@ -1968,26 +1953,22 @@ function codegenInstructionValue(
} else {
if (t.isVariableDeclaration(stmt)) {
const declarator = stmt.declarations[0];
cx.recordError(
new CompilerErrorDetail({
reason: `(CodegenReactiveFunction::codegenInstructionValue) Cannot declare variables in a value block, tried to declare '${
(declarator.id as t.Identifier).name
}'`,
category: ErrorCategory.Todo,
loc: declarator.loc ?? null,
suggestions: null,
}),
);
cx.errors.push({
reason: `(CodegenReactiveFunction::codegenInstructionValue) Cannot declare variables in a value block, tried to declare '${
(declarator.id as t.Identifier).name
}'`,
category: ErrorCategory.Todo,
loc: declarator.loc ?? null,
suggestions: null,
});
return t.stringLiteral(`TODO handle ${declarator.id}`);
} else {
cx.recordError(
new CompilerErrorDetail({
reason: `(CodegenReactiveFunction::codegenInstructionValue) Handle conversion of ${stmt.type} to expression`,
category: ErrorCategory.Todo,
loc: stmt.loc ?? null,
suggestions: null,
}),
);
cx.errors.push({
reason: `(CodegenReactiveFunction::codegenInstructionValue) Handle conversion of ${stmt.type} to expression`,
category: ErrorCategory.Todo,
loc: stmt.loc ?? null,
suggestions: null,
});
return t.stringLiteral(`TODO handle ${stmt.type}`);
}
}

View File

@@ -102,6 +102,7 @@ export function validateExhaustiveDependencies(fn: HIRFunction): void {
loc: place.loc,
});
}
const error = new CompilerError();
let startMemo: StartMemoize | null = null;
function onStartMemoize(
@@ -142,7 +143,7 @@ export function validateExhaustiveDependencies(fn: HIRFunction): void {
'all',
);
if (diagnostic != null) {
fn.env.recordError(diagnostic);
error.pushDiagnostic(diagnostic);
}
}
@@ -207,12 +208,15 @@ export function validateExhaustiveDependencies(fn: HIRFunction): void {
effectReportMode,
);
if (diagnostic != null) {
fn.env.recordError(diagnostic);
error.pushDiagnostic(diagnostic);
}
},
},
false, // isFunctionExpression
);
if (error.hasAnyErrors()) {
fn.env.recordErrors(error);
}
}
function validateDependencies(

View File

@@ -6,9 +6,13 @@
*/
import * as t from '@babel/types';
import {CompilerErrorDetail, ErrorCategory} from '../CompilerError';
import {
CompilerError,
CompilerErrorDetail,
ErrorCategory,
} from '../CompilerError';
import {computeUnconditionalBlocks} from '../HIR/ComputeUnconditionalBlocks';
import {Environment, isHookName} from '../HIR/Environment';
import {isHookName} from '../HIR/Environment';
import {
HIRFunction,
IdentifierId,
@@ -86,14 +90,15 @@ function joinKinds(a: Kind, b: Kind): Kind {
export function validateHooksUsage(fn: HIRFunction): void {
const unconditionalBlocks = computeUnconditionalBlocks(fn);
const errors = new CompilerError();
const errorsByPlace = new Map<t.SourceLocation, CompilerErrorDetail>();
function trackError(
function recordError(
loc: SourceLocation,
errorDetail: CompilerErrorDetail,
): void {
if (typeof loc === 'symbol') {
fn.env.recordError(errorDetail);
errors.pushErrorDetail(errorDetail);
} else {
errorsByPlace.set(loc, errorDetail);
}
@@ -113,7 +118,7 @@ export function validateHooksUsage(fn: HIRFunction): void {
* If that same place is also used as a conditional call, upgrade the error to a conditonal hook error
*/
if (previousError === undefined || previousError.reason !== reason) {
trackError(
recordError(
place.loc,
new CompilerErrorDetail({
category: ErrorCategory.Hooks,
@@ -129,7 +134,7 @@ export function validateHooksUsage(fn: HIRFunction): void {
const previousError =
typeof place.loc !== 'symbol' ? errorsByPlace.get(place.loc) : undefined;
if (previousError === undefined) {
trackError(
recordError(
place.loc,
new CompilerErrorDetail({
category: ErrorCategory.Hooks,
@@ -146,7 +151,7 @@ export function validateHooksUsage(fn: HIRFunction): void {
const previousError =
typeof place.loc !== 'symbol' ? errorsByPlace.get(place.loc) : undefined;
if (previousError === undefined) {
trackError(
recordError(
place.loc,
new CompilerErrorDetail({
category: ErrorCategory.Hooks,
@@ -391,7 +396,7 @@ export function validateHooksUsage(fn: HIRFunction): void {
}
case 'ObjectMethod':
case 'FunctionExpression': {
visitFunctionExpression(fn.env, instr.value.loweredFunc.func);
visitFunctionExpression(errors, instr.value.loweredFunc.func);
break;
}
default: {
@@ -416,17 +421,20 @@ export function validateHooksUsage(fn: HIRFunction): void {
}
for (const [, error] of errorsByPlace) {
fn.env.recordError(error);
errors.pushErrorDetail(error);
}
if (errors.hasAnyErrors()) {
fn.env.recordErrors(errors);
}
}
function visitFunctionExpression(env: Environment, fn: HIRFunction): void {
function visitFunctionExpression(errors: CompilerError, fn: HIRFunction): void {
for (const [, block] of fn.body.blocks) {
for (const instr of block.instructions) {
switch (instr.value.kind) {
case 'ObjectMethod':
case 'FunctionExpression': {
visitFunctionExpression(env, instr.value.loweredFunc.func);
visitFunctionExpression(errors, instr.value.loweredFunc.func);
break;
}
case 'MethodCall':
@@ -437,7 +445,7 @@ function visitFunctionExpression(env: Environment, fn: HIRFunction): void {
: instr.value.property;
const hookKind = getHookKind(fn.env, callee.identifier);
if (hookKind != null) {
env.recordError(
errors.pushErrorDetail(
new CompilerErrorDetail({
category: ErrorCategory.Hooks,
reason:

View File

@@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerErrorDetail, EnvironmentConfig} from '..';
import {CompilerError, CompilerErrorDetail, EnvironmentConfig} from '..';
import {ErrorCategory} from '../CompilerError';
import {HIRFunction, IdentifierId} from '../HIR';
import {DEFAULT_GLOBALS} from '../HIR/Globals';
@@ -20,6 +20,7 @@ export function validateNoCapitalizedCalls(fn: HIRFunction): void {
return ALLOW_LIST.has(name);
};
const errors = new CompilerError();
const capitalLoadGlobals = new Map<IdentifierId, string>();
const capitalizedProperties = new Map<IdentifierId, string>();
const reason =
@@ -71,19 +72,20 @@ export function validateNoCapitalizedCalls(fn: HIRFunction): void {
const propertyIdentifier = value.property.identifier.id;
const propertyName = capitalizedProperties.get(propertyIdentifier);
if (propertyName != null) {
fn.env.recordError(
new CompilerErrorDetail({
category: ErrorCategory.CapitalizedCalls,
reason,
description: `${propertyName} may be a component`,
loc: value.loc,
suggestions: null,
}),
);
errors.push({
category: ErrorCategory.CapitalizedCalls,
reason,
description: `${propertyName} may be a component`,
loc: value.loc,
suggestions: null,
});
}
break;
}
}
}
}
if (errors.hasAnyErrors()) {
fn.env.recordErrors(errors);
}
}

View File

@@ -6,7 +6,7 @@
*/
import {CompilerError, SourceLocation} from '..';
import {CompilerErrorDetail, ErrorCategory} from '../CompilerError';
import {ErrorCategory} from '../CompilerError';
import {
ArrayExpression,
BlockId,
@@ -20,7 +20,6 @@ import {
eachInstructionValueOperand,
eachTerminalOperand,
} from '../HIR/visitors';
import {Environment} from '../HIR/Environment';
/**
* Validates that useEffect is not used for derived computations which could/should
@@ -50,6 +49,8 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
const functions: Map<IdentifierId, FunctionExpression> = new Map();
const locals: Map<IdentifierId, IdentifierId> = new Map();
const errors = new CompilerError();
for (const block of fn.body.blocks.values()) {
for (const instr of block.instructions) {
const {lvalue, value} = instr;
@@ -89,19 +90,20 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
validateEffect(
effectFunction.loweredFunc.func,
dependencies,
fn.env,
errors,
);
}
}
}
}
}
fn.env.recordErrors(errors);
}
function validateEffect(
effectFunction: HIRFunction,
effectDeps: Array<IdentifierId>,
env: Environment,
errors: CompilerError,
): void {
for (const operand of effectFunction.context) {
if (isSetStateType(operand.identifier)) {
@@ -215,15 +217,13 @@ function validateEffect(
}
for (const loc of setStateLocations) {
env.recordError(
new CompilerErrorDetail({
category: ErrorCategory.EffectDerivationsOfState,
reason:
'Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)',
description: null,
loc,
suggestions: null,
}),
);
errors.push({
category: ErrorCategory.EffectDerivationsOfState,
reason:
'Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)',
description: null,
loc,
suggestions: null,
});
}
}

View File

@@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerDiagnostic, Effect} from '..';
import {CompilerDiagnostic, CompilerError, Effect} from '..';
import {ErrorCategory} from '../CompilerError';
import {
HIRFunction,
@@ -43,6 +43,7 @@ import {AliasingEffect} from '../Inference/AliasingEffects';
* that are passed where a frozen value is expected and rejects them.
*/
export function validateNoFreezingKnownMutableFunctions(fn: HIRFunction): void {
const errors = new CompilerError();
const contextMutationEffects: Map<
IdentifierId,
Extract<AliasingEffect, {kind: 'Mutate'} | {kind: 'MutateTransitive'}>
@@ -59,7 +60,7 @@ export function validateNoFreezingKnownMutableFunctions(fn: HIRFunction): void {
place.identifier.name.kind === 'named'
? `\`${place.identifier.name.value}\``
: 'a local variable';
fn.env.recordError(
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Immutability,
reason: 'Cannot modify local variables after render completes',
@@ -158,4 +159,7 @@ export function validateNoFreezingKnownMutableFunctions(fn: HIRFunction): void {
visitOperand(operand);
}
}
if (errors.hasAnyErrors()) {
fn.env.recordErrors(errors);
}
}

View File

@@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerDiagnostic} from '..';
import {CompilerDiagnostic, CompilerError} from '..';
import {ErrorCategory} from '../CompilerError';
import {HIRFunction} from '../HIR';
import {getFunctionCallSignature} from '../Inference/InferMutationAliasingEffects';
@@ -20,6 +20,7 @@ import {getFunctionCallSignature} from '../Inference/InferMutationAliasingEffect
* and use it here.
*/
export function validateNoImpureFunctionsInRender(fn: HIRFunction): void {
const errors = new CompilerError();
for (const [, block] of fn.body.blocks) {
for (const instr of block.instructions) {
const value = instr.value;
@@ -31,7 +32,7 @@ export function validateNoImpureFunctionsInRender(fn: HIRFunction): void {
callee.identifier.type,
);
if (signature != null && signature.impure === true) {
fn.env.recordError(
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Purity,
reason: 'Cannot call impure function during render',
@@ -51,4 +52,7 @@ export function validateNoImpureFunctionsInRender(fn: HIRFunction): void {
}
}
}
if (errors.hasAnyErrors()) {
fn.env.recordErrors(errors);
}
}

View File

@@ -124,8 +124,8 @@ export function validateNoRefAccessInRender(fn: HIRFunction): void {
collectTemporariesSidemap(fn, env);
const errors = new CompilerError();
validateNoRefAccessInRenderImpl(fn, env, errors);
for (const detail of errors.details) {
fn.env.recordError(detail);
if (errors.hasAnyErrors()) {
fn.env.recordErrors(errors);
}
}

View File

@@ -48,8 +48,8 @@ export function validateNoSetStateInRender(fn: HIRFunction): void {
fn,
unconditionalSetStateFunctions,
);
for (const detail of errors.details) {
fn.env.recordError(detail);
if (errors.hasAnyErrors()) {
fn.env.recordErrors(errors);
}
}

View File

@@ -27,7 +27,6 @@ import {
ScopeId,
SourceLocation,
} from '../HIR';
import {Environment} from '../HIR/Environment';
import {printIdentifier, printManualMemoDependency} from '../HIR/PrintHIR';
import {
eachInstructionValueLValue,
@@ -49,10 +48,11 @@ import {getOrInsertDefault} from '../Utils/utils';
*/
export function validatePreservedManualMemoization(fn: ReactiveFunction): void {
const state = {
env: fn.env,
errors: new CompilerError(),
manualMemoState: null,
};
visitReactiveFunction(fn, new Visitor(), state);
fn.env.recordErrors(state.errors);
}
const DEBUG = false;
@@ -110,7 +110,7 @@ type ManualMemoBlockState = {
};
type VisitorState = {
env: Environment;
errors: CompilerError;
manualMemoState: ManualMemoBlockState | null;
};
@@ -230,7 +230,7 @@ function validateInferredDep(
temporaries: Map<IdentifierId, ManualMemoDependency>,
declsWithinMemoBlock: Set<DeclarationId>,
validDepsInMemoBlock: Array<ManualMemoDependency>,
env: Environment,
errorState: CompilerError,
memoLocation: SourceLocation,
): void {
let normalizedDep: ManualMemoDependency;
@@ -280,7 +280,7 @@ function validateInferredDep(
errorDiagnostic = merge(errorDiagnostic ?? compareResult, compareResult);
}
}
env.recordError(
errorState.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.PreserveManualMemo,
reason: 'Existing memoization could not be preserved',
@@ -426,7 +426,7 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
this.temporaries,
state.manualMemoState.decls,
state.manualMemoState.depsFromSource,
state.env,
state.errors,
state.manualMemoState.loc,
);
}
@@ -529,7 +529,7 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
!this.scopes.has(identifier.scope.id) &&
!this.prunedScopes.has(identifier.scope.id)
) {
state.env.recordError(
state.errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.PreserveManualMemo,
reason: 'Existing memoization could not be preserved',
@@ -575,7 +575,7 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
for (const identifier of decls) {
if (isUnmemoized(identifier, this.scopes)) {
state.env.recordError(
state.errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.PreserveManualMemo,
reason: 'Existing memoization could not be preserved',

View File

@@ -7,7 +7,7 @@
import {NodePath} from '@babel/traverse';
import * as t from '@babel/types';
import {CompilerDiagnostic, ErrorCategory} from '..';
import {CompilerDiagnostic, CompilerError, ErrorCategory} from '..';
import {CodegenFunction} from '../ReactiveScopes';
import {Environment} from '../HIR/Environment';
@@ -125,6 +125,8 @@ export function validateSourceLocations(
generatedAst: CodegenFunction,
env: Environment,
): void {
const errors = new CompilerError();
/*
* Step 1: Collect important locations from the original source
* Note: Multiple node types can share the same location (e.g. VariableDeclarator and Identifier)
@@ -239,7 +241,7 @@ export function validateSourceLocations(
loc: t.SourceLocation,
nodeType: string,
): void => {
env.recordError(
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Todo,
reason: 'Important source location missing in generated code',
@@ -259,7 +261,7 @@ export function validateSourceLocations(
expectedType: string,
actualTypes: Set<string>,
): void => {
env.recordError(
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Todo,
reason:
@@ -307,4 +309,6 @@ export function validateSourceLocations(
}
}
}
env.recordErrors(errors);
}

View File

@@ -16,13 +16,13 @@ import {
IdentifierId,
SourceLocation,
} from '../HIR';
import {Environment} from '../HIR/Environment';
import {
eachInstructionValueOperand,
eachTerminalOperand,
} from '../HIR/visitors';
export function validateUseMemo(fn: HIRFunction): void {
const errors = new CompilerError();
const voidMemoErrors = new CompilerError();
const useMemos = new Set<IdentifierId>();
const react = new Set<IdentifierId>();
@@ -90,7 +90,7 @@ export function validateUseMemo(fn: HIRFunction): void {
firstParam.kind === 'Identifier'
? firstParam.loc
: firstParam.place.loc;
fn.env.recordError(
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
reason: 'useMemo() callbacks may not accept parameters',
@@ -106,7 +106,7 @@ export function validateUseMemo(fn: HIRFunction): void {
}
if (body.loweredFunc.func.async || body.loweredFunc.func.generator) {
fn.env.recordError(
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
reason:
@@ -122,7 +122,7 @@ export function validateUseMemo(fn: HIRFunction): void {
);
}
validateNoContextVariableAssignment(body.loweredFunc.func, fn.env);
validateNoContextVariableAssignment(body.loweredFunc.func, errors);
if (fn.env.config.validateNoVoidUseMemo) {
if (!hasNonVoidReturn(body.loweredFunc.func)) {
@@ -176,11 +176,14 @@ export function validateUseMemo(fn: HIRFunction): void {
}
}
fn.env.logErrors(voidMemoErrors.asResult());
if (errors.hasAnyErrors()) {
fn.env.recordErrors(errors);
}
}
function validateNoContextVariableAssignment(
fn: HIRFunction,
env: Environment,
errors: CompilerError,
): void {
const context = new Set(fn.context.map(place => place.identifier.id));
for (const block of fn.body.blocks.values()) {
@@ -189,7 +192,7 @@ function validateNoContextVariableAssignment(
switch (value.kind) {
case 'StoreContext': {
if (context.has(value.lvalue.place.identifier.id)) {
env.recordError(
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
reason:

View File

@@ -24,9 +24,9 @@ function useThing(fn) {
```
Found 1 error:
Error: Expected a non-reserved identifier name
Invariant: [HIRBuilder] Unexpected null block
`this` is a reserved word in JavaScript and cannot be used as an identifier name.
expected block 0 to exist.
```

View File

@@ -0,0 +1,39 @@
## Input
```javascript
function useFoo(props: {value: {x: string; y: string} | null}) {
const value = props.value;
return createArray(value?.x, value?.y)?.join(', ');
}
function createArray<T>(...args: Array<T>): Array<T> {
return args;
}
export const FIXTURE_ENTRYPONT = {
fn: useFoo,
props: [{value: null}],
};
```
## Error
```
Found 1 error:
Todo: Unexpected terminal kind `optional` for optional fallthrough block
error.todo-optional-call-chain-in-optional.ts:3:21
1 | function useFoo(props: {value: {x: string; y: string} | null}) {
2 | const value = props.value;
> 3 | return createArray(value?.x, value?.y)?.join(', ');
| ^^^^^^^^ Unexpected terminal kind `optional` for optional fallthrough block
4 | }
5 |
6 | function createArray<T>(...args: Array<T>): Array<T> {
```

View File

@@ -50,7 +50,7 @@ export const FIXTURE_ENTRYPOINT = {
## Error
```
Found 4 errors:
Found 1 error:
Todo: Support local variables named `fbt`
@@ -60,49 +60,10 @@ error.todo-fbt-as-local.ts:18:19
16 |
17 | function Foo(props) {
> 18 | const getText1 = fbt =>
| ^^^ Support local variables named `fbt`
| ^^^ Rename to avoid conflict with fbt plugin
19 | fbt(
20 | `Hello, ${fbt.param('(key) name', identity(props.name))}!`,
21 | '(description) Greeting'
Todo: Support local variables named `fbt`
Local variables named `fbt` may conflict with the fbt plugin and are not yet supported.
error.todo-fbt-as-local.ts:18:19
16 |
17 | function Foo(props) {
> 18 | const getText1 = fbt =>
| ^^^ Support local variables named `fbt`
19 | fbt(
20 | `Hello, ${fbt.param('(key) name', identity(props.name))}!`,
21 | '(description) Greeting'
Todo: Support local variables named `fbt`
Local variables named `fbt` may conflict with the fbt plugin and are not yet supported.
error.todo-fbt-as-local.ts:18:19
16 |
17 | function Foo(props) {
> 18 | const getText1 = fbt =>
| ^^^ Support local variables named `fbt`
19 | fbt(
20 | `Hello, ${fbt.param('(key) name', identity(props.name))}!`,
21 | '(description) Greeting'
Todo: Support local variables named `fbt`
Local variables named `fbt` may conflict with the fbt plugin and are not yet supported.
error.todo-fbt-as-local.ts:24:19
22 | );
23 |
> 24 | const getText2 = fbt =>
| ^^^ Support local variables named `fbt`
25 | fbt(
26 | `Goodbye, ${fbt.param('(key) name', identity(props.name))}!`,
27 | '(description) Greeting2'
```

View File

@@ -16,15 +16,17 @@ function Component(props) {
```
Found 1 error:
Invariant: <fbt> tags should be module-level imports
Todo: Support local variables named `fbt`
error.todo-locally-require-fbt.ts:4:10
2 | const fbt = require('fbt');
Local variables named `fbt` may conflict with the fbt plugin and are not yet supported.
error.todo-locally-require-fbt.ts:2:8
1 | function Component(props) {
> 2 | const fbt = require('fbt');
| ^^^ Rename to avoid conflict with fbt plugin
3 |
> 4 | return <fbt desc="Description">{'Text'}</fbt>;
| ^^^ <fbt> tags should be module-level imports
4 | return <fbt desc="Description">{'Text'}</fbt>;
5 | }
6 |
```

View File

@@ -0,0 +1,40 @@
## Input
```javascript
// @enablePropagateDepsInHIR
function useFoo(props: {value: {x: string; y: string} | null}) {
const value = props.value;
return createArray(value?.x, value?.y)?.join(', ');
}
function createArray<T>(...args: Array<T>): Array<T> {
return args;
}
export const FIXTURE_ENTRYPONT = {
fn: useFoo,
props: [{value: null}],
};
```
## Error
```
Found 1 error:
Todo: Unexpected terminal kind `optional` for optional fallthrough block
error.todo-optional-call-chain-in-optional.ts:4:21
2 | function useFoo(props: {value: {x: string; y: string} | null}) {
3 | const value = props.value;
> 4 | return createArray(value?.x, value?.y)?.join(', ');
| ^^^^^^^^ Unexpected terminal kind `optional` for optional fallthrough block
5 | }
6 |
7 | function createArray<T>(...args: Array<T>): Array<T> {
```

View File

@@ -1,54 +0,0 @@
## Input
```javascript
// @enablePropagateDepsInHIR
function useFoo(props: {value: {x: string; y: string} | null}) {
const value = props.value;
return createArray(value?.x, value?.y)?.join(', ');
}
function createArray<T>(...args: Array<T>): Array<T> {
return args;
}
export const FIXTURE_ENTRYPONT = {
fn: useFoo,
props: [{value: null}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR
function useFoo(props) {
const $ = _c(3);
const value = props.value;
let t0;
if ($[0] !== value?.x || $[1] !== value?.y) {
t0 = createArray(value?.x, value?.y)?.join(", ");
$[0] = value?.x;
$[1] = value?.y;
$[2] = t0;
} else {
t0 = $[2];
}
return t0;
}
function createArray(...t0) {
const args = t0;
return args;
}
export const FIXTURE_ENTRYPONT = {
fn: useFoo,
props: [{ value: null }],
};
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -1,53 +0,0 @@
## Input
```javascript
function useFoo(props: {value: {x: string; y: string} | null}) {
const value = props.value;
return createArray(value?.x, value?.y)?.join(', ');
}
function createArray<T>(...args: Array<T>): Array<T> {
return args;
}
export const FIXTURE_ENTRYPONT = {
fn: useFoo,
props: [{value: null}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
function useFoo(props) {
const $ = _c(3);
const value = props.value;
let t0;
if ($[0] !== value?.x || $[1] !== value?.y) {
t0 = createArray(value?.x, value?.y)?.join(", ");
$[0] = value?.x;
$[1] = value?.y;
$[2] = t0;
} else {
t0 = $[2];
}
return t0;
}
function createArray(...t0) {
const args = t0;
return args;
}
export const FIXTURE_ENTRYPONT = {
fn: useFoo,
props: [{ value: null }],
};
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -378,17 +378,6 @@ export async function transformFixtureInput(
msg: 'Expected nothing to be compiled (from `// @expectNothingCompiled`), but some functions compiled or errored',
};
}
const unexpectedThrows = logs.filter(
log => log.event.kind === 'CompileUnexpectedThrow',
);
if (unexpectedThrows.length > 0) {
return {
kind: 'err',
msg:
`Compiler pass(es) threw instead of recording errors:\n` +
unexpectedThrows.map(l => (l.event as any).data).join('\n'),
};
}
return {
kind: 'ok',
value: {