Compare commits

..

7 Commits

Author SHA1 Message Date
Joe Savona
e9d30d513b [compiler] Phase 3: Make lower() always produce HIRFunction 2026-02-20 18:03:02 -08:00
Joe Savona
cd207083ee [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-20 18:02:42 -08:00
Joe Savona
a6d3d93d4f [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-20 18:02:42 -08:00
Joe Savona
821d6a8be2 [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-20 18:01:44 -08:00
Joe Savona
966a5195ab [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-20 18:01:15 -08:00
Joe Savona
05243074f3 [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-20 17:57:42 -08:00
Joe Savona
056a8e127f [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-20 17:57:41 -08:00
34 changed files with 342 additions and 737 deletions

View File

@@ -327,7 +327,4 @@ Walk through `runWithEnvironment` and wrap each pass call site. This is the inte
* **Phase 3 (BuildHIR) revealed that most error sites already used `builder.errors.push()` for accumulation.** The existing lowering code was designed to accumulate errors rather than throw. The main changes were: (1) changing `lower()` return type from `Result` to `HIRFunction`, (2) recording builder errors on env, (3) adding a try/catch around body lowering to catch thrown CompilerErrors from sub-calls like `resolveBinding()`, (4) treating `var` as `let` instead of skipping declarations, and (5) fixing ForStatement init/test handling to produce valid CFG structure.
* **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,25 +720,10 @@ 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

@@ -185,32 +185,51 @@ export function lower(
let directives: Array<string> = [];
const body = func.get('body');
if (body.isExpression()) {
const fallthrough = builder.reserve('block');
const terminal: ReturnTerminal = {
kind: 'return',
returnVariant: 'Implicit',
loc: GeneratedSource,
value: lowerExpressionToTemporary(builder, body),
id: makeInstructionId(0),
effects: null,
};
builder.terminateWithContinuation(terminal, fallthrough);
} else if (body.isBlockStatement()) {
lowerStatement(builder, body);
directives = body.get('directives').map(d => d.node.value.value);
} else {
builder.errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Syntax,
reason: `Unexpected function body kind`,
description: `Expected function body to be an expression or a block statement, got \`${body.type}\``,
}).withDetails({
kind: 'error',
loc: body.node.loc ?? null,
message: 'Expected a block statement or expression',
}),
);
try {
if (body.isExpression()) {
const fallthrough = builder.reserve('block');
const terminal: ReturnTerminal = {
kind: 'return',
returnVariant: 'Implicit',
loc: GeneratedSource,
value: lowerExpressionToTemporary(builder, body),
id: makeInstructionId(0),
effects: null,
};
builder.terminateWithContinuation(terminal, fallthrough);
} else if (body.isBlockStatement()) {
lowerStatement(builder, body);
directives = body.get('directives').map(d => d.node.value.value);
} else {
builder.errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Syntax,
reason: `Unexpected function body kind`,
description: `Expected function body to be an expression or a block statement, got \`${body.type}\``,
}).withDetails({
kind: 'error',
loc: body.node.loc ?? null,
message: 'Expected a block statement or expression',
}),
);
}
} catch (err) {
if (err instanceof CompilerError) {
// Re-throw invariant errors immediately
for (const detail of err.details) {
if (
(detail instanceof CompilerDiagnostic
? detail.category
: detail.category) === ErrorCategory.Invariant
) {
throw err;
}
}
// Record non-invariant errors and continue to produce partial HIR
builder.errors.merge(err);
} else {
throw err;
}
}
let validatedId: HIRFunction['id'] = null;

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

@@ -308,23 +308,33 @@ export default class HIRBuilder {
resolveBinding(node: t.Identifier): Identifier {
if (node.name === 'fbt') {
this.errors.push({
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',
loc: node.loc ?? GeneratedSource,
suggestions: null,
details: [
{
kind: 'error',
message: 'Rename to avoid conflict with fbt plugin',
loc: node.loc ?? GeneratedSource,
},
],
});
}
if (node.name === 'this') {
this.errors.push({
CompilerError.throwDiagnostic({
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,
details: [
{
kind: 'error',
message: '`this` was used here',
loc: node.loc ?? GeneratedSource,
},
],
});
}
const originalName = node.name;

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

@@ -775,13 +775,12 @@ function codegenTerminal(
loc: terminal.init.loc,
});
if (terminal.init.instructions.length !== 2) {
cx.errors.push({
CompilerError.throwTodo({
reason: 'Support non-trivial for..in inits',
category: ErrorCategory.Todo,
description: null,
loc: terminal.init.loc,
suggestions: null,
});
return t.emptyStatement();
}
const iterableCollection = terminal.init.instructions[0];
const iterableItem = terminal.init.instructions[1];
@@ -796,13 +795,12 @@ function codegenTerminal(
break;
}
case 'StoreContext': {
cx.errors.push({
CompilerError.throwTodo({
reason: 'Support non-trivial for..in inits',
category: ErrorCategory.Todo,
description: null,
loc: terminal.init.loc,
suggestions: null,
});
return t.emptyStatement();
}
default:
CompilerError.invariant(false, {
@@ -872,13 +870,12 @@ function codegenTerminal(
loc: terminal.test.loc,
});
if (terminal.test.instructions.length !== 2) {
cx.errors.push({
CompilerError.throwTodo({
reason: 'Support non-trivial for..of inits',
category: ErrorCategory.Todo,
description: null,
loc: terminal.init.loc,
suggestions: null,
});
return t.emptyStatement();
}
const iterableItem = terminal.test.instructions[1];
let lval: t.LVal;
@@ -892,13 +889,12 @@ function codegenTerminal(
break;
}
case 'StoreContext': {
cx.errors.push({
CompilerError.throwTodo({
reason: 'Support non-trivial for..of inits',
category: ErrorCategory.Todo,
description: null,
loc: terminal.init.loc,
suggestions: null,
});
return t.emptyStatement();
}
default:
CompilerError.invariant(false, {

View File

@@ -97,7 +97,9 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
}
}
}
fn.env.recordErrors(errors);
for (const detail of errors.details) {
fn.env.recordError(detail);
}
}
function validateEffect(

View File

@@ -52,7 +52,9 @@ export function validatePreservedManualMemoization(fn: ReactiveFunction): void {
manualMemoState: null,
};
visitReactiveFunction(fn, new Visitor(), state);
fn.env.recordErrors(state.errors);
for (const detail of state.errors.details) {
fn.env.recordError(detail);
}
}
const DEBUG = false;

View File

@@ -310,5 +310,7 @@ export function validateSourceLocations(
}
}
env.recordErrors(errors);
for (const detail of errors.details) {
env.recordError(detail);
}
}

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

@@ -1,66 +0,0 @@
## Input
```javascript
/**
* Fault tolerance test: two independent errors should both be reported.
*
* Error 1 (BuildHIR): `try/finally` is not supported
* Error 2 (InferMutationAliasingEffects): Mutation of frozen props
*/
function Component(props) {
// Error: try/finally (Todo from BuildHIR)
try {
doWork();
} finally {
doCleanup();
}
// Error: mutating frozen props
props.value = 1;
return <div>{props.value}</div>;
}
```
## Error
```
Found 2 errors:
Todo: (BuildHIR::lowerStatement) Handle TryStatement without a catch clause
error.try-finally-and-mutation-of-props.ts:9:2
7 | function Component(props) {
8 | // Error: try/finally (Todo from BuildHIR)
> 9 | try {
| ^^^^^
> 10 | doWork();
| ^^^^^^^^^^^^^
> 11 | } finally {
| ^^^^^^^^^^^^^
> 12 | doCleanup();
| ^^^^^^^^^^^^^
> 13 | }
| ^^^^ (BuildHIR::lowerStatement) Handle TryStatement without a catch clause
14 |
15 | // Error: mutating frozen props
16 | props.value = 1;
Error: This value cannot be modified
Modifying component props or hook arguments is not allowed. Consider using a local variable instead.
error.try-finally-and-mutation-of-props.ts:16:2
14 |
15 | // Error: mutating frozen props
> 16 | props.value = 1;
| ^^^^^ value cannot be modified
17 |
18 | return <div>{props.value}</div>;
19 | }
```

View File

@@ -1,19 +0,0 @@
/**
* Fault tolerance test: two independent errors should both be reported.
*
* Error 1 (BuildHIR): `try/finally` is not supported
* Error 2 (InferMutationAliasingEffects): Mutation of frozen props
*/
function Component(props) {
// Error: try/finally (Todo from BuildHIR)
try {
doWork();
} finally {
doCleanup();
}
// Error: mutating frozen props
props.value = 1;
return <div>{props.value}</div>;
}

View File

@@ -1,69 +0,0 @@
## Input
```javascript
// @validateRefAccessDuringRender
/**
* Fault tolerance test: two independent errors should both be reported.
*
* Error 1 (BuildHIR): `try/finally` is not supported
* Error 2 (ValidateNoRefAccessInRender): reading ref.current during render
*/
function Component() {
const ref = useRef(null);
// Error: try/finally (Todo from BuildHIR)
try {
doSomething();
} finally {
cleanup();
}
// Error: reading ref during render
const value = ref.current;
return <div>{value}</div>;
}
```
## Error
```
Found 2 errors:
Todo: (BuildHIR::lowerStatement) Handle TryStatement without a catch clause
error.try-finally-and-ref-access.ts:12:2
10 |
11 | // Error: try/finally (Todo from BuildHIR)
> 12 | try {
| ^^^^^
> 13 | doSomething();
| ^^^^^^^^^^^^^^^^^^
> 14 | } finally {
| ^^^^^^^^^^^^^^^^^^
> 15 | cleanup();
| ^^^^^^^^^^^^^^^^^^
> 16 | }
| ^^^^ (BuildHIR::lowerStatement) Handle TryStatement without a catch clause
17 |
18 | // Error: reading ref during render
19 | const value = ref.current;
Error: Cannot access refs during render
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
error.try-finally-and-ref-access.ts:19:16
17 |
18 | // Error: reading ref during render
> 19 | const value = ref.current;
| ^^^^^^^^^^^ Cannot access ref value during render
20 |
21 | return <div>{value}</div>;
22 | }
```

View File

@@ -1,22 +0,0 @@
// @validateRefAccessDuringRender
/**
* Fault tolerance test: two independent errors should both be reported.
*
* Error 1 (BuildHIR): `try/finally` is not supported
* Error 2 (ValidateNoRefAccessInRender): reading ref.current during render
*/
function Component() {
const ref = useRef(null);
// Error: try/finally (Todo from BuildHIR)
try {
doSomething();
} finally {
cleanup();
}
// Error: reading ref during render
const value = ref.current;
return <div>{value}</div>;
}

View File

@@ -1,86 +0,0 @@
## Input
```javascript
// @validateRefAccessDuringRender
/**
* Fault tolerance test: three independent errors should all be reported.
*
* Error 1 (BuildHIR): `try/finally` is not supported
* Error 2 (ValidateNoRefAccessInRender): reading ref.current during render
* Error 3 (InferMutationAliasingEffects): Mutation of frozen props
*/
function Component(props) {
const ref = useRef(null);
// Error: try/finally (Todo from BuildHIR)
try {
doWork();
} finally {
cleanup();
}
// Error: reading ref during render
const value = ref.current;
// Error: mutating frozen props
props.items = [];
return <div>{value}</div>;
}
```
## Error
```
Found 3 errors:
Todo: (BuildHIR::lowerStatement) Handle TryStatement without a catch clause
error.try-finally-ref-access-and-mutation.ts:13:2
11 |
12 | // Error: try/finally (Todo from BuildHIR)
> 13 | try {
| ^^^^^
> 14 | doWork();
| ^^^^^^^^^^^^^
> 15 | } finally {
| ^^^^^^^^^^^^^
> 16 | cleanup();
| ^^^^^^^^^^^^^
> 17 | }
| ^^^^ (BuildHIR::lowerStatement) Handle TryStatement without a catch clause
18 |
19 | // Error: reading ref during render
20 | const value = ref.current;
Error: This value cannot be modified
Modifying component props or hook arguments is not allowed. Consider using a local variable instead.
error.try-finally-ref-access-and-mutation.ts:23:2
21 |
22 | // Error: mutating frozen props
> 23 | props.items = [];
| ^^^^^ value cannot be modified
24 |
25 | return <div>{value}</div>;
26 | }
Error: Cannot access refs during render
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
error.try-finally-ref-access-and-mutation.ts:20:16
18 |
19 | // Error: reading ref during render
> 20 | const value = ref.current;
| ^^^^^^^^^^^ Cannot access ref value during render
21 |
22 | // Error: mutating frozen props
23 | props.items = [];
```

View File

@@ -1,26 +0,0 @@
// @validateRefAccessDuringRender
/**
* Fault tolerance test: three independent errors should all be reported.
*
* Error 1 (BuildHIR): `try/finally` is not supported
* Error 2 (ValidateNoRefAccessInRender): reading ref.current during render
* Error 3 (InferMutationAliasingEffects): Mutation of frozen props
*/
function Component(props) {
const ref = useRef(null);
// Error: try/finally (Todo from BuildHIR)
try {
doWork();
} finally {
cleanup();
}
// Error: reading ref during render
const value = ref.current;
// Error: mutating frozen props
props.items = [];
return <div>{value}</div>;
}

View File

@@ -1,54 +0,0 @@
## Input
```javascript
/**
* Fault tolerance test: two independent errors should both be reported.
*
* Error 1 (BuildHIR): `var` declarations are not supported (treated as `let`)
* Error 2 (InferMutationAliasingEffects): Mutation of frozen props
*/
function Component(props) {
// Error: var declaration (Todo from BuildHIR)
var items = props.items;
// Error: mutating frozen props (detected during inference)
props.items = [];
return <div>{items.length}</div>;
}
```
## Error
```
Found 2 errors:
Todo: (BuildHIR::lowerStatement) Handle var kinds in VariableDeclaration
error.var-declaration-and-mutation-of-props.ts:9:2
7 | function Component(props) {
8 | // Error: var declaration (Todo from BuildHIR)
> 9 | var items = props.items;
| ^^^^^^^^^^^^^^^^^^^^^^^^ (BuildHIR::lowerStatement) Handle var kinds in VariableDeclaration
10 |
11 | // Error: mutating frozen props (detected during inference)
12 | props.items = [];
Error: This value cannot be modified
Modifying component props or hook arguments is not allowed. Consider using a local variable instead.
error.var-declaration-and-mutation-of-props.ts:12:2
10 |
11 | // Error: mutating frozen props (detected during inference)
> 12 | props.items = [];
| ^^^^^ value cannot be modified
13 |
14 | return <div>{items.length}</div>;
15 | }
```

View File

@@ -1,15 +0,0 @@
/**
* Fault tolerance test: two independent errors should both be reported.
*
* Error 1 (BuildHIR): `var` declarations are not supported (treated as `let`)
* Error 2 (InferMutationAliasingEffects): Mutation of frozen props
*/
function Component(props) {
// Error: var declaration (Todo from BuildHIR)
var items = props.items;
// Error: mutating frozen props (detected during inference)
props.items = [];
return <div>{items.length}</div>;
}

View File

@@ -1,62 +0,0 @@
## Input
```javascript
// @validateRefAccessDuringRender
/**
* Fault tolerance test: two independent errors should both be reported.
*
* Error 1 (BuildHIR): `var` declarations are not supported (treated as `let`)
* Error 2 (ValidateNoRefAccessInRender): reading ref.current during render
*/
function Component() {
const ref = useRef(null);
// Error: var declaration (Todo from BuildHIR)
var items = [1, 2, 3];
// Error: reading ref during render
const value = ref.current;
return (
<div>
{value}
{items.length}
</div>
);
}
```
## Error
```
Found 2 errors:
Todo: (BuildHIR::lowerStatement) Handle var kinds in VariableDeclaration
error.var-declaration-and-ref-access.ts:12:2
10 |
11 | // Error: var declaration (Todo from BuildHIR)
> 12 | var items = [1, 2, 3];
| ^^^^^^^^^^^^^^^^^^^^^^ (BuildHIR::lowerStatement) Handle var kinds in VariableDeclaration
13 |
14 | // Error: reading ref during render
15 | const value = ref.current;
Error: Cannot access refs during render
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
error.var-declaration-and-ref-access.ts:15:16
13 |
14 | // Error: reading ref during render
> 15 | const value = ref.current;
| ^^^^^^^^^^^ Cannot access ref value during render
16 |
17 | return (
18 | <div>
```

View File

@@ -1,23 +0,0 @@
// @validateRefAccessDuringRender
/**
* Fault tolerance test: two independent errors should both be reported.
*
* Error 1 (BuildHIR): `var` declarations are not supported (treated as `let`)
* Error 2 (ValidateNoRefAccessInRender): reading ref.current during render
*/
function Component() {
const ref = useRef(null);
// Error: var declaration (Todo from BuildHIR)
var items = [1, 2, 3];
// Error: reading ref during render
const value = ref.current;
return (
<div>
{value}
{items.length}
</div>
);
}

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