[interpreter] Implement handling of try-finally constructs.

This models function local control flow through try-finally constructs
using a token dispatch mechanism. All paths through the finally block
are assigned a token, at the end of the finally block a switch construct
dispatches according to this token.

R=oth@chromium.org,rmcilroy@chromium.org
BUG=v8:4674
LOG=n

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

Cr-Commit-Position: refs/heads/master@{#33465}
This commit is contained in:
mstarzinger 2016-01-22 04:43:11 -08:00 committed by Commit bot
parent 847ac580f1
commit e175e39fed
6 changed files with 350 additions and 43 deletions

View File

@ -89,9 +89,13 @@ class BytecodeGenerator::ControlScope BASE_EMBEDDED {
void Break(Statement* stmt) { PerformCommand(CMD_BREAK, stmt); }
void Continue(Statement* stmt) { PerformCommand(CMD_CONTINUE, stmt); }
void ReturnAccumulator() { PerformCommand(CMD_RETURN, nullptr); }
void ReThrowAccumulator() { PerformCommand(CMD_RETHROW, nullptr); }
class DeferredCommands;
protected:
enum Command { CMD_BREAK, CMD_CONTINUE };
enum Command { CMD_BREAK, CMD_CONTINUE, CMD_RETURN, CMD_RETHROW };
void PerformCommand(Command command, Statement* statement);
virtual bool Execute(Command command, Statement* statement) = 0;
@ -108,6 +112,112 @@ class BytecodeGenerator::ControlScope BASE_EMBEDDED {
};
// Helper class for a try-finally control scope. It can record intercepted
// control-flow commands that cause entry into a finally-block, and re-apply
// them after again leaving that block. Special tokens are used to identify
// paths going through the finally-block to dispatch after leaving the block.
class BytecodeGenerator::ControlScope::DeferredCommands final {
public:
DeferredCommands(BytecodeGenerator* generator, Register token_register,
Register result_register)
: generator_(generator),
deferred_(generator->zone()),
token_register_(token_register),
result_register_(result_register) {}
// One recorded control-flow command.
struct Entry {
Command command; // The command type being applied on this path.
Statement* statement; // The target statement for the command or {nullptr}.
int token; // A token identifying this particular path.
};
// Records a control-flow command while entering the finally-block. This also
// generates a new dispatch token that identifies one particular path. This
// expects the result to be in the accumulator.
void RecordCommand(Command command, Statement* statement) {
int token = static_cast<int>(deferred_.size());
deferred_.push_back({command, statement, token});
builder()->StoreAccumulatorInRegister(result_register_);
builder()->LoadLiteral(Smi::FromInt(token));
builder()->StoreAccumulatorInRegister(token_register_);
}
// Records the dispatch token to be used to identify the re-throw path when
// the finally-block has been entered through the exception handler. This
// expects the exception to be in the accumulator.
void RecordHandlerReThrowPath() {
// The accumulator contains the exception object.
RecordCommand(CMD_RETHROW, nullptr);
}
// Records the dispatch token to be used to identify the implicit fall-through
// path at the end of a try-block into the corresponding finally-block.
void RecordFallThroughPath() {
builder()->LoadLiteral(Smi::FromInt(-1));
builder()->StoreAccumulatorInRegister(token_register_);
}
// Applies all recorded control-flow commands after the finally-block again.
// This generates a dynamic dispatch on the token from the entry point.
void ApplyDeferredCommands() {
// The fall-through path is covered by the default case, hence +1 here.
SwitchBuilder dispatch(builder(), static_cast<int>(deferred_.size() + 1));
for (size_t i = 0; i < deferred_.size(); ++i) {
Entry& entry = deferred_[i];
builder()->LoadLiteral(Smi::FromInt(entry.token));
builder()->CompareOperation(Token::EQ_STRICT, token_register_,
Strength::WEAK);
dispatch.Case(static_cast<int>(i));
}
dispatch.DefaultAt(static_cast<int>(deferred_.size()));
for (size_t i = 0; i < deferred_.size(); ++i) {
Entry& entry = deferred_[i];
dispatch.SetCaseTarget(static_cast<int>(i));
builder()->LoadAccumulatorWithRegister(result_register_);
execution_control()->PerformCommand(entry.command, entry.statement);
}
dispatch.SetCaseTarget(static_cast<int>(deferred_.size()));
}
BytecodeArrayBuilder* builder() { return generator_->builder(); }
ControlScope* execution_control() { return generator_->execution_control(); }
private:
BytecodeGenerator* generator_;
ZoneVector<Entry> deferred_;
Register token_register_;
Register result_register_;
};
// Scoped class for dealing with control flow reaching the function level.
class BytecodeGenerator::ControlScopeForTopLevel final
: public BytecodeGenerator::ControlScope {
public:
explicit ControlScopeForTopLevel(BytecodeGenerator* generator)
: ControlScope(generator) {}
protected:
bool Execute(Command command, Statement* statement) override {
switch (command) {
case CMD_BREAK:
case CMD_CONTINUE:
break;
case CMD_RETURN:
generator()->builder()->Return();
return true;
case CMD_RETHROW:
// TODO(mstarzinger): Should be a ReThrow instead.
generator()->builder()->Throw();
return true;
}
return false;
}
};
// Scoped class for enabling break inside blocks and switch blocks.
class BytecodeGenerator::ControlScopeForBreakable final
: public BytecodeGenerator::ControlScope {
@ -127,6 +237,8 @@ class BytecodeGenerator::ControlScopeForBreakable final
control_builder_->Break();
return true;
case CMD_CONTINUE:
case CMD_RETURN:
case CMD_RETHROW:
break;
}
return false;
@ -160,6 +272,9 @@ class BytecodeGenerator::ControlScopeForIteration final
case CMD_CONTINUE:
loop_builder_->Continue();
return true;
case CMD_RETURN:
case CMD_RETHROW:
break;
}
return false;
}
@ -170,6 +285,66 @@ class BytecodeGenerator::ControlScopeForIteration final
};
// Scoped class for enabling 'throw' in try-catch constructs.
class BytecodeGenerator::ControlScopeForTryCatch final
: public BytecodeGenerator::ControlScope {
public:
ControlScopeForTryCatch(BytecodeGenerator* generator,
TryCatchBuilder* try_catch_builder)
: ControlScope(generator), try_catch_builder_(try_catch_builder) {}
protected:
bool Execute(Command command, Statement* statement) override {
switch (command) {
case CMD_BREAK:
case CMD_CONTINUE:
case CMD_RETURN:
break;
case CMD_RETHROW:
// TODO(mstarzinger): Test and implement this!
USE(try_catch_builder_);
UNIMPLEMENTED();
return true;
}
return false;
}
private:
TryCatchBuilder* try_catch_builder_;
};
// Scoped class for enabling control flow through try-finally constructs.
class BytecodeGenerator::ControlScopeForTryFinally final
: public BytecodeGenerator::ControlScope {
public:
ControlScopeForTryFinally(BytecodeGenerator* generator,
TryFinallyBuilder* try_finally_builder,
DeferredCommands* commands)
: ControlScope(generator),
try_finally_builder_(try_finally_builder),
commands_(commands) {}
protected:
bool Execute(Command command, Statement* statement) override {
switch (command) {
case CMD_BREAK:
case CMD_CONTINUE:
case CMD_RETURN:
case CMD_RETHROW:
commands_->RecordCommand(command, statement);
try_finally_builder_->LeaveTry();
return true;
}
return false;
}
private:
TryFinallyBuilder* try_finally_builder_;
DeferredCommands* commands_;
};
void BytecodeGenerator::ControlScope::PerformCommand(Command command,
Statement* statement) {
ControlScope* current = this;
@ -375,6 +550,9 @@ Handle<BytecodeArray> BytecodeGenerator::MakeBytecode(CompilationInfo* info) {
// Initialize the incoming context.
ContextScope incoming_context(this, scope(), false);
// Initialize control scope.
ControlScopeForTopLevel control(this);
builder()->set_parameter_count(info->num_parameters_including_this());
builder()->set_locals_count(scope()->num_stack_slots());
builder()->set_context_count(scope()->MaxNestedContextChainLength());
@ -671,7 +849,7 @@ void BytecodeGenerator::VisitBreakStatement(BreakStatement* stmt) {
void BytecodeGenerator::VisitReturnStatement(ReturnStatement* stmt) {
VisitForAccumulatorValue(stmt->expression());
builder()->Return();
execution_control()->ReturnAccumulator();
}
@ -919,8 +1097,10 @@ void BytecodeGenerator::VisitTryCatchStatement(TryCatchStatement* stmt) {
// Evaluate the try-block inside a control scope. This simulates a handler
// that is intercepting 'throw' control commands.
try_control_builder.BeginTry(context);
// TODO(mstarzinger): Control scope is missing!
Visit(stmt->try_block());
{
ControlScopeForTryCatch scope(this, &try_control_builder);
Visit(stmt->try_block());
}
try_control_builder.EndTry();
// Clear message object as we enter the catch block.
@ -938,6 +1118,25 @@ void BytecodeGenerator::VisitTryCatchStatement(TryCatchStatement* stmt) {
void BytecodeGenerator::VisitTryFinallyStatement(TryFinallyStatement* stmt) {
TryFinallyBuilder try_control_builder(builder());
// We keep a record of all paths that enter the finally-block to be able to
// dispatch to the correct continuation point after the statements in the
// finally-block have been evaluated.
//
// The try-finally construct can enter the finally-block in three ways:
// 1. By exiting the try-block normally, falling through at the end.
// 2. By exiting the try-block with a function-local control flow transfer
// (i.e. through break/continue/return statements).
// 3. By exiting the try-block with a thrown exception.
//
// The result register semantics depend on how the block was entered:
// - ReturnStatement: It represents the return value being returned.
// - ThrowStatement: It represents the exception being thrown.
// - BreakStatement/ContinueStatement: Undefined and not used.
// - Falling through into finally-block: Undefined and not used.
Register token = register_allocator()->NewRegister();
Register result = register_allocator()->NewRegister();
ControlScope::DeferredCommands commands(this, token, result);
// Preserve the context in a dedicated register, so that it can be restored
// when the handler is entered by the stack-unwinding machinery.
// TODO(mstarzinger): Be smarter about register allocation.
@ -946,16 +1145,29 @@ void BytecodeGenerator::VisitTryFinallyStatement(TryFinallyStatement* stmt) {
// Evaluate the try-block inside a control scope. This simulates a handler
// that is intercepting all control commands.
try_control_builder.BeginTry(context);
// TODO(mstarzinger): Control scope is missing!
Visit(stmt->try_block());
{
ControlScopeForTryFinally scope(this, &try_control_builder, &commands);
Visit(stmt->try_block());
}
try_control_builder.EndTry();
// Record fall-through and exception cases.
commands.RecordFallThroughPath();
try_control_builder.LeaveTry();
try_control_builder.BeginHandler();
commands.RecordHandlerReThrowPath();
try_control_builder.BeginFinally();
// Clear message object as we enter the finally block.
// TODO(mstarzinger): Implement this!
// Evaluate the finally-block.
Visit(stmt->finally_block());
try_control_builder.EndFinally();
// Dynamic dispatch after the finally-block.
commands.ApplyDeferredCommands();
}

View File

@ -32,6 +32,9 @@ class BytecodeGenerator final : public AstVisitor {
class ControlScope;
class ControlScopeForBreakable;
class ControlScopeForIteration;
class ControlScopeForTopLevel;
class ControlScopeForTryCatch;
class ControlScopeForTryFinally;
class ExpressionResultScope;
class EffectResultScope;
class AccumulatorResultScope;

View File

@ -159,13 +159,31 @@ void TryFinallyBuilder::BeginTry(Register context) {
}
void TryFinallyBuilder::LeaveTry() {
finalization_sites_.push_back(BytecodeLabel());
builder()->Jump(&finalization_sites_.back());
}
void TryFinallyBuilder::EndTry() {
builder()->MarkTryEnd(handler_id_);
}
void TryFinallyBuilder::BeginHandler() {
builder()->Bind(&handler_);
builder()->MarkHandler(handler_id_, false);
}
void TryFinallyBuilder::BeginFinally() {
for (size_t i = 0; i < finalization_sites_.size(); i++) {
BytecodeLabel& site = finalization_sites_.at(i);
builder()->Bind(&site);
}
}
void TryFinallyBuilder::EndFinally() {
// Nothing to be done here.
}

View File

@ -166,15 +166,23 @@ class TryCatchBuilder final : public ControlFlowBuilder {
class TryFinallyBuilder final : public ControlFlowBuilder {
public:
explicit TryFinallyBuilder(BytecodeArrayBuilder* builder)
: ControlFlowBuilder(builder), handler_id_(builder->NewHandlerEntry()) {}
: ControlFlowBuilder(builder),
handler_id_(builder->NewHandlerEntry()),
finalization_sites_(builder->zone()) {}
void BeginTry(Register context);
void LeaveTry();
void EndTry();
void BeginHandler();
void BeginFinally();
void EndFinally();
private:
int handler_id_;
BytecodeLabel handler_;
// Unbound labels that identify jumps to the finally block in the code.
ZoneVector<BytecodeLabel> finalization_sites_;
};
} // namespace interpreter

View File

@ -4350,92 +4350,128 @@ TEST(TryFinally) {
ExpectedSnippet<const char*> snippets[] = {
{"var a = 1; try { a = 2; } finally { a = 3; }",
2 * kPointerSize,
4 * kPointerSize,
1,
14,
35,
{
B(LdaSmi8), U8(1), //
B(Star), R(0), //
B(LdaSmi8), U8(2), //
B(Star), R(0), //
B(LdaSmi8), U8(3), //
B(Star), R(0), //
B(LdaUndefined), //
B(Return), //
B(LdaSmi8), U8(1), //
B(Star), R(0), //
B(LdaSmi8), U8(2), //
B(Star), R(0), //
B(LdaSmi8), U8(-1), //
B(Star), R(1), //
B(Jump), U8(7), //
B(Star), R(2), //
B(LdaZero), //
B(Star), R(1), //
B(LdaSmi8), U8(3), //
B(Star), R(0), //
B(LdaZero), //
B(TestEqualStrict), R(1), //
B(JumpIfTrue), U8(4), //
B(Jump), U8(5), //
B(Ldar), R(2), //
B(Throw), //
B(LdaUndefined), //
B(Return), //
},
0,
{},
1,
{{4, 8, 8}}},
{{4, 8, 14}}},
{"var a = 1; try { a = 2; } catch(e) { a = 20 } finally { a = 3; }",
7 * kPointerSize,
9 * kPointerSize,
1,
39,
60,
{
B(LdaSmi8), U8(1), //
B(Star), R(0), //
B(LdaSmi8), U8(2), //
B(Star), R(0), //
B(Jump), U8(25), //
B(Star), R(5), //
B(Star), R(7), //
B(LdaConstant), U8(0), //
B(Star), R(4), //
B(Ldar), R(closure), //
B(Star), R(6), //
B(CallRuntime), U16(Runtime::kPushCatchContext), R(4), U8(3), //
B(Ldar), R(closure), //
B(Star), R(8), //
B(CallRuntime), U16(Runtime::kPushCatchContext), R(6), U8(3), //
B(PushContext), R(1), //
B(LdaSmi8), U8(20), //
B(Star), R(0), //
B(PopContext), R(context), //
B(LdaSmi8), U8(-1), //
B(Star), R(2), //
B(Jump), U8(7), //
B(Star), R(3), //
B(LdaZero), //
B(Star), R(2), //
B(LdaSmi8), U8(3), //
B(Star), R(0), //
B(LdaZero), //
B(TestEqualStrict), R(2), //
B(JumpIfTrue), U8(4), //
B(Jump), U8(5), //
B(Ldar), R(3), //
B(Throw), //
B(LdaUndefined), //
B(Return), //
},
1,
{"e"},
2,
{{4, 33, 33}, {4, 8, 10}}},
{{4, 33, 39}, {4, 8, 10}}},
{"var a; try {"
" try { a = 1 } catch(e) { a = 2 }"
"} catch(e) { a = 20 } finally { a = 3; }",
8 * kPointerSize,
10 * kPointerSize,
1,
60,
81,
{
B(LdaSmi8), U8(1), //
B(Star), R(0), //
B(Jump), U8(25), //
B(Star), R(6), //
B(Star), R(8), //
B(LdaConstant), U8(0), //
B(Star), R(5), //
B(Ldar), R(closure), //
B(Star), R(7), //
B(CallRuntime), U16(Runtime::kPushCatchContext), R(5), U8(3), //
B(Ldar), R(closure), //
B(Star), R(9), //
B(CallRuntime), U16(Runtime::kPushCatchContext), R(7), U8(3), //
B(PushContext), R(1), //
B(LdaSmi8), U8(2), //
B(Star), R(0), //
B(PopContext), R(context), //
B(Jump), U8(25), //
B(Star), R(5), //
B(Star), R(7), //
B(LdaConstant), U8(0), //
B(Star), R(4), //
B(Ldar), R(closure), //
B(Star), R(6), //
B(CallRuntime), U16(Runtime::kPushCatchContext), R(4), U8(3), //
B(Ldar), R(closure), //
B(Star), R(8), //
B(CallRuntime), U16(Runtime::kPushCatchContext), R(6), U8(3), //
B(PushContext), R(1), //
B(LdaSmi8), U8(20), //
B(Star), R(0), //
B(PopContext), R(context), //
B(LdaSmi8), U8(-1), //
B(Star), R(2), //
B(Jump), U8(7), //
B(Star), R(3), //
B(LdaZero), //
B(Star), R(2), //
B(LdaSmi8), U8(3), //
B(Star), R(0), //
B(LdaZero), //
B(TestEqualStrict), R(2), //
B(JumpIfTrue), U8(4), //
B(Jump), U8(5), //
B(Ldar), R(3), //
B(Throw), //
B(LdaUndefined), //
B(Return), //
},
1,
{"e"},
3,
{{0, 54, 54}, {0, 29, 31}, {0, 4, 6}}},
{{0, 54, 60}, {0, 29, 31}, {0, 4, 6}}},
};
for (size_t i = 0; i < arraysize(snippets); i++) {

View File

@ -2048,15 +2048,45 @@ TEST(InterpreterTryCatch) {
TEST(InterpreterTryFinally) {
HandleAndZoneScope handles;
i::Isolate* isolate = handles.main_isolate();
i::Factory* factory = isolate->factory();
// TODO(rmcilroy): modify tests when we have real try finally support.
std::string source(InterpreterTester::SourceForBody(
"var a = 1; try { a = a + 1; } finally { a = a + 2; }; return a;"));
InterpreterTester tester(handles.main_isolate(), source.c_str());
auto callable = tester.GetCallable<>();
std::pair<const char*, Handle<Object>> finallies[] = {
std::make_pair(
"var a = 1; try { a = a + 1; } finally { a = a + 2; }; return a;",
factory->NewStringFromStaticChars("R4")),
std::make_pair(
"var a = 1; try { a = 2; return 23; } finally { a = 3 }; return a;",
factory->NewStringFromStaticChars("R23")),
std::make_pair(
"var a = 1; try { a = 2; throw 23; } finally { a = 3 }; return a;",
factory->NewStringFromStaticChars("E23")),
std::make_pair(
"var a = 1; try { a = 2; throw 23; } finally { return a; };",
factory->NewStringFromStaticChars("R2")),
std::make_pair(
"var a = 1; try { a = 2; throw 23; } finally { throw 42; };",
factory->NewStringFromStaticChars("E42")),
std::make_pair("var a = 1; for (var i = 10; i < 20; i += 5) {"
" try { a = 2; break; } finally { a = 3; }"
"} return a + i;",
factory->NewStringFromStaticChars("R13")),
std::make_pair("var a = 1; for (var i = 10; i < 20; i += 5) {"
" try { a = 2; continue; } finally { a = 3; }"
"} return a + i;",
factory->NewStringFromStaticChars("R23")),
};
Handle<Object> return_val = callable().ToHandleChecked();
CHECK_EQ(Smi::cast(*return_val), Smi::FromInt(4));
const char* try_wrapper =
"(function() { try { return 'R' + f() } catch(e) { return 'E' + e }})()";
for (size_t i = 0; i < arraysize(finallies); i++) {
std::string source(InterpreterTester::SourceForBody(finallies[i].first));
InterpreterTester tester(handles.main_isolate(), source.c_str());
tester.GetCallable<>();
Handle<Object> wrapped = v8::Utils::OpenHandle(*CompileRun(try_wrapper));
CHECK(wrapped->SameValue(*finallies[i].second));
}
}