[turbofan] Fix receiver binding for inlined callees.

This introduces a JSConvertReceiver operator to model the implicit
conversion of receiver values for sloppy callees. It is used by the
JSInliner for now, but can also be used to model direction function
calls that bypass call stubs.

Also note that a hint is passed to said operator whenever the source
structure constrains the receiver value type. This hint allows for
optimizations in the lowering of the operator.

The underlying specification in ES6, section 9.2.1.2 is the basis for
this implementation.

R=bmeurer@chromium.org
TEST=mjsunit/compiler/receiver-conversion
BUG=v8:4493, v8:4470
LOG=n

Review URL: https://codereview.chromium.org/1412223015

Cr-Commit-Position: refs/heads/master@{#31598}
This commit is contained in:
mstarzinger 2015-10-27 05:13:32 -07:00 committed by Commit bot
parent 15b4804cdf
commit 37f5e23b5c
13 changed files with 246 additions and 25 deletions

View File

@ -2312,8 +2312,9 @@ void AstGraphBuilder::VisitCall(Call* expr) {
// Prepare the callee and the receiver to the function call. This depends on
// the semantics of the underlying call type.
CallFunctionFlags flags = NO_CALL_FUNCTION_FLAGS;
Node* receiver_value = NULL;
Node* callee_value = NULL;
ConvertReceiverMode receiver_hint = ConvertReceiverMode::kAny;
Node* receiver_value = nullptr;
Node* callee_value = nullptr;
bool possibly_eval = false;
switch (call_type) {
case Call::GLOBAL_CALL: {
@ -2323,6 +2324,7 @@ void AstGraphBuilder::VisitCall(Call* expr) {
callee_value =
BuildVariableLoad(proxy->var(), expr->expression()->id(), states,
pair, OutputFrameStateCombine::Push());
receiver_hint = ConvertReceiverMode::kNullOrUndefined;
receiver_value = jsgraph()->UndefinedConstant();
break;
}
@ -2362,14 +2364,16 @@ void AstGraphBuilder::VisitCall(Call* expr) {
states.AddToNode(callee_value, property->LoadId(),
OutputFrameStateCombine::Push());
}
receiver_value = environment()->Pop();
// Note that a PROPERTY_CALL requires the receiver to be wrapped into an
// object for sloppy callees. This could also be modeled explicitly
// here,
// thereby obsoleting the need for a flag to the call operator.
// object for sloppy callees. However the receiver is guaranteed not to
// be null or undefined at this point.
receiver_hint = ConvertReceiverMode::kNotNullOrUndefined;
receiver_value = environment()->Pop();
flags = CALL_AS_METHOD;
} else {
// TODO(mstarzinger): Cleanup this special handling for super access,
// the stack layout seems to be completely out of sync here, fix this!
VisitForValue(property->obj()->AsSuperPropertyReference()->this_var());
VisitForValue(
property->obj()->AsSuperPropertyReference()->home_object());
@ -2416,6 +2420,7 @@ void AstGraphBuilder::VisitCall(Call* expr) {
case Call::OTHER_CALL:
VisitForValue(callee);
callee_value = environment()->Pop();
receiver_hint = ConvertReceiverMode::kNullOrUndefined;
receiver_value = jsgraph()->UndefinedConstant();
break;
}
@ -2439,7 +2444,7 @@ void AstGraphBuilder::VisitCall(Call* expr) {
Node* source = environment()->Peek(arg_count - 1);
// Create node to ask for help resolving potential eval call. This will
// provide a fully resolved callee and the corresponding receiver.
// provide a fully resolved callee to patch into the environment.
Node* function = GetFunctionClosure();
Node* language = jsgraph()->Constant(language_mode());
Node* position = jsgraph()->Constant(current_scope()->start_position());
@ -2456,8 +2461,8 @@ void AstGraphBuilder::VisitCall(Call* expr) {
// Create node to perform the function call.
VectorSlotPair feedback = CreateVectorSlotPair(expr->CallFeedbackICSlot());
const Operator* call = javascript()->CallFunction(args->length() + 2, flags,
language_mode(), feedback);
const Operator* call = javascript()->CallFunction(
args->length() + 2, flags, language_mode(), feedback, receiver_hint);
Node* value = ProcessArguments(call, args->length() + 2);
environment()->Push(callee_value);
PrepareFrameState(value, expr->ReturnId(), OutputFrameStateCombine::Push());

View File

@ -117,6 +117,7 @@ REPLACE_COMPARE_IC_CALL_WITH_LANGUAGE_MODE(JSGreaterThanOrEqual, Token::GTE)
REPLACE_RUNTIME_CALL(JSCreateFunctionContext, Runtime::kNewFunctionContext)
REPLACE_RUNTIME_CALL(JSCreateWithContext, Runtime::kPushWithContext)
REPLACE_RUNTIME_CALL(JSCreateModuleContext, Runtime::kPushModuleContext)
REPLACE_RUNTIME_CALL(JSConvertReceiver, Runtime::kConvertReceiver)
#undef REPLACE_RUNTIME

View File

@ -420,6 +420,18 @@ Reduction JSInliner::ReduceJSCallFunction(Node* node,
frame_state = CreateArgumentsAdaptorFrameState(&call, info.shared_info());
}
// Insert a JSConvertReceiver node for sloppy callees. Note that the context
// passed into this node has to be the callees context (loaded above).
if (is_sloppy(info.language_mode()) && !function->shared()->native()) {
const CallFunctionParameters& p = CallFunctionParametersOf(node->op());
Node* effect = NodeProperties::GetEffectInput(node);
Node* convert = jsgraph_->graph()->NewNode(
jsgraph_->javascript()->ConvertReceiver(p.convert_mode()),
call.receiver(), context, frame_state, effect, start);
NodeProperties::ReplaceValueInput(node, convert, 1);
NodeProperties::ReplaceEffectInput(node, convert);
}
return InlineCall(node, context, frame_state, start, end);
}

View File

@ -531,7 +531,7 @@ Reduction JSIntrinsicLowering::ReduceToObject(Node* node) {
Reduction JSIntrinsicLowering::ReduceCallFunction(Node* node) {
CallRuntimeParameters params = OpParameter<CallRuntimeParameters>(node->op());
CallRuntimeParameters params = CallRuntimeParametersOf(node->op());
size_t arity = params.arity();
Node* function = node->InputAt(static_cast<int>(arity - 1));
while (--arity != 0) {
@ -540,9 +540,9 @@ Reduction JSIntrinsicLowering::ReduceCallFunction(Node* node) {
}
node->ReplaceInput(0, function);
NodeProperties::ChangeOp(
node,
javascript()->CallFunction(params.arity(), NO_CALL_FUNCTION_FLAGS, STRICT,
VectorSlotPair(), ALLOW_TAIL_CALLS));
node, javascript()->CallFunction(
params.arity(), NO_CALL_FUNCTION_FLAGS, STRICT,
VectorSlotPair(), ConvertReceiverMode::kAny, ALLOW_TAIL_CALLS));
return Changed(node);
}

View File

@ -40,6 +40,25 @@ size_t hash_value(VectorSlotPair const& p) {
}
size_t hash_value(ConvertReceiverMode const& mode) {
return base::hash_value(static_cast<int>(mode));
}
std::ostream& operator<<(std::ostream& os, ConvertReceiverMode const& mode) {
switch (mode) {
case ConvertReceiverMode::kNullOrUndefined:
return os << "NULL_OR_UNDEFINED";
case ConvertReceiverMode::kNotNullOrUndefined:
return os << "NOT_NULL_OR_UNDEFINED";
case ConvertReceiverMode::kAny:
return os << "ANY";
}
UNREACHABLE();
return os;
}
std::ostream& operator<<(std::ostream& os, CallFunctionParameters const& p) {
os << p.arity() << ", " << p.flags() << ", " << p.language_mode();
if (p.AllowTailCalls()) {
@ -496,13 +515,12 @@ CACHED_OP_LIST_WITH_LANGUAGE_MODE(CACHED_WITH_LANGUAGE_MODE)
#undef CACHED_WITH_LANGUAGE_MODE
const Operator* JSOperatorBuilder::CallFunction(size_t arity,
CallFunctionFlags flags,
LanguageMode language_mode,
VectorSlotPair const& feedback,
const Operator* JSOperatorBuilder::CallFunction(
size_t arity, CallFunctionFlags flags, LanguageMode language_mode,
VectorSlotPair const& feedback, ConvertReceiverMode convert_mode,
TailCallMode tail_call_mode) {
CallFunctionParameters parameters(arity, flags, language_mode, feedback,
tail_call_mode);
tail_call_mode, convert_mode);
return new (zone()) Operator1<CallFunctionParameters>( // --
IrOpcode::kJSCallFunction, Operator::kNoProperties, // opcode
"JSCallFunction", // name
@ -533,6 +551,16 @@ const Operator* JSOperatorBuilder::CallConstruct(int arguments) {
}
const Operator* JSOperatorBuilder::ConvertReceiver(
ConvertReceiverMode convert_mode) {
return new (zone()) Operator1<ConvertReceiverMode>( // --
IrOpcode::kJSConvertReceiver, Operator::kNoThrow, // opcode
"JSConvertReceiver", // name
1, 1, 1, 1, 1, 0, // counts
convert_mode); // parameter
}
const Operator* JSOperatorBuilder::LoadNamed(LanguageMode language_mode,
Handle<Name> name,
const VectorSlotPair& feedback) {

View File

@ -41,8 +41,26 @@ bool operator!=(VectorSlotPair const&, VectorSlotPair const&);
size_t hash_value(VectorSlotPair const&);
// Defines hints about receiver values based on structural knowledge. This is
// used as a parameter by JSConvertReceiver operators.
enum class ConvertReceiverMode {
kNullOrUndefined, // Guaranteed to be null or undefined.
kNotNullOrUndefined, // Guaranteed to never be null or undefined.
kAny // No specific knowledge about receiver.
};
size_t hash_value(ConvertReceiverMode const&);
std::ostream& operator<<(std::ostream&, ConvertReceiverMode const&);
const ConvertReceiverMode& ConvertReceiverModeOf(const Operator* op);
// Defines whether tail call optimization is allowed.
enum TailCallMode { NO_TAIL_CALLS, ALLOW_TAIL_CALLS };
// Defines the arity and the call flags for a JavaScript function call. This is
// used as a parameter by JSCallFunction operators.
class CallFunctionParameters final {
@ -50,22 +68,27 @@ class CallFunctionParameters final {
CallFunctionParameters(size_t arity, CallFunctionFlags flags,
LanguageMode language_mode,
VectorSlotPair const& feedback,
TailCallMode tail_call_mode)
TailCallMode tail_call_mode,
ConvertReceiverMode convert_mode)
: bit_field_(ArityField::encode(arity) | FlagsField::encode(flags) |
LanguageModeField::encode(language_mode)),
feedback_(feedback),
tail_call_mode_(tail_call_mode) {}
tail_call_mode_(tail_call_mode),
convert_mode_(convert_mode) {}
size_t arity() const { return ArityField::decode(bit_field_); }
CallFunctionFlags flags() const { return FlagsField::decode(bit_field_); }
LanguageMode language_mode() const {
return LanguageModeField::decode(bit_field_);
}
ConvertReceiverMode convert_mode() const { return convert_mode_; }
VectorSlotPair const& feedback() const { return feedback_; }
bool operator==(CallFunctionParameters const& that) const {
return this->bit_field_ == that.bit_field_ &&
this->feedback_ == that.feedback_;
this->feedback_ == that.feedback_ &&
this->tail_call_mode_ == that.tail_call_mode_ &&
this->convert_mode_ == that.convert_mode_;
}
bool operator!=(CallFunctionParameters const& that) const {
return !(*this == that);
@ -75,7 +98,7 @@ class CallFunctionParameters final {
private:
friend size_t hash_value(CallFunctionParameters const& p) {
return base::hash_combine(p.bit_field_, p.feedback_);
return base::hash_combine(p.bit_field_, p.feedback_, p.convert_mode_);
}
typedef BitField<size_t, 0, 28> ArityField;
@ -84,7 +107,8 @@ class CallFunctionParameters final {
const uint32_t bit_field_;
const VectorSlotPair feedback_;
bool tail_call_mode_;
TailCallMode tail_call_mode_;
ConvertReceiverMode convert_mode_;
};
size_t hash_value(CallFunctionParameters const&);
@ -440,11 +464,13 @@ class JSOperatorBuilder final : public ZoneObject {
const Operator* CallFunction(
size_t arity, CallFunctionFlags flags, LanguageMode language_mode,
VectorSlotPair const& feedback = VectorSlotPair(),
ConvertReceiverMode convert_mode = ConvertReceiverMode::kAny,
TailCallMode tail_call_mode = NO_TAIL_CALLS);
const Operator* CallRuntime(Runtime::FunctionId id, size_t arity);
const Operator* CallConstruct(int arguments);
const Operator* ConvertReceiver(ConvertReceiverMode convert_mode);
const Operator* LoadProperty(LanguageMode language_mode,
VectorSlotPair const& feedback);
const Operator* LoadNamed(LanguageMode language_mode, Handle<Name> name,

View File

@ -139,6 +139,7 @@
V(JSCallConstruct) \
V(JSCallFunction) \
V(JSCallRuntime) \
V(JSConvertReceiver) \
V(JSForInDone) \
V(JSForInNext) \
V(JSForInPrepare) \

View File

@ -60,6 +60,7 @@ int OperatorProperties::GetFrameStateInputCount(const Operator* op) {
case IrOpcode::kJSToName:
// Misc operations
case IrOpcode::kJSConvertReceiver:
case IrOpcode::kJSForInNext:
case IrOpcode::kJSForInPrepare:
case IrOpcode::kJSStackCheck:

View File

@ -1480,6 +1480,11 @@ Type* Typer::Visitor::TypeJSCallRuntime(Node* node) {
}
Type* Typer::Visitor::TypeJSConvertReceiver(Node* node) {
return Type::Receiver();
}
Type* Typer::Visitor::TypeJSForInNext(Node* node) {
return Type::Union(Type::Name(), Type::Undefined(), zone());
}

View File

@ -566,6 +566,7 @@ void Verifier::Visitor::Check(Node* node) {
}
case IrOpcode::kJSCallConstruct:
case IrOpcode::kJSConvertReceiver:
// Type is Receiver.
CheckUpperIs(node, Type::Receiver());
break;

View File

@ -547,6 +547,18 @@ RUNTIME_FUNCTION(Runtime_GetOriginalConstructor) {
}
// ES6 section 9.2.1.2, OrdinaryCallBindThis for sloppy callee.
RUNTIME_FUNCTION(Runtime_ConvertReceiver) {
HandleScope scope(isolate);
DCHECK(args.length() == 1);
CONVERT_ARG_HANDLE_CHECKED(Object, receiver, 0);
if (receiver->IsNull() || receiver->IsUndefined()) {
return isolate->global_proxy();
}
return *Object::ToObject(isolate, receiver).ToHandleChecked();
}
// TODO(bmeurer): Kill %_CallFunction ASAP as it is almost never used
// correctly because of the weird semantics underneath.
RUNTIME_FUNCTION(Runtime_CallFunction) {

View File

@ -259,6 +259,7 @@ namespace internal {
F(Call, -1 /* >= 2 */, 1) \
F(Apply, 5, 1) \
F(GetOriginalConstructor, 0, 1) \
F(ConvertReceiver, 1, 1) \
F(CallFunction, -1 /* receiver + n args + function */, 1) \
F(IsConstructCall, 0, 1) \
F(IsFunction, 1, 1)

View File

@ -0,0 +1,128 @@
// Copyright 2015 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Flags: --allow-natives-syntax
// This test suite checks that the receiver value (i.e. the 'this' binding) is
// correctly converted even when the callee function is inlined. This behavior
// is specified by ES6, section 9.2.1.2 "OrdinaryCallBindThis".
var global = this;
function test(outer, inner, check) {
check(outer());
check(outer());
%OptimizeFunctionOnNextCall(outer);
check(outer());
}
// -----------------------------------------------------------------------------
// Test undefined in sloppy mode.
(function UndefinedSloppy() {
function check(x) {
assertEquals("object", typeof x);
assertSame(global, x);
}
function inner(x) {
return this;
}
function outer() {
return sloppy();
}
global.sloppy = inner;
test(outer, inner, check);
})();
// -----------------------------------------------------------------------------
// Test undefined in strict mode.
(function UndefinedStrict() {
function check(x) {
assertEquals("undefined", typeof x);
assertSame(undefined, x);
}
function inner(x) {
"use strict";
return this;
}
function outer() {
return strict();
}
global.strict = inner;
test(outer, inner, check);
})();
// -----------------------------------------------------------------------------
// Test primitive number in sloppy mode.
(function NumberSloppy() {
function check(x) {
assertEquals("object", typeof x);
assertInstanceof(x, Number);
}
function inner(x) {
return this;
}
function outer() {
return (0).sloppy();
}
Number.prototype.sloppy = inner;
test(outer, inner, check);
})();
// -----------------------------------------------------------------------------
// Test primitive number in strict mode.
(function NumberStrict() {
function check(x) {
assertEquals("number", typeof x);
assertSame(0, x);
}
function inner(x) {
"use strict";
return this;
}
function outer() {
return (0).strict();
}
Number.prototype.strict = inner;
test(outer, inner, check);
})();
// -----------------------------------------------------------------------------
// Test primitive string in sloppy mode.
(function StringSloppy() {
function check(x) {
assertEquals("object", typeof x);
assertInstanceof(x, String);
}
function inner(x) {
return this;
}
function outer() {
return ("s").sloppy();
}
String.prototype.sloppy = inner;
test(outer, inner, check);
})();
// -----------------------------------------------------------------------------
// Test primitive string in strict mode.
(function StringStrict() {
function check(x) {
assertEquals("string", typeof x);
assertSame("s", x);
}
function inner(x) {
"use strict";
return this;
}
function outer() {
return ("s").strict();
}
String.prototype.strict = inner;
test(outer, inner, check);
})();