Fix labeled continue targeting a for-of loop (Issue #1577 Crash #1)

`L: for (const v of [...]) { continue L; }` aborted with
Assertion `!v.isEmpty()' failed, and `continue OUTER` from a nested
loop silently terminated the script.

A `continue <label>` whose label targets a for-of loop was left to be
resolved by LabelledStatementNode after the loop body, by which point
the for-of iterator-cleanup try block had registered the jump as a
complex case. It was then morphed into a JumpComplexCase that unwound
the try block, wrongly closing the iterator and leaving an empty Value
in the result register.

A previous per-loop attempt (8fd141b2) was reverted (60b1202a) because
a single m_currentLoopLabel leaked into nested loops and broke test262.

Track all labels directly targeting a loop (m_currentLoopLabels), clear
the list when entering each loop body so nested loops never inherit it,
and let for-of/for-in resolve continues for its own labels to
continuePosition (a plain jump, identical to an unlabeled continue)
before the try block is registered as a complex case.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Seonghyun Kim <sh8281.kim@samsung.com>
This commit is contained in:
Seonghyun Kim 2026-06-16 17:20:19 +09:00 committed by MuHong Byun
commit e7221f4211
7 changed files with 30 additions and 6 deletions

View file

@ -64,7 +64,6 @@ ByteCodeGenerateContext::ByteCodeGenerateContext(InterpretedCodeBlock* codeBlock
, m_lexicalBlockIndex(0)
, m_classInfo()
, m_numeralLiteralData(numeralLiteralData) // should be NumeralLiteralVector
, m_currentLoopLabel(nullptr)
#if defined(ENABLE_TCO)
, m_returnRegister(SIZE_MAX)
#endif

View file

@ -105,7 +105,7 @@ struct ByteCodeGenerateContext {
, m_lexicalBlockIndex(contextBefore.m_lexicalBlockIndex)
, m_classInfo(contextBefore.m_classInfo)
, m_numeralLiteralData(contextBefore.m_numeralLiteralData) // should be NumeralLiteralVector
, m_currentLoopLabel(contextBefore.m_currentLoopLabel)
, m_currentLoopLabels(contextBefore.m_currentLoopLabels)
#if defined(ENABLE_TCO)
, m_returnRegister(contextBefore.m_returnRegister)
#endif
@ -398,7 +398,10 @@ struct ByteCodeGenerateContext {
ClassContextInformation m_classInfo;
std::map<size_t, size_t> m_complexCaseStatementPositions;
void* m_numeralLiteralData; // should be NumeralLiteralVector
String* m_currentLoopLabel; // Label of the immediately enclosing loop (if in a labeled loop) - Issue #1571
// Labels directly targeting the loop that is about to be generated (a loop may
// carry more than one label, e.g. `L1: L2: for (...)`). A loop clears this list
// before generating its body so that nested loops do not inherit it. - Issue #1571/#1577
std::vector<String*> m_currentLoopLabels;
#if defined(ENABLE_TCO)
size_t m_returnRegister; // for tail call optimizaiton (TCO)

View file

@ -42,6 +42,8 @@ public:
#endif /* ESCARGOT_DEBUGGER */
ByteCodeGenerateContext newContext(*context);
// Do not let nested loops inherit this loop's labels. - Issue #1571/#1577
newContext.m_currentLoopLabels.clear();
size_t doStart = codeBlock->currentCodeSize();
if (context->shouldCareScriptExecutionResult()) {

View file

@ -162,6 +162,10 @@ public:
}
ByteCodeGenerateContext newContext(*context);
// The labels of this loop (collected in `context`) must not be inherited by
// nested loops generated while emitting the head/body, otherwise an inner loop
// would wrongly consume a `continue <label>` that targets this loop. - Issue #1571/#1577
newContext.m_currentLoopLabels.clear();
newContext.getRegister(); // ExecutionResult of m_right should not overwrite any reserved value
if (m_left->type() == ASTNodeType::VariableDeclaration) {
@ -373,6 +377,16 @@ public:
newContext.consumeContinuePositions(codeBlock, continuePosition, newContext.tryCatchWithBlockStatementCount());
newContext.m_positionToContinue = continuePosition;
// A `continue <label>` whose label targets THIS loop is semantically identical
// to an unlabeled `continue`, so resolve it to `continuePosition` here, before the
// iterator-cleanup try block of for-of is registered as a complex case. Otherwise
// the labeled continue would be morphed into a JumpComplexCase that unwinds the
// for-of try (wrongly closing the iterator and corrupting the result register),
// which crashed `L: for (const v of [...]) { continue L; }`. - Issue #1571/#1577
for (size_t i = 0; i < context->m_currentLoopLabels.size(); i++) {
newContext.consumeLabelledContinuePositions(codeBlock, continuePosition, context->m_currentLoopLabels[i], newContext.tryCatchWithBlockStatementCount());
}
if (!m_forIn) {
TryStatementNode::generateTryStatementBodyEndByteCode(codeBlock, &newContext, this, forOfTryStatementContext);
TryStatementNode::generateTryFinalizerStatementStartByteCode(codeBlock, &newContext, this, forOfTryStatementContext, true);

View file

@ -64,6 +64,8 @@ public:
}
ByteCodeGenerateContext newContext(*context);
// Do not let nested loops inherit this loop's labels. - Issue #1571/#1577
newContext.m_currentLoopLabels.clear();
newContext.getRegister(); // ExeuctionResult of m_body should not be overwritten by m_test

View file

@ -38,10 +38,12 @@ public:
{
size_t start = codeBlock->currentCodeSize();
context->m_positionToContinue = start;
String* previousLoopLabel = context->m_currentLoopLabel;
context->m_currentLoopLabel = m_label;
// Make this label visible to the loop that this labeled statement (possibly
// through a chain of labels) directly wraps, so the loop can resolve a
// `continue <label>` targeting itself just like an unlabeled continue. - Issue #1571/#1577
context->m_currentLoopLabels.push_back(m_label);
m_statementNode->generateStatementByteCode(codeBlock, context);
context->m_currentLoopLabel = previousLoopLabel;
context->m_currentLoopLabels.pop_back();
size_t end = codeBlock->currentCodeSize();
context->consumeLabelledBreakPositions(codeBlock, end, m_label, context->tryCatchWithBlockStatementCount());
context->consumeLabelledContinuePositions(codeBlock, context->m_positionToContinue, m_label, context->tryCatchWithBlockStatementCount());

View file

@ -48,6 +48,8 @@ public:
}
ByteCodeGenerateContext newContext(*context);
// Do not let nested loops inherit this loop's labels. - Issue #1571/#1577
newContext.m_currentLoopLabels.clear();
newContext.getRegister(); // ExeuctionResult of m_body should not be overwritten by m_test