mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
a0dc166991
Sorry about the thrash in advance! This removes the top level `forget` directory which adds unnecessary nesting to our repo Hopefully everything still works
1359 lines
41 KiB
TypeScript
1359 lines
41 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 { pruneUnusedLValues, pruneUnusedLabels, renameVariables } from ".";
|
|
import { CompilerError, ErrorSeverity } from "../CompilerError";
|
|
import { Environment } from "../HIR";
|
|
import {
|
|
BlockId,
|
|
GeneratedSource,
|
|
Identifier,
|
|
IdentifierId,
|
|
InstructionKind,
|
|
JsxAttribute,
|
|
Pattern,
|
|
Place,
|
|
ReactiveBlock,
|
|
ReactiveFunction,
|
|
ReactiveInstruction,
|
|
ReactiveScope,
|
|
ReactiveScopeDependency,
|
|
ReactiveTerminal,
|
|
ReactiveValue,
|
|
SourceLocation,
|
|
SpreadPattern,
|
|
} from "../HIR/HIR";
|
|
import { printPlace } from "../HIR/PrintHIR";
|
|
import { eachPatternOperand } from "../HIR/visitors";
|
|
import { Err, Ok, Result } from "../Utils/Result";
|
|
import { assertExhaustive } from "../Utils/utils";
|
|
import { buildReactiveFunction } from "./BuildReactiveFunction";
|
|
|
|
export function codegenReactiveFunction(
|
|
fn: ReactiveFunction
|
|
): Result<t.FunctionDeclaration, CompilerError> {
|
|
const cx = new Context(fn.env, fn.id ?? "[[ anonymous ]]");
|
|
for (const param of fn.params) {
|
|
cx.temp.set(param.identifier.id, null);
|
|
}
|
|
|
|
const params = fn.params.map((param) => convertIdentifier(param.identifier));
|
|
const body = codegenBlock(cx, fn.body);
|
|
const statements = body.body;
|
|
if (statements.length !== 0) {
|
|
const last = statements[statements.length - 1];
|
|
if (last.type === "ReturnStatement" && last.argument == null) {
|
|
statements.pop();
|
|
}
|
|
}
|
|
const cacheCount = cx.nextCacheIndex;
|
|
if (cacheCount !== 0) {
|
|
// The import declaration for `useMemoCache` is inserted in the Babel plugin
|
|
statements.unshift(
|
|
t.variableDeclaration("const", [
|
|
t.variableDeclarator(
|
|
t.identifier("$"),
|
|
t.callExpression(t.identifier("useMemoCache"), [
|
|
t.numericLiteral(cacheCount),
|
|
])
|
|
),
|
|
])
|
|
);
|
|
}
|
|
|
|
if (cx.errors.hasErrors()) {
|
|
return Err(cx.errors);
|
|
}
|
|
|
|
return Ok(
|
|
createFunctionDeclaration(
|
|
fn.loc,
|
|
fn.id !== null ? t.identifier(fn.id) : null,
|
|
params,
|
|
body,
|
|
fn.generator,
|
|
fn.async
|
|
)
|
|
);
|
|
}
|
|
|
|
class Context {
|
|
env: Environment;
|
|
fnName: string;
|
|
#nextCacheIndex: number = 0;
|
|
#declarations: Set<IdentifierId> = new Set();
|
|
temp: Temporaries = new Map();
|
|
errors: CompilerError = new CompilerError();
|
|
constructor(env: Environment, fnName: string) {
|
|
this.env = env;
|
|
this.fnName = fnName;
|
|
}
|
|
get nextCacheIndex(): number {
|
|
return this.#nextCacheIndex++;
|
|
}
|
|
|
|
declare(identifier: Identifier): void {
|
|
this.#declarations.add(identifier.id);
|
|
}
|
|
|
|
hasDeclared(identifier: Identifier): boolean {
|
|
return this.#declarations.has(identifier.id);
|
|
}
|
|
}
|
|
|
|
function codegenBlock(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 "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) {
|
|
const block =
|
|
statement.type === "BlockStatement" && statement.body.length === 1
|
|
? statement.body[0]
|
|
: statement;
|
|
statements.push(
|
|
t.labeledStatement(t.identifier(codegenLabel(item.label)), 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.enableEmitFreeze != null) {
|
|
// The import declaration for emitFreeze is inserted in the Babel plugin
|
|
return t.conditionalExpression(
|
|
t.identifier("__DEV__"),
|
|
t.callExpression(
|
|
t.identifier(cx.env.enableEmitFreeze.importSpecifierName),
|
|
[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 changeIdentifiers: Array<t.Identifier> = [];
|
|
for (const dep of scope.dependencies) {
|
|
const index = cx.nextCacheIndex;
|
|
const changeIdentifier = t.identifier(`c_${index}`);
|
|
const depValue = codegenDependency(cx, dep);
|
|
|
|
changeIdentifiers.push(changeIdentifier);
|
|
statements.push(
|
|
t.variableDeclaration("const", [
|
|
t.variableDeclarator(
|
|
changeIdentifier,
|
|
t.binaryExpression(
|
|
"!==",
|
|
t.memberExpression(
|
|
t.identifier("$"),
|
|
t.numericLiteral(index),
|
|
true
|
|
),
|
|
depValue
|
|
)
|
|
),
|
|
])
|
|
);
|
|
cacheStoreStatements.push(
|
|
t.expressionStatement(
|
|
t.assignmentExpression(
|
|
"=",
|
|
t.memberExpression(t.identifier("$"), t.numericLiteral(index), true),
|
|
depValue
|
|
)
|
|
)
|
|
);
|
|
}
|
|
let firstOutputIndex: number | null = null;
|
|
for (const [, { identifier }] of scope.declarations) {
|
|
const index = cx.nextCacheIndex;
|
|
if (firstOutputIndex === null) {
|
|
firstOutputIndex = index;
|
|
}
|
|
|
|
CompilerError.invariant(identifier.name != null, {
|
|
reason: `Expected identifier '@${identifier.id}' to be named`,
|
|
description: null,
|
|
loc: null,
|
|
suggestions: null,
|
|
});
|
|
|
|
const name = convertIdentifier(identifier);
|
|
if (!cx.hasDeclared(identifier)) {
|
|
statements.push(
|
|
t.variableDeclaration("let", [t.variableDeclarator(name)])
|
|
);
|
|
}
|
|
cacheStoreStatements.push(
|
|
t.expressionStatement(
|
|
t.assignmentExpression(
|
|
"=",
|
|
t.memberExpression(t.identifier("$"), t.numericLiteral(index), true),
|
|
wrapCacheDep(cx, name)
|
|
)
|
|
)
|
|
);
|
|
cacheLoadStatements.push(
|
|
t.expressionStatement(
|
|
t.assignmentExpression(
|
|
"=",
|
|
name,
|
|
t.memberExpression(t.identifier("$"), t.numericLiteral(index), true)
|
|
)
|
|
)
|
|
);
|
|
cx.declare(identifier);
|
|
}
|
|
for (const reassignment of scope.reassignments) {
|
|
const index = cx.nextCacheIndex;
|
|
if (firstOutputIndex === null) {
|
|
firstOutputIndex = index;
|
|
}
|
|
const name = convertIdentifier(reassignment);
|
|
|
|
cacheStoreStatements.push(
|
|
t.expressionStatement(
|
|
t.assignmentExpression(
|
|
"=",
|
|
t.memberExpression(t.identifier("$"), t.numericLiteral(index), true),
|
|
wrapCacheDep(cx, name)
|
|
)
|
|
)
|
|
);
|
|
cacheLoadStatements.push(
|
|
t.expressionStatement(
|
|
t.assignmentExpression(
|
|
"=",
|
|
name,
|
|
t.memberExpression(t.identifier("$"), t.numericLiteral(index), true)
|
|
)
|
|
)
|
|
);
|
|
}
|
|
let testCondition = (changeIdentifiers 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 '@${scope.id}' to have at least one declaration`,
|
|
description: null,
|
|
loc: null,
|
|
suggestions: null,
|
|
});
|
|
testCondition = t.binaryExpression(
|
|
"===",
|
|
t.memberExpression(
|
|
t.identifier("$"),
|
|
t.numericLiteral(firstOutputIndex),
|
|
true
|
|
),
|
|
t.callExpression(
|
|
t.memberExpression(t.identifier("Symbol"), t.identifier("for")),
|
|
[t.stringLiteral("react.memo_cache_sentinel")]
|
|
)
|
|
);
|
|
}
|
|
|
|
const computationBlock = codegenBlock(cx, block);
|
|
computationBlock.body.push(...cacheStoreStatements);
|
|
const memoBlock = t.blockStatement(cacheLoadStatements);
|
|
statements.push(t.ifStatement(testCondition, computationBlock, memoBlock));
|
|
}
|
|
|
|
function codegenTerminal(
|
|
cx: Context,
|
|
terminal: ReactiveTerminal
|
|
): t.Statement | null {
|
|
switch (terminal.kind) {
|
|
case "break": {
|
|
if (terminal.implicit) {
|
|
return null;
|
|
}
|
|
return t.breakStatement(
|
|
terminal.label !== null
|
|
? t.identifier(codegenLabel(terminal.label))
|
|
: null
|
|
);
|
|
}
|
|
case "continue": {
|
|
if (terminal.implicit) {
|
|
return null;
|
|
}
|
|
return t.continueStatement(
|
|
terminal.label !== null
|
|
? t.identifier(codegenLabel(terminal.label))
|
|
: null
|
|
);
|
|
}
|
|
case "for": {
|
|
return t.forStatement(
|
|
codegenForInit(cx, terminal.init),
|
|
codegenInstructionValue(cx, terminal.test),
|
|
terminal.update !== null
|
|
? codegenInstructionValue(cx, terminal.update)
|
|
: null,
|
|
codegenBlock(cx, terminal.loop)
|
|
);
|
|
}
|
|
case "for-of": {
|
|
CompilerError.invariant(terminal.init.kind === "SequenceExpression", {
|
|
reason: `Expected a sequence expression init for ForOf`,
|
|
description: `Got '${terminal.init.kind}' expression instead`,
|
|
loc: terminal.init.loc,
|
|
suggestions: null,
|
|
});
|
|
if (terminal.init.instructions.length !== 2) {
|
|
CompilerError.todo({
|
|
reason: "Support non-trivial ForOf 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(iterableItem.value.lvalue.place);
|
|
break;
|
|
}
|
|
case "Destructure": {
|
|
lval = codegenLValue(iterableItem.value.lvalue.pattern);
|
|
break;
|
|
}
|
|
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,
|
|
});
|
|
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),
|
|
]),
|
|
codegenInstructionValue(cx, iterableCollection.value),
|
|
codegenBlock(cx, terminal.loop)
|
|
);
|
|
}
|
|
case "if": {
|
|
const test = codegenPlace(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 = codegenPlace(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(
|
|
codegenPlace(cx, terminal.test),
|
|
terminal.cases.map((case_) => {
|
|
const test =
|
|
case_.test !== null ? codegenPlace(cx, case_.test) : null;
|
|
const block = codegenBlock(cx, case_.block!);
|
|
return t.switchCase(test, [block]);
|
|
})
|
|
);
|
|
}
|
|
case "throw": {
|
|
return t.throwStatement(codegenPlace(cx, terminal.value));
|
|
}
|
|
case "do-while": {
|
|
const test = codegenInstructionValue(cx, terminal.test);
|
|
return t.doWhileStatement(test, codegenBlock(cx, terminal.loop));
|
|
}
|
|
case "while": {
|
|
const test = codegenInstructionValue(cx, terminal.test);
|
|
return t.whileStatement(test, codegenBlock(cx, terminal.loop));
|
|
}
|
|
case "label": {
|
|
return codegenBlock(cx, terminal.block);
|
|
}
|
|
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 = codegenPlace(cx, instr.value.value);
|
|
} else if (instr.value.kind === "StoreContext") {
|
|
lvalue = instr.value.lvalue.place;
|
|
value = codegenPlace(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 hasReasign = false;
|
|
let hasDeclaration = false;
|
|
for (const place of eachPatternOperand(lvalue)) {
|
|
if (
|
|
kind !== InstructionKind.Reassign &&
|
|
place.identifier.name === null
|
|
) {
|
|
cx.temp.set(place.identifier.id, null);
|
|
}
|
|
const isDeclared = cx.hasDeclared(place.identifier);
|
|
hasReasign ||= isDeclared;
|
|
hasDeclaration ||= !isDeclared;
|
|
}
|
|
if (hasReasign && 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 (hasReasign) {
|
|
kind = InstructionKind.Reassign;
|
|
}
|
|
value = codegenPlace(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(lvalue), value),
|
|
]);
|
|
}
|
|
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(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(lvalue), value);
|
|
if (instr.lvalue !== null) {
|
|
if (instr.value.kind !== "StoreContext") {
|
|
cx.temp.set(instr.lvalue.identifier.id, 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);
|
|
}
|
|
}
|
|
default: {
|
|
assertExhaustive(kind, `Unexpected instruction kind '${kind}'`);
|
|
}
|
|
}
|
|
} else if (instr.value.kind === "Debugger") {
|
|
return t.debuggerStatement();
|
|
} 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 declaration = body[0]!;
|
|
CompilerError.invariant(declaration.type === "VariableDeclaration", {
|
|
reason: "Expected a variable declaration",
|
|
description: null,
|
|
loc: declaration.loc ?? null,
|
|
suggestions: null,
|
|
});
|
|
return declaration;
|
|
} else {
|
|
return codegenInstructionValue(cx, init);
|
|
}
|
|
}
|
|
|
|
function codegenDependency(
|
|
cx: Context,
|
|
dependency: ReactiveScopeDependency
|
|
): t.Expression {
|
|
let object: t.Expression = convertIdentifier(dependency.identifier);
|
|
if (dependency.path !== null) {
|
|
for (const path of dependency.path) {
|
|
object = t.memberExpression(object, t.identifier(path));
|
|
}
|
|
}
|
|
return object;
|
|
}
|
|
|
|
function withLoc<T extends (...args: 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 createCallExpression = withLoc(t.callExpression);
|
|
const createExpressionStatement = withLoc(t.expressionStatement);
|
|
const createFunctionDeclaration = withLoc(t.functionDeclaration);
|
|
const _createLabelledStatement = withLoc(t.labeledStatement);
|
|
const createVariableDeclaration = withLoc(t.variableDeclaration);
|
|
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 createStringLiteral = withLoc(t.stringLiteral);
|
|
|
|
type Temporaries = Map<IdentifierId, t.Expression | null>;
|
|
|
|
function codegenLabel(id: BlockId): string {
|
|
return `bb${id}`;
|
|
}
|
|
|
|
function codegenInstruction(
|
|
cx: Context,
|
|
instr: ReactiveInstruction,
|
|
value: t.Expression
|
|
): t.Statement {
|
|
if (t.isStatement(value)) {
|
|
return value;
|
|
}
|
|
if (instr.lvalue === null) {
|
|
return t.expressionStatement(value);
|
|
}
|
|
if (instr.lvalue.identifier.name === null) {
|
|
// temporary
|
|
cx.temp.set(instr.lvalue.identifier.id, value);
|
|
return t.emptyStatement();
|
|
} else {
|
|
if (cx.hasDeclared(instr.lvalue.identifier)) {
|
|
return createExpressionStatement(
|
|
instr.loc,
|
|
t.assignmentExpression(
|
|
"=",
|
|
convertIdentifier(instr.lvalue.identifier),
|
|
value
|
|
)
|
|
);
|
|
} else {
|
|
return createVariableDeclaration(instr.loc, "const", [
|
|
t.variableDeclarator(convertIdentifier(instr.lvalue.identifier), value),
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
|
|
function codegenInstructionValue(
|
|
cx: Context,
|
|
instrValue: ReactiveValue
|
|
): t.Expression {
|
|
let value: t.Expression;
|
|
switch (instrValue.kind) {
|
|
case "ArrayExpression": {
|
|
const elements = instrValue.elements.map((element) => {
|
|
if (element.kind === "Identifier") {
|
|
return codegenPlace(cx, element);
|
|
} else if (element.kind === "Spread") {
|
|
return t.spreadElement(codegenPlace(cx, element.place));
|
|
} else {
|
|
return null;
|
|
}
|
|
});
|
|
value = t.arrayExpression(elements);
|
|
break;
|
|
}
|
|
case "BinaryExpression": {
|
|
const left = codegenPlace(cx, instrValue.left);
|
|
const right = codegenPlace(cx, instrValue.right);
|
|
value = createBinaryExpression(
|
|
instrValue.loc,
|
|
instrValue.operator,
|
|
left,
|
|
right
|
|
);
|
|
break;
|
|
}
|
|
case "UnaryExpression": {
|
|
value = t.unaryExpression(
|
|
instrValue.operator as "throw", // todo
|
|
codegenPlace(cx, instrValue.value)
|
|
);
|
|
break;
|
|
}
|
|
case "Primitive": {
|
|
value = codegenValue(cx, instrValue.loc, instrValue.value);
|
|
break;
|
|
}
|
|
case "CallExpression": {
|
|
const callee = codegenPlace(cx, instrValue.callee);
|
|
const args = instrValue.args.map((arg) => codegenArgument(cx, arg));
|
|
value = createCallExpression(instrValue.loc, callee, args);
|
|
break;
|
|
}
|
|
case "OptionalExpression": {
|
|
const optionalValue = codegenInstructionValue(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 memberExpr = codegenPlace(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,
|
|
codegenPlace(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(instrValue.loc, memberExpr, args);
|
|
break;
|
|
}
|
|
case "NewExpression": {
|
|
const callee = codegenPlace(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 = t.identifier(property.name);
|
|
const value = codegenPlace(cx, property.place);
|
|
properties.push(
|
|
t.objectProperty(
|
|
key,
|
|
value,
|
|
false,
|
|
value.type === "Identifier" && value.name === key.name
|
|
)
|
|
);
|
|
} else {
|
|
properties.push(t.spreadElement(codegenPlace(cx, property.place)));
|
|
}
|
|
}
|
|
value = t.objectExpression(properties);
|
|
break;
|
|
}
|
|
case "JSXText": {
|
|
value = createStringLiteral(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"
|
|
? codegenPlace(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);
|
|
}
|
|
}
|
|
const children =
|
|
instrValue.children !== null
|
|
? instrValue.children.map((child) => codegenJsxElement(cx, child))
|
|
: [];
|
|
value = createJsxElement(
|
|
instrValue.loc,
|
|
t.jsxOpeningElement(tag, attributes, instrValue.children === null),
|
|
instrValue.children !== null
|
|
? createJsxClosingElement(instrValue.tag.loc, 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": {
|
|
value = t.assignmentExpression(
|
|
"=",
|
|
t.memberExpression(
|
|
codegenPlace(cx, instrValue.object),
|
|
t.identifier(instrValue.property)
|
|
),
|
|
codegenPlace(cx, instrValue.value)
|
|
);
|
|
break;
|
|
}
|
|
case "PropertyLoad": {
|
|
const object = codegenPlace(cx, instrValue.object);
|
|
// We currently only lower single chains of optional memberexpr.
|
|
// (See BuildHIR.ts for more detail.)
|
|
value = t.memberExpression(
|
|
object,
|
|
t.identifier(instrValue.property),
|
|
undefined
|
|
);
|
|
break;
|
|
}
|
|
case "PropertyDelete": {
|
|
value = t.unaryExpression(
|
|
"delete",
|
|
t.memberExpression(
|
|
codegenPlace(cx, instrValue.object),
|
|
t.identifier(instrValue.property)
|
|
)
|
|
);
|
|
break;
|
|
}
|
|
case "ComputedStore": {
|
|
value = t.assignmentExpression(
|
|
"=",
|
|
t.memberExpression(
|
|
codegenPlace(cx, instrValue.object),
|
|
codegenPlace(cx, instrValue.property),
|
|
true
|
|
),
|
|
codegenPlace(cx, instrValue.value)
|
|
);
|
|
break;
|
|
}
|
|
case "ComputedLoad": {
|
|
const object = codegenPlace(cx, instrValue.object);
|
|
const property = codegenPlace(cx, instrValue.property);
|
|
value = t.memberExpression(object, property, true);
|
|
break;
|
|
}
|
|
case "ComputedDelete": {
|
|
value = t.unaryExpression(
|
|
"delete",
|
|
t.memberExpression(
|
|
codegenPlace(cx, instrValue.object),
|
|
codegenPlace(cx, instrValue.property),
|
|
true
|
|
)
|
|
);
|
|
break;
|
|
}
|
|
case "LoadLocal":
|
|
case "LoadContext": {
|
|
value = codegenPlace(cx, instrValue.place);
|
|
break;
|
|
}
|
|
case "FunctionExpression": {
|
|
const loweredFunc = instrValue.loweredFunc;
|
|
const reactiveFunction = buildReactiveFunction(loweredFunc);
|
|
pruneUnusedLabels(reactiveFunction);
|
|
pruneUnusedLValues(reactiveFunction);
|
|
renameVariables(reactiveFunction);
|
|
const fn = codegenReactiveFunction(reactiveFunction).unwrap();
|
|
if (instrValue.expr.type === "ArrowFunctionExpression") {
|
|
let body: t.BlockStatement | t.Expression = fn.body;
|
|
if (body.body.length === 1) {
|
|
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,
|
|
codegenPlace(cx, instrValue.tag),
|
|
t.templateLiteral([t.templateElement(instrValue.value)], [])
|
|
);
|
|
break;
|
|
}
|
|
case "TypeCastExpression": {
|
|
value = t.typeCastExpression(
|
|
codegenPlace(cx, instrValue.value),
|
|
instrValue.type
|
|
);
|
|
break;
|
|
}
|
|
case "LogicalExpression": {
|
|
value = createLogicalExpression(
|
|
instrValue.loc,
|
|
instrValue.operator,
|
|
codegenInstructionValue(cx, instrValue.left),
|
|
codegenInstructionValue(cx, instrValue.right)
|
|
);
|
|
break;
|
|
}
|
|
case "ConditionalExpression": {
|
|
value = createConditionalExpression(
|
|
instrValue.loc,
|
|
codegenInstructionValue(cx, instrValue.test),
|
|
codegenInstructionValue(cx, instrValue.consequent),
|
|
codegenInstructionValue(cx, instrValue.alternate)
|
|
);
|
|
break;
|
|
}
|
|
case "SequenceExpression": {
|
|
const body = codegenBlock(
|
|
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 = codegenInstructionValue(cx, instrValue.value);
|
|
} else {
|
|
value = createSequenceExpression(instrValue.loc, [
|
|
...expressions,
|
|
codegenInstructionValue(cx, instrValue.value),
|
|
]);
|
|
}
|
|
break;
|
|
}
|
|
case "TemplateLiteral": {
|
|
value = createTemplateLiteral(
|
|
instrValue.loc,
|
|
instrValue.quasis.map((q) => t.templateElement(q)),
|
|
instrValue.subexprs.map((p) => codegenPlace(cx, p))
|
|
);
|
|
break;
|
|
}
|
|
case "LoadGlobal": {
|
|
value = t.identifier(instrValue.name);
|
|
break;
|
|
}
|
|
case "RegExpLiteral": {
|
|
value = t.regExpLiteral(instrValue.pattern, instrValue.flags);
|
|
break;
|
|
}
|
|
case "Await": {
|
|
value = t.awaitExpression(codegenPlace(cx, instrValue.value));
|
|
break;
|
|
}
|
|
case "NextIterableOf": {
|
|
value = codegenPlace(cx, instrValue.value);
|
|
break;
|
|
}
|
|
case "PostfixUpdate": {
|
|
value = t.updateExpression(
|
|
instrValue.operation,
|
|
codegenPlace(cx, instrValue.lvalue),
|
|
false
|
|
);
|
|
break;
|
|
}
|
|
case "PrefixUpdate": {
|
|
value = t.updateExpression(
|
|
instrValue.operation,
|
|
codegenPlace(cx, instrValue.lvalue),
|
|
true
|
|
);
|
|
break;
|
|
}
|
|
case "Debugger":
|
|
case "DeclareLocal":
|
|
case "DeclareContext":
|
|
case "Destructure":
|
|
case "StoreLocal":
|
|
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;
|
|
}
|
|
|
|
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 = codegenPlace(cx, attribute.place);
|
|
let value;
|
|
switch (innerValue.type) {
|
|
case "StringLiteral":
|
|
case "JSXElement":
|
|
case "JSXFragment": {
|
|
value = innerValue;
|
|
break;
|
|
}
|
|
default: {
|
|
value = createJsxExpressionContainer(attribute.place.loc, innerValue);
|
|
break;
|
|
}
|
|
}
|
|
return createJsxAttribute(attribute.place.loc, propName, value);
|
|
}
|
|
case "JsxSpreadAttribute": {
|
|
return t.jsxSpreadAttribute(codegenPlace(cx, attribute.argument));
|
|
}
|
|
default: {
|
|
assertExhaustive(
|
|
attribute,
|
|
`Unexpected attribute kind '${(attribute as any).kind}'`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
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 "StringLiteral": {
|
|
return createJsxText(place.loc, value.value);
|
|
}
|
|
case "JSXElement":
|
|
case "JSXFragment": {
|
|
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 codegenLValue(
|
|
pattern: Pattern | Place | SpreadPattern
|
|
): t.ArrayPattern | t.ObjectPattern | t.RestElement | t.Identifier {
|
|
switch (pattern.kind) {
|
|
case "ArrayPattern": {
|
|
return t.arrayPattern(
|
|
pattern.items.map((item) => {
|
|
if (item.kind === "Hole") {
|
|
return null;
|
|
}
|
|
return codegenLValue(item);
|
|
})
|
|
);
|
|
}
|
|
case "ObjectPattern": {
|
|
return t.objectPattern(
|
|
pattern.properties.map((property) => {
|
|
if (property.kind === "ObjectProperty") {
|
|
const key = t.identifier(property.name);
|
|
const value = codegenLValue(property.place);
|
|
return t.objectProperty(
|
|
key,
|
|
value,
|
|
false,
|
|
value.type === "Identifier" && value.name === key.name
|
|
);
|
|
} else {
|
|
return t.restElement(codegenLValue(property.place));
|
|
}
|
|
})
|
|
);
|
|
}
|
|
case "Spread": {
|
|
return t.restElement(codegenLValue(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 codegenPlace(cx, arg);
|
|
} else {
|
|
return t.spreadElement(codegenPlace(cx, arg.place));
|
|
}
|
|
}
|
|
|
|
function codegenPlace(cx: Context, place: Place): t.Expression {
|
|
let tmp = cx.temp.get(place.identifier.id);
|
|
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 {
|
|
if (identifier.name !== null) {
|
|
return t.identifier(`${identifier.name}`);
|
|
}
|
|
return t.identifier(`t${identifier.id}`);
|
|
}
|