Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
deca1ebd8b | ||
|
|
3dc96d6a49 |
@@ -328,4 +328,6 @@ 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.
|
||||
|
||||
|
||||
@@ -254,6 +254,7 @@ export type LoggerEvent =
|
||||
| CompileErrorEvent
|
||||
| CompileDiagnosticEvent
|
||||
| CompileSkipEvent
|
||||
| CompileUnexpectedThrowEvent
|
||||
| PipelineErrorEvent
|
||||
| TimingEvent;
|
||||
|
||||
@@ -288,6 +289,11 @@ 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;
|
||||
|
||||
@@ -13,7 +13,6 @@ import {CompilerError} from '../CompilerError';
|
||||
import {Err, Ok, Result} from '../Utils/Result';
|
||||
import {
|
||||
HIRFunction,
|
||||
IdentifierId,
|
||||
ReactiveFunction,
|
||||
assertConsistentIdentifiers,
|
||||
assertTerminalPredsExist,
|
||||
@@ -159,9 +158,7 @@ function runWithEnvironment(
|
||||
const hir = lower(func, env);
|
||||
log({kind: 'hir', name: 'HIR', value: hir});
|
||||
|
||||
env.tryRecord(() => {
|
||||
pruneMaybeThrows(hir);
|
||||
});
|
||||
pruneMaybeThrows(hir);
|
||||
log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
|
||||
|
||||
validateContextVariableLValues(hir);
|
||||
@@ -172,43 +169,31 @@ function runWithEnvironment(
|
||||
log({kind: 'hir', name: 'DropManualMemoization', value: hir});
|
||||
}
|
||||
|
||||
env.tryRecord(() => {
|
||||
inlineImmediatelyInvokedFunctionExpressions(hir);
|
||||
});
|
||||
inlineImmediatelyInvokedFunctionExpressions(hir);
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'InlineImmediatelyInvokedFunctionExpressions',
|
||||
value: hir,
|
||||
});
|
||||
|
||||
env.tryRecord(() => {
|
||||
mergeConsecutiveBlocks(hir);
|
||||
});
|
||||
mergeConsecutiveBlocks(hir);
|
||||
log({kind: 'hir', name: 'MergeConsecutiveBlocks', value: hir});
|
||||
|
||||
assertConsistentIdentifiers(hir);
|
||||
assertTerminalSuccessorsExist(hir);
|
||||
|
||||
env.tryRecord(() => {
|
||||
enterSSA(hir);
|
||||
});
|
||||
enterSSA(hir);
|
||||
log({kind: 'hir', name: 'SSA', value: hir});
|
||||
|
||||
env.tryRecord(() => {
|
||||
eliminateRedundantPhi(hir);
|
||||
});
|
||||
eliminateRedundantPhi(hir);
|
||||
log({kind: 'hir', name: 'EliminateRedundantPhi', value: hir});
|
||||
|
||||
assertConsistentIdentifiers(hir);
|
||||
|
||||
env.tryRecord(() => {
|
||||
constantPropagation(hir);
|
||||
});
|
||||
constantPropagation(hir);
|
||||
log({kind: 'hir', name: 'ConstantPropagation', value: hir});
|
||||
|
||||
env.tryRecord(() => {
|
||||
inferTypes(hir);
|
||||
});
|
||||
inferTypes(hir);
|
||||
log({kind: 'hir', name: 'InferTypes', value: hir});
|
||||
|
||||
if (env.enableValidations) {
|
||||
@@ -220,34 +205,24 @@ function runWithEnvironment(
|
||||
}
|
||||
}
|
||||
|
||||
env.tryRecord(() => {
|
||||
optimizePropsMethodCalls(hir);
|
||||
});
|
||||
optimizePropsMethodCalls(hir);
|
||||
log({kind: 'hir', name: 'OptimizePropsMethodCalls', value: hir});
|
||||
|
||||
env.tryRecord(() => {
|
||||
analyseFunctions(hir);
|
||||
});
|
||||
analyseFunctions(hir);
|
||||
log({kind: 'hir', name: 'AnalyseFunctions', value: hir});
|
||||
|
||||
inferMutationAliasingEffects(hir);
|
||||
log({kind: 'hir', name: 'InferMutationAliasingEffects', value: hir});
|
||||
|
||||
if (env.outputMode === 'ssr') {
|
||||
env.tryRecord(() => {
|
||||
optimizeForSSR(hir);
|
||||
});
|
||||
optimizeForSSR(hir);
|
||||
log({kind: 'hir', name: 'OptimizeForSSR', value: hir});
|
||||
}
|
||||
|
||||
// Note: Has to come after infer reference effects because "dead" code may still affect inference
|
||||
env.tryRecord(() => {
|
||||
deadCodeElimination(hir);
|
||||
});
|
||||
deadCodeElimination(hir);
|
||||
log({kind: 'hir', name: 'DeadCodeElimination', value: hir});
|
||||
env.tryRecord(() => {
|
||||
pruneMaybeThrows(hir);
|
||||
});
|
||||
pruneMaybeThrows(hir);
|
||||
log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
|
||||
|
||||
inferMutationAliasingRanges(hir, {
|
||||
@@ -256,9 +231,7 @@ function runWithEnvironment(
|
||||
log({kind: 'hir', name: 'InferMutationAliasingRanges', value: hir});
|
||||
if (env.enableValidations) {
|
||||
validateLocalsNotReassignedAfterRender(hir);
|
||||
}
|
||||
|
||||
if (env.enableValidations) {
|
||||
if (env.config.assertValidMutableRanges) {
|
||||
assertValidMutableRanges(hir);
|
||||
}
|
||||
@@ -295,9 +268,7 @@ function runWithEnvironment(
|
||||
validateNoFreezingKnownMutableFunctions(hir);
|
||||
}
|
||||
|
||||
env.tryRecord(() => {
|
||||
inferReactivePlaces(hir);
|
||||
});
|
||||
inferReactivePlaces(hir);
|
||||
log({kind: 'hir', name: 'InferReactivePlaces', value: hir});
|
||||
|
||||
if (env.enableValidations) {
|
||||
@@ -310,9 +281,7 @@ function runWithEnvironment(
|
||||
}
|
||||
}
|
||||
|
||||
env.tryRecord(() => {
|
||||
rewriteInstructionKindsBasedOnReassignment(hir);
|
||||
});
|
||||
rewriteInstructionKindsBasedOnReassignment(hir);
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'RewriteInstructionKindsBasedOnReassignment',
|
||||
@@ -333,16 +302,11 @@ function runWithEnvironment(
|
||||
* if inferred memoization is enabled. This makes all later passes which
|
||||
* transform reactive-scope labeled instructions no-ops.
|
||||
*/
|
||||
env.tryRecord(() => {
|
||||
inferReactiveScopeVariables(hir);
|
||||
});
|
||||
inferReactiveScopeVariables(hir);
|
||||
log({kind: 'hir', name: 'InferReactiveScopeVariables', value: hir});
|
||||
}
|
||||
|
||||
let fbtOperands: Set<IdentifierId> = new Set();
|
||||
env.tryRecord(() => {
|
||||
fbtOperands = memoizeFbtAndMacroOperandsInSameScope(hir);
|
||||
});
|
||||
const fbtOperands = memoizeFbtAndMacroOperandsInSameScope(hir);
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'MemoizeFbtAndMacroOperandsInSameScope',
|
||||
@@ -350,15 +314,11 @@ function runWithEnvironment(
|
||||
});
|
||||
|
||||
if (env.config.enableJsxOutlining) {
|
||||
env.tryRecord(() => {
|
||||
outlineJSX(hir);
|
||||
});
|
||||
outlineJSX(hir);
|
||||
}
|
||||
|
||||
if (env.config.enableNameAnonymousFunctions) {
|
||||
env.tryRecord(() => {
|
||||
nameAnonymousFunctions(hir);
|
||||
});
|
||||
nameAnonymousFunctions(hir);
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'NameAnonymousFunctions',
|
||||
@@ -367,51 +327,39 @@ function runWithEnvironment(
|
||||
}
|
||||
|
||||
if (env.config.enableFunctionOutlining) {
|
||||
env.tryRecord(() => {
|
||||
outlineFunctions(hir, fbtOperands);
|
||||
});
|
||||
outlineFunctions(hir, fbtOperands);
|
||||
log({kind: 'hir', name: 'OutlineFunctions', value: hir});
|
||||
}
|
||||
|
||||
env.tryRecord(() => {
|
||||
alignMethodCallScopes(hir);
|
||||
});
|
||||
alignMethodCallScopes(hir);
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'AlignMethodCallScopes',
|
||||
value: hir,
|
||||
});
|
||||
|
||||
env.tryRecord(() => {
|
||||
alignObjectMethodScopes(hir);
|
||||
});
|
||||
alignObjectMethodScopes(hir);
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'AlignObjectMethodScopes',
|
||||
value: hir,
|
||||
});
|
||||
|
||||
env.tryRecord(() => {
|
||||
pruneUnusedLabelsHIR(hir);
|
||||
});
|
||||
pruneUnusedLabelsHIR(hir);
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'PruneUnusedLabelsHIR',
|
||||
value: hir,
|
||||
});
|
||||
|
||||
env.tryRecord(() => {
|
||||
alignReactiveScopesToBlockScopesHIR(hir);
|
||||
});
|
||||
alignReactiveScopesToBlockScopesHIR(hir);
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'AlignReactiveScopesToBlockScopesHIR',
|
||||
value: hir,
|
||||
});
|
||||
|
||||
env.tryRecord(() => {
|
||||
mergeOverlappingReactiveScopesHIR(hir);
|
||||
});
|
||||
mergeOverlappingReactiveScopesHIR(hir);
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'MergeOverlappingReactiveScopesHIR',
|
||||
@@ -419,9 +367,7 @@ function runWithEnvironment(
|
||||
});
|
||||
assertValidBlockNesting(hir);
|
||||
|
||||
env.tryRecord(() => {
|
||||
buildReactiveScopeTerminalsHIR(hir);
|
||||
});
|
||||
buildReactiveScopeTerminalsHIR(hir);
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'BuildReactiveScopeTerminalsHIR',
|
||||
@@ -430,18 +376,14 @@ function runWithEnvironment(
|
||||
|
||||
assertValidBlockNesting(hir);
|
||||
|
||||
env.tryRecord(() => {
|
||||
flattenReactiveLoopsHIR(hir);
|
||||
});
|
||||
flattenReactiveLoopsHIR(hir);
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'FlattenReactiveLoopsHIR',
|
||||
value: hir,
|
||||
});
|
||||
|
||||
env.tryRecord(() => {
|
||||
flattenScopesWithHooksOrUseHIR(hir);
|
||||
});
|
||||
flattenScopesWithHooksOrUseHIR(hir);
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'FlattenScopesWithHooksOrUseHIR',
|
||||
@@ -449,19 +391,15 @@ function runWithEnvironment(
|
||||
});
|
||||
assertTerminalSuccessorsExist(hir);
|
||||
assertTerminalPredsExist(hir);
|
||||
env.tryRecord(() => {
|
||||
propagateScopeDependenciesHIR(hir);
|
||||
});
|
||||
|
||||
propagateScopeDependenciesHIR(hir);
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'PropagateScopeDependenciesHIR',
|
||||
value: hir,
|
||||
});
|
||||
|
||||
let reactiveFunction!: ReactiveFunction;
|
||||
env.tryRecord(() => {
|
||||
reactiveFunction = buildReactiveFunction(hir);
|
||||
});
|
||||
const reactiveFunction = buildReactiveFunction(hir);
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'BuildReactiveFunction',
|
||||
@@ -470,9 +408,7 @@ function runWithEnvironment(
|
||||
|
||||
assertWellFormedBreakTargets(reactiveFunction);
|
||||
|
||||
env.tryRecord(() => {
|
||||
pruneUnusedLabels(reactiveFunction);
|
||||
});
|
||||
pruneUnusedLabels(reactiveFunction);
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'PruneUnusedLabels',
|
||||
@@ -480,109 +416,84 @@ function runWithEnvironment(
|
||||
});
|
||||
assertScopeInstructionsWithinScopes(reactiveFunction);
|
||||
|
||||
env.tryRecord(() => {
|
||||
pruneNonEscapingScopes(reactiveFunction);
|
||||
});
|
||||
pruneNonEscapingScopes(reactiveFunction);
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'PruneNonEscapingScopes',
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
env.tryRecord(() => {
|
||||
pruneNonReactiveDependencies(reactiveFunction);
|
||||
});
|
||||
pruneNonReactiveDependencies(reactiveFunction);
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'PruneNonReactiveDependencies',
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
env.tryRecord(() => {
|
||||
pruneUnusedScopes(reactiveFunction);
|
||||
});
|
||||
pruneUnusedScopes(reactiveFunction);
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'PruneUnusedScopes',
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
env.tryRecord(() => {
|
||||
mergeReactiveScopesThatInvalidateTogether(reactiveFunction);
|
||||
});
|
||||
mergeReactiveScopesThatInvalidateTogether(reactiveFunction);
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'MergeReactiveScopesThatInvalidateTogether',
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
env.tryRecord(() => {
|
||||
pruneAlwaysInvalidatingScopes(reactiveFunction);
|
||||
});
|
||||
pruneAlwaysInvalidatingScopes(reactiveFunction);
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'PruneAlwaysInvalidatingScopes',
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
env.tryRecord(() => {
|
||||
propagateEarlyReturns(reactiveFunction);
|
||||
});
|
||||
propagateEarlyReturns(reactiveFunction);
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'PropagateEarlyReturns',
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
env.tryRecord(() => {
|
||||
pruneUnusedLValues(reactiveFunction);
|
||||
});
|
||||
pruneUnusedLValues(reactiveFunction);
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'PruneUnusedLValues',
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
env.tryRecord(() => {
|
||||
promoteUsedTemporaries(reactiveFunction);
|
||||
});
|
||||
promoteUsedTemporaries(reactiveFunction);
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'PromoteUsedTemporaries',
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
env.tryRecord(() => {
|
||||
extractScopeDeclarationsFromDestructuring(reactiveFunction);
|
||||
});
|
||||
extractScopeDeclarationsFromDestructuring(reactiveFunction);
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'ExtractScopeDeclarationsFromDestructuring',
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
env.tryRecord(() => {
|
||||
stabilizeBlockIds(reactiveFunction);
|
||||
});
|
||||
stabilizeBlockIds(reactiveFunction);
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'StabilizeBlockIds',
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
let uniqueIdentifiers: Set<string> = new Set();
|
||||
env.tryRecord(() => {
|
||||
uniqueIdentifiers = renameVariables(reactiveFunction);
|
||||
});
|
||||
const uniqueIdentifiers = renameVariables(reactiveFunction);
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'RenameVariables',
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
env.tryRecord(() => {
|
||||
pruneHoistedContexts(reactiveFunction);
|
||||
});
|
||||
pruneHoistedContexts(reactiveFunction);
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'PruneHoistedContexts',
|
||||
|
||||
@@ -720,6 +720,20 @@ 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};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,51 +185,32 @@ export function lower(
|
||||
|
||||
let directives: Array<string> = [];
|
||||
const body = func.get('body');
|
||||
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;
|
||||
}
|
||||
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',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
let validatedId: HIRFunction['id'] = null;
|
||||
|
||||
@@ -310,16 +310,13 @@ 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)!;
|
||||
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,
|
||||
});
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
/**
|
||||
* Recurse into inner optional blocks to collect inner optional-chain
|
||||
|
||||
@@ -773,29 +773,6 @@ 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);
|
||||
}
|
||||
|
||||
@@ -308,33 +308,23 @@ export default class HIRBuilder {
|
||||
|
||||
resolveBinding(node: t.Identifier): Identifier {
|
||||
if (node.name === 'fbt') {
|
||||
CompilerError.throwDiagnostic({
|
||||
this.errors.push({
|
||||
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,
|
||||
},
|
||||
],
|
||||
loc: node.loc ?? GeneratedSource,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
if (node.name === 'this') {
|
||||
CompilerError.throwDiagnostic({
|
||||
this.errors.push({
|
||||
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,
|
||||
},
|
||||
],
|
||||
loc: node.loc ?? GeneratedSource,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
const originalName = node.name;
|
||||
|
||||
@@ -1007,11 +1007,10 @@ class Driver {
|
||||
const test = this.visitValueBlock(testBlockId, loc);
|
||||
const testBlock = this.cx.ir.blocks.get(test.block)!;
|
||||
if (testBlock.terminal.kind !== 'branch') {
|
||||
CompilerError.throwTodo({
|
||||
reason: `Unexpected terminal kind \`${testBlock.terminal.kind}\` for ${terminalKind} test block`,
|
||||
description: null,
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Expected a branch terminal for ${terminalKind} test block`,
|
||||
description: `Got \`${testBlock.terminal.kind}\``,
|
||||
loc: testBlock.terminal.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -775,12 +775,13 @@ function codegenTerminal(
|
||||
loc: terminal.init.loc,
|
||||
});
|
||||
if (terminal.init.instructions.length !== 2) {
|
||||
CompilerError.throwTodo({
|
||||
cx.errors.push({
|
||||
reason: 'Support non-trivial for..in inits',
|
||||
description: null,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: terminal.init.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
return t.emptyStatement();
|
||||
}
|
||||
const iterableCollection = terminal.init.instructions[0];
|
||||
const iterableItem = terminal.init.instructions[1];
|
||||
@@ -795,12 +796,13 @@ function codegenTerminal(
|
||||
break;
|
||||
}
|
||||
case 'StoreContext': {
|
||||
CompilerError.throwTodo({
|
||||
cx.errors.push({
|
||||
reason: 'Support non-trivial for..in inits',
|
||||
description: null,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: terminal.init.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
return t.emptyStatement();
|
||||
}
|
||||
default:
|
||||
CompilerError.invariant(false, {
|
||||
@@ -870,12 +872,13 @@ function codegenTerminal(
|
||||
loc: terminal.test.loc,
|
||||
});
|
||||
if (terminal.test.instructions.length !== 2) {
|
||||
CompilerError.throwTodo({
|
||||
cx.errors.push({
|
||||
reason: 'Support non-trivial for..of inits',
|
||||
description: null,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: terminal.init.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
return t.emptyStatement();
|
||||
}
|
||||
const iterableItem = terminal.test.instructions[1];
|
||||
let lval: t.LVal;
|
||||
@@ -889,12 +892,13 @@ function codegenTerminal(
|
||||
break;
|
||||
}
|
||||
case 'StoreContext': {
|
||||
CompilerError.throwTodo({
|
||||
cx.errors.push({
|
||||
reason: 'Support non-trivial for..of inits',
|
||||
description: null,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: terminal.init.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
return t.emptyStatement();
|
||||
}
|
||||
default:
|
||||
CompilerError.invariant(false, {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -310,7 +310,5 @@ export function validateSourceLocations(
|
||||
}
|
||||
}
|
||||
|
||||
for (const detail of errors.details) {
|
||||
env.recordError(detail);
|
||||
}
|
||||
env.recordErrors(errors);
|
||||
}
|
||||
|
||||
@@ -24,9 +24,9 @@ function useThing(fn) {
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Invariant: [HIRBuilder] Unexpected null block
|
||||
Error: Expected a non-reserved identifier name
|
||||
|
||||
expected block 0 to exist.
|
||||
`this` is a reserved word in JavaScript and cannot be used as an identifier name.
|
||||
```
|
||||
|
||||
|
||||
@@ -21,15 +21,15 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Invariant: [InferMutationAliasingEffects] Expected value kind to be initialized
|
||||
Todo: [hoisting] EnterSSA: Expected identifier to be defined before being used
|
||||
|
||||
<unknown> x$1.
|
||||
Identifier x$1 is undefined.
|
||||
|
||||
error.dont-hoist-inline-reference.ts:3:21
|
||||
error.dont-hoist-inline-reference.ts:3:2
|
||||
1 | import {identity} from 'shared-runtime';
|
||||
2 | function useInvalid() {
|
||||
> 3 | const x = identity(x);
|
||||
| ^ this is uninitialized
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^ [hoisting] EnterSSA: Expected identifier to be defined before being used
|
||||
4 | return x;
|
||||
5 | }
|
||||
6 |
|
||||
|
||||
@@ -1,39 +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}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## 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> {
|
||||
```
|
||||
|
||||
|
||||
@@ -18,7 +18,12 @@ function Component() {
|
||||
// Error: reading ref during render
|
||||
const value = ref.current;
|
||||
|
||||
return <div>{value}{items.length}</div>;
|
||||
return (
|
||||
<div>
|
||||
{value}
|
||||
{items.length}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
```
|
||||
@@ -50,8 +55,8 @@ error.var-declaration-and-ref-access.ts:15:16
|
||||
> 15 | const value = ref.current;
|
||||
| ^^^^^^^^^^^ Cannot access ref value during render
|
||||
16 |
|
||||
17 | return <div>{value}{items.length}</div>;
|
||||
18 | }
|
||||
17 | return (
|
||||
18 | <div>
|
||||
```
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
Found 4 errors:
|
||||
|
||||
Todo: Support local variables named `fbt`
|
||||
|
||||
@@ -60,10 +60,49 @@ error.todo-fbt-as-local.ts:18:19
|
||||
16 |
|
||||
17 | function Foo(props) {
|
||||
> 18 | const getText1 = fbt =>
|
||||
| ^^^ Rename to avoid conflict with fbt plugin
|
||||
| ^^^ 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: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'
|
||||
```
|
||||
|
||||
|
||||
@@ -16,17 +16,15 @@ function Component(props) {
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Todo: Support local variables named `fbt`
|
||||
Invariant: <fbt> tags should be module-level imports
|
||||
|
||||
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
|
||||
error.todo-locally-require-fbt.ts:4:10
|
||||
2 | const fbt = require('fbt');
|
||||
3 |
|
||||
4 | return <fbt desc="Description">{'Text'}</fbt>;
|
||||
> 4 | return <fbt desc="Description">{'Text'}</fbt>;
|
||||
| ^^^ <fbt> tags should be module-level imports
|
||||
5 | }
|
||||
6 |
|
||||
```
|
||||
|
||||
|
||||
@@ -1,40 +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}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## 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> {
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
|
||||
## 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
|
||||
@@ -0,0 +1,53 @@
|
||||
|
||||
## 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
|
||||
@@ -378,6 +378,17 @@ 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: {
|
||||
|
||||
Reference in New Issue
Block a user