JSC Module Loader Rewrite: WHATWG-Era JS Builtins Replaced with Pure C++
Source/JavaScriptCore/runtime/CyclicModuleRecord.cpp
Source/JavaScriptCore/runtime/ModuleRegistryEntry.cpp
This is one of the largest architectural changes to JSC's module system in years — a complete replacement of the module loader. The old implementation split logic across C++ host hooks and a privileged JS builtin file (ModuleLoader.js) compiled into the engine, meaning JS-level logic ran with engine-internal privileges. The new architecture is pure C++, introducing six new types: CyclicModuleRecord (implementing the ECMAScript spec's DFS-based linking/evaluation algorithms), ModuleRegistryEntry (per-module fetch/instantiation/evaluation state), ModuleGraphLoadingState (in-flight import graph tracking), ModuleLoaderPayload, ModuleLoadingContext, and ModuleMap.
Old architecture: New architecture:
JS call: import('foo') JS call: import('foo')
│ │
▼ ▼
C++ JSModuleLoader C++ JSModuleLoader
│ │
└──► ModuleLoader.js (builtin JS) └──► ModuleRegistryEntry
│ resolve() │ ensureFetchPromise()
│ fetch() │ ensureModulePromise()
│ instantiate() │
│ evaluate() ModuleGraphLoadingState
│ │ innerModuleLoading()
└──► back to C++ host hooks │
CyclicModuleRecord
│ innerModuleLinking() [DFS]
│ innerModuleEvaluation() [DFS]
│ gatherAvailableAncestors() [TLA]
▼
Promise resolution / rejection
The rewrite fixes assertion failures on valid ES modules, incorrect top-level await evaluation ordering, and broken concurrent dynamic import behavior. The new ensureFetchPromise/ensureModulePromise deduplication pattern replaces the old JS-side registry for concurrent import() calls. Top-level await is now handled through asyncCapability, pendingAsyncDependencies, and gatherAvailableAncestors, implementing the spec's async evaluation ordering. Resolution failure caching (addResolutionFailure) now produces distinct Error objects per importing context — the test bare-resolution-failure.js explicitly checks e1 !== e2 and e1.message !== e2.message, catching a previously broken invariant.
Significance
This is a high-value audit target: every security-critical operation in the module system — resolution, fetching, linking, evaluation, and error propagation — is now implemented in fresh C++ code managing GC-heap objects across async continuations.
The concurrent import deduplication path (ensureFetchPromise/ensureModulePromise and the decrementRemaining counter in ModuleLoaderPayload) needs careful review — off-by-one errors or use-after-free if a promise settles while another caller is mid-operation. The gatherAvailableAncestors graph traversal and its interaction with pendingAsyncDependencies, asyncEvaluationOrder, and cycleRoot is complex — incorrect traversal could trigger evaluation out of order or skip modules entirely. The old JS builtins ran in a GC-safe context; the new C++ code directly manages JSPromise and CyclicModuleRecord objects across async continuations stored as microtasks — any missing WriteBarrier or incorrect rooting in the new microtask callbacks could produce heap corruption.