Compare commits

..

8 Commits

Author SHA1 Message Date
Joe Savona
d371f07622 [compiler] Add fault tolerance test fixtures 2026-02-21 13:04:12 -08:00
Joe Savona
d401744f03 [compiler] Phase 3: Make lower() always produce HIRFunction 2026-02-21 13:04:10 -08:00
Joe Savona
f92560c498 [compiler] Phase 8: Add multi-error test fixture and update plan
Add test fixture demonstrating fault tolerance: the compiler now reports
both a mutation error and a ref access error in the same function, where
previously only one would be reported before bailing out.

Update plan doc to mark all phases as complete.
2026-02-21 13:03:56 -08:00
Joe Savona
bb174a5230 [compiler] Phase 4 (batch 2), 5, 6: Update remaining passes for fault tolerance
Update remaining validation passes to record errors on env:
- validateMemoizedEffectDependencies
- validatePreservedManualMemoization
- validateSourceLocations (added env parameter)
- validateContextVariableLValues (changed throwTodo to recordError)
- validateLocalsNotReassignedAfterRender (changed throw to recordError)
- validateNoDerivedComputationsInEffects (changed throw to recordError)

Update inference passes:
- inferMutationAliasingEffects: return void, errors on env
- inferMutationAliasingRanges: return Array<AliasingEffect> directly, errors on env

Update codegen:
- codegenFunction: return CodegenFunction directly, errors on env
- codegenReactiveFunction: same pattern

Update Pipeline.ts to call all passes directly without tryRecord/unwrap.
Also update AnalyseFunctions.ts which called inferMutationAliasingRanges.
2026-02-21 13:03:54 -08:00
Joe Savona
70ae844c65 [compiler] Phase 4 (batch 1): Update validation passes to record errors on env
Update 9 validation passes to record errors directly on fn.env instead of
returning Result<void, CompilerError>:
- validateHooksUsage
- validateNoCapitalizedCalls (also changed throwInvalidReact to recordError)
- validateUseMemo
- dropManualMemoization
- validateNoRefAccessInRender
- validateNoSetStateInRender
- validateNoImpureFunctionsInRender
- validateNoFreezingKnownMutableFunctions
- validateExhaustiveDependencies

Each pass now calls fn.env.recordErrors() instead of returning errors.asResult().
Pipeline.ts call sites updated to remove tryRecord() wrappers and .unwrap().
2026-02-21 13:03:38 -08:00
Joe Savona
2bf8fa8e91 [compiler] Phase 2+7: Wrap pipeline passes in tryRecord for fault tolerance
- Change runWithEnvironment/run/compileFn to return Result<CodegenFunction, CompilerError>
- Wrap all pipeline passes in env.tryRecord() to catch and record CompilerErrors
- Record inference pass errors via env.recordErrors() instead of throwing
- Handle codegen Result explicitly, returning Err on failure
- Add final error check: return Err(env.aggregateErrors()) if any errors accumulated
- Update tryCompileFunction and retryCompileFunction in Program.ts to handle Result
- Keep lint-only passes using env.logErrors() (non-blocking)
- Update 52 test fixture expectations that now report additional errors

This is the core integration that enables fault tolerance: errors are caught,
recorded, and the pipeline continues to discover more errors.
2026-02-21 13:03:35 -08:00
Joe Savona
8593874f38 [compiler] Phase 1: Add error accumulation infrastructure to Environment
Add error accumulation methods to the Environment class:
- #errors field to accumulate CompilerErrors across passes
- recordError() to record a single diagnostic (throws if Invariant)
- recordErrors() to record all diagnostics from a CompilerError
- hasErrors() to check if any errors have been recorded
- aggregateErrors() to retrieve the accumulated CompilerError
- tryRecord() to wrap callbacks and catch CompilerErrors
2026-02-21 13:01:33 -08:00
Joe Savona
00ae726dd3 [compiler] Add fault tolerance plan document
Add detailed plan for making the React Compiler fault-tolerant by
accumulating errors across all passes instead of stopping at the first
error. This enables reporting multiple compilation errors at once.
2026-02-21 13:01:33 -08:00
35 changed files with 977 additions and 1064 deletions

View File

