diff --git a/include/hermes/Sema/SemContext.h b/include/hermes/Sema/SemContext.h index 62affaf1684..f2e7cee3cdb 100644 --- a/include/hermes/Sema/SemContext.h +++ b/include/hermes/Sema/SemContext.h @@ -479,6 +479,17 @@ class SemContext { return root_->bindingTable_; } + uint32_t getWithLexicalDepth(ESTree::WithStatementNode * node) const { + if(auto it = withDepths.find(node); it != withDepths.end()) + return it->second; + assert(false && "with statement must have been processed before attempting to get the depth"); + return 0; + } + + void setWithLexicalDepth(ESTree::WithStatementNode * node, uint32_t depth) { + withDepths[node] = depth; + } + private: /// The parent SemContext of this SemContext. /// If null, this SemContext has no parent. @@ -517,6 +528,8 @@ class SemContext { /// "expression decl" are both set and are not the same value. llvh::DenseMap sideIdentifierDeclarationDecl_{}; + + llvh::DenseMap withDepths{}; }; class SemContextDumper { diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 8a04bc66f81..9e696070ed0 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -43,6 +43,7 @@ add_hermes_library(hermesFrontend IRGen/ESTreeIRGen-func.cpp IRGen/ESTreeIRGen-except.cpp IRGen/ESTreeIRGen-typed-class.cpp + IRGen/ESTreeIRGen-with.cpp IR/IR.cpp IR/CFG.cpp IR/IRBuilder.cpp diff --git a/lib/IRGen/ESTreeIRGen-expr.cpp b/lib/IRGen/ESTreeIRGen-expr.cpp index 6f9d58ddfff..d11458dc731 100644 --- a/lib/IRGen/ESTreeIRGen-expr.cpp +++ b/lib/IRGen/ESTreeIRGen-expr.cpp @@ -522,7 +522,10 @@ Value *ESTreeIRGen::genCallExpr(ESTree::CallExpressionNode *call) { // Must call the field init function immediately after. fieldInitClassType = curFunction()->typedClassContext.type; } else { - thisVal = Builder.getLiteralUndefined(); + thisVal = withAwareEmitLoad( + nullptr, + call->_callee, + ConditionalChainType::OBJECT_ONLY_WITH_UNDEFINED_ALTERNATE); callee = genExpression(call->_callee); } @@ -588,7 +591,10 @@ Value *ESTreeIRGen::genOptionalCallExpr( thisVal = Builder.getLiteralUndefined(); callee = genOptionalCallExpr(oce, shortCircuitBB); } else { - thisVal = Builder.getLiteralUndefined(); + thisVal = withAwareEmitLoad( + nullptr, + call->_callee, + ConditionalChainType::OBJECT_ONLY_WITH_UNDEFINED_ALTERNATE); callee = genExpression(getCallee(call)); } @@ -2068,15 +2074,32 @@ Value *ESTreeIRGen::genUnaryExpression(ESTree::UnaryExpressionNode *U) { Identifier name = getNameFieldFromID(iden); auto *var = resolveIdentifier(iden); - if (llvh::isa(var)) { - // If the variable doesn't exist or if it is global, we must generate - // a delete global property instruction. - return Builder.createDeletePropertyInst( - Builder.getGlobalObject(), Builder.getLiteralString(name)); + if (withScopes_.empty()) { + if (llvh::isa(var)) { + // If the variable doesn't exist or if it is global, we must generate + // a delete global property instruction. + return Builder.createDeletePropertyInst( + Builder.getGlobalObject(), Builder.getLiteralString(name)); + } else { + // Otherwise it is a local variable which can't be deleted and we just + // return false. + return Builder.getLiteralBool(false); + } } else { - // Otherwise it is a local variable which can't be deleted and we just - // return false. - return Builder.getLiteralBool(false); + auto withObj = withAwareEmitLoad( + var, + iden, + llvh::isa(var) + ? ConditionalChainType::OBJECT_ONLY_WITH_GLOBAL_ALTERNATE + : ConditionalChainType::OBJECT_ONLY_WITH_UNDEFINED_ALTERNATE); + + return genConditionalExpr( + [&]() -> Value * { return withObj; }, + [&]() -> Value * { + return Builder.createDeletePropertyInst( + withObj, Builder.getLiteralString(name)); + }, + [&]() -> Value * { return Builder.getLiteralBool(false); }); } } @@ -2337,6 +2360,36 @@ Value *ESTreeIRGen::genLogicalAssignmentExpr( return Builder.createPhiInst(std::move(values), std::move(blocks)); } +Value *ESTreeIRGen::genConditionalExpr( + const std::function &conditionGenerator, + const std::function &consequentGenerator, + const std::function &alternateGenerator) { + auto parentFunc = Builder.getInsertionBlock()->getParent(); + + PhiInst::ValueListType values; + PhiInst::BasicBlockListType blocks; + + auto alternateBlock = Builder.createBasicBlock(parentFunc); + auto consequentBlock = Builder.createBasicBlock(parentFunc); + auto continueBlock = Builder.createBasicBlock(parentFunc); + + Builder.createCondBranchInst( + conditionGenerator(), consequentBlock, alternateBlock); + + Builder.setInsertionBlock(consequentBlock); + values.push_back(consequentGenerator()); + blocks.push_back(Builder.getInsertionBlock()); + Builder.createBranchInst(continueBlock); + + Builder.setInsertionBlock(alternateBlock); + values.push_back(alternateGenerator()); + blocks.push_back(Builder.getInsertionBlock()); + Builder.createBranchInst(continueBlock); + + Builder.setInsertionBlock(continueBlock); + return Builder.createPhiInst(values, blocks); +} + Value *ESTreeIRGen::genConditionalExpr(ESTree::ConditionalExpressionNode *C) { auto parentFunc = Builder.getInsertionBlock()->getParent(); @@ -2389,7 +2442,7 @@ Value *ESTreeIRGen::genIdentifierExpression( // For uses of undefined/Infinity/NaN as the global property, we make an // optimization to always return the constant directly. - if (llvh::isa(Var)) { + if (llvh::isa(Var) && withScopes_.empty()) { if (StrName.getUnderlyingPointer() == kw_.identUndefined) { return Builder.getLiteralUndefined(); } @@ -2406,7 +2459,7 @@ Value *ESTreeIRGen::genIdentifierExpression( << curFunction()->function->getInternalNameStr() << "\"\n"); // Typeof does not throw. - return emitLoad(Var, afterTypeOf); + return withAwareEmitLoad(Var, Iden, {}, afterTypeOf); } Value *ESTreeIRGen::genMetaProperty(ESTree::MetaPropertyNode *MP) { diff --git a/lib/IRGen/ESTreeIRGen-stmt.cpp b/lib/IRGen/ESTreeIRGen-stmt.cpp index 588d953e5c4..5e564e9b5b0 100644 --- a/lib/IRGen/ESTreeIRGen-stmt.cpp +++ b/lib/IRGen/ESTreeIRGen-stmt.cpp @@ -219,6 +219,10 @@ void ESTreeIRGen::genStatement(ESTree::Node *stmt) { return genClassDeclaration(classDecl); } + if (auto *withDecl = llvh::dyn_cast(stmt)) { + return genWithStatement(withDecl); + } + Builder.getModule()->getContext().getSourceErrorManager().error( stmt->getSourceRange(), Twine("invalid statement encountered.")); } diff --git a/lib/IRGen/ESTreeIRGen-with.cpp b/lib/IRGen/ESTreeIRGen-with.cpp new file mode 100644 index 00000000000..edd3598702b --- /dev/null +++ b/lib/IRGen/ESTreeIRGen-with.cpp @@ -0,0 +1,133 @@ +#include "ESTreeIRGen.h" + +#include "hermes/IR/Instrs.h" +#include "llvh/ADT/SmallString.h" + +namespace hermes { +namespace irgen { + +void ESTreeIRGen::genWithStatement(ESTree::WithStatementNode *with) { + WithScopeInfo withScope{}; + withScope.depth = semCtx_.getWithLexicalDepth(with); + + withScope.object = Builder.createVariable( + curFunction()->curScope->getVariableScope(), + Builder + .getLiteralString( + "with" + std::to_string(withScope.depth)) + ->getValue(), + Type::createAnyType(), + true); + withScope.object->setIsConst(true); + emitStore(genExpression(with->_object), withScope.object, true); + + withScopes_.push_back(withScope); + genStatement(with->_body); + withScopes_.pop_back(); +} + +template +Value *ESTreeIRGen::createWithConditionalChain( + ESTree::Node *node, + const Callback &callback) { + ESTree::IdentifierNode *identifier = !withScopes_.empty() && node + ? llvh::dyn_cast(node) + : nullptr; + if (!identifier) { + return callback(nullptr, ""); + } + + uint32_t identifierDepth = INT_MAX; + identifierDepth = getIDDecl(identifier)->scope->depth; + std::string_view name = identifier->_name->c_str(); + + auto compareDepth = [](uint32_t searching, const WithScopeInfo &scope) { + return scope.depth > searching; + }; + + auto it = std::upper_bound( + withScopes_.begin(), withScopes_.end(), identifierDepth, compareDepth); + + return createWithConditionalChainImpl(it, withScopes_.end(), callback, name); +} + +template +Value *ESTreeIRGen::createWithConditionalChainImpl( + std::vector::iterator begin, + std::vector::iterator end, + const Callback &callback, + std::string_view name) { + if (begin == end) { + return callback(nullptr, name); + } + + auto ¤t = *(end - 1); + auto conditionGenerator = [&]() -> Value * { + auto *wrapper = Builder.createLoadPropertyInst( + Builder.getGlobalObject(), "HermesWithInternal"); + wrapper = Builder.createLoadPropertyInst(wrapper, "_containsField"); + + auto *call = Builder.createCallInst( + wrapper, + Builder.getLiteralUndefined(), + Builder.getLiteralUndefined(), + {emitLoad(current.object, false), + Builder.getLiteralString(name.data())}); + + return call; + }; + + auto consequentGenerator = [&]() -> Value * { + auto loadWithObj = emitLoad(current.object, false); + return callback(loadWithObj, name); + }; + + auto alternateGenerator = [&]() -> Value * { + return createWithConditionalChainImpl( + begin, end - 1, callback, name); + }; + + return genConditionalExpr( + conditionGenerator, consequentGenerator, alternateGenerator); +} + +Value *ESTreeIRGen::withAwareEmitLoad( + hermes::Value *ptr, + ESTree::Node *node, + ConditionalChainType conditionalChainType, + bool inhibitThrow) { + return createWithConditionalChain( + node, [&](Value *withObject, std::string_view idName) -> Value * { + switch (conditionalChainType) { + case ConditionalChainType::OBJECT_ONLY_WITH_UNDEFINED_ALTERNATE: + return withObject ? withObject : Builder.getLiteralUndefined(); + case ConditionalChainType::OBJECT_ONLY_WITH_GLOBAL_ALTERNATE: + return withObject ? withObject : Builder.getGlobalObject(); + case ConditionalChainType::MEMBER_EXPRESSION: + return withObject + ? Builder.createLoadPropertyInst(withObject, idName.data()) + : emitLoad(ptr, inhibitThrow); + default: + llvm_unreachable("Unhandled conditionalChainType"); + } + }); +} + +void ESTreeIRGen::withAwareEmitStore( + Value *storedValue, + Value *ptr, + bool declInit_, + ESTree::Node *node) { + createWithConditionalChain( + node, [&](Value *withObject, std::string_view idName) { + if (!withObject) + emitStore(storedValue, ptr, declInit_); + else + Builder.createStorePropertyInst( + storedValue, withObject, idName.data()); + return Builder.getLiteralUndefined(); + }); +} + +} // namespace irgen +} // namespace hermes diff --git a/lib/IRGen/ESTreeIRGen.cpp b/lib/IRGen/ESTreeIRGen.cpp index 5830b5feeed..93f85ce5e11 100644 --- a/lib/IRGen/ESTreeIRGen.cpp +++ b/lib/IRGen/ESTreeIRGen.cpp @@ -54,7 +54,8 @@ Value *LReference::emitLoad() { llvh::cast(ast_), base_, property_) .result; case Kind::VarOrGlobal: - return irgen_->emitLoad(base_, false); + return irgen_->withAwareEmitLoad( + base_, ast_, ESTreeIRGen::ConditionalChainType::MEMBER_EXPRESSION); case Kind::Destructuring: assert(false && "destructuring cannot be loaded"); return builder.getLiteralUndefined(); @@ -76,7 +77,7 @@ void LReference::emitStore(Value *value) { base_, property_); case Kind::VarOrGlobal: - irgen_->emitStore(value, base_, declInit_); + irgen_->withAwareEmitStore(value, base_, declInit_, ast_); return; case Kind::Error: return; @@ -414,7 +415,7 @@ LReference ESTreeIRGen::createLRef(ESTree::Node *node, bool declInit) { LReference::Kind::VarOrGlobal, this, declInit, - nullptr, + iden, var, nullptr, sourceLoc); diff --git a/lib/IRGen/ESTreeIRGen.h b/lib/IRGen/ESTreeIRGen.h index 7589aea9d3d..ebd09293482 100644 --- a/lib/IRGen/ESTreeIRGen.h +++ b/lib/IRGen/ESTreeIRGen.h @@ -426,6 +426,21 @@ class ESTreeIRGen { /// "outer" and "inner" generator function. using CompiledMapKey = llvh::PointerIntPair; + // Information about each nested 'with' statement, used to resolve identifiers + // correctly. + struct WithScopeInfo { + + /// Lexical depth of the 'with' body + uint32_t depth; + + /// Variable holding the 'with' statement object + Variable *object; + }; + + /// Vector holding information about all encountered 'with' statements. + using WithScopes = std::vector; + WithScopes withScopes_{}; + /// An "additional key" when mapping from an AST node to a compiled Value, /// used to distinguish between different values that may be associated with /// the same node. @@ -827,6 +842,11 @@ class ESTreeIRGen { Value *genLogicalExpression(ESTree::LogicalExpressionNode *logical); Value *genThisExpression(); + Value *genConditionalExpr( + const std::function &conditionGenerator, + const std::function &consequentGenerator, + const std::function &alternateGenerator); + /// A helper function to unify the largely same IRGen logic of \c genYieldExpr /// and \c genAwaitExpr. /// \param value the value operand of the will-generate SaveAndYieldInst. @@ -1036,6 +1056,10 @@ class ESTreeIRGen { VariableScope *parentScope, const CapturedState &capturedState); + /// Generate IR for a with statement block. + /// \param with is the ESTree with statement node. + void genWithStatement(ESTree::WithStatementNode *with); + /// In the beginning of an ES5 function, initialize the special captured /// variables needed by arrow functions, constructors and methods. /// This is used only by \c genES5Function() and the global scope. @@ -1380,6 +1404,46 @@ class ESTreeIRGen { /// \return the instruction performing the store. Instruction *emitStore(Value *storedValue, Value *ptr, bool declInit); + enum class ConditionalChainType : uint8_t { + // Multiple options to create the conditional chain: + MEMBER_EXPRESSION, // (a.var ? a.var : var) + OBJECT_ONLY_WITH_UNDEFINED_ALTERNATE, // (a.var ? a : undefined) + OBJECT_ONLY_WITH_GLOBAL_ALTERNATE // (a.var ? a : global) + }; + + /// Emit an instruction to load a value considering 'with' statements. + /// This function extends the regular load operation by incorporating logic + /// that handles 'with' statements in the source code, allowing for conditional + /// chaining based on the scope information. + /// \param ptr the location from which to load, possibly subject to 'with' scopes. + /// \param node the AST node associated with the identifier in the load operation. + /// \param conditionalChainType specifies the type of conditional chain to use + /// when resolving property accesses within 'with' contexts. + /// \param inhibitThrow if true, do not throw when loading from missing + /// global properties. + /// \return the instruction performing the load, possibly adjusted for 'with' context resolution. + Value *withAwareEmitLoad( + Value *ptr, + ESTree::Node *node, + ConditionalChainType conditionalChainType, + bool inhibitThrow = false); + + /// Emit an instruction to store a value considering 'with' statements. + /// This function extends the regular store operation by incorporating logic + /// that handles 'with' statements in the source code, allowing for conditional + /// chaining based on the scope information. + /// \param storedValue value to store + /// \param ptr the location to store into, adjusted for 'with' context if necessary. + /// \param declInit_ whether this is a declaration initializer, so the TDZ + /// check should be skipped. + /// \param node the AST node associated with the identifier in the store operation. + /// \return the stored value, possibly modified to respect 'with' context rules. + void withAwareEmitStore( + Value *storedValue, + Value *ptr, + bool declInit_, + ESTree::Node *node); + private: /// Search for the specified AST node in \c compiledEntities_ and return the /// associated IR value, or nullptr if not found. @@ -1409,7 +1473,11 @@ class ESTreeIRGen { CompiledMapKey key(node, (unsigned)extraKey); assert(compiledEntities_.count(key) == 0 && "Overwriting compiled entity"); compiledEntities_[key] = value; - compilationQueue_.emplace_back(f); + compilationQueue_.emplace_back( + [this, f = std::forward(f), withScopesCopy = withScopes_]() mutable { + this->withScopes_ = std::move(withScopesCopy); + f(); + }); } /// Run all tasks in the compilation queue until it is empty. @@ -1434,6 +1502,24 @@ class ESTreeIRGen { assert(decl->customData); return static_cast(decl->customData); } + + /// Generates ternary operator chain for loads/stores on objects in all + /// surrounding with scopes. + /// \p callback void(Value *withObject, string_view name) - is called with the + /// object from `with(object)` if the identifier is found on the object from + /// surrounding with objects. Else is called with nullptr object indicating + /// that the identifier was not found in any of the surrounding with objects. + template + Value *createWithConditionalChain( + ESTree::Node *node, + const Callback &callback); + + template + Value *createWithConditionalChainImpl( + std::vector::iterator begin, + std::vector::iterator end, + const Callback &callback, + std::string_view name); }; template diff --git a/lib/InternalJavaScript/05-With.js b/lib/InternalJavaScript/05-With.js new file mode 100644 index 00000000000..86997a0237a --- /dev/null +++ b/lib/InternalJavaScript/05-With.js @@ -0,0 +1,24 @@ +(function() { + function _containsField(obj, field) { + // We use __containsField to handle multiple scenarios: + // 1. checking for properties in primitive types (e.g. _containsField(1, 'toString') should return true) + // 2. checking for properties in objects and functions, including undefined properties (e.g. _containsField({a:undefined}, 'a') should return true) + if (obj instanceof Object) { + if (obj.hasOwnProperty(Symbol.unscopables)) { + const unscopables = obj[Symbol.unscopables]; + if (unscopables && unscopables[field]) { + return false; + } + } + return obj[field] !== undefined || obj.hasOwnProperty(field); + } else { + return obj[field] !== undefined; + } + } + + var HermesWithInternal = { + _containsField + }; + Object.freeze(HermesWithInternal); + globalThis.HermesWithInternal = HermesWithInternal; +})(); diff --git a/lib/Parser/JSParserImpl.cpp b/lib/Parser/JSParserImpl.cpp index 08b12941fa9..d36a4576116 100644 --- a/lib/Parser/JSParserImpl.cpp +++ b/lib/Parser/JSParserImpl.cpp @@ -715,7 +715,8 @@ Optional JSParserImpl::parseFunctionBody( bool paramAwait, JSLexer::GrammarContext grammarContext, bool parseDirectives) { - if (pass_ == LazyParse && !eagerly) { + // TODO: enable lazy compilation inside with statements + if (pass_ == LazyParse && !eagerly && !insideWithStatement) { auto startLoc = tok_->getStartLoc(); assert( preParsed_->functionInfo.count(startLoc) == 1 && @@ -2031,6 +2032,8 @@ Optional JSParserImpl::parseWithStatement( startLoc)) return None; + auto oldInsideWithStatement = insideWithStatement; + insideWithStatement = true; auto optExpr = parseExpression(); if (!optExpr) return None; @@ -2047,6 +2050,8 @@ Optional JSParserImpl::parseWithStatement( if (!optBody) return None; + insideWithStatement = oldInsideWithStatement; + return setLocation( startLoc, optBody.getValue(), diff --git a/lib/Parser/JSParserImpl.h b/lib/Parser/JSParserImpl.h index 11ef7e6c3ad..e480317cc97 100644 --- a/lib/Parser/JSParserImpl.h +++ b/lib/Parser/JSParserImpl.h @@ -208,6 +208,8 @@ class JSParserImpl { /// This is used when checking if `await` is a valid Identifier name. bool paramAwait_{false}; + bool insideWithStatement{false}; + /// Appended when the parser has seen an directive being visited in the /// current function scope (It's intended to be used with /// `SaveStrictModeAndSeenDirectives`). diff --git a/lib/Sema/SemanticResolver.cpp b/lib/Sema/SemanticResolver.cpp index 632c088e4f2..9287a517b94 100644 --- a/lib/Sema/SemanticResolver.cpp +++ b/lib/Sema/SemanticResolver.cpp @@ -689,17 +689,16 @@ void SemanticResolver::visit(ESTree::ContinueStatementNode *node) { } void SemanticResolver::visit(ESTree::WithStatementNode *node) { - if (compile_) - sm_.error(node->getStartLoc(), "with statement is not supported"); + if (curFunctionInfo()->strict) { + if (compile_) + sm_.error( + node->getStartLoc(), + "with statement is not supported in strict mode"); + return; + } + semCtx_.setWithLexicalDepth(node, curScope_->depth+1); visitESTreeChildren(*this, node); - - uint32_t depth = curScope_->depth; - // Run the Unresolver to avoid resolving to variables past the depth of the - // `with`. - // Pass `depth + 1` because variables declared in this scope also cannot be - // trusted. - Unresolver::run(semCtx_, depth + 1, node->_body); } void SemanticResolver::visit(ESTree::TryStatementNode *tryStatement) { diff --git a/test/Sema/reject-with.js b/test/Sema/reject-with.js deleted file mode 100644 index 4fe0faeac07..00000000000 --- a/test/Sema/reject-with.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * 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. - */ - -// RUN: (! %shermes %s -dump-ir 2>&1 ) | %FileCheck --match-full-lines %s - -with ({a:10}) - print(a); -// CHECK: {{.*}}reject-with.js:[[@LINE-2]]:1: error: with statement is not supported diff --git a/test/hermes/eval-errors.js b/test/hermes/eval-errors.js index e7e11e0013c..7d4d14169c1 100644 --- a/test/hermes/eval-errors.js +++ b/test/hermes/eval-errors.js @@ -25,12 +25,3 @@ try{ global.eval("throw new Error()\n//# sourceURL=foo"); } catch (e) { } //CHECK-NEXT: Error //CHECK-NEXT: at eval (foo:1:16) - -try { - var f = new Function(" 'use strict'; var o = {}; with (o) {}; "); -} catch (e) { - print(e.stack); -} -//CHECK: SyntaxError: 1:41:with statement is not supported -//CHECK-NEXT: at Function (native) -//CHECK-NEXT: at global ({{.*}}eval-errors.js:{{.*}}:25) diff --git a/test/hermes/with.js b/test/hermes/with.js new file mode 100644 index 00000000000..bb5e0a62f12 --- /dev/null +++ b/test/hermes/with.js @@ -0,0 +1,40 @@ +/** + * 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. + */ + +// RUN: %hermes -O %s | %FileCheck --match-full-lines %s +// RUN: %hermes -O -emit-binary -out %t.hbc %s && %hermes %t.hbc | %FileCheck --match-full-lines %s +// RUN: %shermes -exec %s | %FileCheck --match-full-lines %s + +let obj = { a: 5 }; +with (obj) { + print(a); +} +// CHECK: 5 + +obj = { b: 10, nested: { c: 15 } }; +with (obj) { + print(b); + with (nested) { + print(c); + } +} +// CHECK-NEXT: 10 +// CHECK-NEXT: 15 + +obj = { d: 20 }; +with (obj) { + delete d; +} +print('d' in obj); +// CHECK-NEXT: false + +obj = { e: 25 }; +with (obj) { + e = 30; +} +print(obj.e); +// CHECK-NEXT: 30