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 {
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) \
V(Block) \
V(CaseClause) \
......@@ -35,6 +36,7 @@ struct SourceRange {
V(IfStatement) \
V(IterationStatement) \
V(JumpStatement) \
V(Suspend) \
V(SwitchStatement) \
V(Throw) \
V(TryCatchStatement) \
......@@ -164,6 +166,12 @@ class JumpStatementSourceRanges final : public ContinuationSourceRanges {
: ContinuationSourceRanges(continuation_position) {}
};
class SuspendSourceRanges final : public ContinuationSourceRanges {
public:
explicit SuspendSourceRanges(int32_t continuation_position)
: ContinuationSourceRanges(continuation_position) {}
};
class SwitchStatementSourceRanges final : public ContinuationSourceRanges {
public:
explicit SwitchStatementSourceRanges(int32_t continuation_position)
......@@ -219,6 +227,7 @@ class SourceRangeMap final : public ZoneObject {
// Type-checked insertion.
#define DEFINE_MAP_INSERT(type) \
void Insert(type* node, type##SourceRanges* ranges) { \
DCHECK_NOT_NULL(node); \
map_.emplace(node, ranges); \
}
AST_SOURCE_RANGE_LIST(DEFINE_MAP_INSERT)
......@@ -228,8 +237,6 @@ class SourceRangeMap final : public ZoneObject {
ZoneMap<AstNode*, AstNodeSourceRanges*> map_;
};
#undef AST_SOURCE_RANGE_LIST
} // namespace internal
} // namespace v8
......
......@@ -2665,6 +2665,8 @@ void BytecodeGenerator::VisitYield(Yield* expr) {
{
// Resume with next.
builder()->Bind(jump_table, JSGeneratorObject::kNext);
BuildIncrementBlockCoverageCounterIfEnabled(expr,
SourceRangeKind::kContinuation);
builder()->LoadAccumulatorWithRegister(input);
}
}
......@@ -2746,7 +2748,10 @@ void BytecodeGenerator::VisitYieldStar(YieldStar* expr) {
.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);
{
......@@ -2896,6 +2901,8 @@ void BytecodeGenerator::VisitYieldStar(YieldStar* expr) {
execution_control()->ReturnAccumulator();
builder()->Bind(&completion_is_output_value);
BuildIncrementBlockCoverageCounterIfEnabled(expr,
SourceRangeKind::kContinuation);
builder()->LoadAccumulatorWithRegister(output_value);
}
......@@ -2903,7 +2910,7 @@ void BytecodeGenerator::VisitAwait(Await* expr) {
// Rather than HandlerTable::UNCAUGHT, async functions use
// HandlerTable::ASYNC_AWAIT to communicate that top-level exceptions are
// 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
// HandlerTable::UNCAUGHT.
DCHECK(catch_prediction() != HandlerTable::UNCAUGHT);
......@@ -2969,6 +2976,8 @@ void BytecodeGenerator::VisitAwait(Await* expr) {
// Resume with next.
builder()->Bind(&resume_next);
BuildIncrementBlockCoverageCounterIfEnabled(expr,
SourceRangeKind::kContinuation);
builder()->LoadAccumulatorWithRegister(input);
}
......
......@@ -643,6 +643,13 @@ class ParserBase {
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()) {
if (stack_overflow_) return Token::ILLEGAL;
return scanner()->PeekAhead();
......@@ -2940,6 +2947,7 @@ typename ParserBase<Impl>::ExpressionT ParserBase<Impl>::ParseYieldExpression(
// TODO(verwaest): Come up with a better solution.
ExpressionT yield =
factory()->NewYield(expression, pos, Suspend::kOnExceptionThrow);
impl()->RecordSuspendSourceRange(yield, PositionAfterSemicolon());
return yield;
}
......@@ -3115,7 +3123,9 @@ typename ParserBase<Impl>::ExpressionT ParserBase<Impl>::ParseUnaryExpression(
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 {
return ParsePostfixExpression(ok);
}
......
......@@ -4220,7 +4220,9 @@ Expression* Parser::RewriteYieldStar(Expression* iterable, int pos) {
is_async_generator() ? IteratorType::kAsync : 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.
......@@ -4558,6 +4560,10 @@ Expression* Parser::RewriteYieldStar(Expression* iterable, int pos) {
do_block->statements()->Add(do_block_, 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 =
NewTemporary(ast_value_factory()->dot_result_string());
yield_star = factory()->NewDoExpression(do_block, dot_result, nopos);
......
......@@ -1190,6 +1190,14 @@ class V8_EXPORT_PRIVATE Parser : public NON_EXPORTED_BASE(ParserBase<Parser>) {
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(
Statement* node, int32_t continuation_position) {
if (source_range_map_ == nullptr) return;
......
......@@ -1741,29 +1741,13 @@ class PreParser : public ParserBase<PreParser> {
V8_INLINE bool ParsingDynamicFunctionDeclaration() const { return false; }
V8_INLINE void RecordBlockSourceRange(PreParserStatement node,
int32_t continuation_position) {}
V8_INLINE void RecordCaseClauseSourceRange(PreParserStatement node,
const SourceRange& body_range) {}
V8_INLINE void RecordConditionalSourceRange(PreParserExpression node,
const SourceRange& then_range,
const SourceRange& else_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) {}
// Generate empty functions here as the preparser does not collect source
// ranges for block coverage.
#define DEFINE_RECORD_SOURCE_RANGE(Name) \
template <typename... Ts> \
V8_INLINE void Record##Name##SourceRange(Ts... args) {}
AST_SOURCE_RANGE_LIST(DEFINE_RECORD_SOURCE_RANGE)
#undef DEFINE_RECORD_SOURCE_RANGE
// Preparser's private field members.
......
......@@ -515,4 +515,137 @@ var FALSE = false; // 0050
{"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);
......@@ -8,6 +8,7 @@ let TestCoverage;
let TestCoverageNoGC;
let nop;
let gen;
!function() {
function GetCoverage(source) {
......@@ -15,7 +16,7 @@ let nop;
if (script.script.source == source) return script;
}
return undefined;
}
};
function TestCoverageInternal(name, source, expectation, collect_garbage) {
source = source.trim();
......@@ -28,15 +29,21 @@ let nop;
print(stringified_result.replace(/[}],[{]/g, "},\n {"));
}
assertEquals(stringified_expectation, stringified_result, name + " failed");
}
};
TestCoverage = function(name, source, expectation) {
TestCoverageInternal(name, source, expectation, true);
}
};
TestCoverageNoGC = function(name, source, expectation) {
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