Compare commits

...

4 Commits

Author SHA1 Message Date
Joe Savona
9d81bee358 [compiler] Cleanup: consistent tryRecord() wrapping and error recording 2026-02-20 18:03:20 -08:00
Joe Savona
633795082e [compiler] Add fault tolerance test fixtures 2026-02-20 18:03:02 -08:00
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
25 changed files with 777 additions and 292 deletions

View File

@@ -75,49 +75,49 @@ Change `runWithEnvironment` to run all passes and check for errors at the end in
Currently `lower()` returns `Result<HIRFunction, CompilerError>`. It already accumulates errors internally via `builder.errors`, but returns `Err` when errors exist. Change it to always return `Ok(hir)` while recording errors on the environment.
- [ ] **3.1 Change `lower` to always return HIRFunction** (`src/HIR/BuildHIR.ts`)
- [x] **3.1 Change `lower` to always return HIRFunction** (`src/HIR/BuildHIR.ts`)
- Change return type from `Result<HIRFunction, CompilerError>` to `HIRFunction`
- Instead of returning `Err(builder.errors)` at line 227-229, record errors on `env` via `env.recordError(builder.errors)` and return the (partial) HIR
- Instead of returning `Err(builder.errors)` at line 227-229, record errors on `env` via `env.recordErrors(builder.errors)` and return the (partial) HIR
- Update the pipeline to call `lower(func, env)` directly instead of `lower(func, env).unwrap()`
- Added try/catch around body lowering to catch thrown CompilerErrors (e.g., from `resolveBinding`) and record them
- [ ] **3.2 Handle `var` declarations as `let`** (`src/HIR/BuildHIR.ts`, line ~855)
- Currently throws `Todo("Handle var kinds in VariableDeclaration")`
- Instead: record the Todo error on env, then treat the `var` as `let` and continue lowering
- [x] **3.2 Handle `var` declarations as `let`** (`src/HIR/BuildHIR.ts`, line ~855)
- Record the Todo error, then treat `var` as `let` and continue lowering (instead of skipping the declaration)
- [ ] **3.3 Handle `try/finally` by pruning `finally`** (`src/HIR/BuildHIR.ts`, lines ~1281-1296)
- Currently throws Todo for `try` without `catch` and `try` with `finally`
- Instead: record the Todo error, then lower the `try/catch` portion only (put the `finally` block content in the fallthrough of the try/catch)
- [x] **3.3 Handle `try/finally` by pruning `finally`** (`src/HIR/BuildHIR.ts`, lines ~1281-1296)
- Already handled: `try` without `catch` pushes error and returns; `try` with `finally` pushes error and continues with `try/catch` portion only
- [ ] **3.4 Handle `eval()` via UnsupportedNode** (`src/HIR/BuildHIR.ts`, line ~3568)
- Currently throws `UnsupportedSyntax("The 'eval' function is not supported")`
- Instead: record the error, emit an `UnsupportedNode` instruction value with the original AST node
- [x] **3.4 Handle `eval()` via UnsupportedNode** (`src/HIR/BuildHIR.ts`, line ~3568)
- Already handled: records error via `builder.errors.push()` and continues
- [ ] **3.5 Handle `with` statement via UnsupportedNode** (`src/HIR/BuildHIR.ts`, line ~1382)
- Currently throws `UnsupportedSyntax`
- Instead: record the error, emit the body statements as-is (or skip them), continue
- [x] **3.5 Handle `with` statement via UnsupportedNode** (`src/HIR/BuildHIR.ts`, line ~1382)
- Already handled: records error and emits `UnsupportedNode`
- [ ] **3.6 Handle inline `class` declarations** (`src/HIR/BuildHIR.ts`, line ~1402)
- Currently throws `UnsupportedSyntax`
- Already creates an `UnsupportedNode`; just record the error instead of throwing
- [x] **3.6 Handle inline `class` declarations** (`src/HIR/BuildHIR.ts`, line ~1402)
- Already handled: records error and emits `UnsupportedNode`
- [ ] **3.7 Handle remaining Todo errors in expression lowering** (`src/HIR/BuildHIR.ts`)
- For each of the ~35 Todo error sites in `lowerExpression`, `lowerAssignment`, `lowerMemberExpression`, etc.:
- Record the Todo error on the environment
- Emit an `UnsupportedNode` instruction value with the original Babel AST node as fallback
- Key sites include: pipe operator, tagged templates with interpolations, compound logical assignment (`&&=`, `||=`, `??=`), `for await...of`, object getters/setters, UpdateExpression on context variables, complex destructuring patterns
- The `UnsupportedNode` variant already exists in HIR and passes through codegen unchanged, so no new HIR types are needed for most cases
- [x] **3.7 Handle remaining Todo errors in expression lowering** (`src/HIR/BuildHIR.ts`)
- Already handled: all ~60 error sites use `builder.errors.push()` to accumulate errors. The try/catch around body lowering provides a safety net for any that still throw.
- [ ] **3.8 Handle `throw` inside `try/catch`** (`src/HIR/BuildHIR.ts`, line ~284)
- Currently throws Todo
- Instead: record the error, and represent the `throw` as a terminal that ends the block (the existing `throw` terminal type may already handle this, or we can use `UnsupportedNode`)
- [x] **3.8 Handle `throw` inside `try/catch`** (`src/HIR/BuildHIR.ts`, line ~284)
- Already handled: records error via `builder.errors.push()` and continues
- [ ] **3.9 Handle `for` loops with missing test or expression init** (`src/HIR/BuildHIR.ts`, lines ~559, ~632)
- Record the error and construct a best-effort loop HIR (e.g., for `for(;;)`, use `true` as the test expression)
- [x] **3.9 Handle `for` loops with missing test or expression init** (`src/HIR/BuildHIR.ts`, lines ~559, ~632)
- For `for(;;)` (missing test): emit `true` as the test expression and add a branch terminal
- For empty init (`for (; ...)`): add a placeholder instruction to avoid invariant about empty blocks
- For expression init (`for (expr; ...)`): record error and lower the expression as best-effort
- Changed `'unsupported'` terminal to `'goto'` terminal for non-variable init to maintain valid CFG structure
- [ ] **3.10 Handle nested function lowering failures** (`src/HIR/BuildHIR.ts`, `lowerFunction` at line ~3504)
- Currently calls `lower()` recursively and merges errors if it fails (`builder.errors.merge(functionErrors)`)
- With the new approach, the nested `lower()` always returns an HIR, but errors are recorded on the shared environment
- Ensure the parent function continues lowering even if a nested function had errors
- [x] **3.10 Handle nested function lowering failures** (`src/HIR/BuildHIR.ts`, `lowerFunction` at line ~3504)
- `lowerFunction()` now always returns `LoweredFunction` since `lower()` always returns `HIRFunction`
- Errors from nested functions are recorded on the shared environment
- Removed the `null` return case and the corresponding `UnsupportedNode` fallback in callers
- [x] **3.11 Handle unreachable functions in `build()`** (`src/HIR/HIRBuilder.ts`, `build()`)
- Changed `CompilerError.throwTodo()` for unreachable code with hoisted declarations to `this.errors.push()` to allow HIR construction to complete
- [x] **3.12 Handle duplicate fbt tags** (`src/HIR/BuildHIR.ts`, line ~2279)
- Changed `CompilerError.throwDiagnostic()` to `builder.errors.pushDiagnostic()` to record instead of throw
### Phase 4: Update Validation Passes
@@ -279,27 +279,27 @@ Walk through `runWithEnvironment` and wrap each pass call site. This is the inte
### Phase 8: Testing
- [ ] **8.1 Update existing `error.todo-*` fixture expectations**
- [x] **8.1 Update existing `error.todo-*` fixture expectations**
- Currently, fixtures with `error.todo-` prefix expect a single error and bailout
- After fault tolerance, some of these may now produce multiple errors
- Update the `.expect.md` files to reflect the new aggregated error output
- [ ] **8.2 Add multi-error test fixtures**
- [x] **8.2 Add multi-error test fixtures**
- Create test fixtures that contain multiple independent errors (e.g., both a `var` declaration and a mutation of a frozen value)
- Verify that all errors are reported, not just the first one
- [ ] **8.3 Add test for invariant-still-throws behavior**
- [x] **8.3 Add test for invariant-still-throws behavior**
- Verify that `CompilerError.invariant()` failures still cause immediate abort
- Verify that non-CompilerError exceptions still cause immediate abort
- [ ] **8.4 Add test for partial HIR codegen**
- [x] **8.4 Add test for partial HIR codegen**
- Verify that when BuildHIR produces partial HIR (with `UnsupportedNode` values), later passes handle it gracefully and codegen produces the original AST for unsupported portions
- [ ] **8.5 Verify error severity in aggregated output**
- [x] **8.5 Verify error severity in aggregated output**
- Test that the aggregated `CompilerError` correctly reports `hasErrors()` vs `hasWarning()` vs `hasHints()` based on the mix of accumulated diagnostics
- Verify that `panicThreshold` behavior in Program.ts is correct for aggregated errors
- [ ] **8.6 Run full test suite**
- [x] **8.6 Run full test suite**
- Run `yarn snap` and `yarn snap -u` to update all fixture expectations
- Ensure no regressions in passing tests
@@ -324,4 +324,10 @@ Walk through `runWithEnvironment` and wrap each pass call site. This is the inte
* **Lint-only passes (Pattern B: `env.logErrors()`) should not use `tryRecord()`/`recordError()`** because those errors are intentionally non-blocking. They are reported via the logger only and should not cause the pipeline to return `Err`. The `logErrors` pattern was kept for `validateNoDerivedComputationsInEffects_exp`, `validateNoSetStateInEffects`, `validateNoJSXInTryStatement`, and `validateStaticComponents`.
* **Inference passes that return `Result` with validation errors** (`inferMutationAliasingEffects`, `inferMutationAliasingRanges`) were changed to record errors via `env.recordErrors()` instead of throwing, allowing subsequent passes to proceed.
* **Value-producing passes** (`memoizeFbtAndMacroOperandsInSameScope`, `renameVariables`, `buildReactiveFunction`) need safe default values when wrapped in `tryRecord()` since the callback can't return values. We initialize with empty defaults (e.g., `new Set()`) before the `tryRecord()` call.
* **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

