From ea2f4351d06814aaf5976c650ae98da6e97c6696 Mon Sep 17 00:00:00 2001 From: stevenfontanella Date: Wed, 10 Dec 2025 22:16:31 +0000 Subject: [PATCH 1/9] Stub out all imports in ctor-eval --- src/tools/wasm-ctor-eval.cpp | 139 ++++++++++++++---------- test/ctor-eval/global-get-init.wast | 6 +- test/ctor-eval/global-get-init.wast.out | 5 - 3 files changed, 80 insertions(+), 70 deletions(-) diff --git a/src/tools/wasm-ctor-eval.cpp b/src/tools/wasm-ctor-eval.cpp index b33fb5aa999..2f4aab6d476 100644 --- a/src/tools/wasm-ctor-eval.cpp +++ b/src/tools/wasm-ctor-eval.cpp @@ -115,63 +115,78 @@ class EvallingModuleRunner : public ModuleRunnerBase { } }; -// Build an artificial `env` module based on a module's imports, so that the +// Build artificial modules based on a module's imports, so that the // interpreter can use correct object instances. It initializes usable global // imports, and fills the rest with fake values since those are dangerous to -// use. we will fail if dangerous globals are used. -std::unique_ptr buildEnvModule(Module& wasm) { - auto env = std::make_unique(); - env->name = "env"; - - // create empty functions with similar signature - ModuleUtils::iterImportedFunctions(wasm, [&](Function* func) { - if (func->module == env->name) { - Builder builder(*env); - auto* copied = ModuleUtils::copyFunction(func, *env); - copied->module = Name(); - copied->base = Name(); - copied->body = builder.makeUnreachable(); - env->addExport( - builder.makeExport(func->base, copied->name, ExternalKind::Function)); - } - }); - - // create tables with similar initial and max values - ModuleUtils::iterImportedTables(wasm, [&](Table* table) { - if (table->module == env->name) { - auto* copied = ModuleUtils::copyTable(table, *env); - copied->module = Name(); - copied->base = Name(); - env->addExport(Builder(*env).makeExport( - table->base, copied->name, ExternalKind::Table)); - } - }); - - ModuleUtils::iterImportedGlobals(wasm, [&](Global* global) { - if (global->module == env->name) { - auto* copied = ModuleUtils::copyGlobal(global, *env); - copied->module = Name(); - copied->base = Name(); - - Builder builder(*env); - copied->init = builder.makeConst(Literal::makeZero(global->type)); - env->addExport( - builder.makeExport(global->base, copied->name, ExternalKind::Global)); - } - }); - - // create an exported memory with the same initial and max size - ModuleUtils::iterImportedMemories(wasm, [&](Memory* memory) { - if (memory->module == env->name) { - auto* copied = ModuleUtils::copyMemory(memory, *env); - copied->module = Name(); - copied->base = Name(); - env->addExport(Builder(*env).makeExport( - memory->base, copied->name, ExternalKind::Memory)); - } - }); +// use. We will fail if dangerous globals are used. +std::vector> buildStubModules(Module& wasm) { + std::map> modules; + + ModuleUtils::iterImports( + wasm, + [&modules](std::variant import) { + Importable* importable = + std::visit([](auto* i) -> Importable* { return i; }, import); + + Module* module = nullptr; + if (auto it = modules.find(importable->module); it != modules.end()) { + module = it->second.get(); + } else { + auto [inserted, _] = + modules.emplace(importable->module, std::make_unique()); + module = inserted->second.get(); + module->name = importable->module; + } - return env; + struct Visitor { + Module* module; + void operator()(Memory* memory) { + auto* copied = ModuleUtils::copyMemory(memory, *module); + copied->module = Name(); + copied->base = Name(); + module->addExport(Builder(*module).makeExport( + memory->base, copied->name, ExternalKind::Memory)); + } + void operator()(Table* table) { + // create tables with similar initial and max values + auto* copied = ModuleUtils::copyTable(table, *module); + copied->module = Name(); + copied->base = Name(); + module->addExport(Builder(*module).makeExport( + table->base, copied->name, ExternalKind::Table)); + } + void operator()(Global* global) { + auto* copied = ModuleUtils::copyGlobal(global, *module); + copied->module = Name(); + copied->base = Name(); + + Builder builder(*module); + copied->init = builder.makeConst(Literal::makeZero(global->type)); + module->addExport(builder.makeExport( + global->base, copied->name, ExternalKind::Global)); + } + void operator()(Function* func) { + Builder builder(*module); + auto* copied = ModuleUtils::copyFunction(func, *module); + copied->module = Name(); + copied->base = Name(); + copied->body = builder.makeUnreachable(); + module->addExport(builder.makeExport( + func->base, copied->name, ExternalKind::Function)); + } + void operator()(Tag* tag) { + // no-op + } + }; + std::visit(Visitor{module}, import); + }); + + std::vector> modulesVector; + modulesVector.reserve(modules.size()); + for (auto& [_, ptr] : modules) { + modulesVector.push_back(std::move(ptr)); + } + return modulesVector; } // Whether to ignore external input to the program as it runs. If set, we will @@ -1356,12 +1371,16 @@ void evalCtors(Module& wasm, std::map> linkedInstances; - // build and link the env module - auto envModule = buildEnvModule(wasm); - CtorEvalExternalInterface envInterface; - auto envInstance = - std::make_shared(*envModule, &envInterface); - linkedInstances[envModule->name] = envInstance; + // stubModules and interfaces must be kept alive since they are referenced in + // linkedInstances. + std::vector> stubModules = buildStubModules(wasm); + std::vector> interfaces; + + for (auto& module : stubModules) { + interfaces.push_back(std::make_unique()); + linkedInstances[module->name] = + std::make_shared(*module, interfaces.back().get()); + } CtorEvalExternalInterface interface(linkedInstances); try { diff --git a/test/ctor-eval/global-get-init.wast b/test/ctor-eval/global-get-init.wast index 125e672d6c5..95fcadaff59 100644 --- a/test/ctor-eval/global-get-init.wast +++ b/test/ctor-eval/global-get-init.wast @@ -1,8 +1,4 @@ (module (import "import" "global" (global $imported i32)) - (func $test1 (export "test1") - ;; This should be safe to eval in theory, but the imported global stops us, - ;; so this function will not be optimized out. - ;; TODO: perhaps if we never use that global that is ok? - ) + (func $test1 (export "test1")) ) diff --git a/test/ctor-eval/global-get-init.wast.out b/test/ctor-eval/global-get-init.wast.out index 519e96dbabd..4427f36e041 100644 --- a/test/ctor-eval/global-get-init.wast.out +++ b/test/ctor-eval/global-get-init.wast.out @@ -1,7 +1,2 @@ (module - (type $0 (func)) - (export "test1" (func $test1)) - (func $test1 (type $0) - (nop) - ) ) From e1cef5b4575247c254c2dd4a076f154ea7c6c0eb Mon Sep 17 00:00:00 2001 From: stevenfontanella Date: Fri, 12 Dec 2025 00:40:08 +0000 Subject: [PATCH 2/9] Update comment to make it clear how stubbed imported globals work --- src/tools/wasm-ctor-eval.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/tools/wasm-ctor-eval.cpp b/src/tools/wasm-ctor-eval.cpp index 2f4aab6d476..c3d6baa3b4d 100644 --- a/src/tools/wasm-ctor-eval.cpp +++ b/src/tools/wasm-ctor-eval.cpp @@ -118,7 +118,8 @@ class EvallingModuleRunner : public ModuleRunnerBase { // Build artificial modules based on a module's imports, so that the // interpreter can use correct object instances. It initializes usable global // imports, and fills the rest with fake values since those are dangerous to -// use. We will fail if dangerous globals are used. +// use. Imported globals can't be read anyway; see +// `EvallingModuleRunner::visitGlobalGet`. std::vector> buildStubModules(Module& wasm) { std::map> modules; From 8c8b209c114e6d745b4e09a7b23f93f34d2c56d8 Mon Sep 17 00:00:00 2001 From: stevenfontanella Date: Fri, 12 Dec 2025 01:03:23 +0000 Subject: [PATCH 3/9] Remove an extra map lookup --- src/tools/wasm-ctor-eval.cpp | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/tools/wasm-ctor-eval.cpp b/src/tools/wasm-ctor-eval.cpp index c3d6baa3b4d..6661704fd9b 100644 --- a/src/tools/wasm-ctor-eval.cpp +++ b/src/tools/wasm-ctor-eval.cpp @@ -129,15 +129,12 @@ std::vector> buildStubModules(Module& wasm) { Importable* importable = std::visit([](auto* i) -> Importable* { return i; }, import); - Module* module = nullptr; - if (auto it = modules.find(importable->module); it != modules.end()) { - module = it->second.get(); - } else { - auto [inserted, _] = - modules.emplace(importable->module, std::make_unique()); - module = inserted->second.get(); - module->name = importable->module; + auto [it, inserted] = modules.try_emplace(importable->module, nullptr); + if (inserted) { + it->second = std::make_unique(); + it->second->name = importable->module; } + Module* module = it->second.get(); struct Visitor { Module* module; From b81af3538cab5a9d5806fe82d7b9fe1f3888372a Mon Sep 17 00:00:00 2001 From: stevenfontanella Date: Fri, 12 Dec 2025 01:10:55 +0000 Subject: [PATCH 4/9] Don't generate stubs for WASI functions --- src/tools/wasm-ctor-eval.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/tools/wasm-ctor-eval.cpp b/src/tools/wasm-ctor-eval.cpp index 6661704fd9b..0c14bbe0b61 100644 --- a/src/tools/wasm-ctor-eval.cpp +++ b/src/tools/wasm-ctor-eval.cpp @@ -129,6 +129,13 @@ std::vector> buildStubModules(Module& wasm) { Importable* importable = std::visit([](auto* i) -> Importable* { return i; }, import); + // Ignore WASI functions. They are handled separately in + // `getImportedFunction`. + if (std::holds_alternative(import) && + importable->module.startsWith("wasi_")) { + return; + } + auto [it, inserted] = modules.try_emplace(importable->module, nullptr); if (inserted) { it->second = std::make_unique(); From 759b45ca9aa90772a9ef7e68842a2512fbb5e263 Mon Sep 17 00:00:00 2001 From: stevenfontanella Date: Fri, 12 Dec 2025 21:18:30 +0000 Subject: [PATCH 5/9] Generate stubs for WASI functions. They are still needed --- src/tools/wasm-ctor-eval.cpp | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/tools/wasm-ctor-eval.cpp b/src/tools/wasm-ctor-eval.cpp index 0c14bbe0b61..7173ec0fb64 100644 --- a/src/tools/wasm-ctor-eval.cpp +++ b/src/tools/wasm-ctor-eval.cpp @@ -120,6 +120,9 @@ class EvallingModuleRunner : public ModuleRunnerBase { // imports, and fills the rest with fake values since those are dangerous to // use. Imported globals can't be read anyway; see // `EvallingModuleRunner::visitGlobalGet`. +// Note: wasi_ modules have stubs generated but won't be called due to the +// special handling in `CtorEvalExternalInterface::getImportedFunction`. We +// still generate the stubs to ensure the link-time validation passes. std::vector> buildStubModules(Module& wasm) { std::map> modules; @@ -129,13 +132,6 @@ std::vector> buildStubModules(Module& wasm) { Importable* importable = std::visit([](auto* i) -> Importable* { return i; }, import); - // Ignore WASI functions. They are handled separately in - // `getImportedFunction`. - if (std::holds_alternative(import) && - importable->module.startsWith("wasi_")) { - return; - } - auto [it, inserted] = modules.try_emplace(importable->module, nullptr); if (inserted) { it->second = std::make_unique(); From 08cdd5888d5aabd9fa6848ecc2a3253820ced96c Mon Sep 17 00:00:00 2001 From: stevenfontanella Date: Tue, 9 Dec 2025 21:40:04 +0000 Subject: [PATCH 6/9] Check that module imports match their exports --- scripts/test/shared.py | 15 ++-- src/shell-interface.h | 10 ++- src/tools/wasm-shell.cpp | 2 + src/wasm-interpreter.h | 107 ++++++++++++++++++++++++++--- src/wasm.h | 31 +++++++++ test/lit/exec/exported-import.wast | 2 +- test/reduce/imports.wast.txt | 7 +- test/spec/exact-func-import.wast | 40 +++++------ test/spec/ref_func.wast | 3 + test/spec/tags.wast | 6 ++ 10 files changed, 175 insertions(+), 48 deletions(-) diff --git a/scripts/test/shared.py b/scripts/test/shared.py index 30c3593d6d2..1287ed1afb8 100644 --- a/scripts/test/shared.py +++ b/scripts/test/shared.py @@ -440,15 +440,14 @@ def get_tests(test_dir, extensions=[], recursive=False): 'func.wast', # Duplicate parameter names not properly rejected 'global.wast', # Fail to parse table 'if.wast', # Requires more precise unreachable validation - 'imports.wast', # Missing validation of missing function on instantiation + 'imports.wast', # Requires fixing handling of mutation to imported globals 'proposals/threads/imports.wast', # Missing memory type validation on instantiation 'linking.wast', # Missing function type validation on instantiation 'proposals/threads/memory.wast', # Missing memory type validation on instantiation - 'memory64-imports.wast', # Missing validation on instantiation 'annotations.wast', # String annotations IDs should be allowed 'id.wast', # Empty IDs should be disallowed - # Requires correct handling of tag imports from different instances of the same module, - # ref.null wast constants, and splitting for module instances + # Requires correct handling of tag imports from different instances of the same module + # and splitting for module instances 'instance.wast', 'table64.wast', # Requires validations for table size 'table_grow.wast', # Incorrect table linking semantics in interpreter @@ -473,12 +472,8 @@ def get_tests(test_dir, extensions=[], recursive=False): 'type-rec.wast', # Missing function type validation on instantiation 'type-subtyping.wast', # ShellExternalInterface::callTable does not handle subtyping 'call_indirect.wast', # Bug with 64-bit inline element segment parsing - 'memory64.wast', # Requires validations for memory size - 'imports0.wast', # Missing memory type validation on instantiation - 'imports2.wast', # Missing memory type validation on instantiation - 'imports3.wast', # Missing memory type validation on instantiation - 'linking0.wast', # Missing memory type validation on instantiation - 'linking3.wast', # Fatal error on missing table. + 'memory64.wast', # Requires validations on the max memory size + 'imports3.wast', # Requires better checking of exports from the special "spectest" module 'i16x8_relaxed_q15mulr_s.wast', # Requires wast `either` support 'i32x4_relaxed_trunc.wast', # Requires wast `either` support 'i8x16_relaxed_swizzle.wast', # Requires wast `either` support diff --git a/src/shell-interface.h b/src/shell-interface.h index 7a42617e711..96a65ed1edf 100644 --- a/src/shell-interface.h +++ b/src/shell-interface.h @@ -134,8 +134,10 @@ struct ShellExternalInterface : ModuleRunner::ExternalInterface { auto inst = getImportInstance(import); auto* exportedGlobal = inst->wasm.getExportOrNull(import->base); if (!exportedGlobal || exportedGlobal->kind != ExternalKind::Global) { - Fatal() << "importGlobals: unknown import: " << import->module.str - << "." << import->name.str; + trap((std::stringstream() + << "importGlobals: unknown import: " << import->module.str << "." + << import->name.str) + .str()); } globals[import->name] = inst->globals[*exportedGlobal->getInternalName()]; }); @@ -325,7 +327,9 @@ struct ShellExternalInterface : ModuleRunner::ExternalInterface { return true; } - void trap(const char* why) override { + void trap(const char* why) override { trap(std::string_view(why)); } + + void trap(std::string_view why) override { std::cout << "[trap " << why << "]\n"; throw TrapException(); } diff --git a/src/tools/wasm-shell.cpp b/src/tools/wasm-shell.cpp index 154c045f249..810f66a1520 100644 --- a/src/tools/wasm-shell.cpp +++ b/src/tools/wasm-shell.cpp @@ -176,6 +176,8 @@ struct Shell { // SIMD instructions. instance->setRelaxedBehavior(ModuleRunner::RelaxedBehavior::Execute); instance->instantiate(); + } catch (const std::exception& e) { + return Err{std::string("failed to instantiate module: ") + e.what()}; } catch (...) { return Err{"failed to instantiate module"}; } diff --git a/src/wasm-interpreter.h b/src/wasm-interpreter.h index f6fc5586a94..59bef91564f 100644 --- a/src/wasm-interpreter.h +++ b/src/wasm-interpreter.h @@ -29,6 +29,7 @@ #define wasm_wasm_interpreter_h #include +#include #include #include #include @@ -86,7 +87,7 @@ class Flow { } Literals values; - Name breakTo; // if non-null, a break is going on + Name breakTo; // if non-null, a break is going on Tag* suspendTag = nullptr; // if non-null, breakTo must be SUSPEND_FLOW, and // this is the tag being suspended @@ -2658,6 +2659,8 @@ class ExpressionRunner : public OverriddenVisitor { virtual void trap(const char* why) { WASM_UNREACHABLE("unimp"); } + virtual void trap(std::string_view why) { WASM_UNREACHABLE("unimp"); } + virtual void hostLimit(const char* why) { WASM_UNREACHABLE("unimp"); } virtual void throwException(const WasmException& exn) { @@ -2937,6 +2940,7 @@ class ModuleRunnerBase : public ExpressionRunner { Index oldSize, Index newSize) = 0; virtual void trap(const char* why) = 0; + virtual void trap(std::string_view why) { trap(std::string(why).c_str()); } virtual void hostLimit(const char* why) = 0; virtual void throwException(const WasmException& exn) = 0; // Get the Tag instance for a tag implemented in the host, that is, not @@ -3178,6 +3182,8 @@ class ModuleRunnerBase : public ExpressionRunner { // initialize the rest of the external interface externalInterface->init(wasm, *self()); + validateImports(); + initializeTableContents(); initializeMemoryContents(); @@ -3269,6 +3275,78 @@ class ModuleRunnerBase : public ExpressionRunner { Name name; }; + // Trap if the importable's export's kind doesn't match `kind`. + void validateImportKindMatches(ExternalKind kind, + const Importable& importable) { + auto it = linkedInstances.find(importable.module); + if (it == linkedInstances.end()) { + trap((std::stringstream() + << "Import module " << std::quoted(importable.module.toString()) + << " doesn't exist.") + .str()); + } + SubType* importedInstance = it->second.get(); + + Export* export_ = importedInstance->wasm.getExportOrNull(importable.base); + + if (!export_) { + trap((std::stringstream() + << "Export " << importable.base << " doesn't exist.") + .str()); + } + if (export_->kind != kind) { + trap("Exported kind doesn't match"); + } + } + + // Trap if types don't match between all imports and their corresponding + // exports. Imported memories and tables must also be a subtype of their + // export. + void validateImports() { + ModuleUtils::iterImportable( + wasm, + [this](ExternalKind kind, + std::variant import) { + Importable* importable = std::visit( + [](const auto& import) -> Importable* { return import; }, import); + + // These two modules are injected implicitly to tests. We won't find any + // import information for them. + if (importable->module == "binaryen-intrinsics" || + (importable->module == "spectest" && + importable->base.startsWith("print")) || + importable->module == "fuzzing-support") { + return; + } + + validateImportKindMatches(kind, *importable); + + SubType* importedInstance = + linkedInstances.at(importable->module).get(); + Export* export_ = + importedInstance->wasm.getExportOrNull(importable->base); + + if (auto** memory = std::get_if(&import)) { + Memory exportedMemory = + *importedInstance->wasm.getMemory(*export_->getInternalName()); + exportedMemory.initial = + importedInstance->getMemorySize(*export_->getInternalName()); + + if (!exportedMemory.isSubType(**memory)) { + trap("Imported memory isn't compatible."); + } + } + + if (auto** table = std::get_if(&import)) { + Table* exportedTable = + importedInstance->wasm.getTable(*export_->getInternalName()); + if (!(*table)->isSubType(*exportedTable)) { + trap("Imported table isn't compatible"); + } + } + }); + } + TableInstanceInfo getTableInstanceInfo(Name name) { auto* table = wasm.getTable(name); if (table->imported()) { @@ -3326,12 +3404,16 @@ class ModuleRunnerBase : public ExpressionRunner { }; MemoryInstanceInfo getMemoryInstanceInfo(Name name) { - auto* memory = wasm.getMemory(name); - if (memory->imported()) { - auto& importedInstance = linkedInstances.at(memory->module); - auto* memoryExport = importedInstance->wasm.getExport(memory->base); - return importedInstance->getMemoryInstanceInfo( - *memoryExport->getInternalName()); + auto* instance = self(); + Export* memoryExport = nullptr; + for (auto* memory = instance->wasm.getMemory(name); memory->imported(); + memory = instance->wasm.getMemory(*memoryExport->getInternalName())) { + instance = instance->linkedInstances.at(memory->module).get(); + memoryExport = instance->wasm.getExport(memory->base); + } + + if (memoryExport) { + return instance->getMemoryInstanceInfo(*memoryExport->getInternalName()); } return MemoryInstanceInfo{self(), name}; @@ -3385,6 +3467,11 @@ class ModuleRunnerBase : public ExpressionRunner { } Address getMemorySize(Name memory) { + auto info = getMemoryInstanceInfo(memory); + if (info.instance != self()) { + return info.instance->getMemorySize(info.name); + } + auto iter = memorySizes.find(memory); if (iter == memorySizes.end()) { externalInterface->trap("getMemorySize called on non-existing memory"); @@ -3397,7 +3484,7 @@ class ModuleRunnerBase : public ExpressionRunner { if (iter == memorySizes.end()) { externalInterface->trap("setMemorySize called on non-existing memory"); } - memorySizes[memory] = size; + iter->second = size; } public: @@ -4682,12 +4769,14 @@ class ModuleRunnerBase : public ExpressionRunner { } Flow visitStackSwitch(StackSwitch* curr) { return Flow(NONCONSTANT_FLOW); } - void trap(const char* why) override { + void trap(std::string_view why) override { // Traps break all current continuations - they will never be resumable. self()->clearContinuationStore(); externalInterface->trap(why); } + void trap(const char* why) override { trap(std::string_view(why)); } + void hostLimit(const char* why) override { self()->clearContinuationStore(); externalInterface->hostLimit(why); diff --git a/src/wasm.h b/src/wasm.h index 605c7025395..91ed3b3c747 100644 --- a/src/wasm.h +++ b/src/wasm.h @@ -2435,6 +2435,28 @@ class Table : public Importable { initial = 0; max = kMaxSize; } + + static bool isSubType(const Table& a, const Table& b) { + if (a.addressType != b.addressType) { + return false; + } + + if (!Type::isSubType(a.type, b.type)) { + return false; + } + + if (a.initial > b.initial) { + return false; + } + + if (a.max < b.max) { + return false; + } + + return true; + } + + bool isSubType(const Table& other) { return Table::isSubType(*this, other); } }; class DataSegment : public Named { @@ -2470,6 +2492,15 @@ class Memory : public Importable { shared = false; addressType = Type::i32; } + + static bool isSubType(const Memory& a, const Memory& b) { + return a.shared == b.shared && a.addressType == b.addressType && + a.initial >= b.initial && a.max <= b.max; + } + + bool isSubType(const Memory& other) { + return Memory::isSubType(*this, other); + } }; class Global : public Importable { diff --git a/test/lit/exec/exported-import.wast b/test/lit/exec/exported-import.wast index 5264e9e00cb..c0b66d49af4 100644 --- a/test/lit/exec/exported-import.wast +++ b/test/lit/exec/exported-import.wast @@ -4,7 +4,7 @@ ;; no other output is expected (as the import does nothing and is ignored). (module - (import "spectest" "nothing" (func $import)) + (import "spectest" "print" (func $import)) (export "__wasm_call_ctors" (func $import)) ) diff --git a/test/reduce/imports.wast.txt b/test/reduce/imports.wast.txt index f7e68fa636a..d61e8c12e04 100644 --- a/test/reduce/imports.wast.txt +++ b/test/reduce/imports.wast.txt @@ -1,8 +1,5 @@ (module - (type $0 (func (result i32))) - (export "x" (func $0)) - (func $0 (result i32) - (i32.const 5678) - ) + (type $0 (func)) + (import "env" "func" (func $fimport$0)) ) diff --git a/test/spec/exact-func-import.wast b/test/spec/exact-func-import.wast index 49330fc7b84..f5d5b93332f 100644 --- a/test/spec/exact-func-import.wast +++ b/test/spec/exact-func-import.wast @@ -1,29 +1,27 @@ ;; TODO: Use the upstream version from the custom descriptors proposal. -(module +(module definition (type $f (func)) (import "" "" (func $1 (exact (type 0)))) - (import "" "" (func $2 (exact (type $f) (param) (result)))) - (import "" "" (func $3 (exact (type $f)))) - (import "" "" (func $4 (exact (type 1)))) ;; Implicitly defined next - (import "" "" (func $5 (exact (param i32) (result i64)))) + ;; (import "" "" (func $2 (exact (type $f) (param) (result)))) ;; TODO: parser support + (import "" "" (func $2 (exact (type $f)))) + (import "" "" (func $3 (exact (type 1)))) ;; Implicitly defined next + (import "" "" (func $4 (exact (param i32) (result i64)))) - (func $6 (import "" "") (exact (type 0))) - (func $7 (import "" "") (exact (type $f) (param) (result))) - (func $8 (import "" "") (exact (type $f))) - (func $9 (import "" "") (exact (type 2))) ;; Implicitly defined next - (func $10 (import "" "") (exact (param i64) (result i32))) + (func $5 (import "" "") (exact (type 0))) + ;; (func $6 (import "" "") (exact (type $f) (param) (result))) ;; TODO: parser support + (func $6 (import "" "") (exact (type $f))) + ;; (func $7 (import "" "") (exact (type 2))) ;; Implicitly defined next + ;; (func $8 (import "" "") (exact (param i64) (result i32))) ;; TODO: parser support (global (ref (exact $f)) (ref.func $1)) (global (ref (exact $f)) (ref.func $2)) - (global (ref (exact $f)) (ref.func $3)) + (global (ref (exact 1)) (ref.func $3)) (global (ref (exact 1)) (ref.func $4)) - (global (ref (exact 1)) (ref.func $5)) + (global (ref (exact $f)) (ref.func $5)) (global (ref (exact $f)) (ref.func $6)) - (global (ref (exact $f)) (ref.func $7)) - (global (ref (exact $f)) (ref.func $8)) - (global (ref (exact 2)) (ref.func $9)) - (global (ref (exact 2)) (ref.func $10)) + ;; (global (ref (exact 2)) (ref.func $7)) + ;; (global (ref (exact 2)) (ref.func $8)) ) ;; References to inexact imports are not exact. @@ -51,7 +49,7 @@ ;; Inexact imports can still be referenced inexactly, though. -(module +(module definition (type $f (func)) (import "" "" (func $1 (type $f))) (global (ref $f) (ref.func $1)) @@ -70,7 +68,9 @@ ;; Import and re-export inexactly. (module $B (type $f (func)) - (func (export "f") (import "A" "f") (type $f)) + ;; (func (import "A" "f") (export "f") (type $f)) + (func (import "A" "f") (type $f)) + (export "f" (func 0)) ) (register "B") @@ -220,7 +220,7 @@ ;; Test the binary format ;; Exact function imports use 0x20. -(module binary +(module definition binary "\00asm" "\01\00\00\00" "\01" ;; Type section id "\04" ;; Type section length @@ -265,4 +265,4 @@ "\0b" ;; End ) "malformed export kind" -) +) \ No newline at end of file diff --git a/test/spec/ref_func.wast b/test/spec/ref_func.wast index f68e6166de2..60bf8df4a05 100644 --- a/test/spec/ref_func.wast +++ b/test/spec/ref_func.wast @@ -1,6 +1,9 @@ +;; TODO: use test/spec/testsuite/ref_func.wast once it's fixed. + (module (func (export "f") (param $x i32) (result i32) (local.get $x)) ) +(register "M") (module (func $f (import "M" "f") (param i32) (result i32)) (func $g (param $x i32) (result i32) (i32.add (local.get $x) (i32.const 1))) diff --git a/test/spec/tags.wast b/test/spec/tags.wast index 3b259f417c7..3706ef08473 100644 --- a/test/spec/tags.wast +++ b/test/spec/tags.wast @@ -1,5 +1,11 @@ ;; Test tags +(module + (tag (export "im0") (param i32)) + (tag (export "im1") (param i32 f32)) +) +(register "env") + (module (tag $e-import (import "env" "im0") (param i32)) (import "env" "im1" (tag (param i32 f32))) From 6b241a7f5cd83efd833b5d347f47f393f624e0e2 Mon Sep 17 00:00:00 2001 From: stevenfontanella Date: Mon, 15 Dec 2025 18:58:12 +0000 Subject: [PATCH 7/9] Reword comment and improve the error message for missing exports --- src/wasm-interpreter.h | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/wasm-interpreter.h b/src/wasm-interpreter.h index e716402cb9e..e0984bf5869 100644 --- a/src/wasm-interpreter.h +++ b/src/wasm-interpreter.h @@ -3308,7 +3308,8 @@ class ModuleRunnerBase : public ExpressionRunner { Name name; }; - // Trap if the importable's export's kind doesn't match `kind`. + // Validates that the export that provides `importable` exists and has the + // same kind that the import expects (`kind`). void validateImportKindMatches(ExternalKind kind, const Importable& importable) { auto it = linkedInstances.find(importable.module); @@ -3328,7 +3329,9 @@ class ModuleRunnerBase : public ExpressionRunner { .str()); } if (export_->kind != kind) { - trap("Exported kind doesn't match"); + trap((std::stringstream() << "Exported kind: " << export_->kind + << " doesn't match expected kind: " << kind) + .str()); } } From e8e6c43e310deed2c8812728680822f7b6b90740 Mon Sep 17 00:00:00 2001 From: stevenfontanella Date: Mon, 15 Dec 2025 21:16:58 +0000 Subject: [PATCH 8/9] Move subtyping utils --- src/ir/memory-utils.cpp | 5 +++++ src/ir/memory-utils.h | 2 ++ src/ir/table-utils.cpp | 20 ++++++++++++++++++++ src/ir/table-utils.h | 2 ++ src/wasm-interpreter.h | 6 ++++-- src/wasm.h | 31 ------------------------------- 6 files changed, 33 insertions(+), 33 deletions(-) diff --git a/src/ir/memory-utils.cpp b/src/ir/memory-utils.cpp index 70f316028a9..5d377dc4bda 100644 --- a/src/ir/memory-utils.cpp +++ b/src/ir/memory-utils.cpp @@ -20,6 +20,11 @@ namespace wasm::MemoryUtils { +bool isSubType(const Memory& a, const Memory& b) { + return a.shared == b.shared && a.addressType == b.addressType && + a.initial >= b.initial && a.max <= b.max; +} + bool flatten(Module& wasm) { // If there are no memories then they are already flat, in the empty sense. if (wasm.memories.empty()) { diff --git a/src/ir/memory-utils.h b/src/ir/memory-utils.h index fda255caf3a..2929d17ca9f 100644 --- a/src/ir/memory-utils.h +++ b/src/ir/memory-utils.h @@ -28,6 +28,8 @@ namespace wasm::MemoryUtils { +bool isSubType(const Memory& a, const Memory& b); + // Flattens memory into a single data segment, or no segment. If there is // a segment, it starts at 0. // Returns true if successful (e.g. relocatable segments cannot be flattened). diff --git a/src/ir/table-utils.cpp b/src/ir/table-utils.cpp index 124eb2aa3e3..45d9fcebed8 100644 --- a/src/ir/table-utils.cpp +++ b/src/ir/table-utils.cpp @@ -21,6 +21,26 @@ namespace wasm::TableUtils { +bool isSubType(const Table& a, const Table& b) { + if (a.addressType != b.addressType) { + return false; + } + + if (!Type::isSubType(a.type, b.type)) { + return false; + } + + if (a.initial > b.initial) { + return false; + } + + if (a.max < b.max) { + return false; + } + + return true; +} + std::set getFunctionsNeedingElemDeclare(Module& wasm) { // Without reference types there are no ref.funcs or elem declare. if (!wasm.features.hasReferenceTypes()) { diff --git a/src/ir/table-utils.h b/src/ir/table-utils.h index 130a9f84947..eeb03b19030 100644 --- a/src/ir/table-utils.h +++ b/src/ir/table-utils.h @@ -26,6 +26,8 @@ namespace wasm::TableUtils { +bool isSubType(const Table& a, const Table& b); + struct FlatTable { std::vector names; bool valid; diff --git a/src/wasm-interpreter.h b/src/wasm-interpreter.h index e0984bf5869..fc902321837 100644 --- a/src/wasm-interpreter.h +++ b/src/wasm-interpreter.h @@ -37,8 +37,10 @@ #include "fp16.h" #include "ir/intrinsics.h" #include "ir/iteration.h" +#include "ir/memory-utils.h" #include "ir/module-utils.h" #include "ir/properties.h" +#include "ir/table-utils.h" #include "support/bits.h" #include "support/safe_integer.h" #include "support/stdckdint.h" @@ -3368,7 +3370,7 @@ class ModuleRunnerBase : public ExpressionRunner { exportedMemory.initial = importedInstance->getMemorySize(*export_->getInternalName()); - if (!exportedMemory.isSubType(**memory)) { + if (!MemoryUtils::isSubType(exportedMemory, **memory)) { trap("Imported memory isn't compatible."); } } @@ -3376,7 +3378,7 @@ class ModuleRunnerBase : public ExpressionRunner { if (auto** table = std::get_if(&import)) { Table* exportedTable = importedInstance->wasm.getTable(*export_->getInternalName()); - if (!(*table)->isSubType(*exportedTable)) { + if (!TableUtils::isSubType(**table, *exportedTable)) { trap("Imported table isn't compatible"); } } diff --git a/src/wasm.h b/src/wasm.h index 91ed3b3c747..605c7025395 100644 --- a/src/wasm.h +++ b/src/wasm.h @@ -2435,28 +2435,6 @@ class Table : public Importable { initial = 0; max = kMaxSize; } - - static bool isSubType(const Table& a, const Table& b) { - if (a.addressType != b.addressType) { - return false; - } - - if (!Type::isSubType(a.type, b.type)) { - return false; - } - - if (a.initial > b.initial) { - return false; - } - - if (a.max < b.max) { - return false; - } - - return true; - } - - bool isSubType(const Table& other) { return Table::isSubType(*this, other); } }; class DataSegment : public Named { @@ -2492,15 +2470,6 @@ class Memory : public Importable { shared = false; addressType = Type::i32; } - - static bool isSubType(const Memory& a, const Memory& b) { - return a.shared == b.shared && a.addressType == b.addressType && - a.initial >= b.initial && a.max <= b.max; - } - - bool isSubType(const Memory& other) { - return Memory::isSubType(*this, other); - } }; class Global : public Importable { From 5dce759d83a29c0b7c73ed1292a1dc3c7f6f8c06 Mon Sep 17 00:00:00 2001 From: stevenfontanella Date: Mon, 15 Dec 2025 21:33:38 +0000 Subject: [PATCH 9/9] Remove trap overload for string_view --- src/shell-interface.h | 7 +++---- src/wasm-interpreter.h | 16 +++++++--------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src/shell-interface.h b/src/shell-interface.h index 96a65ed1edf..79bab09478b 100644 --- a/src/shell-interface.h +++ b/src/shell-interface.h @@ -137,7 +137,8 @@ struct ShellExternalInterface : ModuleRunner::ExternalInterface { trap((std::stringstream() << "importGlobals: unknown import: " << import->module.str << "." << import->name.str) - .str()); + .str() + .c_str()); } globals[import->name] = inst->globals[*exportedGlobal->getInternalName()]; }); @@ -327,9 +328,7 @@ struct ShellExternalInterface : ModuleRunner::ExternalInterface { return true; } - void trap(const char* why) override { trap(std::string_view(why)); } - - void trap(std::string_view why) override { + void trap(const char* why) override { std::cout << "[trap " << why << "]\n"; throw TrapException(); } diff --git a/src/wasm-interpreter.h b/src/wasm-interpreter.h index fc902321837..78732e395f8 100644 --- a/src/wasm-interpreter.h +++ b/src/wasm-interpreter.h @@ -2694,8 +2694,6 @@ class ExpressionRunner : public OverriddenVisitor { virtual void trap(const char* why) { WASM_UNREACHABLE("unimp"); } - virtual void trap(std::string_view why) { WASM_UNREACHABLE("unimp"); } - virtual void hostLimit(const char* why) { WASM_UNREACHABLE("unimp"); } virtual void throwException(const WasmException& exn) { @@ -2975,7 +2973,6 @@ class ModuleRunnerBase : public ExpressionRunner { Index oldSize, Index newSize) = 0; virtual void trap(const char* why) = 0; - virtual void trap(std::string_view why) { trap(std::string(why).c_str()); } virtual void hostLimit(const char* why) = 0; virtual void throwException(const WasmException& exn) = 0; // Get the Tag instance for a tag implemented in the host, that is, not @@ -3319,7 +3316,8 @@ class ModuleRunnerBase : public ExpressionRunner { trap((std::stringstream() << "Import module " << std::quoted(importable.module.toString()) << " doesn't exist.") - .str()); + .str() + .c_str()); } SubType* importedInstance = it->second.get(); @@ -3328,12 +3326,14 @@ class ModuleRunnerBase : public ExpressionRunner { if (!export_) { trap((std::stringstream() << "Export " << importable.base << " doesn't exist.") - .str()); + .str() + .c_str()); } if (export_->kind != kind) { trap((std::stringstream() << "Exported kind: " << export_->kind << " doesn't match expected kind: " << kind) - .str()); + .str() + .c_str()); } } @@ -4807,14 +4807,12 @@ class ModuleRunnerBase : public ExpressionRunner { } Flow visitStackSwitch(StackSwitch* curr) { return Flow(NONCONSTANT_FLOW); } - void trap(std::string_view why) override { + void trap(const char* why) override { // Traps break all current continuations - they will never be resumable. self()->clearContinuationStore(); externalInterface->trap(why); } - void trap(const char* why) override { trap(std::string_view(why)); } - void hostLimit(const char* why) override { self()->clearContinuationStore(); externalInterface->hostLimit(why);