From 11aec6e4bff58400178d79017dab06bcc279e600 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Tue, 19 May 2026 21:14:08 -0700 Subject: [PATCH 1/5] [NFC] Refactor closedWorld to WorldMode enum Replace the `closedWorld` boolean in `PassOptions` with a `WorldMode` enum which has `Closed` and `Open` variants. Also introduce `ModuleUtils::getExposedPublicHeapTypes` to collect directly exposed public types, and updates `getPublicHeapTypes` to take `WorldMode` as a parameter. These refactorings will make a subsequent commit changing how we collect public heap types in open-world mode simpler. --- src/binaryen-c.cpp | 8 +++- src/ir/module-utils.cpp | 48 +++++++++++++++++------ src/ir/module-utils.h | 10 +++-- src/ir/possible-contents.cpp | 12 +++--- src/ir/type-updating.cpp | 3 +- src/ir/type-updating.h | 18 +++++---- src/pass.h | 48 ++++++++++++----------- src/passes/AbstractTypeRefining.cpp | 14 ++++--- src/passes/ConstantFieldPropagation.cpp | 2 +- src/passes/DeadArgumentElimination2.cpp | 8 ++-- src/passes/GlobalEffects.cpp | 11 +++--- src/passes/GlobalRefining.cpp | 2 +- src/passes/GlobalStructInference.cpp | 2 +- src/passes/GlobalTypeOptimization.cpp | 9 +++-- src/passes/J2CLItableMerging.cpp | 5 ++- src/passes/MinimizeRecGroups.cpp | 3 +- src/passes/RemoveUnusedModuleElements.cpp | 6 +-- src/passes/RemoveUnusedTypes.cpp | 4 +- src/passes/ReorderTypes.cpp | 9 +++-- src/passes/SignaturePruning.cpp | 8 ++-- src/passes/SignatureRefining.cpp | 6 ++- src/passes/StringLowering.cpp | 2 +- src/passes/TypeFinalizing.cpp | 6 ++- src/passes/TypeMerging.cpp | 10 +++-- src/passes/TypeRefining.cpp | 8 ++-- src/passes/Unsubtyping.cpp | 9 +++-- src/passes/pass.cpp | 8 ++-- src/tools/fuzzing.h | 6 +-- src/tools/fuzzing/fuzzing.cpp | 25 ++++++------ src/tools/tool-options.h | 2 +- src/tools/wasm-opt.cpp | 2 +- src/wasm/wasm-validator.cpp | 2 +- 32 files changed, 187 insertions(+), 129 deletions(-) diff --git a/src/binaryen-c.cpp b/src/binaryen-c.cpp index 0f8cb4805f9..7c8dcab97be 100644 --- a/src/binaryen-c.cpp +++ b/src/binaryen-c.cpp @@ -5888,9 +5888,13 @@ void BinaryenSetTrapsNeverHappen(bool on) { globalPassOptions.trapsNeverHappen = on; } -bool BinaryenGetClosedWorld(void) { return globalPassOptions.closedWorld; } +bool BinaryenGetClosedWorld(void) { + return globalPassOptions.worldMode == WorldMode::Closed; +} -void BinaryenSetClosedWorld(bool on) { globalPassOptions.closedWorld = on; } +void BinaryenSetClosedWorld(bool on) { + globalPassOptions.worldMode = on ? WorldMode::Closed : WorldMode::Open; +} bool BinaryenGetLowMemoryUnused(void) { return globalPassOptions.lowMemoryUnused; diff --git a/src/ir/module-utils.cpp b/src/ir/module-utils.cpp index 6ee94d35d65..cf00f201119 100644 --- a/src/ir/module-utils.cpp +++ b/src/ir/module-utils.cpp @@ -481,12 +481,16 @@ struct CodeScanner : PostWalker { }; void classifyTypeVisibility(Module& wasm, - InsertOrderedMap& types); + InsertOrderedMap& types, + WorldMode worldMode); } // anonymous namespace -InsertOrderedMap collectHeapTypeInfo( - Module& wasm, TypeInclusion inclusion, VisibilityHandling visibility) { +InsertOrderedMap +collectHeapTypeInfo(Module& wasm, + WorldMode worldMode, + TypeInclusion inclusion, + VisibilityHandling visibility) { // Collect module-level info. TypeInfos info; CodeScanner(wasm, info).walkModuleCode(&wasm); @@ -593,7 +597,7 @@ InsertOrderedMap collectHeapTypeInfo( } if (visibility == VisibilityHandling::FindVisibility) { - classifyTypeVisibility(wasm, info.info); + classifyTypeVisibility(wasm, info.info, worldMode); } return std::move(info.info); @@ -602,8 +606,9 @@ InsertOrderedMap collectHeapTypeInfo( namespace { void classifyTypeVisibility(Module& wasm, - InsertOrderedMap& types) { - for (auto type : getPublicHeapTypes(wasm)) { + InsertOrderedMap& types, + WorldMode worldMode) { + for (auto type : getPublicHeapTypes(wasm, worldMode)) { if (auto it = types.find(type); it != types.end()) { it->second.visibility = Visibility::Public; } @@ -624,7 +629,7 @@ void setIndices(IndexedHeapTypes& indexedTypes) { } // anonymous namespace std::vector collectHeapTypes(Module& wasm) { - auto info = collectHeapTypeInfo(wasm); + auto info = collectHeapTypeInfo(wasm, WorldMode::Open); std::vector types; types.reserve(info.size()); for (auto& [type, _] : info) { @@ -633,18 +638,22 @@ std::vector collectHeapTypes(Module& wasm) { return types; } -std::vector getPublicHeapTypes(Module& wasm) { +std::vector getExposedPublicHeapTypes(Module& wasm) { // Look at the types of imports as exports to get an initial set of public // types, then traverse the types used by public types and collect the // transitively reachable public types as well. std::vector workList; std::unordered_set publicGroups; + std::unordered_set publicBasicTypes; // The collected types. std::vector publicTypes; auto notePublic = [&](HeapType type) { if (type.isBasic()) { + if (publicBasicTypes.insert(type).second) { + publicTypes.push_back(type); + } return; } auto group = type.getRecGroup(); @@ -725,9 +734,23 @@ std::vector getPublicHeapTypes(Module& wasm) { return publicTypes; } -std::vector getPrivateHeapTypes(Module& wasm) { - auto info = collectHeapTypeInfo( - wasm, TypeInclusion::UsedIRTypes, VisibilityHandling::FindVisibility); +std::vector getPublicHeapTypes(Module& wasm, WorldMode worldMode) { + auto exposed = getExposedPublicHeapTypes(wasm); + std::vector publicTypes; + publicTypes.reserve(exposed.size()); + for (auto type : exposed) { + if (!type.isBasic()) { + publicTypes.push_back(type); + } + } + return publicTypes; +} + +std::vector getPrivateHeapTypes(Module& wasm, WorldMode worldMode) { + auto info = collectHeapTypeInfo(wasm, + worldMode, + TypeInclusion::UsedIRTypes, + VisibilityHandling::FindVisibility); std::vector types; types.reserve(info.size()); for (auto& [type, typeInfo] : info) { @@ -739,7 +762,8 @@ std::vector getPrivateHeapTypes(Module& wasm) { } IndexedHeapTypes getOptimizedIndexedHeapTypes(Module& wasm) { - auto counts = collectHeapTypeInfo(wasm, TypeInclusion::BinaryTypes); + auto counts = + collectHeapTypeInfo(wasm, WorldMode::Open, TypeInclusion::BinaryTypes); // Collect the rec groups. std::unordered_map groupIndices; diff --git a/src/ir/module-utils.h b/src/ir/module-utils.h index 50b67df7cea..2bb8dd914d7 100644 --- a/src/ir/module-utils.h +++ b/src/ir/module-utils.h @@ -472,6 +472,7 @@ struct HeapTypeInfo { InsertOrderedMap collectHeapTypeInfo( Module& wasm, + WorldMode worldMode, TypeInclusion inclusion = TypeInclusion::AllTypes, VisibilityHandling visibility = VisibilityHandling::NoVisibility); @@ -479,13 +480,14 @@ InsertOrderedMap collectHeapTypeInfo( // module, i.e. the types that would appear in the type section. std::vector collectHeapTypes(Module& wasm); +std::vector getExposedPublicHeapTypes(Module& wasm); + // Collect all the heap types visible on the module boundary that cannot be -// changed. TODO: For open world use cases, this needs to include all subtypes -// of public types as well. -std::vector getPublicHeapTypes(Module& wasm); +// changed. +std::vector getPublicHeapTypes(Module& wasm, WorldMode worldMode); // getHeapTypes - getPublicHeapTypes -std::vector getPrivateHeapTypes(Module& wasm); +std::vector getPrivateHeapTypes(Module& wasm, WorldMode worldMode); struct IndexedHeapTypes { std::vector types; diff --git a/src/ir/possible-contents.cpp b/src/ir/possible-contents.cpp index 4b76f467825..ad7026b1247 100644 --- a/src/ir/possible-contents.cpp +++ b/src/ir/possible-contents.cpp @@ -684,7 +684,7 @@ struct InfoCollector SignatureResultLocation{func->type.getHeapType(), i}}); } - if (!options.closedWorld) { + if (options.worldMode != WorldMode::Closed) { info.calledFromOutside.insert(curr->func); } } @@ -1711,7 +1711,7 @@ void TNHOracle::scan(Function* func, void visitCallRef(CallRef* curr) { // We can only optimize call_ref in closed world, as otherwise the // call can go somewhere we can't see. - if (options.closedWorld) { + if (options.worldMode == WorldMode::Closed) { info.callRefs.push_back(curr); } } @@ -1834,7 +1834,7 @@ void TNHOracle::infer() { // that type or a subtype, i.e., might be called when that type is seen in a // call_ref target. std::unordered_map> typeFunctions; - if (options.closedWorld) { + if (options.worldMode == WorldMode::Closed) { for (auto& func : wasm.functions) { auto type = func->type; auto& info = map[wasm.getFunction(func->name)]; @@ -1895,7 +1895,7 @@ void TNHOracle::infer() { // We should only get here in a closed world, in which we know which // functions might be called (the scan phase only notes callRefs if we are // in fact in a closed world). - assert(options.closedWorld); + assert(options.worldMode == WorldMode::Closed); auto iter = typeFunctions.find(targetType.getHeapType()); if (iter == typeFunctions.end()) { @@ -2535,8 +2535,8 @@ Flower::Flower(Module& wasm, const PassOptions& options) } // In open world, public heap types may be written to from the outside. - if (!options.closedWorld) { - for (auto type : ModuleUtils::getPublicHeapTypes(wasm)) { + if (options.worldMode != WorldMode::Closed) { + for (auto type : ModuleUtils::getPublicHeapTypes(wasm, options.worldMode)) { if (type.isStruct()) { auto& fields = type.getStruct().fields; for (Index i = 0; i < fields.size(); i++) { diff --git a/src/ir/type-updating.cpp b/src/ir/type-updating.cpp index 69f29101c86..0ca698a7568 100644 --- a/src/ir/type-updating.cpp +++ b/src/ir/type-updating.cpp @@ -26,7 +26,7 @@ namespace wasm { -GlobalTypeRewriter::GlobalTypeRewriter(Module& wasm) +GlobalTypeRewriter::GlobalTypeRewriter(Module& wasm, WorldMode worldMode) : wasm(wasm), publicGroups(wasm.features) { // Find the heap types that are not publicly observable. Even in a closed // world scenario, don't modify public types because we assume that they may @@ -34,6 +34,7 @@ GlobalTypeRewriter::GlobalTypeRewriter(Module& wasm) // will be located in the builder. typeInfo = ModuleUtils::collectHeapTypeInfo( wasm, + worldMode, ModuleUtils::TypeInclusion::UsedIRTypes, ModuleUtils::VisibilityHandling::FindVisibility); diff --git a/src/ir/type-updating.h b/src/ir/type-updating.h index 0e050becc59..7aaf02a8234 100644 --- a/src/ir/type-updating.h +++ b/src/ir/type-updating.h @@ -358,7 +358,7 @@ class GlobalTypeRewriter { // private types do not conflict with public types. UniqueRecGroups publicGroups; - GlobalTypeRewriter(Module& wasm); + GlobalTypeRewriter(Module& wasm, WorldMode worldMode); virtual ~GlobalTypeRewriter() {} // Main entry point. This performs the entire process of creating new heap @@ -427,7 +427,9 @@ class GlobalTypeRewriter { // Helper for the repeating pattern of just updating Signature types using a // map of old heap type => new Signature. - static void updateSignatures(const SignatureUpdates& updates, Module& wasm) { + static void updateSignatures(const SignatureUpdates& updates, + Module& wasm, + WorldMode worldMode) { if (updates.empty()) { return; } @@ -436,8 +438,10 @@ class GlobalTypeRewriter { const SignatureUpdates& updates; public: - SignatureRewriter(Module& wasm, const SignatureUpdates& updates) - : GlobalTypeRewriter(wasm), updates(updates) { + SignatureRewriter(Module& wasm, + const SignatureUpdates& updates, + WorldMode worldMode) + : GlobalTypeRewriter(wasm, worldMode), updates(updates) { update(); } @@ -448,7 +452,7 @@ class GlobalTypeRewriter { sig.results = getTempType(iter->second.results); } } - } rewriter(wasm, updates); + } rewriter(wasm, updates, worldMode); } protected: @@ -473,8 +477,8 @@ class TypeMapper : public GlobalTypeRewriter { const TypeUpdates& mapping; - TypeMapper(Module& wasm, const TypeUpdates& mapping) - : GlobalTypeRewriter(wasm), mapping(mapping) {} + TypeMapper(Module& wasm, const TypeUpdates& mapping, WorldMode worldMode) + : GlobalTypeRewriter(wasm, worldMode), mapping(mapping) {} void map() { // Update the internals of types (struct fields, signatures, etc.) to diff --git a/src/pass.h b/src/pass.h index f5088e5e7ee..7d7dc0f1afc 100644 --- a/src/pass.h +++ b/src/pass.h @@ -110,6 +110,30 @@ struct InliningOptions { Index partialInliningIfs = 0; }; +// Assume code outside of the module does not inspect or interact with GC and +// function references, with the goal of being able to aggressively optimize +// all user-defined types. The outside may hold on to references and pass them +// back in, but may not inspect their contents, call them, or reflect on their +// types in any way. +// +// By default we do not make this assumption, and assume anything that escapes +// to the outside may be inspected in detail, which prevents us from e.g. +// changing the type of any value that may escape except by refining it (so we +// can't remove or refine fields on an escaping struct type, for example, +// unless the new type declares the original type as a supertype). +// +// Note that the module can still have imports and exports - otherwise it +// could do nothing at all! - so the meaning of "closed world" is a little +// subtle here. We do still want to keep imports and exports unchanged, as +// they form a contract with the outside world. For example, if an import has +// two parameters, we can't remove one of them. A nuance regarding that is how +// type equality works between wasm modules using the isorecursive type +// system: not only do we need to not remove a parameter as just mentioned, +// but we also want to keep types of things on the boundary unchanged. For +// example, we should not change an exported function's signature, as the +// outside may need that type to properly call the export. +enum class WorldMode { Open, Closed }; + struct PassOptions { friend Pass; @@ -196,29 +220,7 @@ struct PassOptions { // creates it and we know it is all zeros right before the active segments are // applied.) bool zeroFilledMemory = false; - // Assume code outside of the module does not inspect or interact with GC and - // function references, with the goal of being able to aggressively optimize - // all user-defined types. The outside may hold on to references and pass them - // back in, but may not inspect their contents, call them, or reflect on their - // types in any way. - // - // By default we do not make this assumption, and assume anything that escapes - // to the outside may be inspected in detail, which prevents us from e.g. - // changing the type of any value that may escape except by refining it (so we - // can't remove or refine fields on an escaping struct type, for example, - // unless the new type declares the original type as a supertype). - // - // Note that the module can still have imports and exports - otherwise it - // could do nothing at all! - so the meaning of "closed world" is a little - // subtle here. We do still want to keep imports and exports unchanged, as - // they form a contract with the outside world. For example, if an import has - // two parameters, we can't remove one of them. A nuance regarding that is how - // type equality works between wasm modules using the isorecursive type - // system: not only do we need to not remove a parameter as just mentioned, - // but we also want to keep types of things on the boundary unchanged. For - // example, we should not change an exported function's signature, as the - // outside may need that type to properly call the export. - bool closedWorld = false; + WorldMode worldMode = WorldMode::Open; // Whether to try to preserve debug info through, which are special calls. bool debugInfo = false; // Whether to generate StackIR during binary writing. This is on by default diff --git a/src/passes/AbstractTypeRefining.cpp b/src/passes/AbstractTypeRefining.cpp index b75153e3d2d..4dd1264a9ca 100644 --- a/src/passes/AbstractTypeRefining.cpp +++ b/src/passes/AbstractTypeRefining.cpp @@ -87,7 +87,7 @@ struct AbstractTypeRefining : public Pass { return; } - if (!getPassOptions().closedWorld) { + if (getPassOptions().worldMode != WorldMode::Closed) { Fatal() << "AbstractTypeRefining requires --closed-world"; } @@ -116,7 +116,8 @@ struct AbstractTypeRefining : public Pass { // module, given closed world, but we'd also need to make sure that // we don't need to make any changes to public types that refer to // them. - for (auto type : ModuleUtils::getPublicHeapTypes(*module)) { + for (auto type : + ModuleUtils::getPublicHeapTypes(*module, getPassOptions().worldMode)) { createdTypes.insert(type); } @@ -289,8 +290,10 @@ struct AbstractTypeRefining : public Pass { // that for Unsubtyping. class AbstractTypeRefiningTypeMapper : public TypeMapper { public: - AbstractTypeRefiningTypeMapper(Module& wasm, const TypeUpdates& mapping) - : TypeMapper(wasm, mapping) {} + AbstractTypeRefiningTypeMapper(Module& wasm, + const TypeUpdates& mapping, + WorldMode worldMode) + : TypeMapper(wasm, mapping, worldMode) {} std::optional getDeclaredSuperType(HeapType oldType) override { // We do not want to update subtype relationships. @@ -298,7 +301,8 @@ struct AbstractTypeRefining : public Pass { } }; - AbstractTypeRefiningTypeMapper(*module, mapping).map(); + AbstractTypeRefiningTypeMapper(*module, mapping, getPassOptions().worldMode) + .map(); // Refinalize to propagate the type changes we made. For example, a refined // cast may lead to a struct.get reading a more refined type using that diff --git a/src/passes/ConstantFieldPropagation.cpp b/src/passes/ConstantFieldPropagation.cpp index 0063a8d3a69..6ae8ea1ac52 100644 --- a/src/passes/ConstantFieldPropagation.cpp +++ b/src/passes/ConstantFieldPropagation.cpp @@ -542,7 +542,7 @@ struct ConstantFieldPropagation : public Pass { return; } - if (!getPassOptions().closedWorld) { + if (getPassOptions().worldMode != WorldMode::Closed) { Fatal() << "CFP requires --closed-world"; } diff --git a/src/passes/DeadArgumentElimination2.cpp b/src/passes/DeadArgumentElimination2.cpp index 11dc2cec1fb..1ebc576fa9a 100644 --- a/src/passes/DeadArgumentElimination2.cpp +++ b/src/passes/DeadArgumentElimination2.cpp @@ -235,7 +235,7 @@ struct DAE2 : public Pass { } optimizeReferencedFuncs = - getPassOptions().closedWorld && wasm->features.hasGC(); + getPassOptions().worldMode == WorldMode::Closed && wasm->features.hasGC(); TIME(Timer timer); @@ -579,7 +579,8 @@ void DAE2::analyzeModule() { // // TODO: Analyze tags and remove their unused parameters. std::unordered_set unrewritableRoots; - publicHeapTypes = ModuleUtils::getPublicHeapTypes(*wasm); + publicHeapTypes = + ModuleUtils::getPublicHeapTypes(*wasm, getPassOptions().worldMode); for (auto type : publicHeapTypes) { if (type.isSignature()) { unrewritableRoots.insert(getRootType(type)); @@ -728,7 +729,8 @@ void DAE2::computeFixedPoint() { struct DAETypeUpdater : GlobalTypeRewriter { DAE2& parent; DAETypeUpdater(DAE2& parent) - : GlobalTypeRewriter(*parent.wasm), parent(parent) {} + : GlobalTypeRewriter(*parent.wasm, parent.getPassOptions().worldMode), + parent(parent) {} void modifySignature(HeapType oldType, Signature& sig) override { // All signature types in a type tree will have the same parameters removed diff --git a/src/passes/GlobalEffects.cpp b/src/passes/GlobalEffects.cpp index ca82b2b3aea..f12a6238c93 100644 --- a/src/passes/GlobalEffects.cpp +++ b/src/passes/GlobalEffects.cpp @@ -87,7 +87,8 @@ std::map analyzeFuncs(Module& module, if (auto* call = curr->dynCast()) { // Note the direct call. funcInfo.calledFunctions.insert(call->target); - } else if (effects.calls && options.closedWorld) { + } else if (effects.calls && + options.worldMode == WorldMode::Closed) { HeapType type; if (auto* callRef = curr->dynCast()) { // call_ref on unreachable does not have a call effect, @@ -102,7 +103,7 @@ std::map analyzeFuncs(Module& module, funcInfo.indirectCalledTypes.insert(type); } else if (effects.calls) { - assert(!options.closedWorld); + assert(options.worldMode != WorldMode::Closed); funcInfo.effects = UnknownEffects; } else { // No call here, but update throwing if we see it. (Only do so, @@ -144,9 +145,9 @@ using CallGraph = CallGraph buildCallGraph(const Module& module, const std::map& funcInfos, - bool closedWorld) { + WorldMode worldMode) { CallGraph callGraph; - if (!closedWorld) { + if (worldMode != WorldMode::Closed) { for (const auto& [caller, callerInfo] : funcInfos) { auto& callees = callGraph[caller]; @@ -344,7 +345,7 @@ struct GenerateGlobalEffects : public Pass { analyzeFuncs(*module, getPassOptions()); auto callGraph = - buildCallGraph(*module, funcInfos, getPassOptions().closedWorld); + buildCallGraph(*module, funcInfos, getPassOptions().worldMode); propagateEffects(*module, getPassOptions(), funcInfos, callGraph); diff --git a/src/passes/GlobalRefining.cpp b/src/passes/GlobalRefining.cpp index 87dc5b259cf..95389f8ccfc 100644 --- a/src/passes/GlobalRefining.cpp +++ b/src/passes/GlobalRefining.cpp @@ -80,7 +80,7 @@ struct GlobalRefining : public Pass { std::unordered_set exportedGlobals(exportedGlobalsVec.begin(), exportedGlobalsVec.end()); for (auto* global : exportedGlobalsVec) { - if (getPassOptions().closedWorld || global->mutable_) { + if (getPassOptions().worldMode == WorldMode::Closed || global->mutable_) { unoptimizable.insert(global->name); } } diff --git a/src/passes/GlobalStructInference.cpp b/src/passes/GlobalStructInference.cpp index bb5a077648a..8de7f20fa50 100644 --- a/src/passes/GlobalStructInference.cpp +++ b/src/passes/GlobalStructInference.cpp @@ -109,7 +109,7 @@ struct GlobalStructInference : public Pass { subTypes = std::make_unique(*module); } - if (getPassOptions().closedWorld) { + if (getPassOptions().worldMode == WorldMode::Closed) { analyzeClosedWorld(module); } diff --git a/src/passes/GlobalTypeOptimization.cpp b/src/passes/GlobalTypeOptimization.cpp index 8171ce1c501..d8b1767c556 100644 --- a/src/passes/GlobalTypeOptimization.cpp +++ b/src/passes/GlobalTypeOptimization.cpp @@ -153,8 +153,7 @@ struct GlobalTypeOptimization : public Pass { if (!module->features.hasGC()) { return; } - - if (!getPassOptions().closedWorld) { + if (getPassOptions().worldMode != WorldMode::Closed) { Fatal() << "GTO requires --closed-world"; } @@ -207,7 +206,8 @@ struct GlobalTypeOptimization : public Pass { propagator.propagateToSubTypes(dataFromSupersMap); // Find the public types, which we must not modify. - auto publicTypes = ModuleUtils::getPublicHeapTypes(*module); + auto publicTypes = + ModuleUtils::getPublicHeapTypes(*module, getPassOptions().worldMode); std::unordered_set publicTypesSet(publicTypes.begin(), publicTypes.end()); @@ -479,7 +479,8 @@ struct GlobalTypeOptimization : public Pass { public: TypeRewriter(Module& wasm, GlobalTypeOptimization& parent) - : GlobalTypeRewriter(wasm), parent(parent) {} + : GlobalTypeRewriter(wasm, parent.getPassOptions().worldMode), + parent(parent) {} void modifyStruct(HeapType oldStructType, Struct& struct_) override { auto& newFields = struct_.fields; diff --git a/src/passes/J2CLItableMerging.cpp b/src/passes/J2CLItableMerging.cpp index 68e610755d2..6d2188129e4 100644 --- a/src/passes/J2CLItableMerging.cpp +++ b/src/passes/J2CLItableMerging.cpp @@ -72,7 +72,7 @@ struct J2CLItableMerging : public Pass { return; } - if (!getPassOptions().closedWorld) { + if (getPassOptions().worldMode != WorldMode::Closed) { Fatal() << "--merge-j2cl-itables requires --closed-world"; } @@ -384,7 +384,8 @@ struct J2CLItableMerging : public Pass { public: TypeRewriter(Module& wasm, J2CLItableMerging& parent) - : GlobalTypeRewriter(wasm), parent(parent) {} + : GlobalTypeRewriter(wasm, parent.getPassOptions().worldMode), + parent(parent) {} void modifyStruct(HeapType oldStructType, Struct& struct_) override { auto structInfoIt = parent.structInfoByVtableType.find(oldStructType); diff --git a/src/passes/MinimizeRecGroups.cpp b/src/passes/MinimizeRecGroups.cpp index a4d5e8211ac..e6757c8ac96 100644 --- a/src/passes/MinimizeRecGroups.cpp +++ b/src/passes/MinimizeRecGroups.cpp @@ -302,6 +302,7 @@ struct MinimizeRecGroups : Pass { auto typeInfo = ModuleUtils::collectHeapTypeInfo( *module, + getPassOptions().worldMode, ModuleUtils::TypeInclusion::AllTypes, ModuleUtils::VisibilityHandling::FindVisibility); @@ -771,7 +772,7 @@ struct MinimizeRecGroups : Pass { ++i; } } - GlobalTypeRewriter rewriter(wasm); + GlobalTypeRewriter rewriter(wasm, getPassOptions().worldMode); rewriter.mapTypes(oldToNew); rewriter.mapTypeNamesAndIndices(oldToNew); } diff --git a/src/passes/RemoveUnusedModuleElements.cpp b/src/passes/RemoveUnusedModuleElements.cpp index fd026490cb1..6de95a74b6d 100644 --- a/src/passes/RemoveUnusedModuleElements.cpp +++ b/src/passes/RemoveUnusedModuleElements.cpp @@ -444,7 +444,7 @@ struct Analyzer { } void useRefFunc(Name func) { - if (!options.closedWorld) { + if (options.worldMode != WorldMode::Closed) { // The world is open, so assume the worst and something (inside or outside // of the module) can call this. use({ModuleElementKind::Function, func}); @@ -610,8 +610,8 @@ struct Analyzer { // outside of the code we can see), and when it is reached (if it's // unreachable then we don't know the type, and can defer that to DCE to // remove). - if (!options.closedWorld || curr->type == Type::unreachable || - !curr->is()) { + if (options.worldMode != WorldMode::Closed || + curr->type == Type::unreachable || !curr->is()) { for (auto* child : ChildIterator(curr)) { use(child); } diff --git a/src/passes/RemoveUnusedTypes.cpp b/src/passes/RemoveUnusedTypes.cpp index b3b0f4a6dd9..b7dfccb5083 100644 --- a/src/passes/RemoveUnusedTypes.cpp +++ b/src/passes/RemoveUnusedTypes.cpp @@ -39,14 +39,14 @@ struct RemoveUnusedTypes : Pass { // would change the identity of $A. Currently we would incorrectly remove // $unused. To fix that, we need to fix our collection of public types to // consider $A (and $unused) public in an open world. - if (!getPassOptions().closedWorld) { + if (getPassOptions().worldMode != WorldMode::Closed) { Fatal() << "RemoveUnusedTypes requires --closed-world"; } // We're not changing the contents of any of the types, so we just round // trip them through GlobalTypeRewriter which will put all the private types // in a single new rec group and leave out all the unused types. - GlobalTypeRewriter(*module).update(); + GlobalTypeRewriter(*module, getPassOptions().worldMode).update(); } }; diff --git a/src/passes/ReorderTypes.cpp b/src/passes/ReorderTypes.cpp index e120822da51..039f709b41b 100644 --- a/src/passes/ReorderTypes.cpp +++ b/src/passes/ReorderTypes.cpp @@ -41,8 +41,8 @@ struct ReorderingTypeRewriter : GlobalTypeRewriter { static constexpr float maxFactor = 1.0; static constexpr Index numFactors = 21; - ReorderingTypeRewriter(Module& wasm, bool forTesting) - : GlobalTypeRewriter(wasm), forTesting(forTesting) {} + ReorderingTypeRewriter(Module& wasm, bool forTesting, WorldMode worldMode) + : GlobalTypeRewriter(wasm, worldMode), forTesting(forTesting) {} std::vector getSortedTypes(PredecessorGraph preds) override { auto numTypes = preds.size(); @@ -142,11 +142,12 @@ struct ReorderTypes : Pass { } // See note in RemoveUnusedTypes. - if (!getPassOptions().closedWorld) { + if (getPassOptions().worldMode != WorldMode::Closed) { Fatal() << "ReorderTypes requires --closed-world"; } - ReorderingTypeRewriter(*module, forTesting).update(); + ReorderingTypeRewriter(*module, forTesting, getPassOptions().worldMode) + .update(); } }; diff --git a/src/passes/SignaturePruning.cpp b/src/passes/SignaturePruning.cpp index 4670d1c0015..7f2b2e40706 100644 --- a/src/passes/SignaturePruning.cpp +++ b/src/passes/SignaturePruning.cpp @@ -65,7 +65,7 @@ struct SignaturePruning : public Pass { return; } - if (!getPassOptions().closedWorld) { + if (getPassOptions().worldMode != WorldMode::Closed) { Fatal() << "SignaturePruning requires --closed-world"; } @@ -187,7 +187,8 @@ struct SignaturePruning : public Pass { } // Find the public types, which cannot be modified. - for (auto type : ModuleUtils::getPublicHeapTypes(*module)) { + for (auto type : + ModuleUtils::getPublicHeapTypes(*module, getPassOptions().worldMode)) { if (type.isFunction()) { allInfo[type].optimizable = false; } @@ -339,7 +340,8 @@ struct SignaturePruning : public Pass { } // Rewrite the types. - GlobalTypeRewriter::updateSignatures(newSignatures, *module); + GlobalTypeRewriter::updateSignatures( + newSignatures, *module, getPassOptions().worldMode); if (callTargetsToLocalize.empty()) { return false; diff --git a/src/passes/SignatureRefining.cpp b/src/passes/SignatureRefining.cpp index c1d72bc8d77..5137b1899a5 100644 --- a/src/passes/SignatureRefining.cpp +++ b/src/passes/SignatureRefining.cpp @@ -156,7 +156,8 @@ struct SignatureRefining : public Pass { } // Find the public types, which we must not modify. - for (auto type : ModuleUtils::getPublicHeapTypes(*module)) { + for (auto type : + ModuleUtils::getPublicHeapTypes(*module, getPassOptions().worldMode)) { if (type.isFunction()) { allInfo[type].canModify = false; } @@ -337,7 +338,8 @@ struct SignatureRefining : public Pass { CodeUpdater(*this, *module).run(getPassRunner(), module); // Rewrite the types. - GlobalTypeRewriter::updateSignatures(newSignatures, *module); + GlobalTypeRewriter::updateSignatures( + newSignatures, *module, getPassOptions().worldMode); // Update intrinsics. updateIntrinsics(module, allInfo); diff --git a/src/passes/StringLowering.cpp b/src/passes/StringLowering.cpp index bd753efaf91..c9e836aefe8 100644 --- a/src/passes/StringLowering.cpp +++ b/src/passes/StringLowering.cpp @@ -338,7 +338,7 @@ struct StringLowering : public StringGathering { // Strings turn into externref. updates[HeapType::string] = HeapType::ext; - TypeMapper(*module, updates).map(); + TypeMapper(*module, updates, getPassOptions().worldMode).map(); } // Imported string functions. diff --git a/src/passes/TypeFinalizing.cpp b/src/passes/TypeFinalizing.cpp index 5ba3459da46..85d218da327 100644 --- a/src/passes/TypeFinalizing.cpp +++ b/src/passes/TypeFinalizing.cpp @@ -52,7 +52,8 @@ struct TypeFinalizing : public Pass { // Note we don't need to worry about signature-called functions here // (configureAll) because such calls don't care about finality. - auto privateTypes = ModuleUtils::getPrivateHeapTypes(*module); + auto privateTypes = + ModuleUtils::getPrivateHeapTypes(*module, getPassOptions().worldMode); for (auto type : privateTypes) { // If we are finalizing types then we can only do that to leaf types. If // we are unfinalizing, we can do that unconditionally. @@ -66,7 +67,8 @@ struct TypeFinalizing : public Pass { public: TypeRewriter(Module& wasm, TypeFinalizing& parent) - : GlobalTypeRewriter(wasm), parent(parent) {} + : GlobalTypeRewriter(wasm, parent.getPassOptions().worldMode), + parent(parent) {} void modifyTypeBuilderEntry(TypeBuilder& typeBuilder, Index i, diff --git a/src/passes/TypeMerging.cpp b/src/passes/TypeMerging.cpp index 8ca33699716..3e5e06fee2a 100644 --- a/src/passes/TypeMerging.cpp +++ b/src/passes/TypeMerging.cpp @@ -243,13 +243,14 @@ void TypeMerging::run(Module* module_) { return; } - if (!getPassOptions().closedWorld) { + if (getPassOptions().worldMode != WorldMode::Closed) { Fatal() << "TypeMerging requires --closed-world"; } // First, find all the cast types and private types. We will need these to // determine whether types are eligible to be merged. - mergeable = ModuleUtils::getPrivateHeapTypes(*module); + mergeable = + ModuleUtils::getPrivateHeapTypes(*module, getPassOptions().worldMode); privateTypes = std::unordered_set(mergeable.begin(), mergeable.end()); auto casts = findCastTypes(); @@ -303,7 +304,8 @@ bool TypeMerging::merge(MergeKind kind) { Partitions partitions; #if TYPE_MERGING_DEBUG - auto printedPrivateTypes = ModuleUtils::getPrivateHeapTypes(*module); + auto printedPrivateTypes = + ModuleUtils::getPrivateHeapTypes(*module, getPassOptions().worldMode); using Fallback = IndexedTypeNameGenerator; Fallback printPrivate(printedPrivateTypes, "private."); ModuleTypeNameGenerator print(*module, printPrivate); @@ -640,7 +642,7 @@ void TypeMerging::applyMerges() { // We found things to optimize! Rewrite types in the module to apply those // changes. - TypeMapper(*module, replacements).map(); + TypeMapper(*module, replacements, getPassOptions().worldMode).map(); } bool shapeEq(HeapType a, HeapType b) { diff --git a/src/passes/TypeRefining.cpp b/src/passes/TypeRefining.cpp index 201360e5aca..f873151476c 100644 --- a/src/passes/TypeRefining.cpp +++ b/src/passes/TypeRefining.cpp @@ -145,7 +145,7 @@ struct TypeRefining : public Pass { return; } - if (!getPassOptions().closedWorld) { + if (getPassOptions().worldMode != WorldMode::Closed) { Fatal() << "TypeRefining requires --closed-world"; } @@ -262,7 +262,8 @@ struct TypeRefining : public Pass { bool canOptimize = false; // We cannot modify public types. - auto publicTypes = ModuleUtils::getPublicHeapTypes(*module); + auto publicTypes = + ModuleUtils::getPublicHeapTypes(*module, getPassOptions().worldMode); std::unordered_set publicTypesSet(publicTypes.begin(), publicTypes.end()); @@ -454,7 +455,8 @@ struct TypeRefining : public Pass { public: TypeRewriter(Module& wasm, TypeRefining& parent) - : GlobalTypeRewriter(wasm), parent(parent) {} + : GlobalTypeRewriter(wasm, parent.getPassOptions().worldMode), + parent(parent) {} void modifyStruct(HeapType oldStructType, Struct& struct_) override { const auto& oldFields = oldStructType.getStruct().fields; diff --git a/src/passes/Unsubtyping.cpp b/src/passes/Unsubtyping.cpp index f3165b8147c..b3acf3c0d8a 100644 --- a/src/passes/Unsubtyping.cpp +++ b/src/passes/Unsubtyping.cpp @@ -567,8 +567,7 @@ struct Unsubtyping : Pass, Noter { if (!wasm->features.hasGC()) { return; } - - if (!getPassOptions().closedWorld) { + if (getPassOptions().worldMode != WorldMode::Closed) { Fatal() << "Unsubtyping requires --closed-world"; } @@ -635,7 +634,8 @@ struct Unsubtyping : Pass, Noter { void analyzePublicTypes(Module& wasm) { // We cannot change supertypes for anything public. - for (auto type : ModuleUtils::getPublicHeapTypes(wasm)) { + for (auto type : + ModuleUtils::getPublicHeapTypes(wasm, getPassOptions().worldMode)) { if (auto super = type.getDeclaredSuperType()) { noteSubtype(type, *super); } @@ -1038,7 +1038,8 @@ struct Unsubtyping : Pass, Noter { struct Rewriter : GlobalTypeRewriter { Unsubtyping& parent; Rewriter(Unsubtyping& parent, Module& wasm) - : GlobalTypeRewriter(wasm), parent(parent) {} + : GlobalTypeRewriter(wasm, parent.getPassOptions().worldMode), + parent(parent) {} std::optional getDeclaredSuperType(HeapType type) override { if (auto super = parent.types.getSupertype(type); super && !super->isBasic()) { diff --git a/src/passes/pass.cpp b/src/passes/pass.cpp index d29a6fcebf5..7235d777ddc 100644 --- a/src/passes/pass.cpp +++ b/src/passes/pass.cpp @@ -760,7 +760,7 @@ void PassRunner::addDefaultGlobalOptimizationPrePasses() { addIfNoDWARFIssues("once-reduction"); } if (wasm->features.hasGC() && options.optimizeLevel >= 2) { - if (options.closedWorld) { + if (options.worldMode == WorldMode::Closed) { addIfNoDWARFIssues("type-refining"); addIfNoDWARFIssues("signature-pruning"); addIfNoDWARFIssues("signature-refining"); @@ -770,11 +770,11 @@ void PassRunner::addDefaultGlobalOptimizationPrePasses() { // remove ref.funcs that were once assigned to vtables but are no longer // needed, which can allow more code to be removed globally. After those, // constant field propagation can be more effective. - if (options.closedWorld) { + if (options.worldMode == WorldMode::Closed) { addIfNoDWARFIssues("gto"); } addIfNoDWARFIssues("remove-unused-module-elements"); - if (options.closedWorld) { + if (options.worldMode == WorldMode::Closed) { addIfNoDWARFIssues("remove-unused-types"); // Allow ref.tests in cfp if we are aggressively optimizing for speed. if (options.optimizeLevel >= 3) { @@ -784,7 +784,7 @@ void PassRunner::addDefaultGlobalOptimizationPrePasses() { } } addIfNoDWARFIssues("gsi"); - if (options.closedWorld) { + if (options.worldMode == WorldMode::Closed) { addIfNoDWARFIssues("abstract-type-refining"); addIfNoDWARFIssues("unsubtyping"); } diff --git a/src/tools/fuzzing.h b/src/tools/fuzzing.h index 803e13d5d0b..fa819e20772 100644 --- a/src/tools/fuzzing.h +++ b/src/tools/fuzzing.h @@ -121,10 +121,10 @@ class TranslateToFuzzReader { public: TranslateToFuzzReader(Module& wasm, std::vector&& input, - bool closedWorld = false); + WorldMode worldMode = WorldMode::Open); TranslateToFuzzReader(Module& wasm, std::string& filename, - bool closedWorld = false); + WorldMode worldMode = WorldMode::Open); void pickPasses(OptimizationOptions& options); void setAllowMemory(bool allowMemory_) { allowMemory = allowMemory_; } @@ -141,7 +141,7 @@ class TranslateToFuzzReader { private: // Whether the module will be tested in a closed-world environment. - bool closedWorld; + WorldMode worldMode; Builder builder; Random random; Intrinsics intrinsics; diff --git a/src/tools/fuzzing/fuzzing.cpp b/src/tools/fuzzing/fuzzing.cpp index 759061da88f..db8327e0a94 100644 --- a/src/tools/fuzzing/fuzzing.cpp +++ b/src/tools/fuzzing/fuzzing.cpp @@ -62,8 +62,8 @@ std::vector getMemoryOrders(const FeatureSet& features) { TranslateToFuzzReader::TranslateToFuzzReader(Module& wasm, std::vector&& input, - bool closedWorld) - : wasm(wasm), closedWorld(closedWorld), builder(wasm), + WorldMode worldMode) + : wasm(wasm), worldMode(worldMode), builder(wasm), random(std::move(input), wasm.features), intrinsics(wasm), loggableTypes(getLoggableTypes(wasm.features)), atomicMemoryOrders(getMemoryOrders(wasm.features)), @@ -123,10 +123,9 @@ TranslateToFuzzReader::TranslateToFuzzReader(Module& wasm, TranslateToFuzzReader::TranslateToFuzzReader(Module& wasm, std::string& filename, - bool closedWorld) - : TranslateToFuzzReader(wasm, - read_file>(filename, Flags::Binary), - closedWorld) {} + WorldMode worldMode) + : TranslateToFuzzReader( + wasm, read_file>(filename, Flags::Binary), worldMode) {} void TranslateToFuzzReader::pickPasses(OptimizationOptions& options) { // Pick random passes to further shape the wasm. This is similar to how we @@ -274,8 +273,8 @@ void TranslateToFuzzReader::pickPasses(OptimizationOptions& options) { // Most of these depend on closed world, so just set that. Set it both // on the global pass options, and in the internal state of this // TranslateToFuzzReader instance. - options.passOptions.closedWorld = true; - closedWorld = true; + options.passOptions.worldMode = WorldMode::Closed; + worldMode = WorldMode::Closed; switch (upTo(16)) { case 0: @@ -343,8 +342,8 @@ void TranslateToFuzzReader::pickPasses(OptimizationOptions& options) { options.passOptions.shrinkLevel = upTo(3); } - if (!options.passOptions.closedWorld && oneIn(2)) { - options.passOptions.closedWorld = true; + if (options.passOptions.worldMode != WorldMode::Closed && oneIn(2)) { + options.passOptions.worldMode = WorldMode::Closed; } // Prune things that error in JS if we call them (like SIMD), some of the @@ -1683,7 +1682,7 @@ void TranslateToFuzzReader::processFunctions() { // Also fix up closed world, if we need to. We must do this at the end, so // nothing can break the closed world assumptions after. - if (closedWorld) { + if (worldMode == WorldMode::Closed) { for (auto& func : wasm.functions) { if (!func->imported()) { fixClosedWorld(func.get()); @@ -2194,7 +2193,7 @@ void TranslateToFuzzReader::mutate(Function* func) { } void TranslateToFuzzReader::fixClosedWorld(Function* func) { - assert(closedWorld); + assert(worldMode == WorldMode::Closed); struct Fixer : public ExpressionStackWalker> { @@ -6699,7 +6698,7 @@ bool TranslateToFuzzReader::isValidRefFuncTarget(Name func) { // reference, but in that mode we must only pass in jsCalled functions. We // handle direct calls in fixClosedWorld, but cannot handle indirect ones // easily, so just disallow taking references of those functions. - if (!closedWorld) { + if (worldMode != WorldMode::Closed) { return true; } return !isCallRefImport(func); diff --git a/src/tools/tool-options.h b/src/tools/tool-options.h index 87362d455d2..1f3ec266bc7 100644 --- a/src/tools/tool-options.h +++ b/src/tools/tool-options.h @@ -173,7 +173,7 @@ struct ToolOptions : public Options { ToolOptionsCategory, Options::Arguments::Zero, [this](Options*, const std::string&) { - passOptions.closedWorld = true; + passOptions.worldMode = WorldMode::Closed; }) .add( "--preserve-type-order", diff --git a/src/tools/wasm-opt.cpp b/src/tools/wasm-opt.cpp index f593428d2b6..9bdfa4b06f7 100644 --- a/src/tools/wasm-opt.cpp +++ b/src/tools/wasm-opt.cpp @@ -353,7 +353,7 @@ For more on how to optimize effectively, see } if (translateToFuzz) { TranslateToFuzzReader reader( - wasm, options.extra["infile"], options.passOptions.closedWorld); + wasm, options.extra["infile"], options.passOptions.worldMode); reader.setAllowMemory(fuzzMemory); reader.setAllowOOB(fuzzOOB); reader.setPreserveImportsAndExports(fuzzPreserveImportsAndExports); diff --git a/src/wasm/wasm-validator.cpp b/src/wasm/wasm-validator.cpp index 64b67e51edd..a8c9f40e188 100644 --- a/src/wasm/wasm-validator.cpp +++ b/src/wasm/wasm-validator.cpp @@ -250,7 +250,7 @@ void validateExactReferences(Module& module, ValidationInfo& info) { return; } - for (auto type : ModuleUtils::getPublicHeapTypes(module)) { + for (auto type : ModuleUtils::getExposedPublicHeapTypes(module)) { for (auto child : type.getTypeChildren()) { if (child.isExact()) { std::string typeName; From cdda414d3a284a131b7af69a75f24beca1281f94 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Thu, 21 May 2026 10:56:22 -0700 Subject: [PATCH 2/5] Consistently use == WorldMode::Open instead of != WorldMode::Closed Update all occurrences of != WorldMode::Closed introduced by the world mode refactoring commit to use == WorldMode::Open instead, in response to reviewer feedback. --- src/ir/possible-contents.cpp | 4 ++-- src/passes/AbstractTypeRefining.cpp | 2 +- src/passes/ConstantFieldPropagation.cpp | 2 +- src/passes/GlobalEffects.cpp | 4 ++-- src/passes/GlobalTypeOptimization.cpp | 2 +- src/passes/J2CLItableMerging.cpp | 2 +- src/passes/RemoveUnusedModuleElements.cpp | 4 ++-- src/passes/RemoveUnusedTypes.cpp | 2 +- src/passes/ReorderTypes.cpp | 2 +- src/passes/SignaturePruning.cpp | 2 +- src/passes/TypeMerging.cpp | 2 +- src/passes/TypeRefining.cpp | 2 +- src/passes/Unsubtyping.cpp | 2 +- src/tools/fuzzing/fuzzing.cpp | 4 ++-- 14 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/ir/possible-contents.cpp b/src/ir/possible-contents.cpp index ad7026b1247..a8a01841fd6 100644 --- a/src/ir/possible-contents.cpp +++ b/src/ir/possible-contents.cpp @@ -684,7 +684,7 @@ struct InfoCollector SignatureResultLocation{func->type.getHeapType(), i}}); } - if (options.worldMode != WorldMode::Closed) { + if (options.worldMode == WorldMode::Open) { info.calledFromOutside.insert(curr->func); } } @@ -2535,7 +2535,7 @@ Flower::Flower(Module& wasm, const PassOptions& options) } // In open world, public heap types may be written to from the outside. - if (options.worldMode != WorldMode::Closed) { + if (options.worldMode == WorldMode::Open) { for (auto type : ModuleUtils::getPublicHeapTypes(wasm, options.worldMode)) { if (type.isStruct()) { auto& fields = type.getStruct().fields; diff --git a/src/passes/AbstractTypeRefining.cpp b/src/passes/AbstractTypeRefining.cpp index 4dd1264a9ca..a22488dd1c4 100644 --- a/src/passes/AbstractTypeRefining.cpp +++ b/src/passes/AbstractTypeRefining.cpp @@ -87,7 +87,7 @@ struct AbstractTypeRefining : public Pass { return; } - if (getPassOptions().worldMode != WorldMode::Closed) { + if (getPassOptions().worldMode == WorldMode::Open) { Fatal() << "AbstractTypeRefining requires --closed-world"; } diff --git a/src/passes/ConstantFieldPropagation.cpp b/src/passes/ConstantFieldPropagation.cpp index 6ae8ea1ac52..66f45bb6047 100644 --- a/src/passes/ConstantFieldPropagation.cpp +++ b/src/passes/ConstantFieldPropagation.cpp @@ -542,7 +542,7 @@ struct ConstantFieldPropagation : public Pass { return; } - if (getPassOptions().worldMode != WorldMode::Closed) { + if (getPassOptions().worldMode == WorldMode::Open) { Fatal() << "CFP requires --closed-world"; } diff --git a/src/passes/GlobalEffects.cpp b/src/passes/GlobalEffects.cpp index f12a6238c93..4cacefd09b5 100644 --- a/src/passes/GlobalEffects.cpp +++ b/src/passes/GlobalEffects.cpp @@ -103,7 +103,7 @@ std::map analyzeFuncs(Module& module, funcInfo.indirectCalledTypes.insert(type); } else if (effects.calls) { - assert(options.worldMode != WorldMode::Closed); + assert(options.worldMode == WorldMode::Open); funcInfo.effects = UnknownEffects; } else { // No call here, but update throwing if we see it. (Only do so, @@ -147,7 +147,7 @@ CallGraph buildCallGraph(const Module& module, const std::map& funcInfos, WorldMode worldMode) { CallGraph callGraph; - if (worldMode != WorldMode::Closed) { + if (worldMode == WorldMode::Open) { for (const auto& [caller, callerInfo] : funcInfos) { auto& callees = callGraph[caller]; diff --git a/src/passes/GlobalTypeOptimization.cpp b/src/passes/GlobalTypeOptimization.cpp index d8b1767c556..46eb698eaa4 100644 --- a/src/passes/GlobalTypeOptimization.cpp +++ b/src/passes/GlobalTypeOptimization.cpp @@ -153,7 +153,7 @@ struct GlobalTypeOptimization : public Pass { if (!module->features.hasGC()) { return; } - if (getPassOptions().worldMode != WorldMode::Closed) { + if (getPassOptions().worldMode == WorldMode::Open) { Fatal() << "GTO requires --closed-world"; } diff --git a/src/passes/J2CLItableMerging.cpp b/src/passes/J2CLItableMerging.cpp index 6d2188129e4..55bcfc6a917 100644 --- a/src/passes/J2CLItableMerging.cpp +++ b/src/passes/J2CLItableMerging.cpp @@ -72,7 +72,7 @@ struct J2CLItableMerging : public Pass { return; } - if (getPassOptions().worldMode != WorldMode::Closed) { + if (getPassOptions().worldMode == WorldMode::Open) { Fatal() << "--merge-j2cl-itables requires --closed-world"; } diff --git a/src/passes/RemoveUnusedModuleElements.cpp b/src/passes/RemoveUnusedModuleElements.cpp index 6de95a74b6d..864123c4d99 100644 --- a/src/passes/RemoveUnusedModuleElements.cpp +++ b/src/passes/RemoveUnusedModuleElements.cpp @@ -444,7 +444,7 @@ struct Analyzer { } void useRefFunc(Name func) { - if (options.worldMode != WorldMode::Closed) { + if (options.worldMode == WorldMode::Open) { // The world is open, so assume the worst and something (inside or outside // of the module) can call this. use({ModuleElementKind::Function, func}); @@ -610,7 +610,7 @@ struct Analyzer { // outside of the code we can see), and when it is reached (if it's // unreachable then we don't know the type, and can defer that to DCE to // remove). - if (options.worldMode != WorldMode::Closed || + if (options.worldMode == WorldMode::Open || curr->type == Type::unreachable || !curr->is()) { for (auto* child : ChildIterator(curr)) { use(child); diff --git a/src/passes/RemoveUnusedTypes.cpp b/src/passes/RemoveUnusedTypes.cpp index b7dfccb5083..3e4e2f83b88 100644 --- a/src/passes/RemoveUnusedTypes.cpp +++ b/src/passes/RemoveUnusedTypes.cpp @@ -39,7 +39,7 @@ struct RemoveUnusedTypes : Pass { // would change the identity of $A. Currently we would incorrectly remove // $unused. To fix that, we need to fix our collection of public types to // consider $A (and $unused) public in an open world. - if (getPassOptions().worldMode != WorldMode::Closed) { + if (getPassOptions().worldMode == WorldMode::Open) { Fatal() << "RemoveUnusedTypes requires --closed-world"; } diff --git a/src/passes/ReorderTypes.cpp b/src/passes/ReorderTypes.cpp index 039f709b41b..0b394890aab 100644 --- a/src/passes/ReorderTypes.cpp +++ b/src/passes/ReorderTypes.cpp @@ -142,7 +142,7 @@ struct ReorderTypes : Pass { } // See note in RemoveUnusedTypes. - if (getPassOptions().worldMode != WorldMode::Closed) { + if (getPassOptions().worldMode == WorldMode::Open) { Fatal() << "ReorderTypes requires --closed-world"; } diff --git a/src/passes/SignaturePruning.cpp b/src/passes/SignaturePruning.cpp index 7f2b2e40706..fc95a66bad8 100644 --- a/src/passes/SignaturePruning.cpp +++ b/src/passes/SignaturePruning.cpp @@ -65,7 +65,7 @@ struct SignaturePruning : public Pass { return; } - if (getPassOptions().worldMode != WorldMode::Closed) { + if (getPassOptions().worldMode == WorldMode::Open) { Fatal() << "SignaturePruning requires --closed-world"; } diff --git a/src/passes/TypeMerging.cpp b/src/passes/TypeMerging.cpp index 3e5e06fee2a..64a2df6bb7d 100644 --- a/src/passes/TypeMerging.cpp +++ b/src/passes/TypeMerging.cpp @@ -243,7 +243,7 @@ void TypeMerging::run(Module* module_) { return; } - if (getPassOptions().worldMode != WorldMode::Closed) { + if (getPassOptions().worldMode == WorldMode::Open) { Fatal() << "TypeMerging requires --closed-world"; } diff --git a/src/passes/TypeRefining.cpp b/src/passes/TypeRefining.cpp index f873151476c..720233cf9ae 100644 --- a/src/passes/TypeRefining.cpp +++ b/src/passes/TypeRefining.cpp @@ -145,7 +145,7 @@ struct TypeRefining : public Pass { return; } - if (getPassOptions().worldMode != WorldMode::Closed) { + if (getPassOptions().worldMode == WorldMode::Open) { Fatal() << "TypeRefining requires --closed-world"; } diff --git a/src/passes/Unsubtyping.cpp b/src/passes/Unsubtyping.cpp index b3acf3c0d8a..866e32ba58a 100644 --- a/src/passes/Unsubtyping.cpp +++ b/src/passes/Unsubtyping.cpp @@ -567,7 +567,7 @@ struct Unsubtyping : Pass, Noter { if (!wasm->features.hasGC()) { return; } - if (getPassOptions().worldMode != WorldMode::Closed) { + if (getPassOptions().worldMode == WorldMode::Open) { Fatal() << "Unsubtyping requires --closed-world"; } diff --git a/src/tools/fuzzing/fuzzing.cpp b/src/tools/fuzzing/fuzzing.cpp index db8327e0a94..69e97fa5aa2 100644 --- a/src/tools/fuzzing/fuzzing.cpp +++ b/src/tools/fuzzing/fuzzing.cpp @@ -342,7 +342,7 @@ void TranslateToFuzzReader::pickPasses(OptimizationOptions& options) { options.passOptions.shrinkLevel = upTo(3); } - if (options.passOptions.worldMode != WorldMode::Closed && oneIn(2)) { + if (options.passOptions.worldMode == WorldMode::Open && oneIn(2)) { options.passOptions.worldMode = WorldMode::Closed; } @@ -6698,7 +6698,7 @@ bool TranslateToFuzzReader::isValidRefFuncTarget(Name func) { // reference, but in that mode we must only pass in jsCalled functions. We // handle direct calls in fixClosedWorld, but cannot handle indirect ones // easily, so just disallow taking references of those functions. - if (worldMode != WorldMode::Closed) { + if (worldMode == WorldMode::Open) { return true; } return !isCallRefImport(func); From 248a1ea15c5a46d2808673d28308c7fe42c0e405 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Thu, 21 May 2026 13:24:50 -0700 Subject: [PATCH 3/5] Refactor getExposedPublicHeapTypes and extract getTransitivelyReachable Simplify getExposedPublicHeapTypes to only collect directly exposed public types (roots). Move transitive propagation into getPublicHeapTypes using the new getTransitivelyReachable helper. Add extensive doc comments detailing the purpose and differences of both functions. --- src/ir/module-utils.cpp | 116 ++++++++++++++++++++++++++++------------ 1 file changed, 81 insertions(+), 35 deletions(-) diff --git a/src/ir/module-utils.cpp b/src/ir/module-utils.cpp index cf00f201119..ca3f6d0a659 100644 --- a/src/ir/module-utils.cpp +++ b/src/ir/module-utils.cpp @@ -620,6 +620,64 @@ void classifyTypeVisibility(Module& wasm, } } +// Collects all heap types transitively reachable from a root set of types. +// Options are provided to customize the traversal: +// - `includeSupertypes`: if true, declared supertypes are also traversed. +// - `includeRecGroups`: if true, all types in the same recursion group +// are also traversed. +std::vector +getTransitivelyReachable(const std::vector& roots, + bool includeSupertypes, + bool includeRecGroups) { + std::vector result; + std::vector worklist; + std::unordered_set seen; + std::unordered_set seenRecGroups; + + auto note = [&](HeapType type) { + if (type.isBasic()) { + if (seen.insert(type).second) { + result.push_back(type); + } + return; + } + + if (includeRecGroups) { + auto group = type.getRecGroup(); + if (seenRecGroups.insert(group).second) { + for (auto member : group) { + result.push_back(member); + worklist.push_back(member); + } + } + } else { + if (seen.insert(type).second) { + result.push_back(type); + worklist.push_back(type); + } + } + }; + + for (auto type : roots) { + note(type); + } + + while (!worklist.empty()) { + auto curr = worklist.back(); + worklist.pop_back(); + std::optional super = + includeSupertypes ? std::nullopt : curr.getDeclaredSuperType(); + for (auto t : curr.getReferencedHeapTypes()) { + if (super && t == *super) { + continue; + } + note(t); + } + } + + return result; +} + void setIndices(IndexedHeapTypes& indexedTypes) { for (Index i = 0; i < indexedTypes.types.size(); i++) { indexedTypes.indices[indexedTypes.types[i]] = i; @@ -638,31 +696,29 @@ std::vector collectHeapTypes(Module& wasm) { return types; } +// Returns the directly "exposed" public heap types in the module. These are the +// root types appearing on the module boundary: types of exported functions, +// globals, tables, and tags, as well as imported ones. +// +// How it differs from getPublicHeapTypes: +// - getExposedPublicHeapTypes returns ONLY the explicitly declared boundary +// types (roots). It does NOT traverse the type structure recursively to find +// transitively reachable types, nor does it filter out basic types. +// - getPublicHeapTypes returns ALL public heap types in the module. Under +// closed-world mode, it transitively expands the exposed boundary types +// recursively to include all referenced types and their rec group siblings +// (filtering out basic types). Under open-world mode, it runs Unified +// Propagation to trace subtype and structural visibility. std::vector getExposedPublicHeapTypes(Module& wasm) { - // Look at the types of imports as exports to get an initial set of public - // types, then traverse the types used by public types and collect the - // transitively reachable public types as well. - std::vector workList; - std::unordered_set publicGroups; - std::unordered_set publicBasicTypes; - - // The collected types. + // Look at the types of imports and exports to get an initial set of public + // types. std::vector publicTypes; + std::unordered_set seenTypes; auto notePublic = [&](HeapType type) { - if (type.isBasic()) { - if (publicBasicTypes.insert(type).second) { - publicTypes.push_back(type); - } - return; - } - auto group = type.getRecGroup(); - if (!publicGroups.insert(group).second) { - // The groups in this type have already been marked public. - return; + if (seenTypes.insert(type).second) { + publicTypes.push_back(type); } - publicTypes.insert(publicTypes.end(), group.begin(), group.end()); - workList.insert(workList.end(), group.begin(), group.end()); }; ModuleUtils::iterImportedTags(wasm, [&](Tag* tag) { notePublic(tag->type); }); @@ -719,26 +775,16 @@ std::vector getExposedPublicHeapTypes(Module& wasm) { notePublic(type); } - // Find all the other public types reachable from directly publicized types. - while (!workList.empty()) { - auto curr = workList.back(); - workList.pop_back(); - for (auto t : curr.getReferencedHeapTypes()) { - notePublic(t); - } - } - - // TODO: In an open world, we need to consider subtypes of public types public - // as well, or potentially even consider all types to be public unless - // otherwise annotated. return publicTypes; } std::vector getPublicHeapTypes(Module& wasm, WorldMode worldMode) { - auto exposed = getExposedPublicHeapTypes(wasm); + auto directlyExposed = getExposedPublicHeapTypes(wasm); + auto transitivelyExposed = getTransitivelyReachable( + directlyExposed, /*includeSupertypes=*/true, /*includeRecGroups=*/true); std::vector publicTypes; - publicTypes.reserve(exposed.size()); - for (auto type : exposed) { + publicTypes.reserve(transitivelyExposed.size()); + for (auto type : transitivelyExposed) { if (!type.isBasic()) { publicTypes.push_back(type); } From 76206c1307c305513e2b26e06fc4f08e6bce138a Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Fri, 22 May 2026 15:54:05 -0700 Subject: [PATCH 4/5] rewrite comments --- src/ir/module-utils.cpp | 13 ------------- src/ir/module-utils.h | 10 +++++++--- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/src/ir/module-utils.cpp b/src/ir/module-utils.cpp index ca3f6d0a659..4d4a565a7ce 100644 --- a/src/ir/module-utils.cpp +++ b/src/ir/module-utils.cpp @@ -696,19 +696,6 @@ std::vector collectHeapTypes(Module& wasm) { return types; } -// Returns the directly "exposed" public heap types in the module. These are the -// root types appearing on the module boundary: types of exported functions, -// globals, tables, and tags, as well as imported ones. -// -// How it differs from getPublicHeapTypes: -// - getExposedPublicHeapTypes returns ONLY the explicitly declared boundary -// types (roots). It does NOT traverse the type structure recursively to find -// transitively reachable types, nor does it filter out basic types. -// - getPublicHeapTypes returns ALL public heap types in the module. Under -// closed-world mode, it transitively expands the exposed boundary types -// recursively to include all referenced types and their rec group siblings -// (filtering out basic types). Under open-world mode, it runs Unified -// Propagation to trace subtype and structural visibility. std::vector getExposedPublicHeapTypes(Module& wasm) { // Look at the types of imports and exports to get an initial set of public // types. diff --git a/src/ir/module-utils.h b/src/ir/module-utils.h index 2bb8dd914d7..09b7900d3c9 100644 --- a/src/ir/module-utils.h +++ b/src/ir/module-utils.h @@ -480,13 +480,17 @@ InsertOrderedMap collectHeapTypeInfo( // module, i.e. the types that would appear in the type section. std::vector collectHeapTypes(Module& wasm); +// Get the types directly made public by imported or exported module items. For +// example, the types of imported or exported globals or functions, but not +// other types reachable from those types. Includes abstract heap types. std::vector getExposedPublicHeapTypes(Module& wasm); -// Collect all the heap types visible on the module boundary that cannot be -// changed. +// Collect all the defined heap types visible on the module boundary that cannot +// be changed, e.g. the defined types from getExposedPublicHeapTypes and those +// they reach. std::vector getPublicHeapTypes(Module& wasm, WorldMode worldMode); -// getHeapTypes - getPublicHeapTypes +// All the defined heap types that are not public. std::vector getPrivateHeapTypes(Module& wasm, WorldMode worldMode); struct IndexedHeapTypes { From 3feeeec22194e14f3eff2b9653403962c195c884 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Fri, 22 May 2026 15:59:57 -0700 Subject: [PATCH 5/5] add note about external creation --- src/pass.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pass.h b/src/pass.h index 7d7dc0f1afc..ab2482d5b87 100644 --- a/src/pass.h +++ b/src/pass.h @@ -113,8 +113,8 @@ struct InliningOptions { // Assume code outside of the module does not inspect or interact with GC and // function references, with the goal of being able to aggressively optimize // all user-defined types. The outside may hold on to references and pass them -// back in, but may not inspect their contents, call them, or reflect on their -// types in any way. +// back in, but may not create them, inspect their contents, call them, or +// reflect on their types in any way. // // By default we do not make this assumption, and assume anything that escapes // to the outside may be inspected in detail, which prevents us from e.g.