mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 12:20:20 +01:00
```js
function Component() {
useEffect(() => {
let hasCleanedUp = false;
document.addEventListener(..., () => hasCleanedUp ? foo() : bar());
// effect return values shouldn't be typed as frozen
return () => {
hasCleanedUp = true;
}
};
}
```
### Problem
`PruneHoistedContexts` currently strips hoisted declarations and
rewrites the first `StoreContext` reassignment to a declaration. For
example, in the following example, instruction 0 is removed while a
synthetic `DeclareContext let` is inserted before instruction 1.
```js
// source
const cb = () => x; // reference that causes x to be hoisted
let x = 4;
x = 5;
// React Compiler IR
[0] DeclareContext HoistedLet 'x'
...
[1] StoreContext reassign 'x' = 4
[2] StoreContext reassign 'x' = 5
```
Currently, we don't account for `DeclareContext let`. As a result, we're
rewriting to insert duplicate declarations.
```js
// source
const cb = () => x; // reference that causes x to be hoisted
let x;
x = 5;
// React Compiler IR
[0] DeclareContext HoistedLet 'x'
...
[1] DeclareContext Let 'x'
[2] StoreContext reassign 'x' = 5
```
### Solution
Instead of always lowering context variables to a DeclareContext
followed by a StoreContext reassign, we can keep `kind: 'Const' | 'Let'
| 'Reassign' | etc` on StoreContext.
Pros:
- retain more information in HIR, so we can codegen easily `const` and
`let` context variable declarations back
- pruning hoisted `DeclareContext` instructions is simple.
Cons:
- passes are more verbose as we need to check for both `DeclareContext`
and `StoreContext` declarations
~(note: also see alternative implementation in
https://github.com/facebook/react/pull/32745)~
### Testing
Context variables are tricky. I synced and diffed changes in a large
meta codebase and feel pretty confident about landing this. About 0.01%
of compiled files changed. Among these changes, ~25% were [direct
bugfixes](https://www.internalfb.com/phabricator/paste/view/P1800029094).
The [other
changes](https://www.internalfb.com/phabricator/paste/view/P1800028575)
were primarily due to changed (corrected) mutable ranges from
https://github.com/facebook/react/pull/33047. I tried to represent most
interesting changes in new test fixtures
`
2684 lines
82 KiB
TypeScript
2684 lines
82 KiB
TypeScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*/
|
|
|
|
import * as t from '@babel/types';
|
|
import {createHmac} from 'crypto';
|
|
import {
|
|
pruneHoistedContexts,
|
|
pruneUnusedLValues,
|
|
pruneUnusedLabels,
|
|
renameVariables,
|
|
} from '.';
|
|
import {CompilerError, ErrorSeverity} from '../CompilerError';
|
|
import {Environment, ExternalFunction} from '../HIR';
|
|
import {
|
|
ArrayPattern,
|
|
BlockId,
|
|
DeclarationId,
|
|
GeneratedSource,
|
|
Identifier,
|
|
IdentifierId,
|
|
InstructionKind,
|
|
JsxAttribute,
|
|
ObjectMethod,
|
|
ObjectPropertyKey,
|
|
Pattern,
|
|
Place,
|
|
PrunedReactiveScopeBlock,
|
|
ReactiveBlock,
|
|
ReactiveFunction,
|
|
ReactiveInstruction,
|
|
ReactiveScope,
|
|
ReactiveScopeBlock,
|
|
ReactiveScopeDeclaration,
|
|
ReactiveScopeDependency,
|
|
ReactiveTerminal,
|
|
ReactiveValue,
|
|
SourceLocation,
|
|
SpreadPattern,
|
|
ValidIdentifierName,
|
|
getHookKind,
|
|
makeIdentifierName,
|
|
} from '../HIR/HIR';
|
|
import {printIdentifier, printPlace} from '../HIR/PrintHIR';
|
|
import {eachPatternOperand} from '../HIR/visitors';
|
|
import {Err, Ok, Result} from '../Utils/Result';
|
|
import {GuardKind} from '../Utils/RuntimeDiagnosticConstants';
|
|
import {assertExhaustive} from '../Utils/utils';
|
|
import {buildReactiveFunction} from './BuildReactiveFunction';
|
|
import {SINGLE_CHILD_FBT_TAGS} from './MemoizeFbtAndMacroOperandsInSameScope';
|
|
import {ReactiveFunctionVisitor, visitReactiveFunction} from './visitors';
|
|
import {EMIT_FREEZE_GLOBAL_GATING, ReactFunctionType} from '../HIR/Environment';
|
|
import {ProgramContext} from '../Entrypoint';
|
|
|
|
export const MEMO_CACHE_SENTINEL = 'react.memo_cache_sentinel';
|
|
export const EARLY_RETURN_SENTINEL = 'react.early_return_sentinel';
|
|
|
|
export type CodegenFunction = {
|
|
type: 'CodegenFunction';
|
|
id: t.Identifier | null;
|
|
params: t.FunctionDeclaration['params'];
|
|
body: t.BlockStatement;
|
|
generator: boolean;
|
|
async: boolean;
|
|
loc: SourceLocation;
|
|
|
|
/*
|
|
* Compiler info for logging and heuristics
|
|
* Number of memo slots (value passed to useMemoCache)
|
|
*/
|
|
memoSlotsUsed: number;
|
|
/*
|
|
* Number of memo *blocks* (reactive scopes) regardless of
|
|
* how many inputs/outputs each block has
|
|
*/
|
|
memoBlocks: number;
|
|
|
|
/**
|
|
* Number of memoized values across all reactive scopes
|
|
*/
|
|
memoValues: number;
|
|
|
|
/**
|
|
* The number of reactive scopes that were created but had to be discarded
|
|
* because they contained hook calls.
|
|
*/
|
|
prunedMemoBlocks: number;
|
|
|
|
/**
|
|
* The total number of values that should have been memoized but weren't
|
|
* because they were part of a pruned memo block.
|
|
*/
|
|
prunedMemoValues: number;
|
|
|
|
outlined: Array<{
|
|
fn: CodegenFunction;
|
|
type: ReactFunctionType | null;
|
|
}>;
|
|
|
|
/**
|
|
* This is true if the compiler has compiled inferred effect dependencies
|
|
*/
|
|
hasInferredEffect: boolean;
|
|
inferredEffectLocations: Set<SourceLocation>;
|
|
|
|
/**
|
|
* This is true if the compiler has compiled a fire to a useFire call
|
|
*/
|
|
hasFireRewrite: boolean;
|
|
};
|
|
|
|
export function codegenFunction(
|
|
fn: ReactiveFunction,
|
|
{
|
|
uniqueIdentifiers,
|
|
fbtOperands,
|
|
}: {
|
|
uniqueIdentifiers: Set<string>;
|
|
fbtOperands: Set<IdentifierId>;
|
|
},
|
|
): Result<CodegenFunction, CompilerError> {
|
|
const cx = new Context(
|
|
fn.env,
|
|
fn.id ?? '[[ anonymous ]]',
|
|
uniqueIdentifiers,
|
|
fbtOperands,
|
|
null,
|
|
);
|
|
|
|
/**
|
|
* Fast Refresh reuses component instances at runtime even as the source of the component changes.
|
|
* The generated code needs to prevent values from one version of the code being reused after a code cange.
|
|
* If HMR detection is enabled and we know the source code of the component, assign a cache slot to track
|
|
* the source hash, and later, emit code to check for source changes and reset the cache on source changes.
|
|
*/
|
|
let fastRefreshState: {
|
|
cacheIndex: number;
|
|
hash: string;
|
|
} | null = null;
|
|
if (
|
|
fn.env.config.enableResetCacheOnSourceFileChanges &&
|
|
fn.env.code !== null
|
|
) {
|
|
const hash = createHmac('sha256', fn.env.code).digest('hex');
|
|
fastRefreshState = {
|
|
cacheIndex: cx.nextCacheIndex,
|
|
hash,
|
|
};
|
|
}
|
|
|
|
const compileResult = codegenReactiveFunction(cx, fn);
|
|
if (compileResult.isErr()) {
|
|
return compileResult;
|
|
}
|
|
const compiled = compileResult.unwrap();
|
|
|
|
const hookGuard = fn.env.config.enableEmitHookGuards;
|
|
if (hookGuard != null && fn.env.isInferredMemoEnabled) {
|
|
compiled.body = t.blockStatement([
|
|
createHookGuard(
|
|
hookGuard,
|
|
fn.env.programContext,
|
|
compiled.body.body,
|
|
GuardKind.PushHookGuard,
|
|
GuardKind.PopHookGuard,
|
|
),
|
|
]);
|
|
}
|
|
|
|
const cacheCount = compiled.memoSlotsUsed;
|
|
if (cacheCount !== 0) {
|
|
const preface: Array<t.Statement> = [];
|
|
const useMemoCacheIdentifier =
|
|
fn.env.programContext.addMemoCacheImport().name;
|
|
|
|
// The import declaration for `useMemoCache` is inserted in the Babel plugin
|
|
preface.push(
|
|
t.variableDeclaration('const', [
|
|
t.variableDeclarator(
|
|
t.identifier(cx.synthesizeName('$')),
|
|
t.callExpression(t.identifier(useMemoCacheIdentifier), [
|
|
t.numericLiteral(cacheCount),
|
|
]),
|
|
),
|
|
]),
|
|
);
|
|
if (fastRefreshState !== null) {
|
|
// HMR detection is enabled, emit code to reset the memo cache on source changes
|
|
const index = cx.synthesizeName('$i');
|
|
preface.push(
|
|
t.ifStatement(
|
|
t.binaryExpression(
|
|
'!==',
|
|
t.memberExpression(
|
|
t.identifier(cx.synthesizeName('$')),
|
|
t.numericLiteral(fastRefreshState.cacheIndex),
|
|
true,
|
|
),
|
|
t.stringLiteral(fastRefreshState.hash),
|
|
),
|
|
t.blockStatement([
|
|
t.forStatement(
|
|
t.variableDeclaration('let', [
|
|
t.variableDeclarator(t.identifier(index), t.numericLiteral(0)),
|
|
]),
|
|
t.binaryExpression(
|
|
'<',
|
|
t.identifier(index),
|
|
t.numericLiteral(cacheCount),
|
|
),
|
|
t.assignmentExpression(
|
|
'+=',
|
|
t.identifier(index),
|
|
t.numericLiteral(1),
|
|
),
|
|
t.blockStatement([
|
|
t.expressionStatement(
|
|
t.assignmentExpression(
|
|
'=',
|
|
t.memberExpression(
|
|
t.identifier(cx.synthesizeName('$')),
|
|
t.identifier(index),
|
|
true,
|
|
),
|
|
t.callExpression(
|
|
t.memberExpression(
|
|
t.identifier('Symbol'),
|
|
t.identifier('for'),
|
|
),
|
|
[t.stringLiteral(MEMO_CACHE_SENTINEL)],
|
|
),
|
|
),
|
|
),
|
|
]),
|
|
),
|
|
t.expressionStatement(
|
|
t.assignmentExpression(
|
|
'=',
|
|
t.memberExpression(
|
|
t.identifier(cx.synthesizeName('$')),
|
|
t.numericLiteral(fastRefreshState.cacheIndex),
|
|
true,
|
|
),
|
|
t.stringLiteral(fastRefreshState.hash),
|
|
),
|
|
),
|
|
]),
|
|
),
|
|
);
|
|
}
|
|
compiled.body.body.unshift(...preface);
|
|
}
|
|
|
|
const emitInstrumentForget = fn.env.config.enableEmitInstrumentForget;
|
|
if (
|
|
emitInstrumentForget != null &&
|
|
fn.id != null &&
|
|
fn.env.isInferredMemoEnabled
|
|
) {
|
|
/*
|
|
* Technically, this is a conditional hook call. However, we expect
|
|
* __DEV__ and gating identifier to be runtime constants
|
|
*/
|
|
const gating =
|
|
emitInstrumentForget.gating != null
|
|
? t.identifier(
|
|
fn.env.programContext.addImportSpecifier(
|
|
emitInstrumentForget.gating,
|
|
).name,
|
|
)
|
|
: null;
|
|
|
|
const globalGating =
|
|
emitInstrumentForget.globalGating != null
|
|
? t.identifier(emitInstrumentForget.globalGating)
|
|
: null;
|
|
|
|
if (emitInstrumentForget.globalGating != null) {
|
|
const assertResult = fn.env.programContext.assertGlobalBinding(
|
|
emitInstrumentForget.globalGating,
|
|
);
|
|
if (assertResult.isErr()) {
|
|
return assertResult;
|
|
}
|
|
}
|
|
|
|
let ifTest: t.Expression;
|
|
if (gating != null && globalGating != null) {
|
|
ifTest = t.logicalExpression('&&', globalGating, gating);
|
|
} else if (gating != null) {
|
|
ifTest = gating;
|
|
} else {
|
|
CompilerError.invariant(globalGating != null, {
|
|
reason:
|
|
'Bad config not caught! Expected at least one of gating or globalGating',
|
|
loc: null,
|
|
suggestions: null,
|
|
});
|
|
ifTest = globalGating;
|
|
}
|
|
|
|
const instrumentFnIdentifier = fn.env.programContext.addImportSpecifier(
|
|
emitInstrumentForget.fn,
|
|
).name;
|
|
const test: t.IfStatement = t.ifStatement(
|
|
ifTest,
|
|
t.expressionStatement(
|
|
t.callExpression(t.identifier(instrumentFnIdentifier), [
|
|
t.stringLiteral(fn.id),
|
|
t.stringLiteral(fn.env.filename ?? ''),
|
|
]),
|
|
),
|
|
);
|
|
compiled.body.body.unshift(test);
|
|
}
|
|
|
|
const outlined: CodegenFunction['outlined'] = [];
|
|
for (const {fn: outlinedFunction, type} of cx.env.getOutlinedFunctions()) {
|
|
const reactiveFunction = buildReactiveFunction(outlinedFunction);
|
|
pruneUnusedLabels(reactiveFunction);
|
|
pruneUnusedLValues(reactiveFunction);
|
|
pruneHoistedContexts(reactiveFunction);
|
|
|
|
const identifiers = renameVariables(reactiveFunction);
|
|
const codegen = codegenReactiveFunction(
|
|
new Context(
|
|
cx.env,
|
|
reactiveFunction.id ?? '[[ anonymous ]]',
|
|
identifiers,
|
|
cx.fbtOperands,
|
|
),
|
|
reactiveFunction,
|
|
);
|
|
if (codegen.isErr()) {
|
|
return codegen;
|
|
}
|
|
outlined.push({fn: codegen.unwrap(), type});
|
|
}
|
|
compiled.outlined = outlined;
|
|
|
|
return compileResult;
|
|
}
|
|
|
|
function codegenReactiveFunction(
|
|
cx: Context,
|
|
fn: ReactiveFunction,
|
|
): Result<CodegenFunction, CompilerError> {
|
|
for (const param of fn.params) {
|
|
if (param.kind === 'Identifier') {
|
|
cx.temp.set(param.identifier.declarationId, null);
|
|
} else {
|
|
cx.temp.set(param.place.identifier.declarationId, null);
|
|
}
|
|
}
|
|
|
|
const params = fn.params.map(param => convertParameter(param));
|
|
const body: t.BlockStatement = codegenBlock(cx, fn.body);
|
|
body.directives = fn.directives.map(d => t.directive(t.directiveLiteral(d)));
|
|
const statements = body.body;
|
|
if (statements.length !== 0) {
|
|
const last = statements[statements.length - 1];
|
|
if (last.type === 'ReturnStatement' && last.argument == null) {
|
|
statements.pop();
|
|
}
|
|
}
|
|
|
|
if (cx.errors.hasErrors()) {
|
|
return Err(cx.errors);
|
|
}
|
|
|
|
const countMemoBlockVisitor = new CountMemoBlockVisitor(fn.env);
|
|
visitReactiveFunction(fn, countMemoBlockVisitor, undefined);
|
|
|
|
return Ok({
|
|
type: 'CodegenFunction',
|
|
loc: fn.loc,
|
|
id: fn.id !== null ? t.identifier(fn.id) : null,
|
|
params,
|
|
body,
|
|
generator: fn.generator,
|
|
async: fn.async,
|
|
memoSlotsUsed: cx.nextCacheIndex,
|
|
memoBlocks: countMemoBlockVisitor.memoBlocks,
|
|
memoValues: countMemoBlockVisitor.memoValues,
|
|
prunedMemoBlocks: countMemoBlockVisitor.prunedMemoBlocks,
|
|
prunedMemoValues: countMemoBlockVisitor.prunedMemoValues,
|
|
outlined: [],
|
|
hasFireRewrite: fn.env.hasFireRewrite,
|
|
hasInferredEffect: fn.env.hasInferredEffect,
|
|
inferredEffectLocations: fn.env.inferredEffectLocations,
|
|
});
|
|
}
|
|
|
|
class CountMemoBlockVisitor extends ReactiveFunctionVisitor<void> {
|
|
env: Environment;
|
|
memoBlocks: number = 0;
|
|
memoValues: number = 0;
|
|
prunedMemoBlocks: number = 0;
|
|
prunedMemoValues: number = 0;
|
|
|
|
constructor(env: Environment) {
|
|
super();
|
|
this.env = env;
|
|
}
|
|
|
|
override visitScope(scopeBlock: ReactiveScopeBlock, state: void): void {
|
|
this.memoBlocks += 1;
|
|
this.memoValues += scopeBlock.scope.declarations.size;
|
|
this.traverseScope(scopeBlock, state);
|
|
}
|
|
|
|
override visitPrunedScope(
|
|
scopeBlock: PrunedReactiveScopeBlock,
|
|
state: void,
|
|
): void {
|
|
this.prunedMemoBlocks += 1;
|
|
this.prunedMemoValues += scopeBlock.scope.declarations.size;
|
|
this.traversePrunedScope(scopeBlock, state);
|
|
}
|
|
}
|
|
|
|
function convertParameter(
|
|
param: Place | SpreadPattern,
|
|
): t.Identifier | t.RestElement {
|
|
if (param.kind === 'Identifier') {
|
|
return convertIdentifier(param.identifier);
|
|
} else {
|
|
return t.restElement(convertIdentifier(param.place.identifier));
|
|
}
|
|
}
|
|
|
|
class Context {
|
|
env: Environment;
|
|
fnName: string;
|
|
#nextCacheIndex: number = 0;
|
|
/**
|
|
* Tracks which named variables have been declared to dedupe declarations,
|
|
* so this uses DeclarationId instead of IdentifierId
|
|
*/
|
|
#declarations: Set<DeclarationId> = new Set();
|
|
temp: Temporaries;
|
|
errors: CompilerError = new CompilerError();
|
|
objectMethods: Map<IdentifierId, ObjectMethod> = new Map();
|
|
uniqueIdentifiers: Set<string>;
|
|
fbtOperands: Set<IdentifierId>;
|
|
synthesizedNames: Map<string, ValidIdentifierName> = new Map();
|
|
|
|
constructor(
|
|
env: Environment,
|
|
fnName: string,
|
|
uniqueIdentifiers: Set<string>,
|
|
fbtOperands: Set<IdentifierId>,
|
|
temporaries: Temporaries | null = null,
|
|
) {
|
|
this.env = env;
|
|
this.fnName = fnName;
|
|
this.uniqueIdentifiers = uniqueIdentifiers;
|
|
this.fbtOperands = fbtOperands;
|
|
this.temp = temporaries !== null ? new Map(temporaries) : new Map();
|
|
}
|
|
get nextCacheIndex(): number {
|
|
return this.#nextCacheIndex++;
|
|
}
|
|
|
|
declare(identifier: Identifier): void {
|
|
this.#declarations.add(identifier.declarationId);
|
|
}
|
|
|
|
hasDeclared(identifier: Identifier): boolean {
|
|
return this.#declarations.has(identifier.declarationId);
|
|
}
|
|
|
|
synthesizeName(name: string): ValidIdentifierName {
|
|
const previous = this.synthesizedNames.get(name);
|
|
if (previous !== undefined) {
|
|
return previous;
|
|
}
|
|
let validated = makeIdentifierName(name).value;
|
|
let index = 0;
|
|
while (this.uniqueIdentifiers.has(validated)) {
|
|
validated = makeIdentifierName(`${name}${index++}`).value;
|
|
}
|
|
this.uniqueIdentifiers.add(validated);
|
|
this.synthesizedNames.set(name, validated);
|
|
return validated;
|
|
}
|
|
}
|
|
|
|
function codegenBlock(cx: Context, block: ReactiveBlock): t.BlockStatement {
|
|
const temp = new Map(cx.temp);
|
|
const result = codegenBlockNoReset(cx, block);
|
|
/*
|
|
* Check that the block only added new temporaries and did not update the
|
|
* value of any existing temporary
|
|
*/
|
|
for (const [key, value] of cx.temp) {
|
|
if (!temp.has(key)) {
|
|
continue;
|
|
}
|
|
CompilerError.invariant(temp.get(key)! === value, {
|
|
loc: null,
|
|
reason: 'Expected temporary value to be unchanged',
|
|
description: null,
|
|
suggestions: null,
|
|
});
|
|
}
|
|
cx.temp = temp;
|
|
return result;
|
|
}
|
|
|
|
/*
|
|
* Generates code for the block, without resetting the Context's temporary state.
|
|
* This should not be used unless it is expected that temporaries from this block
|
|
* can be referenced later, which is currently only true for sequence expressions
|
|
* where the final `value` is expected to reference the temporary created in the
|
|
* preceding instructions of the sequence.
|
|
*/
|
|
function codegenBlockNoReset(
|
|
cx: Context,
|
|
block: ReactiveBlock,
|
|
): t.BlockStatement {
|
|
const statements: Array<t.Statement> = [];
|
|
for (const item of block) {
|
|
switch (item.kind) {
|
|
case 'instruction': {
|
|
const statement = codegenInstructionNullable(cx, item.instruction);
|
|
if (statement !== null) {
|
|
statements.push(statement);
|
|
}
|
|
break;
|
|
}
|
|
case 'pruned-scope': {
|
|
const scopeBlock = codegenBlockNoReset(cx, item.instructions);
|
|
statements.push(...scopeBlock.body);
|
|
break;
|
|
}
|
|
case 'scope': {
|
|
const temp = new Map(cx.temp);
|
|
codegenReactiveScope(cx, statements, item.scope, item.instructions);
|
|
cx.temp = temp;
|
|
break;
|
|
}
|
|
case 'terminal': {
|
|
const statement = codegenTerminal(cx, item.terminal);
|
|
if (statement === null) {
|
|
break;
|
|
}
|
|
if (item.label !== null && !item.label.implicit) {
|
|
const block =
|
|
statement.type === 'BlockStatement' && statement.body.length === 1
|
|
? statement.body[0]
|
|
: statement;
|
|
statements.push(
|
|
t.labeledStatement(
|
|
t.identifier(codegenLabel(item.label.id)),
|
|
block,
|
|
),
|
|
);
|
|
} else if (statement.type === 'BlockStatement') {
|
|
statements.push(...statement.body);
|
|
} else {
|
|
statements.push(statement);
|
|
}
|
|
break;
|
|
}
|
|
default: {
|
|
assertExhaustive(
|
|
item,
|
|
`Unexpected item kind \`${(item as any).kind}\``,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
return t.blockStatement(statements);
|
|
}
|
|
|
|
function wrapCacheDep(cx: Context, value: t.Expression): t.Expression {
|
|
if (cx.env.config.enableEmitFreeze != null && cx.env.isInferredMemoEnabled) {
|
|
const emitFreezeIdentifier = cx.env.programContext.addImportSpecifier(
|
|
cx.env.config.enableEmitFreeze,
|
|
).name;
|
|
cx.env.programContext
|
|
.assertGlobalBinding(EMIT_FREEZE_GLOBAL_GATING, cx.env.scope)
|
|
.unwrap();
|
|
return t.conditionalExpression(
|
|
t.identifier(EMIT_FREEZE_GLOBAL_GATING),
|
|
t.callExpression(t.identifier(emitFreezeIdentifier), [
|
|
value,
|
|
t.stringLiteral(cx.fnName),
|
|
]),
|
|
value,
|
|
);
|
|
} else {
|
|
return value;
|
|
}
|
|
}
|
|
|
|
function codegenReactiveScope(
|
|
cx: Context,
|
|
statements: Array<t.Statement>,
|
|
scope: ReactiveScope,
|
|
block: ReactiveBlock,
|
|
): void {
|
|
const cacheStoreStatements: Array<t.Statement> = [];
|
|
const cacheLoadStatements: Array<t.Statement> = [];
|
|
const cacheLoads: Array<{
|
|
name: t.Identifier;
|
|
index: number;
|
|
value: t.Expression;
|
|
}> = [];
|
|
const changeExpressions: Array<t.Expression> = [];
|
|
const changeExpressionComments: Array<string> = [];
|
|
const outputComments: Array<string> = [];
|
|
|
|
for (const dep of [...scope.dependencies].sort(compareScopeDependency)) {
|
|
const index = cx.nextCacheIndex;
|
|
changeExpressionComments.push(printDependencyComment(dep));
|
|
const comparison = t.binaryExpression(
|
|
'!==',
|
|
t.memberExpression(
|
|
t.identifier(cx.synthesizeName('$')),
|
|
t.numericLiteral(index),
|
|
true,
|
|
),
|
|
codegenDependency(cx, dep),
|
|
);
|
|
|
|
if (cx.env.config.enableChangeVariableCodegen) {
|
|
const changeIdentifier = t.identifier(cx.synthesizeName(`c_${index}`));
|
|
statements.push(
|
|
t.variableDeclaration('const', [
|
|
t.variableDeclarator(changeIdentifier, comparison),
|
|
]),
|
|
);
|
|
changeExpressions.push(changeIdentifier);
|
|
} else {
|
|
changeExpressions.push(comparison);
|
|
}
|
|
/*
|
|
* Adding directly to cacheStoreStatements rather than cacheLoads, because there
|
|
* is no corresponding cacheLoadStatement for dependencies
|
|
*/
|
|
cacheStoreStatements.push(
|
|
t.expressionStatement(
|
|
t.assignmentExpression(
|
|
'=',
|
|
t.memberExpression(
|
|
t.identifier(cx.synthesizeName('$')),
|
|
t.numericLiteral(index),
|
|
true,
|
|
),
|
|
codegenDependency(cx, dep),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
let firstOutputIndex: number | null = null;
|
|
|
|
for (const [, {identifier}] of [...scope.declarations].sort(([, a], [, b]) =>
|
|
compareScopeDeclaration(a, b),
|
|
)) {
|
|
const index = cx.nextCacheIndex;
|
|
if (firstOutputIndex === null) {
|
|
firstOutputIndex = index;
|
|
}
|
|
|
|
CompilerError.invariant(identifier.name != null, {
|
|
reason: `Expected scope declaration identifier to be named`,
|
|
description: `Declaration \`${printIdentifier(
|
|
identifier,
|
|
)}\` is unnamed in scope @${scope.id}`,
|
|
loc: null,
|
|
suggestions: null,
|
|
});
|
|
|
|
const name = convertIdentifier(identifier);
|
|
outputComments.push(name.name);
|
|
if (!cx.hasDeclared(identifier)) {
|
|
statements.push(
|
|
t.variableDeclaration('let', [t.variableDeclarator(name)]),
|
|
);
|
|
}
|
|
cacheLoads.push({name, index, value: wrapCacheDep(cx, name)});
|
|
cx.declare(identifier);
|
|
}
|
|
for (const reassignment of scope.reassignments) {
|
|
const index = cx.nextCacheIndex;
|
|
if (firstOutputIndex === null) {
|
|
firstOutputIndex = index;
|
|
}
|
|
const name = convertIdentifier(reassignment);
|
|
outputComments.push(name.name);
|
|
cacheLoads.push({name, index, value: wrapCacheDep(cx, name)});
|
|
}
|
|
|
|
let testCondition = (changeExpressions as Array<t.Expression>).reduce(
|
|
(acc: t.Expression | null, ident: t.Expression) => {
|
|
if (acc == null) {
|
|
return ident;
|
|
}
|
|
return t.logicalExpression('||', acc, ident);
|
|
},
|
|
null as t.Expression | null,
|
|
);
|
|
if (testCondition === null) {
|
|
CompilerError.invariant(firstOutputIndex !== null, {
|
|
reason: `Expected scope to have at least one declaration`,
|
|
description: `Scope '@${scope.id}' has no declarations`,
|
|
loc: null,
|
|
suggestions: null,
|
|
});
|
|
testCondition = t.binaryExpression(
|
|
'===',
|
|
t.memberExpression(
|
|
t.identifier(cx.synthesizeName('$')),
|
|
t.numericLiteral(firstOutputIndex),
|
|
true,
|
|
),
|
|
t.callExpression(
|
|
t.memberExpression(t.identifier('Symbol'), t.identifier('for')),
|
|
[t.stringLiteral(MEMO_CACHE_SENTINEL)],
|
|
),
|
|
);
|
|
}
|
|
|
|
if (cx.env.config.disableMemoizationForDebugging) {
|
|
CompilerError.invariant(
|
|
cx.env.config.enableChangeDetectionForDebugging == null,
|
|
{
|
|
reason: `Expected to not have both change detection enabled and memoization disabled`,
|
|
description: `Incompatible config options`,
|
|
loc: null,
|
|
},
|
|
);
|
|
testCondition = t.logicalExpression(
|
|
'||',
|
|
testCondition,
|
|
t.booleanLiteral(true),
|
|
);
|
|
}
|
|
let computationBlock = codegenBlock(cx, block);
|
|
|
|
let memoStatement;
|
|
const detectionFunction = cx.env.config.enableChangeDetectionForDebugging;
|
|
if (detectionFunction != null && changeExpressions.length > 0) {
|
|
const loc =
|
|
typeof scope.loc === 'symbol'
|
|
? 'unknown location'
|
|
: `(${scope.loc.start.line}:${scope.loc.end.line})`;
|
|
const importedDetectionFunctionIdentifier =
|
|
cx.env.programContext.addImportSpecifier(detectionFunction).name;
|
|
const cacheLoadOldValueStatements: Array<t.Statement> = [];
|
|
const changeDetectionStatements: Array<t.Statement> = [];
|
|
const idempotenceDetectionStatements: Array<t.Statement> = [];
|
|
|
|
for (const {name, index, value} of cacheLoads) {
|
|
const loadName = cx.synthesizeName(`old$${name.name}`);
|
|
const slot = t.memberExpression(
|
|
t.identifier(cx.synthesizeName('$')),
|
|
t.numericLiteral(index),
|
|
true,
|
|
);
|
|
cacheStoreStatements.push(
|
|
t.expressionStatement(t.assignmentExpression('=', slot, value)),
|
|
);
|
|
cacheLoadOldValueStatements.push(
|
|
t.variableDeclaration('let', [
|
|
t.variableDeclarator(t.identifier(loadName), slot),
|
|
]),
|
|
);
|
|
changeDetectionStatements.push(
|
|
t.expressionStatement(
|
|
t.callExpression(t.identifier(importedDetectionFunctionIdentifier), [
|
|
t.identifier(loadName),
|
|
t.cloneNode(name, true),
|
|
t.stringLiteral(name.name),
|
|
t.stringLiteral(cx.fnName),
|
|
t.stringLiteral('cached'),
|
|
t.stringLiteral(loc),
|
|
]),
|
|
),
|
|
);
|
|
idempotenceDetectionStatements.push(
|
|
t.expressionStatement(
|
|
t.callExpression(t.identifier(importedDetectionFunctionIdentifier), [
|
|
t.cloneNode(slot, true),
|
|
t.cloneNode(name, true),
|
|
t.stringLiteral(name.name),
|
|
t.stringLiteral(cx.fnName),
|
|
t.stringLiteral('recomputed'),
|
|
t.stringLiteral(loc),
|
|
]),
|
|
),
|
|
);
|
|
idempotenceDetectionStatements.push(
|
|
t.expressionStatement(t.assignmentExpression('=', name, slot)),
|
|
);
|
|
}
|
|
const condition = cx.synthesizeName('condition');
|
|
const recomputationBlock = t.cloneNode(computationBlock, true);
|
|
memoStatement = t.blockStatement([
|
|
...computationBlock.body,
|
|
t.variableDeclaration('let', [
|
|
t.variableDeclarator(t.identifier(condition), testCondition),
|
|
]),
|
|
t.ifStatement(
|
|
t.unaryExpression('!', t.identifier(condition)),
|
|
t.blockStatement([
|
|
...cacheLoadOldValueStatements,
|
|
...changeDetectionStatements,
|
|
]),
|
|
),
|
|
...cacheStoreStatements,
|
|
t.ifStatement(
|
|
t.identifier(condition),
|
|
t.blockStatement([
|
|
...recomputationBlock.body,
|
|
...idempotenceDetectionStatements,
|
|
]),
|
|
),
|
|
]);
|
|
} else {
|
|
for (const {name, index, value} of cacheLoads) {
|
|
cacheStoreStatements.push(
|
|
t.expressionStatement(
|
|
t.assignmentExpression(
|
|
'=',
|
|
t.memberExpression(
|
|
t.identifier(cx.synthesizeName('$')),
|
|
t.numericLiteral(index),
|
|
true,
|
|
),
|
|
value,
|
|
),
|
|
),
|
|
);
|
|
cacheLoadStatements.push(
|
|
t.expressionStatement(
|
|
t.assignmentExpression(
|
|
'=',
|
|
name,
|
|
t.memberExpression(
|
|
t.identifier(cx.synthesizeName('$')),
|
|
t.numericLiteral(index),
|
|
true,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
computationBlock.body.push(...cacheStoreStatements);
|
|
memoStatement = t.ifStatement(
|
|
testCondition,
|
|
computationBlock,
|
|
t.blockStatement(cacheLoadStatements),
|
|
);
|
|
}
|
|
|
|
if (cx.env.config.enableMemoizationComments) {
|
|
if (changeExpressionComments.length) {
|
|
t.addComment(
|
|
memoStatement,
|
|
'leading',
|
|
` check if ${printDelimitedCommentList(
|
|
changeExpressionComments,
|
|
'or',
|
|
)} changed`,
|
|
true,
|
|
);
|
|
t.addComment(
|
|
memoStatement,
|
|
'leading',
|
|
` "useMemo" for ${printDelimitedCommentList(outputComments, 'and')}:`,
|
|
true,
|
|
);
|
|
} else {
|
|
t.addComment(
|
|
memoStatement,
|
|
'leading',
|
|
' cache value with no dependencies',
|
|
true,
|
|
);
|
|
t.addComment(
|
|
memoStatement,
|
|
'leading',
|
|
` "useMemo" for ${printDelimitedCommentList(outputComments, 'and')}:`,
|
|
true,
|
|
);
|
|
}
|
|
if (computationBlock.body.length > 0) {
|
|
t.addComment(
|
|
computationBlock.body[0]!,
|
|
'leading',
|
|
` Inputs changed, recompute`,
|
|
true,
|
|
);
|
|
}
|
|
if (cacheLoadStatements.length > 0) {
|
|
t.addComment(
|
|
cacheLoadStatements[0]!,
|
|
'leading',
|
|
` Inputs did not change, use cached value`,
|
|
true,
|
|
);
|
|
}
|
|
}
|
|
statements.push(memoStatement);
|
|
|
|
const earlyReturnValue = scope.earlyReturnValue;
|
|
if (earlyReturnValue !== null) {
|
|
CompilerError.invariant(
|
|
earlyReturnValue.value.name !== null &&
|
|
earlyReturnValue.value.name.kind === 'named',
|
|
{
|
|
reason: `Expected early return value to be promoted to a named variable`,
|
|
loc: earlyReturnValue.loc,
|
|
description: null,
|
|
suggestions: null,
|
|
},
|
|
);
|
|
const name: ValidIdentifierName = earlyReturnValue.value.name.value;
|
|
statements.push(
|
|
t.ifStatement(
|
|
t.binaryExpression(
|
|
'!==',
|
|
t.identifier(name),
|
|
t.callExpression(
|
|
t.memberExpression(t.identifier('Symbol'), t.identifier('for')),
|
|
[t.stringLiteral(EARLY_RETURN_SENTINEL)],
|
|
),
|
|
),
|
|
t.blockStatement([t.returnStatement(t.identifier(name))]),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
function codegenTerminal(
|
|
cx: Context,
|
|
terminal: ReactiveTerminal,
|
|
): t.Statement | null {
|
|
switch (terminal.kind) {
|
|
case 'break': {
|
|
if (terminal.targetKind === 'implicit') {
|
|
return null;
|
|
}
|
|
return t.breakStatement(
|
|
terminal.targetKind === 'labeled'
|
|
? t.identifier(codegenLabel(terminal.target))
|
|
: null,
|
|
);
|
|
}
|
|
case 'continue': {
|
|
if (terminal.targetKind === 'implicit') {
|
|
return null;
|
|
}
|
|
return t.continueStatement(
|
|
terminal.targetKind === 'labeled'
|
|
? t.identifier(codegenLabel(terminal.target))
|
|
: null,
|
|
);
|
|
}
|
|
case 'for': {
|
|
return t.forStatement(
|
|
codegenForInit(cx, terminal.init),
|
|
codegenInstructionValueToExpression(cx, terminal.test),
|
|
terminal.update !== null
|
|
? codegenInstructionValueToExpression(cx, terminal.update)
|
|
: null,
|
|
codegenBlock(cx, terminal.loop),
|
|
);
|
|
}
|
|
case 'for-in': {
|
|
CompilerError.invariant(terminal.init.kind === 'SequenceExpression', {
|
|
reason: `Expected a sequence expression init for for..in`,
|
|
description: `Got \`${terminal.init.kind}\` expression instead`,
|
|
loc: terminal.init.loc,
|
|
suggestions: null,
|
|
});
|
|
if (terminal.init.instructions.length !== 2) {
|
|
CompilerError.throwTodo({
|
|
reason: 'Support non-trivial for..in inits',
|
|
description: null,
|
|
loc: terminal.init.loc,
|
|
suggestions: null,
|
|
});
|
|
}
|
|
const iterableCollection = terminal.init.instructions[0];
|
|
const iterableItem = terminal.init.instructions[1];
|
|
let lval: t.LVal;
|
|
switch (iterableItem.value.kind) {
|
|
case 'StoreLocal': {
|
|
lval = codegenLValue(cx, iterableItem.value.lvalue.place);
|
|
break;
|
|
}
|
|
case 'Destructure': {
|
|
lval = codegenLValue(cx, iterableItem.value.lvalue.pattern);
|
|
break;
|
|
}
|
|
case 'StoreContext': {
|
|
CompilerError.throwTodo({
|
|
reason: 'Support non-trivial for..in inits',
|
|
description: null,
|
|
loc: terminal.init.loc,
|
|
suggestions: null,
|
|
});
|
|
}
|
|
default:
|
|
CompilerError.invariant(false, {
|
|
reason: `Expected a StoreLocal or Destructure to be assigned to the collection`,
|
|
description: `Found ${iterableItem.value.kind}`,
|
|
loc: iterableItem.value.loc,
|
|
suggestions: null,
|
|
});
|
|
}
|
|
let varDeclKind: 'const' | 'let';
|
|
switch (iterableItem.value.lvalue.kind) {
|
|
case InstructionKind.Const:
|
|
varDeclKind = 'const' as const;
|
|
break;
|
|
case InstructionKind.Let:
|
|
varDeclKind = 'let' as const;
|
|
break;
|
|
case InstructionKind.Reassign:
|
|
CompilerError.invariant(false, {
|
|
reason:
|
|
'Destructure should never be Reassign as it would be an Object/ArrayPattern',
|
|
description: null,
|
|
loc: iterableItem.loc,
|
|
suggestions: null,
|
|
});
|
|
case InstructionKind.Catch:
|
|
case InstructionKind.HoistedConst:
|
|
case InstructionKind.HoistedLet:
|
|
case InstructionKind.HoistedFunction:
|
|
case InstructionKind.Function:
|
|
CompilerError.invariant(false, {
|
|
reason: `Unexpected ${iterableItem.value.lvalue.kind} variable in for..in collection`,
|
|
description: null,
|
|
loc: iterableItem.loc,
|
|
suggestions: null,
|
|
});
|
|
default:
|
|
assertExhaustive(
|
|
iterableItem.value.lvalue.kind,
|
|
`Unhandled lvalue kind: ${iterableItem.value.lvalue.kind}`,
|
|
);
|
|
}
|
|
return t.forInStatement(
|
|
/*
|
|
* Special handling here since we only want the VariableDeclarators without any inits
|
|
* This needs to be updated when we handle non-trivial ForOf inits
|
|
*/
|
|
createVariableDeclaration(iterableItem.value.loc, varDeclKind, [
|
|
t.variableDeclarator(lval, null),
|
|
]),
|
|
codegenInstructionValueToExpression(cx, iterableCollection.value),
|
|
codegenBlock(cx, terminal.loop),
|
|
);
|
|
}
|
|
case 'for-of': {
|
|
CompilerError.invariant(
|
|
terminal.init.kind === 'SequenceExpression' &&
|
|
terminal.init.instructions.length === 1 &&
|
|
terminal.init.instructions[0].value.kind === 'GetIterator',
|
|
{
|
|
reason: `Expected a single-expression sequence expression init for for..of`,
|
|
description: `Got \`${terminal.init.kind}\` expression instead`,
|
|
loc: terminal.init.loc,
|
|
suggestions: null,
|
|
},
|
|
);
|
|
const iterableCollection = terminal.init.instructions[0].value;
|
|
|
|
CompilerError.invariant(terminal.test.kind === 'SequenceExpression', {
|
|
reason: `Expected a sequence expression test for for..of`,
|
|
description: `Got \`${terminal.init.kind}\` expression instead`,
|
|
loc: terminal.test.loc,
|
|
suggestions: null,
|
|
});
|
|
if (terminal.test.instructions.length !== 2) {
|
|
CompilerError.throwTodo({
|
|
reason: 'Support non-trivial for..of inits',
|
|
description: null,
|
|
loc: terminal.init.loc,
|
|
suggestions: null,
|
|
});
|
|
}
|
|
const iterableItem = terminal.test.instructions[1];
|
|
let lval: t.LVal;
|
|
switch (iterableItem.value.kind) {
|
|
case 'StoreLocal': {
|
|
lval = codegenLValue(cx, iterableItem.value.lvalue.place);
|
|
break;
|
|
}
|
|
case 'Destructure': {
|
|
lval = codegenLValue(cx, iterableItem.value.lvalue.pattern);
|
|
break;
|
|
}
|
|
case 'StoreContext': {
|
|
CompilerError.throwTodo({
|
|
reason: 'Support non-trivial for..of inits',
|
|
description: null,
|
|
loc: terminal.init.loc,
|
|
suggestions: null,
|
|
});
|
|
}
|
|
default:
|
|
CompilerError.invariant(false, {
|
|
reason: `Expected a StoreLocal or Destructure to be assigned to the collection`,
|
|
description: `Found ${iterableItem.value.kind}`,
|
|
loc: iterableItem.value.loc,
|
|
suggestions: null,
|
|
});
|
|
}
|
|
let varDeclKind: 'const' | 'let';
|
|
switch (iterableItem.value.lvalue.kind) {
|
|
case InstructionKind.Const:
|
|
varDeclKind = 'const' as const;
|
|
break;
|
|
case InstructionKind.Let:
|
|
varDeclKind = 'let' as const;
|
|
break;
|
|
case InstructionKind.Reassign:
|
|
case InstructionKind.Catch:
|
|
case InstructionKind.HoistedConst:
|
|
case InstructionKind.HoistedLet:
|
|
case InstructionKind.HoistedFunction:
|
|
case InstructionKind.Function:
|
|
CompilerError.invariant(false, {
|
|
reason: `Unexpected ${iterableItem.value.lvalue.kind} variable in for..of collection`,
|
|
description: null,
|
|
loc: iterableItem.loc,
|
|
suggestions: null,
|
|
});
|
|
default:
|
|
assertExhaustive(
|
|
iterableItem.value.lvalue.kind,
|
|
`Unhandled lvalue kind: ${iterableItem.value.lvalue.kind}`,
|
|
);
|
|
}
|
|
return t.forOfStatement(
|
|
/*
|
|
* Special handling here since we only want the VariableDeclarators without any inits
|
|
* This needs to be updated when we handle non-trivial ForOf inits
|
|
*/
|
|
createVariableDeclaration(iterableItem.value.loc, varDeclKind, [
|
|
t.variableDeclarator(lval, null),
|
|
]),
|
|
codegenInstructionValueToExpression(cx, iterableCollection),
|
|
codegenBlock(cx, terminal.loop),
|
|
);
|
|
}
|
|
case 'if': {
|
|
const test = codegenPlaceToExpression(cx, terminal.test);
|
|
const consequent = codegenBlock(cx, terminal.consequent);
|
|
let alternate: t.Statement | null = null;
|
|
if (terminal.alternate !== null) {
|
|
const block = codegenBlock(cx, terminal.alternate);
|
|
if (block.body.length !== 0) {
|
|
alternate = block;
|
|
}
|
|
}
|
|
return t.ifStatement(test, consequent, alternate);
|
|
}
|
|
case 'return': {
|
|
const value = codegenPlaceToExpression(cx, terminal.value);
|
|
if (value.type === 'Identifier' && value.name === 'undefined') {
|
|
// Use implicit undefined
|
|
return t.returnStatement();
|
|
}
|
|
return t.returnStatement(value);
|
|
}
|
|
case 'switch': {
|
|
return t.switchStatement(
|
|
codegenPlaceToExpression(cx, terminal.test),
|
|
terminal.cases.map(case_ => {
|
|
const test =
|
|
case_.test !== null
|
|
? codegenPlaceToExpression(cx, case_.test)
|
|
: null;
|
|
const block = codegenBlock(cx, case_.block!);
|
|
return t.switchCase(test, [block]);
|
|
}),
|
|
);
|
|
}
|
|
case 'throw': {
|
|
return t.throwStatement(codegenPlaceToExpression(cx, terminal.value));
|
|
}
|
|
case 'do-while': {
|
|
const test = codegenInstructionValueToExpression(cx, terminal.test);
|
|
return t.doWhileStatement(test, codegenBlock(cx, terminal.loop));
|
|
}
|
|
case 'while': {
|
|
const test = codegenInstructionValueToExpression(cx, terminal.test);
|
|
return t.whileStatement(test, codegenBlock(cx, terminal.loop));
|
|
}
|
|
case 'label': {
|
|
return codegenBlock(cx, terminal.block);
|
|
}
|
|
case 'try': {
|
|
let catchParam = null;
|
|
if (terminal.handlerBinding !== null) {
|
|
catchParam = convertIdentifier(terminal.handlerBinding.identifier);
|
|
cx.temp.set(terminal.handlerBinding.identifier.declarationId, null);
|
|
}
|
|
return t.tryStatement(
|
|
codegenBlock(cx, terminal.block),
|
|
t.catchClause(catchParam, codegenBlock(cx, terminal.handler)),
|
|
);
|
|
}
|
|
default: {
|
|
assertExhaustive(
|
|
terminal,
|
|
`Unexpected terminal kind \`${(terminal as any).kind}\``,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
function codegenInstructionNullable(
|
|
cx: Context,
|
|
instr: ReactiveInstruction,
|
|
): t.Statement | null {
|
|
if (
|
|
instr.value.kind === 'StoreLocal' ||
|
|
instr.value.kind === 'StoreContext' ||
|
|
instr.value.kind === 'Destructure' ||
|
|
instr.value.kind === 'DeclareLocal' ||
|
|
instr.value.kind === 'DeclareContext'
|
|
) {
|
|
let kind: InstructionKind = instr.value.lvalue.kind;
|
|
let lvalue: Place | Pattern;
|
|
let value: t.Expression | null;
|
|
if (instr.value.kind === 'StoreLocal') {
|
|
kind = cx.hasDeclared(instr.value.lvalue.place.identifier)
|
|
? InstructionKind.Reassign
|
|
: kind;
|
|
lvalue = instr.value.lvalue.place;
|
|
value = codegenPlaceToExpression(cx, instr.value.value);
|
|
} else if (instr.value.kind === 'StoreContext') {
|
|
lvalue = instr.value.lvalue.place;
|
|
value = codegenPlaceToExpression(cx, instr.value.value);
|
|
} else if (
|
|
instr.value.kind === 'DeclareLocal' ||
|
|
instr.value.kind === 'DeclareContext'
|
|
) {
|
|
if (cx.hasDeclared(instr.value.lvalue.place.identifier)) {
|
|
return null;
|
|
}
|
|
kind = instr.value.lvalue.kind;
|
|
lvalue = instr.value.lvalue.place;
|
|
value = null;
|
|
} else {
|
|
lvalue = instr.value.lvalue.pattern;
|
|
let hasReassign = false;
|
|
let hasDeclaration = false;
|
|
for (const place of eachPatternOperand(lvalue)) {
|
|
if (
|
|
kind !== InstructionKind.Reassign &&
|
|
place.identifier.name === null
|
|
) {
|
|
cx.temp.set(place.identifier.declarationId, null);
|
|
}
|
|
const isDeclared = cx.hasDeclared(place.identifier);
|
|
hasReassign ||= isDeclared;
|
|
hasDeclaration ||= !isDeclared;
|
|
}
|
|
if (hasReassign && hasDeclaration) {
|
|
CompilerError.invariant(false, {
|
|
reason:
|
|
'Encountered a destructuring operation where some identifiers are already declared (reassignments) but others are not (declarations)',
|
|
description: null,
|
|
loc: instr.loc,
|
|
suggestions: null,
|
|
});
|
|
} else if (hasReassign) {
|
|
kind = InstructionKind.Reassign;
|
|
}
|
|
value = codegenPlaceToExpression(cx, instr.value.value);
|
|
}
|
|
switch (kind) {
|
|
case InstructionKind.Const: {
|
|
CompilerError.invariant(instr.lvalue === null, {
|
|
reason: `Const declaration cannot be referenced as an expression`,
|
|
description: null,
|
|
loc: instr.value.loc,
|
|
suggestions: null,
|
|
});
|
|
return createVariableDeclaration(instr.loc, 'const', [
|
|
t.variableDeclarator(codegenLValue(cx, lvalue), value),
|
|
]);
|
|
}
|
|
case InstructionKind.Function: {
|
|
CompilerError.invariant(instr.lvalue === null, {
|
|
reason: `Function declaration cannot be referenced as an expression`,
|
|
description: null,
|
|
loc: instr.value.loc,
|
|
suggestions: null,
|
|
});
|
|
const genLvalue = codegenLValue(cx, lvalue);
|
|
CompilerError.invariant(genLvalue.type === 'Identifier', {
|
|
reason: 'Expected an identifier as a function declaration lvalue',
|
|
description: null,
|
|
loc: instr.value.loc,
|
|
suggestions: null,
|
|
});
|
|
CompilerError.invariant(value?.type === 'FunctionExpression', {
|
|
reason: 'Expected a function as a function declaration value',
|
|
description: null,
|
|
loc: instr.value.loc,
|
|
suggestions: null,
|
|
});
|
|
return createFunctionDeclaration(
|
|
instr.loc,
|
|
genLvalue,
|
|
value.params,
|
|
value.body,
|
|
value.generator,
|
|
value.async,
|
|
);
|
|
}
|
|
case InstructionKind.Let: {
|
|
CompilerError.invariant(instr.lvalue === null, {
|
|
reason: `Const declaration cannot be referenced as an expression`,
|
|
description: null,
|
|
loc: instr.value.loc,
|
|
suggestions: null,
|
|
});
|
|
return createVariableDeclaration(instr.loc, 'let', [
|
|
t.variableDeclarator(codegenLValue(cx, lvalue), value),
|
|
]);
|
|
}
|
|
case InstructionKind.Reassign: {
|
|
CompilerError.invariant(value !== null, {
|
|
reason: 'Expected a value for reassignment',
|
|
description: null,
|
|
loc: instr.value.loc,
|
|
suggestions: null,
|
|
});
|
|
const expr = t.assignmentExpression(
|
|
'=',
|
|
codegenLValue(cx, lvalue),
|
|
value,
|
|
);
|
|
if (instr.lvalue !== null) {
|
|
if (instr.value.kind !== 'StoreContext') {
|
|
cx.temp.set(instr.lvalue.identifier.declarationId, expr);
|
|
return null;
|
|
} else {
|
|
// Handle chained reassignments for context variables
|
|
const statement = codegenInstruction(cx, instr, expr);
|
|
if (statement.type === 'EmptyStatement') {
|
|
return null;
|
|
}
|
|
return statement;
|
|
}
|
|
} else {
|
|
return createExpressionStatement(instr.loc, expr);
|
|
}
|
|
}
|
|
case InstructionKind.Catch: {
|
|
return t.emptyStatement();
|
|
}
|
|
case InstructionKind.HoistedLet:
|
|
case InstructionKind.HoistedConst:
|
|
case InstructionKind.HoistedFunction: {
|
|
CompilerError.invariant(false, {
|
|
reason: `Expected ${kind} to have been pruned in PruneHoistedContexts`,
|
|
description: null,
|
|
loc: instr.loc,
|
|
suggestions: null,
|
|
});
|
|
}
|
|
default: {
|
|
assertExhaustive(kind, `Unexpected instruction kind \`${kind}\``);
|
|
}
|
|
}
|
|
} else if (
|
|
instr.value.kind === 'StartMemoize' ||
|
|
instr.value.kind === 'FinishMemoize'
|
|
) {
|
|
return null;
|
|
} else if (instr.value.kind === 'Debugger') {
|
|
return t.debuggerStatement();
|
|
} else if (instr.value.kind === 'ObjectMethod') {
|
|
CompilerError.invariant(instr.lvalue, {
|
|
reason: 'Expected object methods to have a temp lvalue',
|
|
loc: null,
|
|
suggestions: null,
|
|
});
|
|
cx.objectMethods.set(instr.lvalue.identifier.id, instr.value);
|
|
return null;
|
|
} else {
|
|
const value = codegenInstructionValue(cx, instr.value);
|
|
const statement = codegenInstruction(cx, instr, value);
|
|
if (statement.type === 'EmptyStatement') {
|
|
return null;
|
|
}
|
|
return statement;
|
|
}
|
|
}
|
|
|
|
function codegenForInit(
|
|
cx: Context,
|
|
init: ReactiveValue,
|
|
): t.Expression | t.VariableDeclaration | null {
|
|
if (init.kind === 'SequenceExpression') {
|
|
const body = codegenBlock(
|
|
cx,
|
|
init.instructions.map(instruction => ({
|
|
kind: 'instruction',
|
|
instruction,
|
|
})),
|
|
).body;
|
|
const declarators: Array<t.VariableDeclarator> = [];
|
|
let kind: 'let' | 'const' = 'const';
|
|
body.forEach(instr => {
|
|
let top: undefined | t.VariableDeclarator = undefined;
|
|
if (
|
|
instr.type === 'ExpressionStatement' &&
|
|
instr.expression.type === 'AssignmentExpression' &&
|
|
instr.expression.operator === '=' &&
|
|
instr.expression.left.type === 'Identifier' &&
|
|
(top = declarators.at(-1))?.id.type === 'Identifier' &&
|
|
top?.id.name === instr.expression.left.name &&
|
|
top?.init == null
|
|
) {
|
|
top.init = instr.expression.right;
|
|
} else {
|
|
CompilerError.invariant(
|
|
instr.type === 'VariableDeclaration' &&
|
|
(instr.kind === 'let' || instr.kind === 'const'),
|
|
{
|
|
reason: 'Expected a variable declaration',
|
|
loc: init.loc,
|
|
description: `Got ${instr.type}`,
|
|
suggestions: null,
|
|
},
|
|
);
|
|
if (instr.kind === 'let') {
|
|
kind = 'let';
|
|
}
|
|
declarators.push(...instr.declarations);
|
|
}
|
|
});
|
|
CompilerError.invariant(declarators.length > 0, {
|
|
reason: 'Expected a variable declaration',
|
|
loc: init.loc,
|
|
description: null,
|
|
suggestions: null,
|
|
});
|
|
return t.variableDeclaration(kind, declarators);
|
|
} else {
|
|
return codegenInstructionValueToExpression(cx, init);
|
|
}
|
|
}
|
|
|
|
function printDependencyComment(dependency: ReactiveScopeDependency): string {
|
|
const identifier = convertIdentifier(dependency.identifier);
|
|
let name = identifier.name;
|
|
if (dependency.path !== null) {
|
|
for (const path of dependency.path) {
|
|
name += `.${path.property}`;
|
|
}
|
|
}
|
|
return name;
|
|
}
|
|
|
|
function printDelimitedCommentList(
|
|
items: Array<string>,
|
|
finalCompletion: string,
|
|
): string {
|
|
if (items.length === 2) {
|
|
return items.join(` ${finalCompletion} `);
|
|
} else if (items.length <= 1) {
|
|
return items.join('');
|
|
}
|
|
|
|
let output = [];
|
|
for (let i = 0; i < items.length; i++) {
|
|
const item = items[i]!;
|
|
if (i < items.length - 2) {
|
|
output.push(`${item}, `);
|
|
} else if (i === items.length - 2) {
|
|
output.push(`${item}, ${finalCompletion} `);
|
|
} else {
|
|
output.push(item);
|
|
}
|
|
}
|
|
return output.join('');
|
|
}
|
|
|
|
function codegenDependency(
|
|
cx: Context,
|
|
dependency: ReactiveScopeDependency,
|
|
): t.Expression {
|
|
let object: t.Expression = convertIdentifier(dependency.identifier);
|
|
if (dependency.path.length !== 0) {
|
|
const hasOptional = dependency.path.some(path => path.optional);
|
|
for (const path of dependency.path) {
|
|
const property =
|
|
typeof path.property === 'string'
|
|
? t.identifier(path.property)
|
|
: t.numericLiteral(path.property);
|
|
const isComputed = typeof path.property !== 'string';
|
|
if (hasOptional) {
|
|
object = t.optionalMemberExpression(
|
|
object,
|
|
property,
|
|
isComputed,
|
|
path.optional,
|
|
);
|
|
} else {
|
|
object = t.memberExpression(object, property, isComputed);
|
|
}
|
|
}
|
|
}
|
|
return object;
|
|
}
|
|
|
|
function withLoc<T extends (...args: Array<any>) => t.Node>(
|
|
fn: T,
|
|
): (
|
|
loc: SourceLocation | null | undefined,
|
|
...args: Parameters<T>
|
|
) => ReturnType<T> {
|
|
return (
|
|
loc: SourceLocation | null | undefined,
|
|
...args: Parameters<T>
|
|
): ReturnType<T> => {
|
|
const node = fn(...args);
|
|
if (loc != null && loc != GeneratedSource) {
|
|
node.loc = loc;
|
|
}
|
|
return node as ReturnType<T>;
|
|
};
|
|
}
|
|
|
|
const createBinaryExpression = withLoc(t.binaryExpression);
|
|
const createExpressionStatement = withLoc(t.expressionStatement);
|
|
const _createLabelledStatement = withLoc(t.labeledStatement);
|
|
const createVariableDeclaration = withLoc(t.variableDeclaration);
|
|
const createFunctionDeclaration = withLoc(t.functionDeclaration);
|
|
const _createWhileStatement = withLoc(t.whileStatement);
|
|
const createTaggedTemplateExpression = withLoc(t.taggedTemplateExpression);
|
|
const createLogicalExpression = withLoc(t.logicalExpression);
|
|
const createSequenceExpression = withLoc(t.sequenceExpression);
|
|
const createConditionalExpression = withLoc(t.conditionalExpression);
|
|
const createTemplateLiteral = withLoc(t.templateLiteral);
|
|
const createJsxNamespacedName = withLoc(t.jsxNamespacedName);
|
|
const createJsxElement = withLoc(t.jsxElement);
|
|
const createJsxAttribute = withLoc(t.jsxAttribute);
|
|
const createJsxIdentifier = withLoc(t.jsxIdentifier);
|
|
const createJsxExpressionContainer = withLoc(t.jsxExpressionContainer);
|
|
const createJsxText = withLoc(t.jsxText);
|
|
const createJsxClosingElement = withLoc(t.jsxClosingElement);
|
|
const createJsxOpeningElement = withLoc(t.jsxOpeningElement);
|
|
const createStringLiteral = withLoc(t.stringLiteral);
|
|
|
|
function createHookGuard(
|
|
guard: ExternalFunction,
|
|
context: ProgramContext,
|
|
stmts: Array<t.Statement>,
|
|
before: GuardKind,
|
|
after: GuardKind,
|
|
): t.TryStatement {
|
|
const guardFnName = context.addImportSpecifier(guard).name;
|
|
function createHookGuardImpl(kind: number): t.ExpressionStatement {
|
|
return t.expressionStatement(
|
|
t.callExpression(t.identifier(guardFnName), [t.numericLiteral(kind)]),
|
|
);
|
|
}
|
|
|
|
return t.tryStatement(
|
|
t.blockStatement([createHookGuardImpl(before), ...stmts]),
|
|
null,
|
|
t.blockStatement([createHookGuardImpl(after)]),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Create a call expression.
|
|
* If enableEmitHookGuards is set and the callExpression is a hook call,
|
|
* the following transform will be made.
|
|
* ```js
|
|
* // source
|
|
* useHook(arg1, arg2)
|
|
*
|
|
* // codegen
|
|
* (() => {
|
|
* try {
|
|
* $dispatcherGuard(PUSH_EXPECT_HOOK);
|
|
* return useHook(arg1, arg2);
|
|
* } finally {
|
|
* $dispatcherGuard(POP_EXPECT_HOOK);
|
|
* }
|
|
* })()
|
|
* ```
|
|
*/
|
|
function createCallExpression(
|
|
env: Environment,
|
|
callee: t.Expression,
|
|
args: Array<t.Expression | t.SpreadElement>,
|
|
loc: SourceLocation | null,
|
|
isHook: boolean,
|
|
): t.CallExpression {
|
|
const callExpr = t.callExpression(callee, args);
|
|
if (loc != null && loc != GeneratedSource) {
|
|
callExpr.loc = loc;
|
|
}
|
|
|
|
const hookGuard = env.config.enableEmitHookGuards;
|
|
if (hookGuard != null && isHook && env.isInferredMemoEnabled) {
|
|
const iife = t.functionExpression(
|
|
null,
|
|
[],
|
|
t.blockStatement([
|
|
createHookGuard(
|
|
hookGuard,
|
|
env.programContext,
|
|
[t.returnStatement(callExpr)],
|
|
GuardKind.AllowHook,
|
|
GuardKind.DisallowHook,
|
|
),
|
|
]),
|
|
);
|
|
return t.callExpression(iife, []);
|
|
} else {
|
|
return callExpr;
|
|
}
|
|
}
|
|
|
|
type Temporaries = Map<DeclarationId, t.Expression | t.JSXText | null>;
|
|
|
|
function codegenLabel(id: BlockId): string {
|
|
return `bb${id}`;
|
|
}
|
|
|
|
function codegenInstruction(
|
|
cx: Context,
|
|
instr: ReactiveInstruction,
|
|
value: t.Expression | t.JSXText,
|
|
): t.Statement {
|
|
if (t.isStatement(value)) {
|
|
return value;
|
|
}
|
|
if (instr.lvalue === null) {
|
|
return t.expressionStatement(convertValueToExpression(value));
|
|
}
|
|
if (instr.lvalue.identifier.name === null) {
|
|
// temporary
|
|
cx.temp.set(instr.lvalue.identifier.declarationId, value);
|
|
return t.emptyStatement();
|
|
} else {
|
|
const expressionValue = convertValueToExpression(value);
|
|
if (cx.hasDeclared(instr.lvalue.identifier)) {
|
|
return createExpressionStatement(
|
|
instr.loc,
|
|
t.assignmentExpression(
|
|
'=',
|
|
convertIdentifier(instr.lvalue.identifier),
|
|
expressionValue,
|
|
),
|
|
);
|
|
} else {
|
|
return createVariableDeclaration(instr.loc, 'const', [
|
|
t.variableDeclarator(
|
|
convertIdentifier(instr.lvalue.identifier),
|
|
expressionValue,
|
|
),
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
|
|
function convertValueToExpression(
|
|
value: t.JSXText | t.Expression,
|
|
): t.Expression {
|
|
if (value.type === 'JSXText') {
|
|
return createStringLiteral(value.loc, value.value);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function codegenInstructionValueToExpression(
|
|
cx: Context,
|
|
instrValue: ReactiveValue,
|
|
): t.Expression {
|
|
const value = codegenInstructionValue(cx, instrValue);
|
|
return convertValueToExpression(value);
|
|
}
|
|
|
|
function codegenInstructionValue(
|
|
cx: Context,
|
|
instrValue: ReactiveValue,
|
|
): t.Expression | t.JSXText {
|
|
let value: t.Expression | t.JSXText;
|
|
switch (instrValue.kind) {
|
|
case 'ArrayExpression': {
|
|
const elements = instrValue.elements.map(element => {
|
|
if (element.kind === 'Identifier') {
|
|
return codegenPlaceToExpression(cx, element);
|
|
} else if (element.kind === 'Spread') {
|
|
return t.spreadElement(codegenPlaceToExpression(cx, element.place));
|
|
} else {
|
|
return null;
|
|
}
|
|
});
|
|
value = t.arrayExpression(elements);
|
|
break;
|
|
}
|
|
case 'BinaryExpression': {
|
|
const left = codegenPlaceToExpression(cx, instrValue.left);
|
|
const right = codegenPlaceToExpression(cx, instrValue.right);
|
|
value = createBinaryExpression(
|
|
instrValue.loc,
|
|
instrValue.operator,
|
|
left,
|
|
right,
|
|
);
|
|
break;
|
|
}
|
|
case 'UnaryExpression': {
|
|
value = t.unaryExpression(
|
|
instrValue.operator as 'throw', // todo
|
|
codegenPlaceToExpression(cx, instrValue.value),
|
|
);
|
|
break;
|
|
}
|
|
case 'Primitive': {
|
|
value = codegenValue(cx, instrValue.loc, instrValue.value);
|
|
break;
|
|
}
|
|
case 'CallExpression': {
|
|
if (cx.env.config.enableForest) {
|
|
const callee = codegenPlaceToExpression(cx, instrValue.callee);
|
|
const args = instrValue.args.map(arg => codegenArgument(cx, arg));
|
|
value = t.callExpression(callee, args);
|
|
if (instrValue.typeArguments != null) {
|
|
value.typeArguments = t.typeParameterInstantiation(
|
|
instrValue.typeArguments,
|
|
);
|
|
}
|
|
break;
|
|
}
|
|
const isHook = getHookKind(cx.env, instrValue.callee.identifier) != null;
|
|
const callee = codegenPlaceToExpression(cx, instrValue.callee);
|
|
const args = instrValue.args.map(arg => codegenArgument(cx, arg));
|
|
value = createCallExpression(
|
|
cx.env,
|
|
callee,
|
|
args,
|
|
instrValue.loc,
|
|
isHook,
|
|
);
|
|
break;
|
|
}
|
|
case 'OptionalExpression': {
|
|
const optionalValue = codegenInstructionValueToExpression(
|
|
cx,
|
|
instrValue.value,
|
|
);
|
|
switch (optionalValue.type) {
|
|
case 'OptionalCallExpression':
|
|
case 'CallExpression': {
|
|
CompilerError.invariant(t.isExpression(optionalValue.callee), {
|
|
reason: 'v8 intrinsics are validated during lowering',
|
|
description: null,
|
|
loc: optionalValue.callee.loc ?? null,
|
|
suggestions: null,
|
|
});
|
|
value = t.optionalCallExpression(
|
|
optionalValue.callee,
|
|
optionalValue.arguments,
|
|
instrValue.optional,
|
|
);
|
|
break;
|
|
}
|
|
case 'OptionalMemberExpression':
|
|
case 'MemberExpression': {
|
|
const property = optionalValue.property;
|
|
CompilerError.invariant(t.isExpression(property), {
|
|
reason: 'Private names are validated during lowering',
|
|
description: null,
|
|
loc: property.loc ?? null,
|
|
suggestions: null,
|
|
});
|
|
value = t.optionalMemberExpression(
|
|
optionalValue.object,
|
|
property,
|
|
optionalValue.computed,
|
|
instrValue.optional,
|
|
);
|
|
break;
|
|
}
|
|
default: {
|
|
CompilerError.invariant(false, {
|
|
reason:
|
|
'Expected an optional value to resolve to a call expression or member expression',
|
|
description: `Got a \`${optionalValue.type}\``,
|
|
loc: instrValue.loc,
|
|
suggestions: null,
|
|
});
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case 'MethodCall': {
|
|
const isHook =
|
|
getHookKind(cx.env, instrValue.property.identifier) != null;
|
|
const memberExpr = codegenPlaceToExpression(cx, instrValue.property);
|
|
CompilerError.invariant(
|
|
t.isMemberExpression(memberExpr) ||
|
|
t.isOptionalMemberExpression(memberExpr),
|
|
{
|
|
reason:
|
|
'[Codegen] Internal error: MethodCall::property must be an unpromoted + unmemoized MemberExpression. ' +
|
|
`Got a \`${memberExpr.type}\``,
|
|
description: null,
|
|
loc: memberExpr.loc ?? null,
|
|
suggestions: null,
|
|
},
|
|
);
|
|
CompilerError.invariant(
|
|
t.isNodesEquivalent(
|
|
memberExpr.object,
|
|
codegenPlaceToExpression(cx, instrValue.receiver),
|
|
),
|
|
{
|
|
reason:
|
|
'[Codegen] Internal error: Forget should always generate MethodCall::property ' +
|
|
'as a MemberExpression of MethodCall::receiver',
|
|
description: null,
|
|
loc: memberExpr.loc ?? null,
|
|
suggestions: null,
|
|
},
|
|
);
|
|
const args = instrValue.args.map(arg => codegenArgument(cx, arg));
|
|
value = createCallExpression(
|
|
cx.env,
|
|
memberExpr,
|
|
args,
|
|
instrValue.loc,
|
|
isHook,
|
|
);
|
|
break;
|
|
}
|
|
case 'NewExpression': {
|
|
const callee = codegenPlaceToExpression(cx, instrValue.callee);
|
|
const args = instrValue.args.map(arg => codegenArgument(cx, arg));
|
|
value = t.newExpression(callee, args);
|
|
break;
|
|
}
|
|
case 'ObjectExpression': {
|
|
const properties = [];
|
|
for (const property of instrValue.properties) {
|
|
if (property.kind === 'ObjectProperty') {
|
|
const key = codegenObjectPropertyKey(cx, property.key);
|
|
|
|
switch (property.type) {
|
|
case 'property': {
|
|
const value = codegenPlaceToExpression(cx, property.place);
|
|
properties.push(
|
|
t.objectProperty(
|
|
key,
|
|
value,
|
|
property.key.kind === 'computed',
|
|
key.type === 'Identifier' &&
|
|
value.type === 'Identifier' &&
|
|
value.name === key.name,
|
|
),
|
|
);
|
|
break;
|
|
}
|
|
case 'method': {
|
|
const method = cx.objectMethods.get(property.place.identifier.id);
|
|
CompilerError.invariant(method, {
|
|
reason: 'Expected ObjectMethod instruction',
|
|
loc: null,
|
|
suggestions: null,
|
|
});
|
|
const loweredFunc = method.loweredFunc;
|
|
const reactiveFunction = buildReactiveFunction(loweredFunc.func);
|
|
pruneUnusedLabels(reactiveFunction);
|
|
pruneUnusedLValues(reactiveFunction);
|
|
const fn = codegenReactiveFunction(
|
|
new Context(
|
|
cx.env,
|
|
reactiveFunction.id ?? '[[ anonymous ]]',
|
|
cx.uniqueIdentifiers,
|
|
cx.fbtOperands,
|
|
cx.temp,
|
|
),
|
|
reactiveFunction,
|
|
).unwrap();
|
|
|
|
/*
|
|
* ObjectMethod builder must be backwards compatible with older versions of babel.
|
|
* https://github.com/babel/babel/blob/v7.7.4/packages/babel-types/src/definitions/core.js#L599-L603
|
|
*/
|
|
const babelNode = t.objectMethod(
|
|
'method',
|
|
key,
|
|
fn.params,
|
|
fn.body,
|
|
false,
|
|
);
|
|
babelNode.async = fn.async;
|
|
babelNode.generator = fn.generator;
|
|
properties.push(babelNode);
|
|
break;
|
|
}
|
|
default:
|
|
assertExhaustive(
|
|
property.type,
|
|
`Unexpected property type: ${property.type}`,
|
|
);
|
|
}
|
|
} else {
|
|
properties.push(
|
|
t.spreadElement(codegenPlaceToExpression(cx, property.place)),
|
|
);
|
|
}
|
|
}
|
|
value = t.objectExpression(properties);
|
|
break;
|
|
}
|
|
case 'JSXText': {
|
|
value = createJsxText(instrValue.loc, instrValue.value);
|
|
break;
|
|
}
|
|
case 'JsxExpression': {
|
|
const attributes: Array<t.JSXAttribute | t.JSXSpreadAttribute> = [];
|
|
for (const attribute of instrValue.props) {
|
|
attributes.push(codegenJsxAttribute(cx, attribute));
|
|
}
|
|
let tagValue =
|
|
instrValue.tag.kind === 'Identifier'
|
|
? codegenPlaceToExpression(cx, instrValue.tag)
|
|
: t.stringLiteral(instrValue.tag.name);
|
|
let tag: t.JSXIdentifier | t.JSXNamespacedName | t.JSXMemberExpression;
|
|
if (tagValue.type === 'Identifier') {
|
|
tag = createJsxIdentifier(instrValue.tag.loc, tagValue.name);
|
|
} else if (tagValue.type === 'MemberExpression') {
|
|
tag = convertMemberExpressionToJsx(tagValue);
|
|
} else {
|
|
CompilerError.invariant(tagValue.type === 'StringLiteral', {
|
|
reason: `Expected JSX tag to be an identifier or string, got \`${tagValue.type}\``,
|
|
description: null,
|
|
loc: tagValue.loc ?? null,
|
|
suggestions: null,
|
|
});
|
|
if (tagValue.value.indexOf(':') >= 0) {
|
|
const [namespace, name] = tagValue.value.split(':', 2);
|
|
tag = createJsxNamespacedName(
|
|
instrValue.tag.loc,
|
|
createJsxIdentifier(instrValue.tag.loc, namespace),
|
|
createJsxIdentifier(instrValue.tag.loc, name),
|
|
);
|
|
} else {
|
|
tag = createJsxIdentifier(instrValue.loc, tagValue.value);
|
|
}
|
|
}
|
|
let children;
|
|
if (
|
|
tagValue.type === 'StringLiteral' &&
|
|
SINGLE_CHILD_FBT_TAGS.has(tagValue.value)
|
|
) {
|
|
CompilerError.invariant(instrValue.children != null, {
|
|
loc: instrValue.loc,
|
|
reason: 'Expected fbt element to have children',
|
|
suggestions: null,
|
|
description: null,
|
|
});
|
|
children = instrValue.children.map(child =>
|
|
codegenJsxFbtChildElement(cx, child),
|
|
);
|
|
} else {
|
|
children =
|
|
instrValue.children !== null
|
|
? instrValue.children.map(child => codegenJsxElement(cx, child))
|
|
: [];
|
|
}
|
|
value = createJsxElement(
|
|
instrValue.loc,
|
|
createJsxOpeningElement(
|
|
instrValue.openingLoc,
|
|
tag,
|
|
attributes,
|
|
instrValue.children === null,
|
|
),
|
|
instrValue.children !== null
|
|
? createJsxClosingElement(instrValue.closingLoc, tag)
|
|
: null,
|
|
children,
|
|
instrValue.children === null,
|
|
);
|
|
break;
|
|
}
|
|
case 'JsxFragment': {
|
|
value = t.jsxFragment(
|
|
t.jsxOpeningFragment(),
|
|
t.jsxClosingFragment(),
|
|
instrValue.children.map(child => codegenJsxElement(cx, child)),
|
|
);
|
|
break;
|
|
}
|
|
case 'UnsupportedNode': {
|
|
const node = instrValue.node;
|
|
if (!t.isExpression(node)) {
|
|
return node as any; // TODO handle statements, jsx fragments
|
|
}
|
|
value = node;
|
|
break;
|
|
}
|
|
case 'PropertyStore':
|
|
case 'PropertyLoad':
|
|
case 'PropertyDelete': {
|
|
let memberExpr;
|
|
/*
|
|
* We currently only lower single chains of optional memberexpr.
|
|
* (See BuildHIR.ts for more detail.)
|
|
*/
|
|
if (typeof instrValue.property === 'string') {
|
|
memberExpr = t.memberExpression(
|
|
codegenPlaceToExpression(cx, instrValue.object),
|
|
t.identifier(instrValue.property),
|
|
);
|
|
} else {
|
|
memberExpr = t.memberExpression(
|
|
codegenPlaceToExpression(cx, instrValue.object),
|
|
t.numericLiteral(instrValue.property),
|
|
true,
|
|
);
|
|
}
|
|
if (instrValue.kind === 'PropertyStore') {
|
|
value = t.assignmentExpression(
|
|
'=',
|
|
memberExpr,
|
|
codegenPlaceToExpression(cx, instrValue.value),
|
|
);
|
|
} else if (instrValue.kind === 'PropertyLoad') {
|
|
value = memberExpr;
|
|
} else {
|
|
value = t.unaryExpression('delete', memberExpr);
|
|
}
|
|
break;
|
|
}
|
|
case 'ComputedStore': {
|
|
value = t.assignmentExpression(
|
|
'=',
|
|
t.memberExpression(
|
|
codegenPlaceToExpression(cx, instrValue.object),
|
|
codegenPlaceToExpression(cx, instrValue.property),
|
|
true,
|
|
),
|
|
codegenPlaceToExpression(cx, instrValue.value),
|
|
);
|
|
break;
|
|
}
|
|
case 'ComputedLoad': {
|
|
const object = codegenPlaceToExpression(cx, instrValue.object);
|
|
const property = codegenPlaceToExpression(cx, instrValue.property);
|
|
value = t.memberExpression(object, property, true);
|
|
break;
|
|
}
|
|
case 'ComputedDelete': {
|
|
value = t.unaryExpression(
|
|
'delete',
|
|
t.memberExpression(
|
|
codegenPlaceToExpression(cx, instrValue.object),
|
|
codegenPlaceToExpression(cx, instrValue.property),
|
|
true,
|
|
),
|
|
);
|
|
break;
|
|
}
|
|
case 'LoadLocal':
|
|
case 'LoadContext': {
|
|
value = codegenPlaceToExpression(cx, instrValue.place);
|
|
break;
|
|
}
|
|
case 'FunctionExpression': {
|
|
const loweredFunc = instrValue.loweredFunc.func;
|
|
const reactiveFunction = buildReactiveFunction(loweredFunc);
|
|
pruneUnusedLabels(reactiveFunction);
|
|
pruneUnusedLValues(reactiveFunction);
|
|
pruneHoistedContexts(reactiveFunction);
|
|
const fn = codegenReactiveFunction(
|
|
new Context(
|
|
cx.env,
|
|
reactiveFunction.id ?? '[[ anonymous ]]',
|
|
cx.uniqueIdentifiers,
|
|
cx.fbtOperands,
|
|
cx.temp,
|
|
),
|
|
reactiveFunction,
|
|
).unwrap();
|
|
if (instrValue.type === 'ArrowFunctionExpression') {
|
|
let body: t.BlockStatement | t.Expression = fn.body;
|
|
if (body.body.length === 1 && loweredFunc.directives.length == 0) {
|
|
const stmt = body.body[0]!;
|
|
if (stmt.type === 'ReturnStatement' && stmt.argument != null) {
|
|
body = stmt.argument;
|
|
}
|
|
}
|
|
value = t.arrowFunctionExpression(fn.params, body, fn.async);
|
|
} else {
|
|
value = t.functionExpression(
|
|
fn.id ??
|
|
(instrValue.name != null ? t.identifier(instrValue.name) : null),
|
|
fn.params,
|
|
fn.body,
|
|
fn.generator,
|
|
fn.async,
|
|
);
|
|
}
|
|
break;
|
|
}
|
|
case 'TaggedTemplateExpression': {
|
|
value = createTaggedTemplateExpression(
|
|
instrValue.loc,
|
|
codegenPlaceToExpression(cx, instrValue.tag),
|
|
t.templateLiteral([t.templateElement(instrValue.value)], []),
|
|
);
|
|
break;
|
|
}
|
|
case 'TypeCastExpression': {
|
|
if (t.isTSType(instrValue.typeAnnotation)) {
|
|
if (instrValue.typeAnnotationKind === 'satisfies') {
|
|
value = t.tsSatisfiesExpression(
|
|
codegenPlaceToExpression(cx, instrValue.value),
|
|
instrValue.typeAnnotation,
|
|
);
|
|
} else {
|
|
value = t.tsAsExpression(
|
|
codegenPlaceToExpression(cx, instrValue.value),
|
|
instrValue.typeAnnotation,
|
|
);
|
|
}
|
|
} else {
|
|
value = t.typeCastExpression(
|
|
codegenPlaceToExpression(cx, instrValue.value),
|
|
t.typeAnnotation(instrValue.typeAnnotation),
|
|
);
|
|
}
|
|
break;
|
|
}
|
|
case 'LogicalExpression': {
|
|
value = createLogicalExpression(
|
|
instrValue.loc,
|
|
instrValue.operator,
|
|
codegenInstructionValueToExpression(cx, instrValue.left),
|
|
codegenInstructionValueToExpression(cx, instrValue.right),
|
|
);
|
|
break;
|
|
}
|
|
case 'ConditionalExpression': {
|
|
value = createConditionalExpression(
|
|
instrValue.loc,
|
|
codegenInstructionValueToExpression(cx, instrValue.test),
|
|
codegenInstructionValueToExpression(cx, instrValue.consequent),
|
|
codegenInstructionValueToExpression(cx, instrValue.alternate),
|
|
);
|
|
break;
|
|
}
|
|
case 'SequenceExpression': {
|
|
const body = codegenBlockNoReset(
|
|
cx,
|
|
instrValue.instructions.map(instruction => ({
|
|
kind: 'instruction',
|
|
instruction,
|
|
})),
|
|
).body;
|
|
const expressions = body.map(stmt => {
|
|
if (stmt.type === 'ExpressionStatement') {
|
|
return stmt.expression;
|
|
} else {
|
|
if (t.isVariableDeclaration(stmt)) {
|
|
const declarator = stmt.declarations[0];
|
|
cx.errors.push({
|
|
reason: `(CodegenReactiveFunction::codegenInstructionValue) Cannot declare variables in a value block, tried to declare '${
|
|
(declarator.id as t.Identifier).name
|
|
}'`,
|
|
severity: ErrorSeverity.Todo,
|
|
loc: declarator.loc ?? null,
|
|
suggestions: null,
|
|
});
|
|
return t.stringLiteral(`TODO handle ${declarator.id}`);
|
|
} else {
|
|
cx.errors.push({
|
|
reason: `(CodegenReactiveFunction::codegenInstructionValue) Handle conversion of ${stmt.type} to expression`,
|
|
severity: ErrorSeverity.Todo,
|
|
loc: stmt.loc ?? null,
|
|
suggestions: null,
|
|
});
|
|
return t.stringLiteral(`TODO handle ${stmt.type}`);
|
|
}
|
|
}
|
|
});
|
|
if (expressions.length === 0) {
|
|
value = codegenInstructionValueToExpression(cx, instrValue.value);
|
|
} else {
|
|
value = createSequenceExpression(instrValue.loc, [
|
|
...expressions,
|
|
codegenInstructionValueToExpression(cx, instrValue.value),
|
|
]);
|
|
}
|
|
break;
|
|
}
|
|
case 'TemplateLiteral': {
|
|
value = createTemplateLiteral(
|
|
instrValue.loc,
|
|
instrValue.quasis.map(q => t.templateElement(q)),
|
|
instrValue.subexprs.map(p => codegenPlaceToExpression(cx, p)),
|
|
);
|
|
break;
|
|
}
|
|
case 'LoadGlobal': {
|
|
value = t.identifier(instrValue.binding.name);
|
|
break;
|
|
}
|
|
case 'RegExpLiteral': {
|
|
value = t.regExpLiteral(instrValue.pattern, instrValue.flags);
|
|
break;
|
|
}
|
|
case 'MetaProperty': {
|
|
value = t.metaProperty(
|
|
t.identifier(instrValue.meta),
|
|
t.identifier(instrValue.property),
|
|
);
|
|
break;
|
|
}
|
|
case 'Await': {
|
|
value = t.awaitExpression(codegenPlaceToExpression(cx, instrValue.value));
|
|
break;
|
|
}
|
|
case 'GetIterator': {
|
|
value = codegenPlaceToExpression(cx, instrValue.collection);
|
|
break;
|
|
}
|
|
case 'IteratorNext': {
|
|
value = codegenPlaceToExpression(cx, instrValue.iterator);
|
|
break;
|
|
}
|
|
case 'NextPropertyOf': {
|
|
value = codegenPlaceToExpression(cx, instrValue.value);
|
|
break;
|
|
}
|
|
case 'PostfixUpdate': {
|
|
value = t.updateExpression(
|
|
instrValue.operation,
|
|
codegenPlaceToExpression(cx, instrValue.lvalue),
|
|
false,
|
|
);
|
|
break;
|
|
}
|
|
case 'PrefixUpdate': {
|
|
value = t.updateExpression(
|
|
instrValue.operation,
|
|
codegenPlaceToExpression(cx, instrValue.lvalue),
|
|
true,
|
|
);
|
|
break;
|
|
}
|
|
case 'StoreLocal': {
|
|
CompilerError.invariant(
|
|
instrValue.lvalue.kind === InstructionKind.Reassign,
|
|
{
|
|
reason: `Unexpected StoreLocal in codegenInstructionValue`,
|
|
description: null,
|
|
loc: instrValue.loc,
|
|
suggestions: null,
|
|
},
|
|
);
|
|
value = t.assignmentExpression(
|
|
'=',
|
|
codegenLValue(cx, instrValue.lvalue.place),
|
|
codegenPlaceToExpression(cx, instrValue.value),
|
|
);
|
|
break;
|
|
}
|
|
case 'StoreGlobal': {
|
|
value = t.assignmentExpression(
|
|
'=',
|
|
t.identifier(instrValue.name),
|
|
codegenPlaceToExpression(cx, instrValue.value),
|
|
);
|
|
break;
|
|
}
|
|
case 'StartMemoize':
|
|
case 'FinishMemoize':
|
|
case 'Debugger':
|
|
case 'DeclareLocal':
|
|
case 'DeclareContext':
|
|
case 'Destructure':
|
|
case 'ObjectMethod':
|
|
case 'StoreContext': {
|
|
CompilerError.invariant(false, {
|
|
reason: `Unexpected ${instrValue.kind} in codegenInstructionValue`,
|
|
description: null,
|
|
loc: instrValue.loc,
|
|
suggestions: null,
|
|
});
|
|
}
|
|
default: {
|
|
assertExhaustive(
|
|
instrValue,
|
|
`Unexpected instruction value kind \`${(instrValue as any).kind}\``,
|
|
);
|
|
}
|
|
}
|
|
return value;
|
|
}
|
|
|
|
/**
|
|
* Due to a bug in earlier Babel versions, JSX string attributes with double quotes, unicode characters, or special
|
|
* control characters such as \n may be escaped unnecessarily. To avoid trigger this Babel bug, we use a
|
|
* JsxExpressionContainer for such strings.
|
|
*
|
|
* u0000 to u001F: C0 control codes
|
|
* u007F : Delete character
|
|
* u0080 to u009F: C1 control codes
|
|
* u00A0 to uFFFF: All non-basic Latin characters
|
|
* https://en.wikipedia.org/wiki/List_of_Unicode_characters#Control_codes
|
|
*/
|
|
const STRING_REQUIRES_EXPR_CONTAINER_PATTERN =
|
|
/[\u{0000}-\u{001F}\u{007F}\u{0080}-\u{FFFF}]|"|\\/u;
|
|
function codegenJsxAttribute(
|
|
cx: Context,
|
|
attribute: JsxAttribute,
|
|
): t.JSXAttribute | t.JSXSpreadAttribute {
|
|
switch (attribute.kind) {
|
|
case 'JsxAttribute': {
|
|
let propName: t.JSXIdentifier | t.JSXNamespacedName;
|
|
if (attribute.name.indexOf(':') === -1) {
|
|
propName = createJsxIdentifier(attribute.place.loc, attribute.name);
|
|
} else {
|
|
const [namespace, name] = attribute.name.split(':', 2);
|
|
propName = createJsxNamespacedName(
|
|
attribute.place.loc,
|
|
createJsxIdentifier(attribute.place.loc, namespace),
|
|
createJsxIdentifier(attribute.place.loc, name),
|
|
);
|
|
}
|
|
const innerValue = codegenPlaceToExpression(cx, attribute.place);
|
|
let value;
|
|
switch (innerValue.type) {
|
|
case 'StringLiteral': {
|
|
value = innerValue;
|
|
if (
|
|
STRING_REQUIRES_EXPR_CONTAINER_PATTERN.test(value.value) &&
|
|
!cx.fbtOperands.has(attribute.place.identifier.id)
|
|
) {
|
|
value = createJsxExpressionContainer(value.loc, value);
|
|
}
|
|
break;
|
|
}
|
|
default: {
|
|
/*
|
|
* NOTE JSXFragment is technically allowed as an attribute value per the spec
|
|
* but many tools do not support this case. We emit fragments wrapped in an
|
|
* expression container for compatibility purposes.
|
|
* spec: https://github.com/facebook/jsx/blob/main/AST.md#jsx-attributes
|
|
*/
|
|
value = createJsxExpressionContainer(attribute.place.loc, innerValue);
|
|
break;
|
|
}
|
|
}
|
|
return createJsxAttribute(attribute.place.loc, propName, value);
|
|
}
|
|
case 'JsxSpreadAttribute': {
|
|
return t.jsxSpreadAttribute(
|
|
codegenPlaceToExpression(cx, attribute.argument),
|
|
);
|
|
}
|
|
default: {
|
|
assertExhaustive(
|
|
attribute,
|
|
`Unexpected attribute kind \`${(attribute as any).kind}\``,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
const JSX_TEXT_CHILD_REQUIRES_EXPR_CONTAINER_PATTERN = /[<>&{}]/;
|
|
function codegenJsxElement(
|
|
cx: Context,
|
|
place: Place,
|
|
):
|
|
| t.JSXText
|
|
| t.JSXExpressionContainer
|
|
| t.JSXSpreadChild
|
|
| t.JSXElement
|
|
| t.JSXFragment {
|
|
const value = codegenPlace(cx, place);
|
|
switch (value.type) {
|
|
case 'JSXText': {
|
|
if (JSX_TEXT_CHILD_REQUIRES_EXPR_CONTAINER_PATTERN.test(value.value)) {
|
|
return createJsxExpressionContainer(
|
|
place.loc,
|
|
createStringLiteral(place.loc, value.value),
|
|
);
|
|
}
|
|
return createJsxText(place.loc, value.value);
|
|
}
|
|
case 'JSXElement':
|
|
case 'JSXFragment': {
|
|
return value;
|
|
}
|
|
default: {
|
|
return createJsxExpressionContainer(place.loc, value);
|
|
}
|
|
}
|
|
}
|
|
|
|
function codegenJsxFbtChildElement(
|
|
cx: Context,
|
|
place: Place,
|
|
):
|
|
| t.JSXText
|
|
| t.JSXExpressionContainer
|
|
| t.JSXSpreadChild
|
|
| t.JSXElement
|
|
| t.JSXFragment {
|
|
const value = codegenPlace(cx, place);
|
|
switch (value.type) {
|
|
// fbt:param only allows JSX element or expression container as children
|
|
case 'JSXText':
|
|
case 'JSXElement': {
|
|
return value;
|
|
}
|
|
default: {
|
|
return createJsxExpressionContainer(place.loc, value);
|
|
}
|
|
}
|
|
}
|
|
|
|
function convertMemberExpressionToJsx(
|
|
expr: t.MemberExpression,
|
|
): t.JSXMemberExpression {
|
|
CompilerError.invariant(expr.property.type === 'Identifier', {
|
|
reason: 'Expected JSX member expression property to be a string',
|
|
description: null,
|
|
loc: expr.loc ?? null,
|
|
suggestions: null,
|
|
});
|
|
const property = t.jsxIdentifier(expr.property.name);
|
|
if (expr.object.type === 'Identifier') {
|
|
return t.jsxMemberExpression(t.jsxIdentifier(expr.object.name), property);
|
|
} else {
|
|
CompilerError.invariant(expr.object.type === 'MemberExpression', {
|
|
reason:
|
|
'Expected JSX member expression to be an identifier or nested member expression',
|
|
description: null,
|
|
loc: expr.object.loc ?? null,
|
|
suggestions: null,
|
|
});
|
|
const object = convertMemberExpressionToJsx(expr.object);
|
|
return t.jsxMemberExpression(object, property);
|
|
}
|
|
}
|
|
|
|
function codegenObjectPropertyKey(
|
|
cx: Context,
|
|
key: ObjectPropertyKey,
|
|
): t.Expression {
|
|
switch (key.kind) {
|
|
case 'string': {
|
|
return t.stringLiteral(key.name);
|
|
}
|
|
case 'identifier': {
|
|
return t.identifier(key.name);
|
|
}
|
|
case 'computed': {
|
|
const expr = codegenPlace(cx, key.name);
|
|
CompilerError.invariant(t.isExpression(expr), {
|
|
reason: 'Expected object property key to be an expression',
|
|
description: null,
|
|
loc: key.name.loc,
|
|
suggestions: null,
|
|
});
|
|
return expr;
|
|
}
|
|
case 'number': {
|
|
return t.numericLiteral(key.name);
|
|
}
|
|
}
|
|
}
|
|
|
|
function codegenArrayPattern(
|
|
cx: Context,
|
|
pattern: ArrayPattern,
|
|
): t.ArrayPattern {
|
|
const hasHoles = !pattern.items.every(e => e.kind !== 'Hole');
|
|
if (hasHoles) {
|
|
const result = t.arrayPattern([]);
|
|
/*
|
|
* Older versions of babel have a validation bug fixed by
|
|
* https://github.com/babel/babel/pull/10917
|
|
* https://github.com/babel/babel/commit/e7b80a2cb93cf28010207fc3cdd19b4568ca35b9#diff-19b555d2f3904c206af406540d9df200b1e16befedb83ff39ebfcbd876f7fa8aL52
|
|
*
|
|
* Link to buggy older version (observe that elements must be PatternLikes here)
|
|
* https://github.com/babel/babel/blob/v7.7.4/packages/babel-types/src/definitions/es2015.js#L50-L53
|
|
*
|
|
* Link to newer versions with correct validation (observe elements can be PatternLike | null)
|
|
* https://github.com/babel/babel/blob/v7.23.0/packages/babel-types/src/definitions/core.ts#L1306-L1311
|
|
*/
|
|
for (const item of pattern.items) {
|
|
if (item.kind === 'Hole') {
|
|
result.elements.push(null);
|
|
} else {
|
|
result.elements.push(codegenLValue(cx, item));
|
|
}
|
|
}
|
|
return result;
|
|
} else {
|
|
return t.arrayPattern(
|
|
pattern.items.map(item => {
|
|
if (item.kind === 'Hole') {
|
|
return null;
|
|
}
|
|
return codegenLValue(cx, item);
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
|
|
function codegenLValue(
|
|
cx: Context,
|
|
pattern: Pattern | Place | SpreadPattern,
|
|
): t.ArrayPattern | t.ObjectPattern | t.RestElement | t.Identifier {
|
|
switch (pattern.kind) {
|
|
case 'ArrayPattern': {
|
|
return codegenArrayPattern(cx, pattern);
|
|
}
|
|
case 'ObjectPattern': {
|
|
return t.objectPattern(
|
|
pattern.properties.map(property => {
|
|
if (property.kind === 'ObjectProperty') {
|
|
const key = codegenObjectPropertyKey(cx, property.key);
|
|
const value = codegenLValue(cx, property.place);
|
|
return t.objectProperty(
|
|
key,
|
|
value,
|
|
property.key.kind === 'computed',
|
|
key.type === 'Identifier' &&
|
|
value.type === 'Identifier' &&
|
|
value.name === key.name,
|
|
);
|
|
} else {
|
|
return t.restElement(codegenLValue(cx, property.place));
|
|
}
|
|
}),
|
|
);
|
|
}
|
|
case 'Spread': {
|
|
return t.restElement(codegenLValue(cx, pattern.place));
|
|
}
|
|
case 'Identifier': {
|
|
return convertIdentifier(pattern.identifier);
|
|
}
|
|
default: {
|
|
assertExhaustive(
|
|
pattern,
|
|
`Unexpected pattern kind \`${(pattern as any).kind}\``,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
function codegenValue(
|
|
cx: Context,
|
|
loc: SourceLocation,
|
|
value: boolean | number | string | null | undefined,
|
|
): t.Expression {
|
|
if (typeof value === 'number') {
|
|
return t.numericLiteral(value);
|
|
} else if (typeof value === 'boolean') {
|
|
return t.booleanLiteral(value);
|
|
} else if (typeof value === 'string') {
|
|
return createStringLiteral(loc, value);
|
|
} else if (value === null) {
|
|
return t.nullLiteral();
|
|
} else if (value === undefined) {
|
|
return t.identifier('undefined');
|
|
} else {
|
|
assertExhaustive(value, 'Unexpected primitive value kind');
|
|
}
|
|
}
|
|
|
|
function codegenArgument(
|
|
cx: Context,
|
|
arg: Place | SpreadPattern,
|
|
): t.Expression | t.SpreadElement {
|
|
if (arg.kind === 'Identifier') {
|
|
return codegenPlaceToExpression(cx, arg);
|
|
} else {
|
|
return t.spreadElement(codegenPlaceToExpression(cx, arg.place));
|
|
}
|
|
}
|
|
|
|
function codegenPlaceToExpression(cx: Context, place: Place): t.Expression {
|
|
const value = codegenPlace(cx, place);
|
|
return convertValueToExpression(value);
|
|
}
|
|
|
|
function codegenPlace(cx: Context, place: Place): t.Expression | t.JSXText {
|
|
let tmp = cx.temp.get(place.identifier.declarationId);
|
|
if (tmp != null) {
|
|
return tmp;
|
|
}
|
|
CompilerError.invariant(place.identifier.name !== null || tmp !== undefined, {
|
|
reason: `[Codegen] No value found for temporary`,
|
|
description: `Value for '${printPlace(
|
|
place,
|
|
)}' was not set in the codegen context`,
|
|
loc: place.loc,
|
|
suggestions: null,
|
|
});
|
|
const identifier = convertIdentifier(place.identifier);
|
|
identifier.loc = place.loc as any;
|
|
return identifier;
|
|
}
|
|
|
|
function convertIdentifier(identifier: Identifier): t.Identifier {
|
|
CompilerError.invariant(
|
|
identifier.name !== null && identifier.name.kind === 'named',
|
|
{
|
|
reason: `Expected temporaries to be promoted to named identifiers in an earlier pass`,
|
|
loc: GeneratedSource,
|
|
description: `identifier ${identifier.id} is unnamed`,
|
|
suggestions: null,
|
|
},
|
|
);
|
|
return t.identifier(identifier.name.value);
|
|
}
|
|
|
|
function compareScopeDependency(
|
|
a: ReactiveScopeDependency,
|
|
b: ReactiveScopeDependency,
|
|
): number {
|
|
CompilerError.invariant(
|
|
a.identifier.name?.kind === 'named' && b.identifier.name?.kind === 'named',
|
|
{
|
|
reason: '[Codegen] Expected named identifier for dependency',
|
|
loc: a.identifier.loc,
|
|
},
|
|
);
|
|
const aName = [
|
|
a.identifier.name.value,
|
|
...a.path.map(entry => `${entry.optional ? '?' : ''}${entry.property}`),
|
|
].join('.');
|
|
const bName = [
|
|
b.identifier.name.value,
|
|
...b.path.map(entry => `${entry.optional ? '?' : ''}${entry.property}`),
|
|
].join('.');
|
|
if (aName < bName) return -1;
|
|
else if (aName > bName) return 1;
|
|
else return 0;
|
|
}
|
|
|
|
function compareScopeDeclaration(
|
|
a: ReactiveScopeDeclaration,
|
|
b: ReactiveScopeDeclaration,
|
|
): number {
|
|
CompilerError.invariant(
|
|
a.identifier.name?.kind === 'named' && b.identifier.name?.kind === 'named',
|
|
{
|
|
reason: '[Codegen] Expected named identifier for declaration',
|
|
loc: a.identifier.loc,
|
|
},
|
|
);
|
|
const aName = a.identifier.name.value;
|
|
const bName = b.identifier.name.value;
|
|
if (aName < bName) return -1;
|
|
else if (aName > bName) return 1;
|
|
else return 0;
|
|
}
|