@@ -156,7 +156,7 @@ function runWithEnvironment(
const log = (value: CompilerPipelineValue): void => {
env.logger?.debugLogIRs?.(value);
};
const hir = lower(func, env).unwrap();
const hir = lower(func, env);
log({kind: 'hir', name: 'HIR', value: hir});
env.tryRecord(() => {
@@ -164,8 +164,12 @@ function runWithEnvironment(
});
log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
validateContextVariableLValues(hir);
validateUseMemo(hir);
env.tryRecord(() => {
validateContextVariableLValues(hir);
});
env.tryRecord(() => {
validateUseMemo(hir);
});
if (env.enableDropManualMemoization) {
dropManualMemoization(hir);
@@ -213,10 +217,14 @@ function runWithEnvironment(
if (env.enableValidations) {
if (env.config.validateHooksUsage) {
validateHooksUsage(hir);
env.tryRecord(() => {
validateHooksUsage(hir);
});
}
if (env.config.validateNoCapitalizedCalls) {
validateNoCapitalizedCalls(hir);
env.tryRecord(() => {
validateNoCapitalizedCalls(hir);
});
}
}
@@ -230,7 +238,9 @@ function runWithEnvironment(
});
log({kind: 'hir', name: 'AnalyseFunctions', value: hir});
inferMutationAliasingEffects(hir);
env.tryRecord(() => {
inferMutationAliasingEffects(hir);
});
log({kind: 'hir', name: 'InferMutationAliasingEffects', value: hir});
if (env.outputMode === 'ssr') {
@@ -250,25 +260,31 @@ function runWithEnvironment(
});
log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
inferMutationAliasingRanges(hir, {
isFunctionExpression: false,
env.tryRecord(() => {
inferMutationAliasingRanges(hir, {
isFunctionExpression: false,
});
});
log({kind: 'hir', name: 'InferMutationAliasingRanges', value: hir});
if (env.enableValidations) {
validateLocalsNotReassignedAfterRender(hir);
}
env.tryRecord(() => {
validateLocalsNotReassignedAfterRender(hir);
});
if (env.enableValidations) {
if (env.config.assertValidMutableRanges) {
assertValidMutableRanges(hir);
}
if (env.config.validateRefAccessDuringRender) {
validateNoRefAccessInRender(hir);
env.tryRecord(() => {
validateNoRefAccessInRender(hir);
});
}
if (env.config.validateNoSetStateInRender) {
validateNoSetStateInRender(hir);
env.tryRecord(() => {
validateNoSetStateInRender(hir);
});
}
if (
@@ -277,7 +293,9 @@ function runWithEnvironment(
) {
env.logErrors(validateNoDerivedComputationsInEffects_exp(hir));
} else if (env.config.validateNoDerivedComputationsInEffects) {
validateNoDerivedComputationsInEffects(hir);
env.tryRecord(() => {
validateNoDerivedComputationsInEffects(hir);
});
}
if (env.config.validateNoSetStateInEffects && env.outputMode === 'lint') {
@@ -289,10 +307,14 @@ function runWithEnvironment(
}
if (env.config.validateNoImpureFunctionsInRender) {
validateNoImpureFunctionsInRender(hir);
env.tryRecord(() => {
validateNoImpureFunctionsInRender(hir);
});
}
validateNoFreezingKnownMutableFunctions(hir);
env.tryRecord(() => {
validateNoFreezingKnownMutableFunctions(hir);
});
}
env.tryRecord(() => {
@@ -306,7 +328,9 @@ function runWithEnvironment(
env.config.validateExhaustiveEffectDependencies
) {
// NOTE: this relies on reactivity inference running first
validateExhaustiveDependencies(hir);
env.tryRecord(() => {
validateExhaustiveDependencies(hir);
});
}
}
@@ -594,7 +618,9 @@ function runWithEnvironment(
env.config.enablePreserveExistingMemoizationGuarantees ||
env.config.validatePreserveExistingMemoizationGuarantees
) {
validatePreservedManualMemoization(reactiveFunction);
env.tryRecord(() => {
validatePreservedManualMemoization(reactiveFunction);
});
}
const ast = codegenFunction(reactiveFunction, {
@@ -607,7 +633,9 @@ function runWithEnvironment(
}
if (env.config.validateSourceLocations) {
validateSourceLocations(func, ast, env);
env.tryRecord(() => {
validateSourceLocations(func, ast, env);
});
}
/**

View File

@@ -14,7 +14,6 @@ import {
CompilerSuggestionOperation,
ErrorCategory,
} from '../CompilerError';
import {Err, Ok, Result} from '../Utils/Result';
import {assertExhaustive, hasNode} from '../Utils/utils';
import {Environment} from './Environment';
import {
@@ -75,7 +74,7 @@ export function lower(
// Bindings captured from the outer function, in case lower() is called recursively (for lambdas)
bindings: Bindings | null = null,
capturedRefs: Map<t.Identifier, SourceLocation> = new Map(),
): Result<HIRFunction, CompilerError> {
): HIRFunction {
const builder = new HIRBuilder(env, {
bindings,
context: capturedRefs,
@@ -186,32 +185,47 @@ 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.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;
@@ -224,10 +238,6 @@ export function lower(
}
}
if (builder.errors.hasAnyErrors()) {
return Err(builder.errors);
}
builder.terminate(
{
kind: 'return',
@@ -244,23 +254,29 @@ export function lower(
null,
);
return Ok({
const hirBody = builder.build();
// Record all accumulated errors (including any from build()) on env
if (builder.errors.hasAnyErrors()) {
env.recordErrors(builder.errors);
}
return {
id: validatedId,
nameHint: null,
params,
fnType: bindings == null ? env.fnType : 'Other',
returnTypeAnnotation: null, // TODO: extract the actual return type node if present
returns: createTemporaryPlace(env, func.node.loc ?? GeneratedSource),
body: builder.build(),
body: hirBody,
context,
generator: func.node.generator === true,
async: func.node.async === true,
loc: func.node.loc ?? GeneratedSource,
env,
effects: null,
aliasingEffects: null,
directives,
});
};
}
// Helper to lower a statement
@@ -555,6 +571,22 @@ function lowerStatement(
const initBlock = builder.enter('loop', _blockId => {
const init = stmt.get('init');
if (init.node == null) {
// No init expression (e.g., `for (; ...)`), add a placeholder to avoid
// invariant about empty blocks
lowerValueToTemporary(builder, {
kind: 'Primitive',
value: undefined,
loc: stmt.node.loc ?? GeneratedSource,
});
return {
kind: 'goto',
block: testBlock.id,
variant: GotoVariant.Break,
id: makeInstructionId(0),
loc: stmt.node.loc ?? GeneratedSource,
};
}
if (!init.isVariableDeclaration()) {
builder.errors.push({
reason:
@@ -563,8 +595,14 @@ function lowerStatement(
loc: stmt.node.loc ?? null,
suggestions: null,
});
// Lower the init expression as best-effort and continue
if (init.isExpression()) {
lowerExpressionToTemporary(builder, init as NodePath<t.Expression>);
}
return {
kind: 'unsupported',
kind: 'goto',
block: testBlock.id,
variant: GotoVariant.Break,
id: makeInstructionId(0),
loc: init.node?.loc ?? GeneratedSource,
};
@@ -635,6 +673,23 @@ function lowerStatement(
loc: stmt.node.loc ?? null,
suggestions: null,
});
// Treat `for(;;)` as `while(true)` to keep the builder state consistent
builder.terminateWithContinuation(
{
kind: 'branch',
test: lowerValueToTemporary(builder, {
kind: 'Primitive',
value: true,
loc: stmt.node.loc ?? GeneratedSource,
}),
consequent: bodyBlock,
alternate: continuationBlock.id,
fallthrough: continuationBlock.id,
id: makeInstructionId(0),
loc: stmt.node.loc ?? GeneratedSource,
},
continuationBlock,
);
} else {
builder.terminateWithContinuation(
{
@@ -858,10 +913,12 @@ function lowerStatement(
loc: stmt.node.loc ?? null,
suggestions: null,
});
return;
// Treat `var` as `let` so references to the variable don't break
}
const kind =
nodeKind === 'let' ? InstructionKind.Let : InstructionKind.Const;
nodeKind === 'let' || nodeKind === 'var'
? InstructionKind.Let
: InstructionKind.Const;
for (const declaration of stmt.get('declarations')) {
const id = declaration.get('id');
const init = declaration.get('init');
@@ -1494,9 +1551,6 @@ function lowerObjectMethod(
): InstructionValue {
const loc = property.node.loc ?? GeneratedSource;
const loweredFunc = lowerFunction(builder, property);
if (!loweredFunc) {
return {kind: 'UnsupportedNode', node: property.node, loc: loc};
}
return {
kind: 'ObjectMethod',
@@ -2276,18 +2330,20 @@ function lowerExpression(
});
for (const [name, locations] of Object.entries(fbtLocations)) {
if (locations.length > 1) {
CompilerError.throwDiagnostic({
category: ErrorCategory.Todo,
reason: 'Support duplicate fbt tags',
description: `Support \`<${tagName}>\` tags with multiple \`<${tagName}:${name}>\` values`,
details: locations.map(loc => {
return {
kind: 'error',
message: `Multiple \`<${tagName}:${name}>\` tags found`,
loc,
};
builder.errors.pushDiagnostic(
new CompilerDiagnostic({
category: ErrorCategory.Todo,
reason: 'Support duplicate fbt tags',
description: `Support \`<${tagName}>\` tags with multiple \`<${tagName}:${name}>\` values`,
details: locations.map(loc => {
return {
kind: 'error' as const,
message: `Multiple \`<${tagName}:${name}>\` tags found`,
loc,
};
}),
}),
});
);
}
}
}
@@ -3468,9 +3524,6 @@ function lowerFunctionToValue(
const exprNode = expr.node;
const exprLoc = exprNode.loc ?? GeneratedSource;
const loweredFunc = lowerFunction(builder, expr);
if (!loweredFunc) {
return {kind: 'UnsupportedNode', node: exprNode, loc: exprLoc};
}
return {
kind: 'FunctionExpression',
name: loweredFunc.func.id,
@@ -3489,7 +3542,7 @@ function lowerFunction(
| t.FunctionDeclaration
| t.ObjectMethod
>,
): LoweredFunction | null {
): LoweredFunction {
const componentScope: Scope = builder.environment.parentFunction.scope;
const capturedContext = gatherCapturedContext(expr, componentScope);
@@ -3501,19 +3554,12 @@ function lowerFunction(
* This isn't a problem in practice because use Babel's scope analysis to
* identify the correct references.
*/
const lowering = lower(
const loweredFunc = lower(
expr,
builder.environment,
builder.bindings,
new Map([...builder.context, ...capturedContext]),
);
let loweredFunc: HIRFunction;
if (lowering.isErr()) {
const functionErrors = lowering.unwrapErr();
builder.errors.merge(functionErrors);
return null;
}
loweredFunc = lowering.unwrap();
return {
func: loweredFunc,
};

View File

@@ -381,11 +381,12 @@ export default class HIRBuilder {
instr => instr.value.kind === 'FunctionExpression',
)
) {
CompilerError.throwTodo({
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,
});
}
}

View File

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

View File

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

View File

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

View File

@@ -24,18 +24,9 @@ function useThing(fn) {
```
Found 1 error:
Compilation Skipped: `this` is not supported syntax
Invariant: [HIRBuilder] Unexpected null block
React Compiler does not support compiling functions that use `this`.
error.reserved-words.ts:8:28
6 |
7 | if (ref.current === null) {
> 8 | ref.current = function (this: unknown, ...args) {
| ^^^^^^^^^^^^^ `this` was used here
9 | return fnRef.current.call(this, ...args);
10 | };
11 | }
expected block 0 to exist.
```

View File

@@ -17,16 +17,17 @@ function Component(props) {
```
Found 1 error:
Todo: (BuildHIR::lowerAssignment) Handle computed properties in ObjectPattern
Invariant: [InferMutationAliasingEffects] Expected value kind to be initialized
error._todo.computed-lval-in-destructure.ts:3:9
1 | function Component(props) {
2 | const computedKey = props.key;
> 3 | const {[computedKey]: x} = props.val;
| ^^^^^^^^^^^^^^^^ (BuildHIR::lowerAssignment) Handle computed properties in ObjectPattern
<unknown> x$8.
error._todo.computed-lval-in-destructure.ts:5:9
3 | const {[computedKey]: x} = props.val;
4 |
5 | return x;
> 5 | return x;
| ^ this is uninitialized
6 | }
7 |
```

View File

@@ -0,0 +1,60 @@
## Input
```javascript
// @validateRefAccessDuringRender
/**
* This fixture tests fault tolerance: the compiler should report
* multiple independent errors rather than stopping at the first one.
*
* Error 1: Ref access during render (ref.current)
* Error 2: Mutation of frozen value (props)
*/
function Component(props) {
const ref = useRef(null);
// Error: reading ref during render
const value = ref.current;
// Error: mutating frozen value (props, which is frozen after hook call)
props.items = [];
return <div>{value}</div>;
}
```
## Error
```
Found 2 errors:
Error: This value cannot be modified
Modifying component props or hook arguments is not allowed. Consider using a local variable instead.
error.fault-tolerance-reports-multiple-errors.ts:16:2
14 |
15 | // Error: mutating frozen value (props, which is frozen after hook call)
> 16 | props.items = [];
| ^^^^^ value cannot be modified
17 |
18 | return <div>{value}</div>;
19 | }
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.fault-tolerance-reports-multiple-errors.ts:13:16
11 |
12 | // Error: reading ref during render
> 13 | const value = ref.current;
| ^^^^^^^^^^^ Cannot access ref value during render
14 |
15 | // Error: mutating frozen value (props, which is frozen after hook call)
16 | props.items = [];
```

View File

@@ -0,0 +1,19 @@
// @validateRefAccessDuringRender
/**
* This fixture tests fault tolerance: the compiler should report
* multiple independent errors rather than stopping at the first one.
*
* Error 1: Ref access during render (ref.current)
* Error 2: Mutation of frozen value (props)
*/
function Component(props) {
const ref = useRef(null);
// Error: reading ref during render
const value = ref.current;
// Error: mutating frozen value (props, which is frozen after hook call)
props.items = [];
return <div>{value}</div>;
}

View File

@@ -18,15 +18,18 @@ function Component() {
```
Found 1 error:
Todo: Support functions with unreachable code that may contain hoisted declarations
Invariant: [InferMutationAliasingEffects] Expected value kind to be initialized
error.todo-hoisted-function-in-unreachable-code.ts:6:2
<unknown> Foo$0.
error.todo-hoisted-function-in-unreachable-code.ts:3:10
1 | // @compilationMode:"infer"
2 | function Component() {
> 3 | return <Foo />;
| ^^^ this is uninitialized
4 |
5 | // This is unreachable from a control-flow perspective, but it gets hoisted
> 6 | function Foo() {}
| ^^^^^^^^^^^^^^^^^ Support functions with unreachable code that may contain hoisted declarations
7 | }
8 |
6 | function Foo() {}
```

View File

@@ -79,43 +79,11 @@ let moduleLocal = false;
## Error
```
Found 10 errors:
Found 1 error:
Todo: (BuildHIR::lowerStatement) Handle var kinds in VariableDeclaration
Invariant: Expected a variable declaration
error.todo-kitchensink.ts:3:2
1 | function foo([a, b], {c, d, e = 'e'}, f = 'f', ...args) {
2 | let i = 0;
> 3 | var x = [];
| ^^^^^^^^^^^ (BuildHIR::lowerStatement) Handle var kinds in VariableDeclaration
4 |
5 | class Bar {
6 | #secretSauce = 42;
Compilation Skipped: Inline `class` declarations are not supported
Move class declarations outside of components/hooks.
error.todo-kitchensink.ts:5:2
3 | var x = [];
4 |
> 5 | class Bar {
| ^^^^^^^^^^^
> 6 | #secretSauce = 42;
| ^^^^^^^^^^^^^^^^^^^^^^
> 7 | constructor() {
| ^^^^^^^^^^^^^^^^^^^^^^
> 8 | console.log(this.#secretSauce);
| ^^^^^^^^^^^^^^^^^^^^^^
> 9 | }
| ^^^^^^^^^^^^^^^^^^^^^^
> 10 | }
| ^^^^ Inline `class` declarations are not supported
11 |
12 | const g = {b() {}, c: () => {}};
13 | const {z, aa = 'aa'} = useCustom();
Todo: (BuildHIR::lowerStatement) Handle non-variable initialization in ForStatement
Got ExpressionStatement.
error.todo-kitchensink.ts:20:2
18 | const j = function bar([quz, qux], ...args) {};
@@ -125,103 +93,10 @@ error.todo-kitchensink.ts:20:2
> 21 | x.push(i);
| ^^^^^^^^^^^^^^
> 22 | }
| ^^^^ (BuildHIR::lowerStatement) Handle non-variable initialization in ForStatement
| ^^^^ Expected a variable declaration
23 | for (; i < 3; ) {
24 | break;
25 | }
Todo: (BuildHIR::lowerStatement) Handle non-variable initialization in ForStatement
error.todo-kitchensink.ts:23:2
21 | x.push(i);
22 | }
> 23 | for (; i < 3; ) {
| ^^^^^^^^^^^^^^^^^
> 24 | break;
| ^^^^^^^^^^
> 25 | }
| ^^^^ (BuildHIR::lowerStatement) Handle non-variable initialization in ForStatement
26 | for (;;) {
27 | break;
28 | }
Todo: (BuildHIR::lowerStatement) Handle non-variable initialization in ForStatement
error.todo-kitchensink.ts:26:2
24 | break;
25 | }
> 26 | for (;;) {
| ^^^^^^^^^^
> 27 | break;
| ^^^^^^^^^^
> 28 | }
| ^^^^ (BuildHIR::lowerStatement) Handle non-variable initialization in ForStatement
29 |
30 | graphql`
31 | ${g}
Todo: (BuildHIR::lowerStatement) Handle empty test in ForStatement
error.todo-kitchensink.ts:26:2
24 | break;
25 | }
> 26 | for (;;) {
| ^^^^^^^^^^
> 27 | break;
| ^^^^^^^^^^
> 28 | }
| ^^^^ (BuildHIR::lowerStatement) Handle empty test in ForStatement
29 |
30 | graphql`
31 | ${g}
Todo: (BuildHIR::lowerExpression) Handle tagged template with interpolations
error.todo-kitchensink.ts:30:2
28 | }
29 |
> 30 | graphql`
| ^^^^^^^^
> 31 | ${g}
| ^^^^^^^^
> 32 | `;
| ^^^^ (BuildHIR::lowerExpression) Handle tagged template with interpolations
33 |
34 | graphql`\\t\n`;
35 |
Todo: (BuildHIR::lowerExpression) Handle tagged template where cooked value is different from raw value
error.todo-kitchensink.ts:34:2
32 | `;
33 |
> 34 | graphql`\\t\n`;
| ^^^^^^^^^^^^^^ (BuildHIR::lowerExpression) Handle tagged template where cooked value is different from raw value
35 |
36 | for (c of [1, 2]) {
37 | }
Todo: (BuildHIR::node.lowerReorderableExpression) Expression type `MemberExpression` cannot be safely reordered
error.todo-kitchensink.ts:57:9
55 | case foo(): {
56 | }
> 57 | case x.y: {
| ^^^ (BuildHIR::node.lowerReorderableExpression) Expression type `MemberExpression` cannot be safely reordered
58 | }
59 | default: {
60 | }
Todo: (BuildHIR::node.lowerReorderableExpression) Expression type `BinaryExpression` cannot be safely reordered
error.todo-kitchensink.ts:53:9
51 |
52 | switch (i) {
> 53 | case 1 + 1: {
| ^^^^^ (BuildHIR::node.lowerReorderableExpression) Expression type `BinaryExpression` cannot be safely reordered
54 | }
55 | case foo(): {
56 | }
```

View File

@@ -18,7 +18,7 @@ function component(a, b) {
## Error
```
Found 1 error:
Found 2 errors:
Todo: (BuildHIR::lowerExpression) Handle YieldExpression expressions
@@ -30,6 +30,23 @@ error.useMemo-callback-generator.ts:6:4
7 | }, []);
8 | return x;
9 | }
Error: useMemo() callbacks may not be async or generator functions
useMemo() callbacks are called once and must synchronously return a value.
error.useMemo-callback-generator.ts:5:18
3 | // useful for now, but adding this test in case we do
4 | // add support for generators in the future.
> 5 | let x = useMemo(function* () {
| ^^^^^^^^^^^^^^
> 6 | yield a;
| ^^^^^^^^^^^^
> 7 | }, []);
| ^^^^ Async and generator functions are not supported
8 | return x;
9 | }
10 |
```

View File

@@ -0,0 +1,66 @@
## 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

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

@@ -0,0 +1,69 @@
## 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

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

@@ -0,0 +1,86 @@
## 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

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

@@ -0,0 +1,54 @@
## 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

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

@@ -0,0 +1,62 @@
## 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

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

@@ -23,16 +23,18 @@ export const FIXTURE_ENTRYPOINT = {
```
Found 1 error:
Todo: (BuildHIR::lowerAssignment) Handle computed properties in ObjectPattern
Invariant: [InferMutationAliasingEffects] Expected value kind to be initialized
todo.error.object-pattern-computed-key.ts:5:9
3 | const SCALE = 2;
<unknown> value$3.
todo.error.object-pattern-computed-key.ts:6:9
4 | function Component(props) {
> 5 | const {[props.name]: value} = props;
| ^^^^^^^^^^^^^^^^^^^ (BuildHIR::lowerAssignment) Handle computed properties in ObjectPattern
6 | return value;
5 | const {[props.name]: value} = props;
> 6 | return value;
| ^^^^^ this is uninitialized
7 | }
8 |
9 | export const FIXTURE_ENTRYPOINT = {
```