lib/IRGen/ESTreeIRGen.cpp (897 lines of code) (raw):
/*
* 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.
*/
#include "ESTreeIRGen.h"
#include "llvh/ADT/StringSet.h"
#include "llvh/Support/Debug.h"
#include "llvh/Support/SaveAndRestore.h"
namespace hermes {
namespace irgen {
//===----------------------------------------------------------------------===//
// Free standing helpers.
Instruction *emitLoad(IRBuilder &builder, Value *from, bool inhibitThrow) {
if (auto *var = llvh::dyn_cast<Variable>(from)) {
Instruction *res = builder.createLoadFrameInst(var);
if (var->getObeysTDZ())
res = builder.createThrowIfEmptyInst(res);
return res;
} else if (auto *globalProp = llvh::dyn_cast<GlobalObjectProperty>(from)) {
if (globalProp->isDeclared() || inhibitThrow)
return builder.createLoadPropertyInst(
builder.getGlobalObject(), globalProp->getName());
else
return builder.createTryLoadGlobalPropertyInst(globalProp);
} else {
llvm_unreachable("invalid value to load from");
}
}
Instruction *
emitStore(IRBuilder &builder, Value *storedValue, Value *ptr, bool declInit) {
if (auto *var = llvh::dyn_cast<Variable>(ptr)) {
if (!declInit && var->getObeysTDZ()) {
// Must verify whether the variable is initialized.
builder.createThrowIfEmptyInst(builder.createLoadFrameInst(var));
}
return builder.createStoreFrameInst(storedValue, var);
} else if (auto *globalProp = llvh::dyn_cast<GlobalObjectProperty>(ptr)) {
if (globalProp->isDeclared() || !builder.getFunction()->isStrictMode())
return builder.createStorePropertyInst(
storedValue, builder.getGlobalObject(), globalProp->getName());
else
return builder.createTryStoreGlobalPropertyInst(storedValue, globalProp);
} else {
llvm_unreachable("unvalid value to load from");
}
}
/// \returns true if \p node is a constant expression.
bool isConstantExpr(ESTree::Node *node) {
// TODO: a little more aggressive constant folding.
switch (node->getKind()) {
case ESTree::NodeKind::StringLiteral:
case ESTree::NodeKind::NumericLiteral:
case ESTree::NodeKind::NullLiteral:
case ESTree::NodeKind::BooleanLiteral:
return true;
default:
return false;
}
}
//===----------------------------------------------------------------------===//
// LReference
IRBuilder &LReference::getBuilder() {
return irgen_->Builder;
}
Value *LReference::emitLoad() {
auto &builder = getBuilder();
IRBuilder::ScopedLocationChange slc(builder, loadLoc_);
switch (kind_) {
case Kind::Empty:
assert(false && "empty cannot be loaded");
return builder.getLiteralUndefined();
case Kind::Member:
return builder.createLoadPropertyInst(base_, property_);
case Kind::VarOrGlobal:
return irgen::emitLoad(builder, base_);
case Kind::Destructuring:
assert(false && "destructuring cannot be loaded");
return builder.getLiteralUndefined();
case Kind::Error:
return builder.getLiteralUndefined();
}
llvm_unreachable("invalid LReference kind");
}
void LReference::emitStore(Value *value) {
auto &builder = getBuilder();
switch (kind_) {
case Kind::Empty:
return;
case Kind::Member:
builder.createStorePropertyInst(value, base_, property_);
return;
case Kind::VarOrGlobal:
irgen::emitStore(builder, value, base_, declInit_);
return;
case Kind::Error:
return;
case Kind::Destructuring:
return irgen_->emitDestructuringAssignment(
declInit_, destructuringTarget_, value);
}
llvm_unreachable("invalid LReference kind");
}
bool LReference::canStoreWithoutSideEffects() const {
return kind_ == Kind::VarOrGlobal && llvh::isa<Variable>(base_);
}
Variable *LReference::castAsVariable() const {
return kind_ == Kind::VarOrGlobal ? dyn_cast_or_null<Variable>(base_)
: nullptr;
}
GlobalObjectProperty *LReference::castAsGlobalObjectProperty() const {
return kind_ == Kind::VarOrGlobal
? dyn_cast_or_null<GlobalObjectProperty>(base_)
: nullptr;
}
//===----------------------------------------------------------------------===//
// ESTreeIRGen
ESTreeIRGen::ESTreeIRGen(
ESTree::Node *root,
const DeclarationFileListTy &declFileList,
Module *M,
const ScopeChain &scopeChain)
: Mod(M),
Builder(Mod),
instrumentIR_(M, Builder),
Root(root),
DeclarationFileList(declFileList),
lexicalScopeChain(resolveScopeIdentifiers(scopeChain)),
identEval_(Builder.createIdentifier("eval")),
identLet_(Builder.createIdentifier("let")),
identDefaultExport_(Builder.createIdentifier("?default")) {}
void ESTreeIRGen::doIt() {
LLVM_DEBUG(dbgs() << "Processing top level program.\n");
ESTree::ProgramNode *Program;
Program = llvh::dyn_cast<ESTree::ProgramNode>(Root);
if (!Program) {
Builder.getModule()->getContext().getSourceErrorManager().error(
SMLoc{}, "missing 'Program' AST node");
return;
}
LLVM_DEBUG(dbgs() << "Found Program decl.\n");
// The function which will "execute" the module.
Function *topLevelFunction;
// Function context used only when compiling in an existing lexical scope
// chain. It is only initialized if we have a lexical scope chain.
llvh::Optional<FunctionContext> wrapperFunctionContext{};
if (!lexicalScopeChain) {
topLevelFunction = Builder.createTopLevelFunction(
ESTree::isStrict(Program->strictness),
Program->sourceVisibility,
Program->getSourceRange());
} else {
// If compiling in an existing lexical context, we need to install the
// scopes in a wrapper function, which represents the "global" code.
Function *wrapperFunction = Builder.createFunction(
"",
Function::DefinitionKind::ES5Function,
ESTree::isStrict(Program->strictness),
Program->sourceVisibility,
Program->getSourceRange(),
true);
// Initialize the wrapper context.
wrapperFunctionContext.emplace(this, wrapperFunction, nullptr);
// Populate it with dummy code so it doesn't crash the back-end.
genDummyFunction(wrapperFunction);
// Restore the previously saved parent scopes.
materializeScopesInChain(wrapperFunction, lexicalScopeChain, -1);
// Finally create the function which will actually be executed.
topLevelFunction = Builder.createFunction(
"eval",
Function::DefinitionKind::ES5Function,
ESTree::isStrict(Program->strictness),
Program->sourceVisibility,
Program->getSourceRange(),
false);
}
Mod->setTopLevelFunction(topLevelFunction);
// Function context for topLevelFunction.
FunctionContext topLevelFunctionContext{
this, topLevelFunction, Program->getSemInfo()};
// IRGen needs a pointer to the outer-most context, which is either
// topLevelContext or wrapperFunctionContext, depending on whether the latter
// was created.
// We want to set the pointer to that outer-most context, but ensure that it
// doesn't outlive the context it is pointing to.
llvh::SaveAndRestore<FunctionContext *> saveTopLevelContext(
topLevelContext,
!wrapperFunctionContext.hasValue() ? &topLevelFunctionContext
: &wrapperFunctionContext.getValue());
// Now declare all externally supplied global properties, but only if we don't
// have a lexical scope chain.
if (!lexicalScopeChain) {
for (auto declFile : DeclarationFileList) {
processDeclarationFile(declFile);
}
}
emitFunctionPrologue(
Program,
Builder.createBasicBlock(topLevelFunction),
InitES5CaptureState::Yes,
DoEmitParameters::Yes);
Value *retVal;
{
// Allocate the return register, initialize it to undefined.
curFunction()->globalReturnRegister =
Builder.createAllocStackInst(genAnonymousLabelName("ret"));
Builder.createStoreStackInst(
Builder.getLiteralUndefined(), curFunction()->globalReturnRegister);
genBody(Program->_body);
// Terminate the top-level scope with a return statement.
retVal = Builder.createLoadStackInst(curFunction()->globalReturnRegister);
}
emitFunctionEpilogue(retVal);
}
void ESTreeIRGen::doCJSModule(
Function *topLevelFunction,
sem::FunctionInfo *semInfo,
uint32_t segmentID,
uint32_t id,
llvh::StringRef filename) {
assert(Root && "no root in ESTreeIRGen");
auto *func = cast<ESTree::FunctionExpressionNode>(Root);
assert(func && "doCJSModule without a module");
FunctionContext topLevelFunctionContext{this, topLevelFunction, semInfo};
llvh::SaveAndRestore<FunctionContext *> saveTopLevelContext(
topLevelContext, &topLevelFunctionContext);
// Now declare all externally supplied global properties, but only if we don't
// have a lexical scope chain.
assert(
!lexicalScopeChain &&
"Lexical scope chain not supported for CJS modules");
for (auto declFile : DeclarationFileList) {
processDeclarationFile(declFile);
}
Identifier functionName = Builder.createIdentifier("cjs_module");
Function *newFunc = genES5Function(functionName, nullptr, func);
Builder.getModule()->addCJSModule(
segmentID, id, Builder.createIdentifier(filename), newFunc);
}
static int getDepth(const std::shared_ptr<SerializedScope> chain) {
int depth = 0;
const SerializedScope *current = chain.get();
while (current) {
depth += 1;
current = current->parentScope.get();
}
return depth;
}
std::pair<Function *, Function *> ESTreeIRGen::doLazyFunction(
hbc::LazyCompilationData *lazyData) {
// Create a top level function that will never be executed, because:
// 1. IRGen assumes the first function always has global scope
// 2. It serves as the root for dummy functions for lexical data
Function *topLevel = Builder.createTopLevelFunction(lazyData->strictMode);
FunctionContext topLevelFunctionContext{this, topLevel, nullptr};
// Save the top-level context, but ensure it doesn't outlive what it is
// pointing to.
llvh::SaveAndRestore<FunctionContext *> saveTopLevelContext(
topLevelContext, &topLevelFunctionContext);
auto *node = cast<ESTree::FunctionLikeNode>(Root);
// We restore scoping information in two separate ways:
// 1. By adding them to ExternalScopes for resolution here
// 2. By adding dummy functions for lexical scoping debug info later
//
// Instruction selection determines the delta between the ExternalScope
// and the dummy function chain, so we add the ExternalScopes with
// positive depth.
lexicalScopeChain = lazyData->parentScope;
materializeScopesInChain(
topLevel, lexicalScopeChain, getDepth(lexicalScopeChain) - 1);
// If lazyData->closureAlias is specified, we must create an alias binding
// between originalName (which must be valid) and the variable identified by
// closureAlias.
Variable *parentVar = nullptr;
if (lazyData->closureAlias.isValid()) {
assert(lazyData->originalName.isValid() && "Original name invalid");
assert(
lazyData->originalName != lazyData->closureAlias &&
"Original name must be different from the alias");
// NOTE: the closureAlias target must exist and must be a Variable.
parentVar = cast<Variable>(nameTable_.lookup(lazyData->closureAlias));
// Re-create the alias.
nameTable_.insert(lazyData->originalName, parentVar);
}
assert(
!llvh::isa<ESTree::ArrowFunctionExpressionNode>(node) &&
"lazy compilation not supported for arrow functions");
// Generators have had their lazy scope set up without setting one up
// for the inner functions. This means that we will never directly generate
// a GeneratorInnerFunction here.
Function *func = ESTree::isAsync(node)
? genAsyncFunction(lazyData->originalName, parentVar, node)
: ESTree::isGenerator(node)
? genGeneratorFunction(lazyData->originalName, parentVar, node)
: genES5Function(lazyData->originalName, parentVar, node, false);
addLexicalDebugInfo(func, topLevel, lexicalScopeChain);
return {func, topLevel};
}
std::pair<Value *, bool> ESTreeIRGen::declareVariableOrGlobalProperty(
Function *inFunc,
VarDecl::Kind declKind,
Identifier name) {
Value *found = nameTable_.lookup(name);
// If the variable is already declared in this scope, do not create a
// second instance.
if (found) {
if (auto *var = llvh::dyn_cast<Variable>(found)) {
if (var->getParent()->getFunction() == inFunc)
return {found, false};
} else {
assert(
llvh::isa<GlobalObjectProperty>(found) &&
"Invalid value found in name table");
if (inFunc->isGlobalScope())
return {found, false};
}
}
// Create a property if global scope, variable otherwise.
Value *res;
if (inFunc->isGlobalScope() && declKind == VarDecl::Kind::Var) {
res = Builder.createGlobalObjectProperty(name, true);
} else {
Variable::DeclKind vdc;
if (declKind == VarDecl::Kind::Let)
vdc = Variable::DeclKind::Let;
else if (declKind == VarDecl::Kind::Const)
vdc = Variable::DeclKind::Const;
else {
assert(declKind == VarDecl::Kind::Var);
vdc = Variable::DeclKind::Var;
}
auto *var = Builder.createVariable(inFunc->getFunctionScope(), vdc, name);
// For "let" and "const" create the related TDZ flag.
if (Variable::declKindNeedsTDZ(vdc) &&
Mod->getContext().getCodeGenerationSettings().enableTDZ) {
var->setObeysTDZ(true);
}
res = var;
}
// Register the variable in the scoped hash table.
nameTable_.insert(name, res);
return {res, true};
}
GlobalObjectProperty *ESTreeIRGen::declareAmbientGlobalProperty(
Identifier name) {
// Avoid redefining global properties.
auto *prop = dyn_cast_or_null<GlobalObjectProperty>(nameTable_.lookup(name));
if (prop)
return prop;
LLVM_DEBUG(
llvh::dbgs() << "declaring ambient global property " << name << " "
<< name.getUnderlyingPointer() << "\n");
prop = Builder.createGlobalObjectProperty(name, false);
nameTable_.insertIntoScope(&topLevelContext->scope, name, prop);
return prop;
}
namespace {
/// This visitor structs collects declarations within a single closure without
/// descending into child closures.
struct DeclHoisting {
/// The list of collected identifiers (variables and functions).
llvh::SmallVector<ESTree::VariableDeclaratorNode *, 8> decls{};
/// A list of functions that need to be hoisted and materialized before we
/// can generate the rest of the function.
llvh::SmallVector<ESTree::FunctionDeclarationNode *, 8> closures;
explicit DeclHoisting() = default;
~DeclHoisting() = default;
/// Extract the variable name from the nodes that can define new variables.
/// The nodes that can define a new variable in the scope are:
/// VariableDeclarator and FunctionDeclaration>
void collectDecls(ESTree::Node *V) {
if (auto VD = llvh::dyn_cast<ESTree::VariableDeclaratorNode>(V)) {
return decls.push_back(VD);
}
if (auto FD = llvh::dyn_cast<ESTree::FunctionDeclarationNode>(V)) {
return closures.push_back(FD);
}
}
bool shouldVisit(ESTree::Node *V) {
// Collect declared names, even if we don't descend into children nodes.
collectDecls(V);
// Do not descend to child closures because the variables they define are
// not exposed to the outside function.
if (llvh::isa<ESTree::FunctionDeclarationNode>(V) ||
llvh::isa<ESTree::FunctionExpressionNode>(V) ||
llvh::isa<ESTree::ArrowFunctionExpressionNode>(V))
return false;
return true;
}
void enter(ESTree::Node *V) {}
void leave(ESTree::Node *V) {}
};
} // anonymous namespace.
void ESTreeIRGen::processDeclarationFile(ESTree::ProgramNode *programNode) {
auto Program = dyn_cast_or_null<ESTree::ProgramNode>(programNode);
if (!Program)
return;
DeclHoisting DH;
Program->visit(DH);
// Create variable declarations for each of the hoisted variables.
for (auto vd : DH.decls)
declareAmbientGlobalProperty(getNameFieldFromID(vd->_id));
for (auto fd : DH.closures)
declareAmbientGlobalProperty(getNameFieldFromID(fd->_id));
}
Value *ESTreeIRGen::ensureVariableExists(ESTree::IdentifierNode *id) {
assert(id && "id must be a valid Identifier node");
Identifier name = getNameFieldFromID(id);
// Check if this is a known variable.
if (auto *var = nameTable_.lookup(name))
return var;
if (curFunction()->function->isStrictMode()) {
// Report a warning in strict mode.
auto currentFunc = Builder.getInsertionBlock()->getParent();
Builder.getModule()->getContext().getSourceErrorManager().warning(
Warning::UndefinedVariable,
id->getSourceRange(),
Twine("the variable \"") + name.str() + "\" was not declared in " +
currentFunc->getDescriptiveDefinitionKindStr() + " \"" +
currentFunc->getInternalNameStr() + "\"");
}
// Undeclared variable is an ambient global property.
return declareAmbientGlobalProperty(name);
}
Value *ESTreeIRGen::genMemberExpressionProperty(
ESTree::MemberExpressionLikeNode *Mem) {
// If computed is true, the node corresponds to a computed (a[b]) member
// lookup and '_property' is an Expression. Otherwise, the node
// corresponds to a static (a.b) member lookup and '_property' is an
// Identifier.
// Details of the computed field are available here:
// https://github.com/estree/estree/blob/master/spec.md#memberexpression
if (getComputed(Mem)) {
return genExpression(getProperty(Mem));
}
// Arrays and objects may be accessed with integer indices.
if (auto N = llvh::dyn_cast<ESTree::NumericLiteralNode>(getProperty(Mem))) {
return Builder.getLiteralNumber(N->_value);
}
// ESTree encodes property access as MemberExpression -> Identifier.
auto Id = cast<ESTree::IdentifierNode>(getProperty(Mem));
Identifier fieldName = getNameFieldFromID(Id);
LLVM_DEBUG(
dbgs() << "Emitting direct label access to field '" << fieldName
<< "'\n");
return Builder.getLiteralString(fieldName);
}
bool ESTreeIRGen::canCreateLRefWithoutSideEffects(
hermes::ESTree::Node *target) {
// Check for an identifier bound to an existing local variable.
if (auto *iden = llvh::dyn_cast<ESTree::IdentifierNode>(target)) {
return dyn_cast_or_null<Variable>(
nameTable_.lookup(getNameFieldFromID(iden)));
}
return false;
}
LReference ESTreeIRGen::createLRef(ESTree::Node *node, bool declInit) {
SMLoc sourceLoc = node->getDebugLoc();
IRBuilder::ScopedLocationChange slc(Builder, sourceLoc);
if (llvh::isa<ESTree::EmptyNode>(node)) {
LLVM_DEBUG(dbgs() << "Creating an LRef for EmptyNode.\n");
return LReference(
LReference::Kind::Empty, this, false, nullptr, nullptr, sourceLoc);
}
/// Create lref for member expression (ex: o.f).
if (auto *ME = llvh::dyn_cast<ESTree::MemberExpressionNode>(node)) {
LLVM_DEBUG(dbgs() << "Creating an LRef for member expression.\n");
Value *obj = genExpression(ME->_object);
Value *prop = genMemberExpressionProperty(ME);
return LReference(
LReference::Kind::Member, this, false, obj, prop, sourceLoc);
}
/// Create lref for identifiers (ex: a).
if (auto *iden = llvh::dyn_cast<ESTree::IdentifierNode>(node)) {
LLVM_DEBUG(dbgs() << "Creating an LRef for identifier.\n");
LLVM_DEBUG(
dbgs() << "Looking for identifier \"" << getNameFieldFromID(iden)
<< "\"\n");
auto *var = ensureVariableExists(iden);
return LReference(
LReference::Kind::VarOrGlobal, this, declInit, var, nullptr, sourceLoc);
}
/// Create lref for variable decls (ex: var a).
if (auto *V = llvh::dyn_cast<ESTree::VariableDeclarationNode>(node)) {
LLVM_DEBUG(dbgs() << "Creating an LRef for variable declaration.\n");
assert(V->_declarations.size() == 1 && "Malformed variable declaration");
auto *decl =
cast<ESTree::VariableDeclaratorNode>(&V->_declarations.front());
return createLRef(decl->_id, true);
}
// Destructuring assignment.
if (auto *pat = llvh::dyn_cast<ESTree::PatternNode>(node)) {
return LReference(this, declInit, pat);
}
Builder.getModule()->getContext().getSourceErrorManager().error(
node->getSourceRange(), "unsupported assignment target");
return LReference(
LReference::Kind::Error, this, false, nullptr, nullptr, sourceLoc);
}
Value *ESTreeIRGen::genHermesInternalCall(
StringRef name,
Value *thisValue,
ArrayRef<Value *> args) {
return Builder.createCallInst(
Builder.createLoadPropertyInst(
Builder.createTryLoadGlobalPropertyInst("HermesInternal"), name),
thisValue,
args);
}
Value *ESTreeIRGen::genBuiltinCall(
hermes::BuiltinMethod::Enum builtinIndex,
ArrayRef<Value *> args) {
return Builder.createCallBuiltinInst(builtinIndex, args);
}
void ESTreeIRGen::emitEnsureObject(Value *value, StringRef message) {
// TODO: use "thisArg" when builtins get fixed to support it.
genBuiltinCall(
BuiltinMethod::HermesBuiltin_ensureObject,
{value, Builder.getLiteralString(message)});
}
Value *ESTreeIRGen::emitIteratorSymbol() {
// FIXME: use the builtin value of @@iterator. Symbol could have been
// overridden.
return Builder.createLoadPropertyInst(
Builder.createTryLoadGlobalPropertyInst("Symbol"), "iterator");
}
ESTreeIRGen::IteratorRecordSlow ESTreeIRGen::emitGetIteratorSlow(Value *obj) {
auto *method = Builder.createLoadPropertyInst(obj, emitIteratorSymbol());
auto *iterator = Builder.createCallInst(method, obj, {});
emitEnsureObject(iterator, "iterator is not an object");
auto *nextMethod = Builder.createLoadPropertyInst(iterator, "next");
return {iterator, nextMethod};
}
Value *ESTreeIRGen::emitIteratorNextSlow(IteratorRecordSlow iteratorRecord) {
auto *nextResult = Builder.createCallInst(
iteratorRecord.nextMethod, iteratorRecord.iterator, {});
emitEnsureObject(nextResult, "iterator.next() did not return an object");
return nextResult;
}
Value *ESTreeIRGen::emitIteratorCompleteSlow(Value *iterResult) {
return Builder.createLoadPropertyInst(iterResult, "done");
}
Value *ESTreeIRGen::emitIteratorValueSlow(Value *iterResult) {
return Builder.createLoadPropertyInst(iterResult, "value");
}
void ESTreeIRGen::emitIteratorCloseSlow(
hermes::irgen::ESTreeIRGen::IteratorRecordSlow iteratorRecord,
bool ignoreInnerException) {
auto *haveReturn = Builder.createBasicBlock(Builder.getFunction());
auto *noReturn = Builder.createBasicBlock(Builder.getFunction());
auto *returnMethod = genBuiltinCall(
BuiltinMethod::HermesBuiltin_getMethod,
{iteratorRecord.iterator, Builder.getLiteralString("return")});
Builder.createCompareBranchInst(
returnMethod,
Builder.getLiteralUndefined(),
BinaryOperatorInst::OpKind::StrictlyEqualKind,
noReturn,
haveReturn);
Builder.setInsertionBlock(haveReturn);
if (ignoreInnerException) {
emitTryCatchScaffolding(
noReturn,
// emitBody.
[this, returnMethod, &iteratorRecord]() {
Builder.createCallInst(returnMethod, iteratorRecord.iterator, {});
},
// emitNormalCleanup.
[]() {},
// emitHandler.
[this](BasicBlock *nextBlock) {
// We need to catch the exception, even if we don't used it.
Builder.createCatchInst();
Builder.createBranchInst(nextBlock);
});
} else {
auto *innerResult =
Builder.createCallInst(returnMethod, iteratorRecord.iterator, {});
emitEnsureObject(innerResult, "iterator.return() did not return an object");
Builder.createBranchInst(noReturn);
}
Builder.setInsertionBlock(noReturn);
}
ESTreeIRGen::IteratorRecord ESTreeIRGen::emitGetIterator(Value *obj) {
// Each of these will be modified by "next", so we use a stack storage.
auto *iterStorage =
Builder.createAllocStackInst(genAnonymousLabelName("iter"));
auto *sourceOrNext =
Builder.createAllocStackInst(genAnonymousLabelName("sourceOrNext"));
Builder.createStoreStackInst(obj, sourceOrNext);
auto *iter = Builder.createIteratorBeginInst(sourceOrNext);
Builder.createStoreStackInst(iter, iterStorage);
return IteratorRecord{iterStorage, sourceOrNext};
}
void ESTreeIRGen::emitDestructuringAssignment(
bool declInit,
ESTree::PatternNode *target,
Value *source) {
if (auto *APN = llvh::dyn_cast<ESTree::ArrayPatternNode>(target))
return emitDestructuringArray(declInit, APN, source);
else if (auto *OPN = llvh::dyn_cast<ESTree::ObjectPatternNode>(target))
return emitDestructuringObject(declInit, OPN, source);
else {
Mod->getContext().getSourceErrorManager().error(
target->getSourceRange(), "unsupported destructuring target");
}
}
void ESTreeIRGen::emitDestructuringArray(
bool declInit,
ESTree::ArrayPatternNode *targetPat,
Value *source) {
const IteratorRecord iteratorRecord = emitGetIterator(source);
/// iteratorDone = undefined.
auto *iteratorDone =
Builder.createAllocStackInst(genAnonymousLabelName("iterDone"));
Builder.createStoreStackInst(Builder.getLiteralUndefined(), iteratorDone);
auto *value =
Builder.createAllocStackInst(genAnonymousLabelName("iterValue"));
SharedExceptionHandler handler{};
handler.exc = Builder.createAllocStackInst(genAnonymousLabelName("exc"));
// All exception handlers branch to this block.
handler.exceptionBlock = Builder.createBasicBlock(Builder.getFunction());
bool first = true;
bool emittedRest = false;
// The LReference created in the previous iteration of the destructuring
// loop. We need it because we want to put the previous store and the creation
// of the next LReference under one try block.
llvh::Optional<LReference> lref;
/// If the previous LReference is valid and non-empty, store "value" into
/// it and reset the LReference.
auto storePreviousValue = [&lref, &handler, this, value]() {
if (lref && !lref->isEmpty()) {
if (lref->canStoreWithoutSideEffects()) {
lref->emitStore(Builder.createLoadStackInst(value));
} else {
// If we can't store without side effects, wrap the store in try/catch.
emitTryWithSharedHandler(&handler, [this, &lref, value]() {
lref->emitStore(Builder.createLoadStackInst(value));
});
}
lref.reset();
}
};
for (auto &elem : targetPat->_elements) {
ESTree::Node *target = &elem;
ESTree::Node *init = nullptr;
if (auto *rest = llvh::dyn_cast<ESTree::RestElementNode>(target)) {
storePreviousValue();
emitRestElement(declInit, rest, iteratorRecord, iteratorDone, &handler);
emittedRest = true;
break;
}
// If we have an initializer, unwrap it.
if (auto *assign = llvh::dyn_cast<ESTree::AssignmentPatternNode>(target)) {
target = assign->_left;
init = assign->_right;
}
// Can we create the new LReference without side effects and avoid a
// try/catch. The complexity comes from having to check whether the last
// LReference also can avoid a try/catch or not.
if (canCreateLRefWithoutSideEffects(target)) {
// We don't need a try/catch, but last lref might. Just let the routine
// do the right thing.
storePreviousValue();
lref = createLRef(target, declInit);
} else {
// We need a try/catch, but last lref might not. If it doesn't, emit it
// directly and clear it, so we won't do anything inside our try/catch.
if (lref && lref->canStoreWithoutSideEffects()) {
lref->emitStore(Builder.createLoadStackInst(value));
lref.reset();
}
emitTryWithSharedHandler(
&handler, [this, &lref, value, target, declInit]() {
// Store the previous value, if we have one.
if (lref && !lref->isEmpty())
lref->emitStore(Builder.createLoadStackInst(value));
lref = createLRef(target, declInit);
});
}
// Pseudocode of the algorithm for a step:
//
// value = undefined;
// if (iteratorDone) goto nextBlock
// notDoneBlock:
// stepResult = IteratorNext(iteratorRecord)
// stepDone = IteratorComplete(stepResult)
// iteratorDone = stepDone
// if (stepDone) goto nextBlock
// newValueBlock:
// value = IteratorValue(stepResult)
// nextBlock:
// if (value !== undefined) goto storeBlock [if initializer present]
// value = initializer [if initializer present]
// storeBlock:
// lref.emitStore(value)
auto *notDoneBlock = Builder.createBasicBlock(Builder.getFunction());
auto *newValueBlock = Builder.createBasicBlock(Builder.getFunction());
auto *nextBlock = Builder.createBasicBlock(Builder.getFunction());
auto *getDefaultBlock =
init ? Builder.createBasicBlock(Builder.getFunction()) : nullptr;
auto *storeBlock =
init ? Builder.createBasicBlock(Builder.getFunction()) : nullptr;
Builder.createStoreStackInst(Builder.getLiteralUndefined(), value);
// In the first iteration we know that "done" is false.
if (first) {
first = false;
Builder.createBranchInst(notDoneBlock);
} else {
Builder.createCondBranchInst(
Builder.createLoadStackInst(iteratorDone), nextBlock, notDoneBlock);
}
// notDoneBlock:
Builder.setInsertionBlock(notDoneBlock);
auto *stepValue = emitIteratorNext(iteratorRecord);
auto *stepDone = emitIteratorComplete(iteratorRecord);
Builder.createStoreStackInst(stepDone, iteratorDone);
Builder.createCondBranchInst(
stepDone, init ? getDefaultBlock : nextBlock, newValueBlock);
// newValueBlock:
Builder.setInsertionBlock(newValueBlock);
Builder.createStoreStackInst(stepValue, value);
Builder.createBranchInst(nextBlock);
// nextBlock:
Builder.setInsertionBlock(nextBlock);
// NOTE: we can't use emitOptionalInitializationHere() because we want to
// be able to jump directly to getDefaultBlock.
if (init) {
// if (value !== undefined) goto storeBlock [if initializer present]
// value = initializer [if initializer present]
// storeBlock:
Builder.createCondBranchInst(
Builder.createBinaryOperatorInst(
Builder.createLoadStackInst(value),
Builder.getLiteralUndefined(),
BinaryOperatorInst::OpKind::StrictlyNotEqualKind),
storeBlock,
getDefaultBlock);
Identifier nameHint = llvh::isa<ESTree::IdentifierNode>(target)
? getNameFieldFromID(target)
: Identifier{};
// getDefaultBlock:
Builder.setInsertionBlock(getDefaultBlock);
Builder.createStoreStackInst(genExpression(init, nameHint), value);
Builder.createBranchInst(storeBlock);
// storeBlock:
Builder.setInsertionBlock(storeBlock);
}
}
storePreviousValue();
// If in the end the iterator is not done, close it. We only need to do
// that if we didn't end with a rest element because it would have exhausted
// the iterator.
if (!emittedRest) {
auto *notDoneBlock = Builder.createBasicBlock(Builder.getFunction());
auto *doneBlock = Builder.createBasicBlock(Builder.getFunction());
Builder.createCondBranchInst(
Builder.createLoadStackInst(iteratorDone), doneBlock, notDoneBlock);
Builder.setInsertionBlock(notDoneBlock);
emitIteratorClose(iteratorRecord, false);
Builder.createBranchInst(doneBlock);
Builder.setInsertionBlock(doneBlock);
}
// If we emitted at least one try block, generate the exception handler.
if (handler.emittedTry) {
IRBuilder::SaveRestore saveRestore{Builder};
Builder.setInsertionBlock(handler.exceptionBlock);
auto *notDoneBlock = Builder.createBasicBlock(Builder.getFunction());
auto *doneBlock = Builder.createBasicBlock(Builder.getFunction());
Builder.createCondBranchInst(
Builder.createLoadStackInst(iteratorDone), doneBlock, notDoneBlock);
Builder.setInsertionBlock(notDoneBlock);
emitIteratorClose(iteratorRecord, true);
Builder.createBranchInst(doneBlock);
Builder.setInsertionBlock(doneBlock);
Builder.createThrowInst(Builder.createLoadStackInst(handler.exc));
} else {
// If we didn't use the exception block, we need to delete it, otherwise
// it fails IR validation even though it will be never executed.
handler.exceptionBlock->eraseFromParent();
// Delete the not needed exception stack allocation. It would be optimized
// out later, but it is nice to produce cleaner non-optimized IR, if it is
// easy to do so.
assert(
!handler.exc->hasUsers() &&
"should not have any users if no try/catch was emitted");
handler.exc->eraseFromParent();
}
}
void ESTreeIRGen::emitRestElement(
bool declInit,
ESTree::RestElementNode *rest,
hermes::irgen::ESTreeIRGen::IteratorRecord iteratorRecord,
hermes::AllocStackInst *iteratorDone,
SharedExceptionHandler *handler) {
// 13.3.3.8 BindingRestElement:...BindingIdentifier
auto *notDoneBlock = Builder.createBasicBlock(Builder.getFunction());
auto *newValueBlock = Builder.createBasicBlock(Builder.getFunction());
auto *doneBlock = Builder.createBasicBlock(Builder.getFunction());
llvh::Optional<LReference> lref;
if (canCreateLRefWithoutSideEffects(rest->_argument)) {
lref = createLRef(rest->_argument, declInit);
} else {
emitTryWithSharedHandler(handler, [this, &lref, rest, declInit]() {
lref = createLRef(rest->_argument, declInit);
});
}
auto *A = Builder.createAllocArrayInst({}, 0);
auto *n = Builder.createAllocStackInst(genAnonymousLabelName("n"));
// n = 0.
Builder.createStoreStackInst(Builder.getLiteralPositiveZero(), n);
Builder.createCondBranchInst(
Builder.createLoadStackInst(iteratorDone), doneBlock, notDoneBlock);
// notDoneBlock:
Builder.setInsertionBlock(notDoneBlock);
auto *stepValue = emitIteratorNext(iteratorRecord);
auto *stepDone = emitIteratorComplete(iteratorRecord);
Builder.createStoreStackInst(stepDone, iteratorDone);
Builder.createCondBranchInst(stepDone, doneBlock, newValueBlock);
// newValueBlock:
Builder.setInsertionBlock(newValueBlock);
auto *nVal = Builder.createLoadStackInst(n);
nVal->setType(Type::createNumber());
// A[n] = stepValue;
// Unfortunately this can throw because our arrays can have limited range.
// The spec doesn't specify what to do in this case, but the reasonable thing
// to do is to what we would if this was a for-of loop doing the same thing.
// See section BindingRestElement:...BindingIdentifier, step f and g:
// https://www.ecma-international.org/ecma-262/9.0/index.html#sec-destructuring-binding-patterns-runtime-semantics-iteratorbindinginitialization
emitTryWithSharedHandler(handler, [this, stepValue, A, nVal]() {
Builder.createStorePropertyInst(stepValue, A, nVal);
});
// ++n;
auto add = Builder.createBinaryOperatorInst(
nVal, Builder.getLiteralNumber(1), BinaryOperatorInst::OpKind::AddKind);
add->setType(Type::createNumber());
Builder.createStoreStackInst(add, n);
Builder.createBranchInst(notDoneBlock);
// doneBlock:
Builder.setInsertionBlock(doneBlock);
if (lref->canStoreWithoutSideEffects()) {
lref->emitStore(A);
} else {
emitTryWithSharedHandler(handler, [&lref, A]() { lref->emitStore(A); });
}
}
void ESTreeIRGen::emitDestructuringObject(
bool declInit,
ESTree::ObjectPatternNode *target,
Value *source) {
// Keep track of which keys have been destructured.
llvh::SmallVector<Value *, 4> excludedItems{};
if (target->_properties.empty() ||
llvh::isa<ESTree::RestElementNode>(target->_properties.front())) {
// ES10.0 13.3.3.5
// 1. Perform ? RequireObjectCoercible(value).
// The extremely unlikely case that the user is attempting to destructure
// into {} or {...rest}. Any other object destructuring will fail upon
// attempting to retrieve a real property from `source`.
// We must check that the source can be destructured,
// and the only time this will throw is if source is undefined or null.
auto *throwBB = Builder.createBasicBlock(Builder.getFunction());
auto *doneBB = Builder.createBasicBlock(Builder.getFunction());
// Use == instead of === to account for both undefined and null.
Builder.createCondBranchInst(
Builder.createBinaryOperatorInst(
source,
Builder.getLiteralNull(),
BinaryOperatorInst::OpKind::EqualKind),
throwBB,
doneBB);
Builder.setInsertionBlock(throwBB);
genBuiltinCall(
BuiltinMethod::HermesBuiltin_throwTypeError,
{source,
Builder.getLiteralString(
"Cannot destructure 'undefined' or 'null'.")});
// throwTypeError will always throw.
// This return is here to ensure well-formed IR, and will not run.
Builder.createReturnInst(Builder.getLiteralUndefined());
Builder.setInsertionBlock(doneBB);
}
for (auto &elem : target->_properties) {
if (auto *rest = llvh::dyn_cast<ESTree::RestElementNode>(&elem)) {
emitRestProperty(declInit, rest, excludedItems, source);
break;
}
auto *propNode = cast<ESTree::PropertyNode>(&elem);
ESTree::Node *valueNode = propNode->_value;
ESTree::Node *init = nullptr;
// If we have an initializer, unwrap it.
if (auto *assign =
llvh::dyn_cast<ESTree::AssignmentPatternNode>(valueNode)) {
valueNode = assign->_left;
init = assign->_right;
}
Identifier nameHint = llvh::isa<ESTree::IdentifierNode>(valueNode)
? getNameFieldFromID(valueNode)
: Identifier{};
if (llvh::isa<ESTree::IdentifierNode>(propNode->_key) &&
!propNode->_computed) {
Identifier key = getNameFieldFromID(propNode->_key);
excludedItems.push_back(Builder.getLiteralString(key));
auto *loadedValue = Builder.createLoadPropertyInst(source, key);
createLRef(valueNode, declInit)
.emitStore(emitOptionalInitialization(loadedValue, init, nameHint));
} else {
Value *key = genExpression(propNode->_key);
excludedItems.push_back(key);
auto *loadedValue = Builder.createLoadPropertyInst(source, key);
createLRef(valueNode, declInit)
.emitStore(emitOptionalInitialization(loadedValue, init, nameHint));
}
}
}
void ESTreeIRGen::emitRestProperty(
bool declInit,
ESTree::RestElementNode *rest,
const llvh::SmallVectorImpl<Value *> &excludedItems,
hermes::Value *source) {
auto lref = createLRef(rest->_argument, declInit);
// Construct the excluded items.
HBCAllocObjectFromBufferInst::ObjectPropertyMap exMap{};
llvh::SmallVector<Value *, 4> computedExcludedItems{};
// Keys need de-duping so we don't create a dummy exclusion object with
// duplicate keys.
llvh::DenseSet<Literal *> keyDeDupeSet;
auto *zeroValue = Builder.getLiteralPositiveZero();
for (Value *key : excludedItems) {
if (auto *lit = llvh::dyn_cast<Literal>(key)) {
// If the key is a literal, we can place it in the
// HBCAllocObjectFromBufferInst buffer.
if (keyDeDupeSet.insert(lit).second) {
exMap.emplace_back(std::make_pair(lit, zeroValue));
}
} else {
// If the key is not a literal, then we have to dynamically populate the
// excluded object with it after creation from the buffer.
computedExcludedItems.push_back(key);
}
}
Value *excludedObj;
if (excludedItems.empty()) {
excludedObj = Builder.getLiteralUndefined();
} else {
// This size is only a hint as the true size may change if there are
// duplicates when computedExcludedItems is processed at run-time.
auto excludedSizeHint = exMap.size() + computedExcludedItems.size();
if (exMap.empty()) {
excludedObj = Builder.createAllocObjectInst(excludedSizeHint);
} else {
excludedObj =
Builder.createHBCAllocObjectFromBufferInst(exMap, excludedSizeHint);
}
for (Value *key : computedExcludedItems) {
Builder.createStorePropertyInst(zeroValue, excludedObj, key);
}
}
auto *restValue = genBuiltinCall(
BuiltinMethod::HermesBuiltin_copyDataProperties,
{Builder.createAllocObjectInst(0), source, excludedObj});
lref.emitStore(restValue);
}
Value *ESTreeIRGen::emitOptionalInitialization(
Value *value,
ESTree::Node *init,
Identifier nameHint) {
if (!init)
return value;
auto *currentBlock = Builder.getInsertionBlock();
auto *getDefaultBlock = Builder.createBasicBlock(Builder.getFunction());
auto *storeBlock = Builder.createBasicBlock(Builder.getFunction());
// if (value !== undefined) goto storeBlock [if initializer present]
// value = initializer [if initializer present]
// storeBlock:
Builder.createCondBranchInst(
Builder.createBinaryOperatorInst(
value,
Builder.getLiteralUndefined(),
BinaryOperatorInst::OpKind::StrictlyNotEqualKind),
storeBlock,
getDefaultBlock);
// getDefaultBlock:
Builder.setInsertionBlock(getDefaultBlock);
auto *defaultValue = genExpression(init, nameHint);
auto *defaultResultBlock = Builder.getInsertionBlock();
Builder.createBranchInst(storeBlock);
// storeBlock:
Builder.setInsertionBlock(storeBlock);
return Builder.createPhiInst(
{value, defaultValue}, {currentBlock, defaultResultBlock});
}
std::shared_ptr<SerializedScope> ESTreeIRGen::resolveScopeIdentifiers(
const ScopeChain &chain) {
std::shared_ptr<SerializedScope> current{};
for (auto it = chain.functions.rbegin(), end = chain.functions.rend();
it < end;
it++) {
auto next = std::make_shared<SerializedScope>();
next->variables.reserve(it->variables.size());
for (auto var : it->variables) {
next->variables.push_back(std::move(Builder.createIdentifier(var)));
}
next->parentScope = current;
current = next;
}
return current;
}
void ESTreeIRGen::materializeScopesInChain(
Function *wrapperFunction,
const std::shared_ptr<const SerializedScope> &scope,
int depth) {
if (!scope)
return;
assert(depth < 1000 && "Excessive scope depth");
// First materialize parent scopes.
materializeScopesInChain(wrapperFunction, scope->parentScope, depth - 1);
// If scope->closureAlias is specified, we must create an alias binding
// between originalName (which must be valid) and the variable identified by
// closureAlias.
//
// We do this *before* inserting the other variables below to reflect that
// the closure alias is conceptually in an outside scope and also avoid the
// closure name incorrectly shadowing the same name inside the closure.
if (scope->closureAlias.isValid()) {
assert(scope->originalName.isValid() && "Original name invalid");
assert(
scope->originalName != scope->closureAlias &&
"Original name must be different from the alias");
// NOTE: the closureAlias target must exist and must be a Variable.
auto *closureVar = cast<Variable>(nameTable_.lookup(scope->closureAlias));
// Re-create the alias.
nameTable_.insert(scope->originalName, closureVar);
}
// Create an external scope.
ExternalScope *ES = Builder.createExternalScope(wrapperFunction, depth);
for (auto variableId : scope->variables) {
auto *variable =
Builder.createVariable(ES, Variable::DeclKind::Var, variableId);
nameTable_.insert(variableId, variable);
}
}
namespace {
void buildDummyLexicalParent(
IRBuilder &builder,
Function *parent,
Function *child) {
// FunctionScopeAnalysis works through CreateFunctionInsts, so we have to add
// that even though these functions are never invoked.
auto *block = builder.createBasicBlock(parent);
builder.setInsertionBlock(block);
builder.createUnreachableInst();
auto *inst = builder.createCreateFunctionInst(child);
builder.createReturnInst(inst);
}
} // namespace
/// Add dummy functions for lexical scope debug info.
// They are never executed and serve no purpose other than filling in debug
// info. This is currently necessary because we can't rely on parent bytecode
// modules for lexical scoping data.
void ESTreeIRGen::addLexicalDebugInfo(
Function *child,
Function *global,
const std::shared_ptr<const SerializedScope> &scope) {
if (!scope || !scope->parentScope) {
buildDummyLexicalParent(Builder, global, child);
return;
}
auto *current = Builder.createFunction(
scope->originalName,
Function::DefinitionKind::ES5Function,
false,
SourceVisibility::Sensitive,
{},
false);
for (auto &var : scope->variables) {
Builder.createVariable(
current->getFunctionScope(), Variable::DeclKind::Var, var);
}
buildDummyLexicalParent(Builder, current, child);
addLexicalDebugInfo(current, global, scope->parentScope);
}
std::shared_ptr<SerializedScope> ESTreeIRGen::serializeScope(
FunctionContext *ctx,
bool includeGlobal) {
// Serialize the global scope if and only if it's the only scope.
// We serialize the global scope to avoid re-declaring variables,
// and only do it once to avoid creating spurious scopes.
if (!ctx || (ctx->function->isGlobalScope() && !includeGlobal))
return lexicalScopeChain;
auto scope = std::make_shared<SerializedScope>();
auto *func = ctx->function;
assert(func && "Missing function when saving scope");
scope->originalName = func->getOriginalOrInferredName();
if (auto *closure = func->getLazyClosureAlias()) {
scope->closureAlias = closure->getName();
}
for (auto *var : func->getFunctionScope()->getVariables()) {
scope->variables.push_back(var->getName());
}
scope->parentScope = serializeScope(ctx->getPreviousContext(), false);
return scope;
}
} // namespace irgen
} // namespace hermes