Commit dc7abf3e authored by jgruber's avatar jgruber Committed by Commit Bot

[coverage] Support Yield and Async

The yield* statement when used in combination with async iterators is not
supported yet, as that is desugared into a more complex construct that doesn't
offer a good dedicated bytecode to attach the source range information yet.

Note that invocation counts of generator functions are incorrect as they count
each resumption as an individual call. See https://crbug.com/v8/6594.

Bug: v8:6000
Change-Id: I7ac7073473c9b64bb207cdbc4dab083ec1145656
Reviewed-on: https://chromium-review.googlesource.com/582690
Commit-Queue: Jakob Gruber <jgruber@chromium.org>
Reviewed-by: 's avatarRoss McIlroy <rmcilroy@chromium.org>
Reviewed-by: 's avatarGeorg Neis <neis@chromium.org>
Cr-Commit-Position: refs/heads/master@{#46890}
parent 637b7d64
...@@ -27,7 +27,8 @@ struct SourceRange { ...@@ -27,7 +27,8 @@ struct SourceRange {
int32_t start, end; int32_t start, end;
}; };
// The list of ast node kinds that have associated source ranges. // The list of ast node kinds that have associated source ranges. Note that this
// macro is not undefined at the end of this file.
#define AST_SOURCE_RANGE_LIST(V) \ #define AST_SOURCE_RANGE_LIST(V) \
V(Block) \ V(Block) \
V(CaseClause) \ V(CaseClause) \
...@@ -35,6 +36,7 @@ struct SourceRange { ...@@ -35,6 +36,7 @@ struct SourceRange {
V(IfStatement) \ V(IfStatement) \
V(IterationStatement) \ V(IterationStatement) \
V(JumpStatement) \ V(JumpStatement) \
V(Suspend) \
V(SwitchStatement) \ V(SwitchStatement) \
V(Throw) \ V(Throw) \
V(TryCatchStatement) \ V(TryCatchStatement) \
...@@ -164,6 +166,12 @@ class JumpStatementSourceRanges final : public ContinuationSourceRanges { ...@@ -164,6 +166,12 @@ class JumpStatementSourceRanges final : public ContinuationSourceRanges {
: ContinuationSourceRanges(continuation_position) {} : ContinuationSourceRanges(continuation_position) {}
}; };
class SuspendSourceRanges final : public ContinuationSourceRanges {
public:
explicit SuspendSourceRanges(int32_t continuation_position)
: ContinuationSourceRanges(continuation_position) {}
};
class SwitchStatementSourceRanges final : public ContinuationSourceRanges { class SwitchStatementSourceRanges final : public ContinuationSourceRanges {
public: public:
explicit SwitchStatementSourceRanges(int32_t continuation_position) explicit SwitchStatementSourceRanges(int32_t continuation_position)
...@@ -219,6 +227,7 @@ class SourceRangeMap final : public ZoneObject { ...@@ -219,6 +227,7 @@ class SourceRangeMap final : public ZoneObject {
// Type-checked insertion. // Type-checked insertion.
#define DEFINE_MAP_INSERT(type) \ #define DEFINE_MAP_INSERT(type) \
void Insert(type* node, type##SourceRanges* ranges) { \ void Insert(type* node, type##SourceRanges* ranges) { \
DCHECK_NOT_NULL(node); \
map_.emplace(node, ranges); \ map_.emplace(node, ranges); \
} }
AST_SOURCE_RANGE_LIST(DEFINE_MAP_INSERT) AST_SOURCE_RANGE_LIST(DEFINE_MAP_INSERT)
...@@ -228,8 +237,6 @@ class SourceRangeMap final : public ZoneObject { ...@@ -228,8 +237,6 @@ class SourceRangeMap final : public ZoneObject {
ZoneMap<AstNode*, AstNodeSourceRanges*> map_; ZoneMap<AstNode*, AstNodeSourceRanges*> map_;
}; };
#undef AST_SOURCE_RANGE_LIST
} // namespace internal } // namespace internal
} // namespace v8 } // namespace v8
......
...@@ -2665,6 +2665,8 @@ void BytecodeGenerator::VisitYield(Yield* expr) { ...@@ -2665,6 +2665,8 @@ void BytecodeGenerator::VisitYield(Yield* expr) {
{ {
// Resume with next. // Resume with next.
builder()->Bind(jump_table, JSGeneratorObject::kNext); builder()->Bind(jump_table, JSGeneratorObject::kNext);
BuildIncrementBlockCoverageCounterIfEnabled(expr,
SourceRangeKind::kContinuation);
builder()->LoadAccumulatorWithRegister(input); builder()->LoadAccumulatorWithRegister(input);
} }
} }
...@@ -2746,7 +2748,10 @@ void BytecodeGenerator::VisitYieldStar(YieldStar* expr) { ...@@ -2746,7 +2748,10 @@ void BytecodeGenerator::VisitYieldStar(YieldStar* expr) {
.StoreAccumulatorInRegister(resume_mode); .StoreAccumulatorInRegister(resume_mode);
{ {
LoopBuilder loop(builder(), block_coverage_builder_, expr); // This loop builder does not construct counters as the loop is not
// visible to the user, and we therefore neither pass the block coverage
// builder nor the expression.
LoopBuilder loop(builder(), nullptr, nullptr);
VisitIterationHeader(expr->suspend_id(), 1, &loop); VisitIterationHeader(expr->suspend_id(), 1, &loop);
{ {
...@@ -2896,6 +2901,8 @@ void BytecodeGenerator::VisitYieldStar(YieldStar* expr) { ...@@ -2896,6 +2901,8 @@ void BytecodeGenerator::VisitYieldStar(YieldStar* expr) {
execution_control()->ReturnAccumulator(); execution_control()->ReturnAccumulator();
builder()->Bind(&completion_is_output_value); builder()->Bind(&completion_is_output_value);
BuildIncrementBlockCoverageCounterIfEnabled(expr,
SourceRangeKind::kContinuation);
builder()->LoadAccumulatorWithRegister(output_value); builder()->LoadAccumulatorWithRegister(output_value);
} }
...@@ -2903,7 +2910,7 @@ void BytecodeGenerator::VisitAwait(Await* expr) { ...@@ -2903,7 +2910,7 @@ void BytecodeGenerator::VisitAwait(Await* expr) {
// Rather than HandlerTable::UNCAUGHT, async functions use // Rather than HandlerTable::UNCAUGHT, async functions use
// HandlerTable::ASYNC_AWAIT to communicate that top-level exceptions are // HandlerTable::ASYNC_AWAIT to communicate that top-level exceptions are
// transformed into promise rejections. This is necessary to prevent emitting // transformed into promise rejections. This is necessary to prevent emitting
// multiple debbug events for the same uncaught exception. There is no point // multiple debug events for the same uncaught exception. There is no point
// in the body of an async function where catch prediction is // in the body of an async function where catch prediction is
// HandlerTable::UNCAUGHT. // HandlerTable::UNCAUGHT.
DCHECK(catch_prediction() != HandlerTable::UNCAUGHT); DCHECK(catch_prediction() != HandlerTable::UNCAUGHT);
...@@ -2969,6 +2976,8 @@ void BytecodeGenerator::VisitAwait(Await* expr) { ...@@ -2969,6 +2976,8 @@ void BytecodeGenerator::VisitAwait(Await* expr) {
// Resume with next. // Resume with next.
builder()->Bind(&resume_next); builder()->Bind(&resume_next);
BuildIncrementBlockCoverageCounterIfEnabled(expr,
SourceRangeKind::kContinuation);
builder()->LoadAccumulatorWithRegister(input); builder()->LoadAccumulatorWithRegister(input);
} }
......
...@@ -643,6 +643,13 @@ class ParserBase { ...@@ -643,6 +643,13 @@ class ParserBase {
return scanner()->peek(); return scanner()->peek();
} }
// Returns the position past the following semicolon (if it exists), and the
// position past the end of the current token otherwise.
int PositionAfterSemicolon() {
return (peek() == Token::SEMICOLON) ? scanner_->peek_location().end_pos
: scanner_->location().end_pos;
}
INLINE(Token::Value PeekAhead()) { INLINE(Token::Value PeekAhead()) {
if (stack_overflow_) return Token::ILLEGAL; if (stack_overflow_) return Token::ILLEGAL;
return scanner()->PeekAhead(); return scanner()->PeekAhead();
...@@ -2940,6 +2947,7 @@ typename ParserBase<Impl>::ExpressionT ParserBase<Impl>::ParseYieldExpression( ...@@ -2940,6 +2947,7 @@ typename ParserBase<Impl>::ExpressionT ParserBase<Impl>::ParseYieldExpression(
// TODO(verwaest): Come up with a better solution. // TODO(verwaest): Come up with a better solution.
ExpressionT yield = ExpressionT yield =
factory()->NewYield(expression, pos, Suspend::kOnExceptionThrow); factory()->NewYield(expression, pos, Suspend::kOnExceptionThrow);
impl()->RecordSuspendSourceRange(yield, PositionAfterSemicolon());
return yield; return yield;
} }
...@@ -3115,7 +3123,9 @@ typename ParserBase<Impl>::ExpressionT ParserBase<Impl>::ParseUnaryExpression( ...@@ -3115,7 +3123,9 @@ typename ParserBase<Impl>::ExpressionT ParserBase<Impl>::ParseUnaryExpression(
ExpressionT value = ParseUnaryExpression(CHECK_OK); ExpressionT value = ParseUnaryExpression(CHECK_OK);
return factory()->NewAwait(value, await_pos); ExpressionT expr = factory()->NewAwait(value, await_pos);
impl()->RecordSuspendSourceRange(expr, PositionAfterSemicolon());
return expr;
} else { } else {
return ParsePostfixExpression(ok); return ParsePostfixExpression(ok);
} }
......
...@@ -4220,7 +4220,9 @@ Expression* Parser::RewriteYieldStar(Expression* iterable, int pos) { ...@@ -4220,7 +4220,9 @@ Expression* Parser::RewriteYieldStar(Expression* iterable, int pos) {
is_async_generator() ? IteratorType::kAsync : IteratorType::kNormal; is_async_generator() ? IteratorType::kAsync : IteratorType::kNormal;
if (type == IteratorType::kNormal) { if (type == IteratorType::kNormal) {
return factory()->NewYieldStar(iterable, pos); Expression* expr = factory()->NewYieldStar(iterable, pos);
RecordSuspendSourceRange(expr, PositionAfterSemicolon());
return expr;
} }
// Forward definition for break/continue statements. // Forward definition for break/continue statements.
...@@ -4558,6 +4560,10 @@ Expression* Parser::RewriteYieldStar(Expression* iterable, int pos) { ...@@ -4558,6 +4560,10 @@ Expression* Parser::RewriteYieldStar(Expression* iterable, int pos) {
do_block->statements()->Add(do_block_, zone()); do_block->statements()->Add(do_block_, zone());
do_block->statements()->Add(get_value, zone()); do_block->statements()->Add(get_value, zone());
// TODO(jgruber): Collect source ranges for YieldStar within async
// generators. The main issue is that we don't have a good dedicated node
// to attach to - the DoExpression seems a bit too generic.
Variable* dot_result = Variable* dot_result =
NewTemporary(ast_value_factory()->dot_result_string()); NewTemporary(ast_value_factory()->dot_result_string());
yield_star = factory()->NewDoExpression(do_block, dot_result, nopos); yield_star = factory()->NewDoExpression(do_block, dot_result, nopos);
......
...@@ -1190,6 +1190,14 @@ class V8_EXPORT_PRIVATE Parser : public NON_EXPORTED_BASE(ParserBase<Parser>) { ...@@ -1190,6 +1190,14 @@ class V8_EXPORT_PRIVATE Parser : public NON_EXPORTED_BASE(ParserBase<Parser>) {
node, new (zone()) IterationStatementSourceRanges(body_range)); node, new (zone()) IterationStatementSourceRanges(body_range));
} }
V8_INLINE void RecordSuspendSourceRange(Expression* node,
int32_t continuation_position) {
if (source_range_map_ == nullptr) return;
source_range_map_->Insert(static_cast<Suspend*>(node),
new (zone())
SuspendSourceRanges(continuation_position));
}
V8_INLINE void RecordSwitchStatementSourceRange( V8_INLINE void RecordSwitchStatementSourceRange(
Statement* node, int32_t continuation_position) { Statement* node, int32_t continuation_position) {
if (source_range_map_ == nullptr) return; if (source_range_map_ == nullptr) return;
......
...@@ -1741,29 +1741,13 @@ class PreParser : public ParserBase<PreParser> { ...@@ -1741,29 +1741,13 @@ class PreParser : public ParserBase<PreParser> {
V8_INLINE bool ParsingDynamicFunctionDeclaration() const { return false; } V8_INLINE bool ParsingDynamicFunctionDeclaration() const { return false; }
V8_INLINE void RecordBlockSourceRange(PreParserStatement node, // Generate empty functions here as the preparser does not collect source
int32_t continuation_position) {} // ranges for block coverage.
V8_INLINE void RecordCaseClauseSourceRange(PreParserStatement node, #define DEFINE_RECORD_SOURCE_RANGE(Name) \
const SourceRange& body_range) {} template <typename... Ts> \
V8_INLINE void RecordConditionalSourceRange(PreParserExpression node, V8_INLINE void Record##Name##SourceRange(Ts... args) {}
const SourceRange& then_range, AST_SOURCE_RANGE_LIST(DEFINE_RECORD_SOURCE_RANGE)
const SourceRange& else_range) {} #undef DEFINE_RECORD_SOURCE_RANGE
V8_INLINE void RecordIfStatementSourceRange(PreParserStatement node,
const SourceRange& then_range,
const SourceRange& else_range) {}
V8_INLINE void RecordJumpStatementSourceRange(PreParserStatement node,
int32_t continuation_position) {
}
V8_INLINE void RecordIterationStatementSourceRange(
PreParserStatement node, const SourceRange& body_range) {}
V8_INLINE void RecordSwitchStatementSourceRange(
PreParserStatement node, int32_t continuation_position) {}
V8_INLINE void RecordThrowSourceRange(PreParserStatement node,
int32_t continuation_position) {}
V8_INLINE void RecordTryCatchStatementSourceRange(
PreParserStatement node, const SourceRange& body_range) {}
V8_INLINE void RecordTryFinallyStatementSourceRange(
PreParserStatement node, const SourceRange& body_range) {}
// Preparser's private field members. // Preparser's private field members.
......
...@@ -515,4 +515,137 @@ var FALSE = false; // 0050 ...@@ -515,4 +515,137 @@ var FALSE = false; // 0050
{"start":775,"end":780,"count":0}] {"start":775,"end":780,"count":0}]
); );
TestCoverage(
"yield expressions",
`
const it = function*() { // 0000
yield nop(); // 0050
yield nop() ? nop() : nop() // 0100
return nop(); // 0150
}(); // 0200
it.next(); it.next(); // 0250
`,
[{"start":0,"end":299,"count":1},
{"start":11,"end":201,"count":3},
{"start":64,"end":116,"count":1},
{"start":116,"end":121,"count":0},
{"start":124,"end":129,"count":1},
{"start":129,"end":201,"count":0}]
);
TestCoverage(
"yield expressions (.return and .throw)",
`
const it0 = function*() { // 0000
yield 1; yield 2; yield 3; // 0050
}(); // 0100
it0.next(); it0.return(); // 0150
try { // 0200
const it1 = function*() { // 0250
yield 1; yield 2; yield 3; // 0300
}(); // 0350
it1.next(); it1.throw(); // 0400
} catch (e) {} // 0450
`,
[{"start":0,"end":499,"count":1},
{"start":12,"end":101,"count":3},
{"start":60,"end":101,"count":0},
{"start":264,"end":353,"count":3},
{"start":312,"end":353,"count":0}]
);
TestCoverage("yield expressions (.return and try/catch/finally)",
`
const it = function*() { // 0000
try { // 0050
yield 1; yield 2; yield 3; // 0100
} catch (e) { // 0150
nop(); // 0200
} finally { nop(); } // 0250
yield 4; // 0300
}(); // 0350
it.next(); it.return(); // 0450
`,
[{"start":0,"end":449,"count":1},
{"start":11,"end":351,"count":3},
{"start":112,"end":253,"count":0},
{"start":262,"end":272,"count":1},
{"start":310,"end":351,"count":0}] // TODO(jgruber): Missing continuation.
);
TestCoverage("yield expressions (.throw and try/catch/finally)",
`
const it = function*() { // 0000
try { // 0050
yield 1; yield 2; yield 3; // 0100
} catch (e) { // 0150
nop(); // 0200
} finally { nop(); } // 0250
yield 4; // 0300
}(); // 0350
it.next(); it.throw(42); // 0550
`,
[{"start":0,"end":449,"count":1},
{"start":11,"end":351,"count":3},
{"start":112,"end":164,"count":0},
{"start":164,"end":253,"count":1},
{"start":262,"end":272,"count":1},
{"start":310,"end":351,"count":0}] // TODO(jgruber): Missing continuation.
);
TestCoverage(
"yield* expressions",
`
const it = function*() { // 0000
yield* gen(); // 0050
yield* nop() ? gen() : gen() // 0100
return gen(); // 0150
}(); // 0200
it.next(); it.next(); it.next(); // 0250
it.next(); it.next(); it.next(); // 0300
`,
[{"start":0,"end":349,"count":1},
{"start":11,"end":201,"count":7},
{"start":65,"end":117,"count":1},
{"start":117,"end":122,"count":0},
{"start":125,"end":130,"count":1},
{"start":130,"end":201,"count":0}]
);
TestCoverage(
"yield* expressions (.return and .throw)",
`
const it0 = function*() { // 0000
yield* gen(); yield* gen(); yield 3; // 0050
}(); // 0100
it0.next(); it0.return(); // 0150
try { // 0200
const it1 = function*() { // 0250
yield* gen(); yield* gen(); yield 3; // 0300
}(); // 0350
it1.next(); it1.throw(); // 0400
} catch (e) {} // 0450
`,
[{"start":0,"end":499,"count":1},
{"start":12,"end":101,"count":3},
{"start":65,"end":101,"count":0},
{"start":264,"end":353,"count":3},
{"start":317,"end":353,"count":0}]
);
TestCoverage(
"await expressions",
`
async function f() { // 0000
await 42; // 0050
await 42; // 0100
}; // 0150
f(); // 0200
%RunMicrotasks(); // 0250
`,
[{"start":0,"end":299,"count":1},
{"start":0,"end":151,"count":3},
{"start":61,"end":151,"count":1}]
);
%DebugToggleBlockCoverage(false); %DebugToggleBlockCoverage(false);
...@@ -8,6 +8,7 @@ let TestCoverage; ...@@ -8,6 +8,7 @@ let TestCoverage;
let TestCoverageNoGC; let TestCoverageNoGC;
let nop; let nop;
let gen;
!function() { !function() {
function GetCoverage(source) { function GetCoverage(source) {
...@@ -15,7 +16,7 @@ let nop; ...@@ -15,7 +16,7 @@ let nop;
if (script.script.source == source) return script; if (script.script.source == source) return script;
} }
return undefined; return undefined;
} };
function TestCoverageInternal(name, source, expectation, collect_garbage) { function TestCoverageInternal(name, source, expectation, collect_garbage) {
source = source.trim(); source = source.trim();
...@@ -28,15 +29,21 @@ let nop; ...@@ -28,15 +29,21 @@ let nop;
print(stringified_result.replace(/[}],[{]/g, "},\n {")); print(stringified_result.replace(/[}],[{]/g, "},\n {"));
} }
assertEquals(stringified_expectation, stringified_result, name + " failed"); assertEquals(stringified_expectation, stringified_result, name + " failed");
} };
TestCoverage = function(name, source, expectation) { TestCoverage = function(name, source, expectation) {
TestCoverageInternal(name, source, expectation, true); TestCoverageInternal(name, source, expectation, true);
} };
TestCoverageNoGC = function(name, source, expectation) { TestCoverageNoGC = function(name, source, expectation) {
TestCoverageInternal(name, source, expectation, false); TestCoverageInternal(name, source, expectation, false);
} };
nop = function() {} nop = function() {};
gen = function*() {
yield 1;
yield 2;
yield 3;
};
}(); }();
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment