Skip to content

Commit 66d314c

Browse files
committed
[MERGE #5238 @rhuanjl] Fix module Load order and re-fix circular loading logic
Merge pull request #5238 from rhuanjl:loadOrder Fixes #5171 but this may take a bit of explaining. @boingoing please could you take a look at this? Note I've targeted this at master - though would prefer to target 1.8 if allowed as this is fixing a regression that occurred in 1.8 I've targeted master as I'm assuming 1.8 is closed to functionality changes at this point. This PR reverts 2dc946c which apparently fixed some circular module loading bugs by reversing module load order. However the reversed load order resulted in observably different runtime results in some cases - introducing an incompatibility with other runtimes described in issue #5171 **As far as I understand the purpose of the order reversal was to resolve problems where circular imports were not handled correctly, these were:** 1. Issue #4482 But since then #4482 was fixed properly in #4583 - after noting that the order reversal fixed the original POC but did not fix a slightly revised version of the same. 2. A non-specific issue that had resulted in the test-case bug-issue-3257 being disabled - this is an issue that can only occur with either an environment that allows dynamic imports OR an environment that allows multiple root modules to be being loaded at the same time in the same context. This PR includes an alternative fix for the issue that caused bug-issue-3257 to be disabled. And adds a test case that reproduces the same bug regardless of the load order. The revised fix for that was to get rid of the concept of numPendingModules that was used to track when all of a module's children were ready. **Previous logic:** 1. Module is parsed that has children 2. For each child: - if child already parsed do nothing - if child not already parsed increment numPendingModules for the parent and add parent to list of modules to notify when child is parsed 3. When a module is parsed that has no pending children notify any parents in its list 4. When a module is notified by a child decrease numPendingModules for it by 1, when it reaches 0 notify its parent OR if it's a root module or a dynamicly imported module instantiate it and notify the host to execute it **Problem with previous logic** 1. Mod0 parsed, has children Mod1 and Mod2 (schedule async parse job for Mod1 and Mod2) 2. Mod1 also a root module or dynamic module and has Mod0 as a child 3. parse job for Mod1 completes -> notes that Mod0 is already parsed so numPendingModules = 0 4. Host notified to execute Mod1 before Mod2 has been parsed **New logic** 1. Module is parsed that has children 2. For each child: - add parent to list of modules to notify when child is parsed 3. Whenever a module is parsed OR a notification is received: - recursively check if all children and children's children etc. have been parsed - if yes notify parent OR if dynamic/root module -> notify host to execute
2 parents 938e7e4 + b71d35f commit 66d314c

File tree

5 files changed

+97
-32
lines changed

5 files changed

+97
-32
lines changed

lib/Runtime/Language/SourceTextModuleRecord.cpp

Lines changed: 47 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ namespace Js
4242
isRootModule(false),
4343
hadNotifyHostReady(false),
4444
localExportSlots(nullptr),
45-
numPendingChildrenModule(0),
4645
moduleId(InvalidModuleIndex),
4746
localSlotCount(InvalidSlotCount),
4847
promise(nullptr),
@@ -225,6 +224,11 @@ namespace Js
225224

226225
void SourceTextModuleRecord::NotifyParentsAsNeeded()
227226
{
227+
if (notifying)
228+
{
229+
return;
230+
}
231+
notifying = true;
228232
// Notify the parent modules that this child module is either in fault state or finished.
229233
if (this->parentModuleList != nullptr)
230234
{
@@ -233,6 +237,7 @@ namespace Js
233237
parentModule->OnChildModuleReady(this, this->errorObject);
234238
});
235239
}
240+
notifying = false;
236241
SetParentsNotified();
237242
}
238243

@@ -326,7 +331,7 @@ namespace Js
326331
OUTPUT_TRACE_DEBUGONLY(Js::ModulePhase, _u("PrepareForModuleDeclarationInitialization(%s)\n"), this->GetSpecifierSz());
327332
HRESULT hr = NO_ERROR;
328333

329-
if (numPendingChildrenModule == 0)
334+
if (ConfirmChildrenParsed())
330335
{
331336
OUTPUT_TRACE_DEBUGONLY(Js::ModulePhase, _u("\t>NotifyParentsAsNeeded\n"));
332337
NotifyParentsAsNeeded();
@@ -355,12 +360,6 @@ namespace Js
355360
}
356361
}
357362
}
358-
#if DBG
359-
else
360-
{
361-
OUTPUT_TRACE_DEBUGONLY(Js::ModulePhase, _u("\tnumPendingChildrenModule=(%d)\n"), numPendingChildrenModule);
362-
}
363-
#endif
364363
return hr;
365364
}
366365

@@ -399,12 +398,6 @@ namespace Js
399398
}
400399
else
401400
{
402-
if (numPendingChildrenModule == 0)
403-
{
404-
return NOERROR; // this is only in case of recursive module reference. Let the higher stack frame handle this module.
405-
}
406-
numPendingChildrenModule--;
407-
408401
hr = PrepareForModuleDeclarationInitialization();
409402
}
410403
return hr;
@@ -715,21 +708,52 @@ namespace Js
715708
{
716709
parentRecord->childrenModuleSet->AddNew(moduleName, this);
717710

718-
if (!this->WasParsed())
711+
if (this->parentModuleList == nullptr)
719712
{
720-
if (this->parentModuleList == nullptr)
713+
Recycler* recycler = GetScriptContext()->GetRecycler();
714+
this->parentModuleList = RecyclerNew(recycler, ModuleRecordList, recycler);
715+
}
716+
717+
if (!this->parentModuleList->Contains(parentRecord))
718+
{
719+
this->parentModuleList->Add(parentRecord);
720+
}
721+
}
722+
}
723+
724+
bool SourceTextModuleRecord::ConfirmChildrenParsed()
725+
{
726+
if (!this->WasParsed())
727+
{
728+
return false;
729+
}
730+
if (confirmedReady || this->ParentsNotified())
731+
{
732+
return true;
733+
}
734+
bool result = true;
735+
confirmedReady = true;
736+
EnsureChildModuleSet(GetScriptContext());
737+
childrenModuleSet->EachValue([&](SourceTextModuleRecord* childModuleRecord) {
738+
if (childModuleRecord->ParentsNotified())
739+
{
740+
return false;
741+
}
742+
else
743+
{
744+
if (childModuleRecord->ConfirmChildrenParsed())
721745
{
722-
Recycler* recycler = GetScriptContext()->GetRecycler();
723-
this->parentModuleList = RecyclerNew(recycler, ModuleRecordList, recycler);
746+
return false;
724747
}
725-
726-
if (!this->parentModuleList->Contains(parentRecord))
748+
else
727749
{
728-
this->parentModuleList->Add(parentRecord);
729-
parentRecord->numPendingChildrenModule++;
750+
result = false;
751+
return true;
730752
}
731753
}
732-
}
754+
});
755+
confirmedReady = false;
756+
return result;
733757
}
734758

735759
void SourceTextModuleRecord::EnsureChildModuleSet(ScriptContext *scriptContext)
@@ -751,16 +775,9 @@ namespace Js
751775
if (requestedModuleList != nullptr)
752776
{
753777
EnsureChildModuleSet(scriptContext);
754-
ArenaAllocator* allocator = scriptContext->GeneralAllocator();
755-
SList<LPCOLESTR> * moduleRecords = Anew(allocator, SList<LPCOLESTR>, allocator);
756778

757-
// Reverse the order for the host. So, host can read the files top-down
758779
requestedModuleList->MapUntil([&](IdentPtr specifier) {
759780
LPCOLESTR moduleName = specifier->Psz();
760-
return !moduleRecords->Prepend(moduleName);
761-
});
762-
763-
moduleRecords->MapUntil([&](LPCOLESTR moduleName) {
764781
ModuleRecordBase* moduleRecordBase = nullptr;
765782
SourceTextModuleRecord* moduleRecord = nullptr;
766783
bool itemFound = childrenModuleSet->TryGetValue(moduleName, &moduleRecord);
@@ -787,7 +804,6 @@ namespace Js
787804
}
788805
return false;
789806
});
790-
moduleRecords->Clear();
791807

792808
if (FAILED(hr))
793809
{

lib/Runtime/Language/SourceTextModuleRecord.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ namespace Js
4343

4444
HRESULT ResolveExternalModuleDependencies();
4545
void EnsureChildModuleSet(ScriptContext *scriptContext);
46+
bool ConfirmChildrenParsed();
4647

4748
void* GetHostDefined() const { return hostDefined; }
4849
void SetHostDefined(void* hostObj) { hostDefined = hostObj; }
@@ -116,6 +117,8 @@ namespace Js
116117
const static uint InvalidSlotIndex = 0xffffffff;
117118
// TODO: move non-GC fields out to avoid false reference?
118119
// This is the parsed tree resulted from compilation.
120+
Field(bool) confirmedReady = false;
121+
Field(bool) notifying = false;
119122
Field(bool) wasParsed;
120123
Field(bool) wasDeclarationInitialized;
121124
Field(bool) parentsNotified;
@@ -136,7 +139,6 @@ namespace Js
136139
Field(LocalExportMap*) localExportMapByExportName; // from propertyId to index map: for bytecode gen.
137140
Field(LocalExportMap*) localExportMapByLocalName; // from propertyId to index map: for bytecode gen.
138141
Field(LocalExportIndexList*) localExportIndexList; // from index to propertyId: for typehandler.
139-
Field(uint) numPendingChildrenModule;
140142
Field(ExportedNames*) exportedNames;
141143
Field(ResolvedExportMap*) resolvedExportMap;
142144

test/es6module/module-bugfixes.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,32 @@ var tests = [
6767
let functionBody = "import 'module_4570_dep1.js';"
6868
testRunner.LoadModule(functionBody);
6969
}
70+
},
71+
{
72+
name: "Issue 5171: Incorrect module load order",
73+
body: function() {
74+
WScript.RegisterModuleSource("obj.js", `export const obj = {a:false, b: false, c: false};`);
75+
WScript.RegisterModuleSource("a.js",`
76+
import {obj} from "obj.js";
77+
assert.isTrue(obj.b);
78+
assert.isFalse(obj.c);
79+
assert.isFalse(obj.a);
80+
obj.a = true;`);
81+
WScript.RegisterModuleSource("b.js",`
82+
import {obj} from "obj.js";
83+
assert.isFalse(obj.b);
84+
assert.isFalse(obj.c);
85+
assert.isFalse(obj.a);
86+
obj.b = true;`);
87+
WScript.RegisterModuleSource("c.js",`
88+
import {obj} from "obj.js";
89+
assert.isTrue(obj.b);
90+
assert.isFalse(obj.c);
91+
assert.isTrue(obj.a);
92+
obj.c = true;`);
93+
const start = 'import "b.js"; import "a.js"; import "c.js";';
94+
testRunner.LoadModule(start);
95+
}
7096
}
7197
];
7298

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
//-------------------------------------------------------------------------------------------------------
2+
// Copyright (C) Microsoft. All rights reserved.
3+
// Licensed under the MIT license. See LICENSE.txt file in the project root for full license information.
4+
//-------------------------------------------------------------------------------------------------------
5+
6+
// Tests that circular overlapping module dependencies are all loaded before execution
7+
8+
WScript.LoadScriptFile("..\\UnitTestFramework\\UnitTestFramework.js");
9+
10+
WScript.RegisterModuleSource("mod0.js", "print('pass')");
11+
WScript.RegisterModuleSource("mod1.js", "import 'mod2.js';");
12+
WScript.RegisterModuleSource("mod2.js", "import 'mod0.js';");
13+
14+
WScript.LoadScriptFile("mod2.js", "module");
15+
WScript.LoadScriptFile("mod1.js", "module");

test/es6module/rlexe.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,12 @@
139139
<baseline>bug_issue_3257.baseline</baseline>
140140
<tags>exclude_sanitize_address</tags>
141141
</default>
142+
</test>
143+
<test>
144+
<default>
145+
<files>multiple-roots-circular.js</files>
146+
<tags>exclude_sanitize_address</tags>
147+
</default>
142148
</test>
143149
<test>
144150
<default>

0 commit comments

Comments
 (0)