@@ -328,6 +328,4 @@ Walk through `runWithEnvironment` and wrap each pass call site. This is the inte
* **Partial HIR can trigger downstream invariants.** When lowering skips or partially handles constructs (e.g., unreachable hoisted functions, `var` declarations before the fix), downstream passes like `InferMutationAliasingEffects` may encounter uninitialized identifiers and throw invariants. This is acceptable since the function still correctly bails out of compilation, but error messages may be less specific. The fix for `var` (treating as `let`) demonstrates how to avoid this: continue lowering with a best-effort representation rather than skipping entirely.
* **Errors accumulated on `env` are lost when an invariant propagates out of the pipeline.** Since invariant CompilerErrors always re-throw through `tryRecord()`, they exit the pipeline as exceptions. The caller only sees the invariant error, not any errors previously recorded on `env`. This is a design limitation that could be addressed by aggregating env errors with caught exceptions in `tryCompileFunction()`.
* **Dedicated fault tolerance test fixtures** were added in `__tests__/fixtures/compiler/fault-tolerance/`. Each fixture combines two or more errors from different passes to verify the compiler reports all of them rather than short-circuiting on the first. Coverage includes: `var`+props mutation (BuildHIR→InferMutationAliasingEffects), `var`+ref access (BuildHIR→ValidateNoRefAccessInRender), `try/finally`+props mutation (BuildHIR→InferMutationAliasingEffects), `try/finally`+ref access (BuildHIR→ValidateNoRefAccessInRender), and a 3-error test combining try/finally+ref access+props mutation.
* **Cleanup: consistent `tryRecord()` wrapping in Pipeline.ts.** All validation passes and inference passes are now wrapped in `env.tryRecord()` for defense-in-depth, consistent with the approach used for transform passes. Previously only transform passes were wrapped. Merged duplicate `env.enableValidations` guard blocks. Pattern B lint-only passes (`env.logErrors()`) were intentionally not wrapped since they use a different error recording strategy.
* **Cleanup: normalized validation error recording pattern.** Four validation passes (`ValidateNoDerivedComputationsInEffects`, `ValidateMemoizedEffectDependencies`, `ValidatePreservedManualMemoization`, `ValidateSourceLocations`) were using `for (const detail of errors.details) { env.recordError(detail); }` instead of the simpler `env.recordErrors(errors)`. Normalized to use the batch method.

View File

@@ -254,7 +254,6 @@ export type LoggerEvent =
| CompileErrorEvent
| CompileDiagnosticEvent
| CompileSkipEvent
| CompileUnexpectedThrowEvent
| PipelineErrorEvent
| TimingEvent;
@@ -289,11 +288,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,
@@ -158,7 +159,9 @@ function runWithEnvironment(
const hir = lower(func, env);
log({kind: 'hir', name: 'HIR', value: hir});
pruneMaybeThrows(hir);
env.tryRecord(() => {
pruneMaybeThrows(hir);
});
log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
validateContextVariableLValues(hir);
@@ -169,31 +172,43 @@ function runWithEnvironment(
log({kind: 'hir', name: 'DropManualMemoization', value: hir});
}
inlineImmediatelyInvokedFunctionExpressions(hir);
env.tryRecord(() => {
inlineImmediatelyInvokedFunctionExpressions(hir);
});
log({
kind: 'hir',
name: 'InlineImmediatelyInvokedFunctionExpressions',
value: hir,
});
mergeConsecutiveBlocks(hir);
env.tryRecord(() => {
mergeConsecutiveBlocks(hir);
});
log({kind: 'hir', name: 'MergeConsecutiveBlocks', value: hir});
assertConsistentIdentifiers(hir);
assertTerminalSuccessorsExist(hir);
enterSSA(hir);
env.tryRecord(() => {
enterSSA(hir);
});
log({kind: 'hir', name: 'SSA', value: hir});
eliminateRedundantPhi(hir);
env.tryRecord(() => {
eliminateRedundantPhi(hir);
});
log({kind: 'hir', name: 'EliminateRedundantPhi', value: hir});
assertConsistentIdentifiers(hir);
constantPropagation(hir);
env.tryRecord(() => {
constantPropagation(hir);
});
log({kind: 'hir', name: 'ConstantPropagation', value: hir});
inferTypes(hir);
env.tryRecord(() => {
inferTypes(hir);
});
log({kind: 'hir', name: 'InferTypes', value: hir});
if (env.enableValidations) {
@@ -205,24 +220,34 @@ function runWithEnvironment(
}
}
optimizePropsMethodCalls(hir);
env.tryRecord(() => {
optimizePropsMethodCalls(hir);
});
log({kind: 'hir', name: 'OptimizePropsMethodCalls', value: hir});
analyseFunctions(hir);
env.tryRecord(() => {
analyseFunctions(hir);
});
log({kind: 'hir', name: 'AnalyseFunctions', value: hir});
inferMutationAliasingEffects(hir);
log({kind: 'hir', name: 'InferMutationAliasingEffects', value: hir});
if (env.outputMode === 'ssr') {
optimizeForSSR(hir);
env.tryRecord(() => {
optimizeForSSR(hir);
});
log({kind: 'hir', name: 'OptimizeForSSR', value: hir});
}
// Note: Has to come after infer reference effects because "dead" code may still affect inference
deadCodeElimination(hir);
env.tryRecord(() => {
deadCodeElimination(hir);
});
log({kind: 'hir', name: 'DeadCodeElimination', value: hir});
pruneMaybeThrows(hir);
env.tryRecord(() => {
pruneMaybeThrows(hir);
});
log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
inferMutationAliasingRanges(hir, {
@@ -231,7 +256,9 @@ function runWithEnvironment(
log({kind: 'hir', name: 'InferMutationAliasingRanges', value: hir});
if (env.enableValidations) {
validateLocalsNotReassignedAfterRender(hir);
}
if (env.enableValidations) {
if (env.config.assertValidMutableRanges) {
assertValidMutableRanges(hir);
}
@@ -268,7 +295,9 @@ function runWithEnvironment(
validateNoFreezingKnownMutableFunctions(hir);
}
inferReactivePlaces(hir);
env.tryRecord(() => {
inferReactivePlaces(hir);
});
log({kind: 'hir', name: 'InferReactivePlaces', value: hir});
if (env.enableValidations) {
@@ -281,7 +310,9 @@ function runWithEnvironment(
}
}
rewriteInstructionKindsBasedOnReassignment(hir);
env.tryRecord(() => {
rewriteInstructionKindsBasedOnReassignment(hir);
});
log({
kind: 'hir',
name: 'RewriteInstructionKindsBasedOnReassignment',
@@ -302,11 +333,16 @@ function runWithEnvironment(
* if inferred memoization is enabled. This makes all later passes which
* transform reactive-scope labeled instructions no-ops.
*/
inferReactiveScopeVariables(hir);
env.tryRecord(() => {
inferReactiveScopeVariables(hir);
});
log({kind: 'hir', name: 'InferReactiveScopeVariables', value: hir});
}
const fbtOperands = memoizeFbtAndMacroOperandsInSameScope(hir);
let fbtOperands: Set<IdentifierId> = new Set();
env.tryRecord(() => {
fbtOperands = memoizeFbtAndMacroOperandsInSameScope(hir);
});
log({
kind: 'hir',
name: 'MemoizeFbtAndMacroOperandsInSameScope',
@@ -314,11 +350,15 @@ function runWithEnvironment(
});
if (env.config.enableJsxOutlining) {
outlineJSX(hir);
env.tryRecord(() => {
outlineJSX(hir);
});
}
if (env.config.enableNameAnonymousFunctions) {
nameAnonymousFunctions(hir);
env.tryRecord(() => {
nameAnonymousFunctions(hir);
});
log({
kind: 'hir',
name: 'NameAnonymousFunctions',
@@ -327,39 +367,51 @@ function runWithEnvironment(
}
if (env.config.enableFunctionOutlining) {
outlineFunctions(hir, fbtOperands);
env.tryRecord(() => {
outlineFunctions(hir, fbtOperands);
});
log({kind: 'hir', name: 'OutlineFunctions', value: hir});
}
alignMethodCallScopes(hir);
env.tryRecord(() => {
alignMethodCallScopes(hir);
});
log({
kind: 'hir',
name: 'AlignMethodCallScopes',
value: hir,
});
alignObjectMethodScopes(hir);
env.tryRecord(() => {
alignObjectMethodScopes(hir);
});
log({
kind: 'hir',
name: 'AlignObjectMethodScopes',
value: hir,
});
pruneUnusedLabelsHIR(hir);
env.tryRecord(() => {
pruneUnusedLabelsHIR(hir);
});
log({
kind: 'hir',
name: 'PruneUnusedLabelsHIR',
value: hir,
});
alignReactiveScopesToBlockScopesHIR(hir);
env.tryRecord(() => {
alignReactiveScopesToBlockScopesHIR(hir);
});
log({
kind: 'hir',
name: 'AlignReactiveScopesToBlockScopesHIR',
value: hir,
});
mergeOverlappingReactiveScopesHIR(hir);
env.tryRecord(() => {
mergeOverlappingReactiveScopesHIR(hir);
});
log({
kind: 'hir',
name: 'MergeOverlappingReactiveScopesHIR',
@@ -367,7 +419,9 @@ function runWithEnvironment(
});
assertValidBlockNesting(hir);
buildReactiveScopeTerminalsHIR(hir);
env.tryRecord(() => {
buildReactiveScopeTerminalsHIR(hir);
});
log({
kind: 'hir',
name: 'BuildReactiveScopeTerminalsHIR',
@@ -376,14 +430,18 @@ function runWithEnvironment(
assertValidBlockNesting(hir);
flattenReactiveLoopsHIR(hir);
env.tryRecord(() => {
flattenReactiveLoopsHIR(hir);
});
log({
kind: 'hir',
name: 'FlattenReactiveLoopsHIR',
value: hir,
});
flattenScopesWithHooksOrUseHIR(hir);
env.tryRecord(() => {
flattenScopesWithHooksOrUseHIR(hir);
});
log({
kind: 'hir',
name: 'FlattenScopesWithHooksOrUseHIR',
@@ -391,15 +449,19 @@ function runWithEnvironment(
});
assertTerminalSuccessorsExist(hir);
assertTerminalPredsExist(hir);
propagateScopeDependenciesHIR(hir);
env.tryRecord(() => {
propagateScopeDependenciesHIR(hir);
});
log({
kind: 'hir',
name: 'PropagateScopeDependenciesHIR',
value: hir,
});
const reactiveFunction = buildReactiveFunction(hir);
let reactiveFunction!: ReactiveFunction;
env.tryRecord(() => {
reactiveFunction = buildReactiveFunction(hir);
});
log({
kind: 'reactive',
name: 'BuildReactiveFunction',
@@ -408,7 +470,9 @@ function runWithEnvironment(
assertWellFormedBreakTargets(reactiveFunction);
pruneUnusedLabels(reactiveFunction);
env.tryRecord(() => {
pruneUnusedLabels(reactiveFunction);
});
log({
kind: 'reactive',
name: 'PruneUnusedLabels',
@@ -416,84 +480,109 @@ function runWithEnvironment(
});
assertScopeInstructionsWithinScopes(reactiveFunction);
pruneNonEscapingScopes(reactiveFunction);
env.tryRecord(() => {
pruneNonEscapingScopes(reactiveFunction);
});
log({
kind: 'reactive',
name: 'PruneNonEscapingScopes',
value: reactiveFunction,
});
pruneNonReactiveDependencies(reactiveFunction);
env.tryRecord(() => {
pruneNonReactiveDependencies(reactiveFunction);
});
log({
kind: 'reactive',
name: 'PruneNonReactiveDependencies',
value: reactiveFunction,
});
pruneUnusedScopes(reactiveFunction);
env.tryRecord(() => {
pruneUnusedScopes(reactiveFunction);
});
log({
kind: 'reactive',
name: 'PruneUnusedScopes',
value: reactiveFunction,
});
mergeReactiveScopesThatInvalidateTogether(reactiveFunction);
env.tryRecord(() => {
mergeReactiveScopesThatInvalidateTogether(reactiveFunction);
});
log({
kind: 'reactive',
name: 'MergeReactiveScopesThatInvalidateTogether',
value: reactiveFunction,
});
pruneAlwaysInvalidatingScopes(reactiveFunction);
env.tryRecord(() => {
pruneAlwaysInvalidatingScopes(reactiveFunction);
});
log({
kind: 'reactive',
name: 'PruneAlwaysInvalidatingScopes',
value: reactiveFunction,
});
propagateEarlyReturns(reactiveFunction);
env.tryRecord(() => {
propagateEarlyReturns(reactiveFunction);
});
log({
kind: 'reactive',
name: 'PropagateEarlyReturns',
value: reactiveFunction,
});
pruneUnusedLValues(reactiveFunction);
env.tryRecord(() => {
pruneUnusedLValues(reactiveFunction);
});
log({
kind: 'reactive',
name: 'PruneUnusedLValues',
value: reactiveFunction,
});
promoteUsedTemporaries(reactiveFunction);
env.tryRecord(() => {
promoteUsedTemporaries(reactiveFunction);
});
log({
kind: 'reactive',
name: 'PromoteUsedTemporaries',
value: reactiveFunction,
});
extractScopeDeclarationsFromDestructuring(reactiveFunction);
env.tryRecord(() => {
extractScopeDeclarationsFromDestructuring(reactiveFunction);
});
log({
kind: 'reactive',
name: 'ExtractScopeDeclarationsFromDestructuring',
value: reactiveFunction,
});
stabilizeBlockIds(reactiveFunction);
env.tryRecord(() => {
stabilizeBlockIds(reactiveFunction);
});
log({
kind: 'reactive',
name: 'StabilizeBlockIds',
value: reactiveFunction,
});
const uniqueIdentifiers = renameVariables(reactiveFunction);
let uniqueIdentifiers: Set<string> = new Set();
env.tryRecord(() => {
uniqueIdentifiers = renameVariables(reactiveFunction);
});
log({
kind: 'reactive',
name: 'RenameVariables',
value: reactiveFunction,
});
pruneHoistedContexts(reactiveFunction);
env.tryRecord(() => {
pruneHoistedContexts(reactiveFunction);
});
log({
kind: 'reactive',
name: 'PruneHoistedContexts',

View File

@@ -720,24 +720,11 @@ 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};
}
}
/**
* Applies React Compiler generated functions to the babel AST by replacing
* existing functions in place or inserting new declarations.

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

@@ -773,6 +773,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,11 +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++;
}
@@ -779,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];
@@ -802,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, {
@@ -880,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;
@@ -902,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, {
@@ -1969,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,22 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
validateEffect(
effectFunction.loweredFunc.func,
dependencies,
fn.env,
errors,
);
}
}
}
}
}
for (const detail of errors.details) {
fn.env.recordError(detail);
}
}
function validateEffect(
effectFunction: HIRFunction,
effectDeps: Array<IdentifierId>,
env: Environment,
errors: CompilerError,
): void {
for (const operand of effectFunction.context) {
if (isSetStateType(operand.identifier)) {
@@ -215,15 +219,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,13 @@ 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);
for (const detail of state.errors.details) {
fn.env.recordError(detail);
}
}
const DEBUG = false;
@@ -110,7 +112,7 @@ type ManualMemoBlockState = {
};
type VisitorState = {
env: Environment;
errors: CompilerError;
manualMemoState: ManualMemoBlockState | null;
};
@@ -230,7 +232,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 +282,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 +428,7 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
this.temporaries,
state.manualMemoState.decls,
state.manualMemoState.depsFromSource,
state.env,
state.errors,
state.manualMemoState.loc,
);
}
@@ -529,7 +531,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 +577,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,8 @@ export function validateSourceLocations(
}
}
}
for (const detail of errors.details) {
env.recordError(detail);
}
}

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

@@ -21,15 +21,15 @@ export const FIXTURE_ENTRYPOINT = {
```
Found 1 error:
Todo: [hoisting] EnterSSA: Expected identifier to be defined before being used
Invariant: [InferMutationAliasingEffects] Expected value kind to be initialized
Identifier x$1 is undefined.
<unknown> x$1.
error.dont-hoist-inline-reference.ts:3:2
error.dont-hoist-inline-reference.ts:3:21
1 | import {identity} from 'shared-runtime';
2 | function useInvalid() {
> 3 | const x = identity(x);
| ^^^^^^^^^^^^^^^^^^^^^^ [hoisting] EnterSSA: Expected identifier to be defined before being used
| ^ this is uninitialized
4 | return x;
5 | }
6 |

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

@@ -18,12 +18,7 @@ function Component() {
// Error: reading ref during render
const value = ref.current;
return (
<div>
{value}
{items.length}
</div>
);
return <div>{value}{items.length}</div>;
}
```
@@ -55,8 +50,8 @@ error.var-declaration-and-ref-access.ts:15:16
> 15 | const value = ref.current;
| ^^^^^^^^^^^ Cannot access ref value during render
16 |
17 | return (
18 | <div>
17 | return <div>{value}{items.length}</div>;
18 | }
```

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: {