diff --git a/.flake8 b/.flake8 index 4e967e90899..0202f1680f6 100644 --- a/.flake8 +++ b/.flake8 @@ -6,4 +6,4 @@ ignore = E241, ; line break after binary operator W504 -exclude = third_party,./test/emscripten,./test/spec,./test/wasm-install,./test/lit +exclude = third_party,./test/emscripten,./test/spec,./test/wasm-install,./test/lit,./_deps diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5bb8aef00c3..2ed88400d8d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -147,6 +147,34 @@ jobs: - name: test run: python check.py --binaryen-bin=out/bin + # Copied and modified from build-clang + build-fuzztest: + name: clang with fuzztest + runs-on: ubuntu-latest + steps: + - uses: actions/setup-python@v5 + with: + python-version: '3.x' + - uses: actions/checkout@v4 + with: + submodules: true + - name: install ninja + run: sudo apt-get install ninja-build + - name: install v8 + run: | + npm install jsvu -g + jsvu --os=default --engines=v8 + - name: install Python dev dependencies + run: pip3 install -r requirements-dev.txt + - name: cmake + run: | + mkdir -p out + cmake -S . -B out -G Ninja -DCMAKE_INSTALL_PREFIX=out/install -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DBUILD_FUZZTEST=ON + - name: build + run: cmake --build out -v + - name: test + run: python check.py --binaryen-bin=out/bin + # TODO(sbc): Find a way to reduce the duplicate between these sanitizer jobs build-asan: name: asan diff --git a/.gitignore b/.gitignore index 45a0fcad856..7acc6f9cd76 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,8 @@ CMakeFiles /.ninja_log /bin/ /lib/ +/_deps/ +/dist/ /config.h /emcc-build compile_commands.json diff --git a/.gitmodules b/.gitmodules index 671d8041992..990b797a0e0 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "test/spec/testsuite"] path = test/spec/testsuite url = https://github.com/WebAssembly/testsuite.git +[submodule "third_party/fuzztest"] + path = third_party/fuzztest + url = https://github.com/google/fuzztest diff --git a/CMakeLists.txt b/CMakeLists.txt index 54103c7b6c7..7f9278158bf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,6 +36,8 @@ option(BYN_ENABLE_LTO "Build with LTO" Off) # Turn this off to avoid the dependency on gtest. option(BUILD_TESTS "Build GTest-based tests" ON) +option(BUILD_FUZZTEST "Build fuzztest-based tests and fuzzers" OFF) + # Turn this off to build only the library. option(BUILD_TOOLS "Build tools" ON) @@ -161,8 +163,8 @@ endfunction() function(binaryen_add_executable name sources) add_executable(${name} ${sources}) - target_link_libraries(${name} Threads::Threads) - target_link_libraries(${name} binaryen) + target_link_libraries(${name} PRIVATE Threads::Threads) + target_link_libraries(${name} PRIVATE binaryen) binaryen_setup_rpath(${name}) install(TARGETS ${name} DESTINATION ${CMAKE_INSTALL_BINDIR}) endfunction() @@ -258,7 +260,10 @@ if(MSVC) else() # MSVC add_compile_flag("-fno-omit-frame-pointer") - add_compile_flag("-fno-rtti") + if(NOT BUILD_FUZZTEST) + # fuzztest depends on RTTIs. + add_compile_flag("-fno-rtti") + endif() if(WIN32) add_compile_flag("-D_GNU_SOURCE") add_compile_flag("-D__STDC_FORMAT_MACROS") @@ -276,10 +281,6 @@ else() # MSVC # explicitly undefine it: add_nondebug_compile_flag("-UNDEBUG") endif() - if(NOT APPLE AND NOT "${CMAKE_CXX_FLAGS}" MATCHES "-fsanitize") - # This flag only applies to shared libraries so don't use add_link_flag - set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--no-undefined") - endif() endif() if(EMSCRIPTEN) @@ -416,6 +417,25 @@ else() # MSVC add_compile_flag("-Wno-deprecated-declarations") endif() + if(BUILD_FUZZTEST) + add_compile_flag("-DFUZZTEST") + fuzztest_setup_fuzzing_flags() + + # Enabling fuzzing mode turns on sanitizers, which turn on additional + # warnings. To keep the build working, do not treat these warnings as + # errors. + add_compile_flag("-Wno-error=maybe-uninitialized") + add_compile_flag("-Wno-error=uninitialized") + add_compile_flag("-Wno-error=array-bounds") + add_compile_flag("-Wno-error=stringop-overread") + add_compile_flag("-Wno-error=missing-field-initializers") + endif() + + if(NOT APPLE AND NOT "${CMAKE_CXX_FLAGS}" MATCHES "-fsanitize") + # This flag only applies to shared libraries so don't use add_link_flag + set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--no-undefined") + endif() + endif() # Declare libbinaryen @@ -483,83 +503,84 @@ endif() if(EMSCRIPTEN) # binaryen.js WebAssembly variant add_executable(binaryen_wasm ${binaryen_SOURCES}) - target_link_libraries(binaryen_wasm binaryen) - target_link_libraries(binaryen_wasm "-sFILESYSTEM") - target_link_libraries(binaryen_wasm "-sEXPORT_NAME=Binaryen") - target_link_libraries(binaryen_wasm "-sNODERAWFS=0") + target_link_libraries(binaryen_wasm PRIVATE binaryen) + target_link_libraries(binaryen_wasm PRIVATE "-sFILESYSTEM") + target_link_libraries(binaryen_wasm PRIVATE "-sEXPORT_NAME=Binaryen") + target_link_libraries(binaryen_wasm PRIVATE "-sNODERAWFS=0") # Do not error on the repeated NODERAWFS argument - target_link_libraries(binaryen_wasm "-Wno-unused-command-line-argument") + target_link_libraries(binaryen_wasm PRIVATE "-Wno-unused-command-line-argument") # Emit a single file for convenience of people using binaryen.js as a library, # so they only need to distribute a single file. if(EMSCRIPTEN_ENABLE_SINGLE_FILE) - target_link_libraries(binaryen_wasm "-sSINGLE_FILE") - endif() - target_link_libraries(binaryen_wasm "-sEXPORT_ES6") - target_link_libraries(binaryen_wasm "-sEXPORTED_RUNTIME_METHODS=stringToUTF8OnStack,stringToAscii") - target_link_libraries(binaryen_wasm "-sEXPORTED_FUNCTIONS=_malloc,_free") - target_link_libraries(binaryen_wasm "--post-js=${CMAKE_CURRENT_SOURCE_DIR}/src/js/binaryen.js-post.js") - target_link_libraries(binaryen_wasm "-msign-ext") - target_link_libraries(binaryen_wasm "-mbulk-memory") - target_link_libraries(binaryen_wasm optimized "--closure=1") + target_link_libraries(binaryen_wasm PRIVATE "-sSINGLE_FILE") + endif() + target_link_libraries(binaryen_wasm PRIVATE "-sEXPORT_ES6") + target_link_libraries(binaryen_wasm PRIVATE "-sEXPORTED_RUNTIME_METHODS=stringToUTF8OnStack,stringToAscii") + target_link_libraries(binaryen_wasm PRIVATE "-sEXPORTED_FUNCTIONS=_malloc,_free") + target_link_libraries(binaryen_wasm PRIVATE "--post-js=${CMAKE_CURRENT_SOURCE_DIR}/src/js/binaryen.js-post.js") + target_link_libraries(binaryen_wasm PRIVATE "-msign-ext") + target_link_libraries(binaryen_wasm PRIVATE "-mbulk-memory") + target_link_libraries(binaryen_wasm PRIVATE optimized "--closure=1") # TODO: Fix closure warnings! (#5062) - target_link_libraries(binaryen_wasm optimized "-Wno-error=closure") - target_link_libraries(binaryen_wasm optimized "-flto") - target_link_libraries(binaryen_wasm debug "--profiling") + target_link_libraries(binaryen_wasm PRIVATE optimized "-Wno-error=closure") + target_link_libraries(binaryen_wasm PRIVATE optimized "-flto") + target_link_libraries(binaryen_wasm PRIVATE debug "--profiling") # Avoid catching exit as that can confuse error reporting in Node, # see https://github.com/emscripten-core/emscripten/issues/17228 - target_link_libraries(binaryen_wasm "-sNODEJS_CATCH_EXIT=0") + target_link_libraries(binaryen_wasm PRIVATE "-sNODEJS_CATCH_EXIT=0") install(TARGETS binaryen_wasm DESTINATION ${CMAKE_INSTALL_BINDIR}) # binaryen.js JavaScript variant add_executable(binaryen_js ${binaryen_SOURCES}) - target_link_libraries(binaryen_js binaryen) - target_link_libraries(binaryen_js "-sWASM=0") - target_link_libraries(binaryen_js "-sWASM_ASYNC_COMPILATION=0") + target_link_libraries(binaryen_js PRIVATE binaryen) + target_link_libraries(binaryen_js PRIVATE "-sWASM=0") + target_link_libraries(binaryen_js PRIVATE "-sWASM_ASYNC_COMPILATION=0") + if(${CMAKE_CXX_COMPILER_VERSION} STREQUAL "6.0.1") # only valid with fastcomp and WASM=0 - target_link_libraries(binaryen_js "-sELIMINATE_DUPLICATE_FUNCTIONS") + target_link_libraries(binaryen_js PRIVATE "-sELIMINATE_DUPLICATE_FUNCTIONS") endif() # Disabling filesystem and setting web environment for js_of_ocaml # so it doesn't try to detect the "node" environment if(JS_OF_OCAML) - target_link_libraries(binaryen_js "-sFILESYSTEM=0") - target_link_libraries(binaryen_js "-sENVIRONMENT=web,worker") + target_link_libraries(binaryen_js PRIVATE "-sFILESYSTEM=0") + target_link_libraries(binaryen_js PRIVATE "-sENVIRONMENT=web,worker") else() - target_link_libraries(binaryen_js "-sFILESYSTEM=1") + target_link_libraries(binaryen_js PRIVATE "-sFILESYSTEM=1") endif() - target_link_libraries(binaryen_js "-sNODERAWFS=0") + target_link_libraries(binaryen_js PRIVATE "-sNODERAWFS=0") # Do not error on the repeated NODERAWFS argument - target_link_libraries(binaryen_js "-Wno-unused-command-line-argument") + target_link_libraries(binaryen_js PRIVATE "-Wno-unused-command-line-argument") if(EMSCRIPTEN_ENABLE_SINGLE_FILE) - target_link_libraries(binaryen_js "-sSINGLE_FILE") + target_link_libraries(binaryen_js PRIVATE "-sSINGLE_FILE") endif() - target_link_libraries(binaryen_js "-sEXPORT_NAME=Binaryen") + target_link_libraries(binaryen_js PRIVATE "-sEXPORT_NAME=Binaryen") # Currently, js_of_ocaml can only process ES5 code if(JS_OF_OCAML) - target_link_libraries(binaryen_js "-sEXPORT_ES6=0") + target_link_libraries(binaryen_js PRIVATE "-sEXPORT_ES6=0") else() - target_link_libraries(binaryen_js "-sEXPORT_ES6=1") + target_link_libraries(binaryen_js PRIVATE "-sEXPORT_ES6=1") endif() - target_link_libraries(binaryen_js "-sEXPORTED_RUNTIME_METHODS=stringToUTF8OnStack,stringToAscii") - target_link_libraries(binaryen_js "-sEXPORTED_FUNCTIONS=_malloc,_free") - target_link_libraries(binaryen_js "--post-js=${CMAKE_CURRENT_SOURCE_DIR}/src/js/binaryen.js-post.js") + target_link_libraries(binaryen_js PRIVATE "-sEXPORTED_RUNTIME_METHODS=stringToUTF8OnStack,stringToAscii") + target_link_libraries(binaryen_js PRIVATE "-sEXPORTED_FUNCTIONS=_malloc,_free") + target_link_libraries(binaryen_js PRIVATE "--post-js=${CMAKE_CURRENT_SOURCE_DIR}/src/js/binaryen.js-post.js") # js_of_ocaml needs a specified variable with special comment to provide the library to consumers if(JS_OF_OCAML) - target_link_libraries(binaryen_js "--extern-pre-js=${CMAKE_CURRENT_SOURCE_DIR}/src/js/binaryen.jsoo-extern-pre.js") + target_link_libraries(binaryen_js PRIVATE "--extern-pre-js=${CMAKE_CURRENT_SOURCE_DIR}/src/js/binaryen.jsoo-extern-pre.js") endif() - target_link_libraries(binaryen_js optimized "--closure=1") + target_link_libraries(binaryen_js PRIVATE optimized "--closure=1") # Currently, js_of_ocaml can only process ES5 code if(JS_OF_OCAML) - target_link_libraries(binaryen_js optimized "--closure-args=\"--language_out=ECMASCRIPT5\"") + target_link_libraries(binaryen_js PRIVATE optimized "--closure-args=\"--language_out=ECMASCRIPT5\"") endif() # TODO: Fix closure warnings! (#5062) - target_link_libraries(binaryen_js optimized "-Wno-error=closure") - target_link_libraries(binaryen_js optimized "-flto") - target_link_libraries(binaryen_js debug "--profiling") - target_link_libraries(binaryen_js debug "-sASSERTIONS") + target_link_libraries(binaryen_js PRIVATE optimized "-Wno-error=closure") + target_link_libraries(binaryen_js PRIVATE optimized "-flto") + target_link_libraries(binaryen_js PRIVATE debug "--profiling") + target_link_libraries(binaryen_js PRIVATE debug "-sASSERTIONS") # Avoid catching exit as that can confuse error reporting in Node, # see https://github.com/emscripten-core/emscripten/issues/17228 - target_link_libraries(binaryen_js "-sNODEJS_CATCH_EXIT=0") + target_link_libraries(binaryen_js PRIVATE "-sNODEJS_CATCH_EXIT=0") install(TARGETS binaryen_js DESTINATION ${CMAKE_INSTALL_BINDIR}) endif() diff --git a/check.py b/check.py index dfaada0ab93..7385fb0f0cb 100755 --- a/check.py +++ b/check.py @@ -43,11 +43,11 @@ def run_version_tests(): print('[ checking --version ... ]\n') not_executable_suffix = ['.DS_Store', '.txt', '.js', '.ilk', '.pdb', '.dll', '.wasm', '.manifest'] - not_executable_prefix = ['binaryen-lit', 'binaryen-unittests'] + executable_prefix = ['wasm'] bin_files = [os.path.join(shared.options.binaryen_bin, f) for f in os.listdir(shared.options.binaryen_bin)] executables = [f for f in bin_files if os.path.isfile(f) and not any(f.endswith(s) for s in not_executable_suffix) and - not any(os.path.basename(f).startswith(s) for s in not_executable_prefix)] + any(os.path.basename(f).startswith(s) for s in executable_prefix)] executables = sorted(executables) assert len(executables) diff --git a/test/gtest/CMakeLists.txt b/test/gtest/CMakeLists.txt index 3a586e00e47..b72094a0204 100644 --- a/test/gtest/CMakeLists.txt +++ b/test/gtest/CMakeLists.txt @@ -1,4 +1,8 @@ -include_directories(SYSTEM ${PROJECT_SOURCE_DIR}/third_party/googletest/googletest/include) +if(BUILD_FUZZTEST) + include_directories(SYSTEM ${PROJECT_SOURCE_DIR}/third_party/fuzztest) +else() + include_directories(SYSTEM ${PROJECT_SOURCE_DIR}/third_party/googletest/googletest/include) +endif() set(unittest_SOURCES arena.cpp @@ -21,10 +25,21 @@ set(unittest_SOURCES validator.cpp ) +if(BUILD_FUZZTEST) + set(unittest_SOURCES ${unittest_SOURCES} type-domains.cpp) +endif() + # suffix_tree.cpp includes LLVM header using std::iterator (deprecated in C++17) if (NOT MSVC) set_source_files_properties(suffix_tree.cpp PROPERTIES COMPILE_FLAGS -Wno-deprecated-declarations) endif() +enable_testing() +include(GoogleTest) binaryen_add_executable(binaryen-unittests "${unittest_SOURCES}") -target_link_libraries(binaryen-unittests gtest gtest_main) +if(BUILD_FUZZTEST) + link_fuzztest(binaryen-unittests) + gtest_discover_tests(binaryen-unittests) +else() + target_link_libraries(binaryen-unittests PRIVATE gtest gtest_main) +endif() diff --git a/test/gtest/type-builder.cpp b/test/gtest/type-builder.cpp index 514df0c59a0..ac95d0e11b4 100644 --- a/test/gtest/type-builder.cpp +++ b/test/gtest/type-builder.cpp @@ -5,6 +5,10 @@ #include "wasm-type.h" #include "gtest/gtest.h" +#ifdef FUZZTEST +#include "type-domains.h" +#endif + using namespace wasm; TEST_F(TypeTest, TypeBuilderGrowth) { @@ -1056,6 +1060,41 @@ TEST_F(TypeTest, TestHeapTypeRelations) { } } +#ifdef FUZZTEST + +void TestHeapTypeRelationsFuzz(std::pair pair) { + auto [a, b] = pair; + auto lub = HeapType::getLeastUpperBound(a, b); + auto otherLub = HeapType::getLeastUpperBound(b, a); + EXPECT_EQ(lub, otherLub); + if (lub) { + EXPECT_EQ(a.getTop(), b.getTop()); + EXPECT_EQ(a.getBottom(), b.getBottom()); + EXPECT_TRUE(HeapType::isSubType(a, *lub)); + EXPECT_TRUE(HeapType::isSubType(b, *lub)); + } else { + EXPECT_NE(a.getTop(), b.getTop()); + EXPECT_NE(a.getBottom(), b.getBottom()); + } + if (a == b) { + EXPECT_EQ(lub, a); + EXPECT_EQ(lub, b); + } else if (lub && *lub == b) { + EXPECT_TRUE(HeapType::isSubType(a, b)); + EXPECT_FALSE(HeapType::isSubType(b, a)); + } else if (lub && *lub == a) { + EXPECT_FALSE(HeapType::isSubType(a, b)); + EXPECT_TRUE(HeapType::isSubType(b, a)); + } else if (lub) { + EXPECT_FALSE(HeapType::isSubType(a, b)); + EXPECT_FALSE(HeapType::isSubType(b, a)); + } +} +FUZZ_TEST(TypeFuzzTest, TestHeapTypeRelationsFuzz) + .WithDomains(ArbitraryHeapTypePair()); + +#endif // FUZZTEST + TEST_F(TypeTest, TestSubtypeErrors) { Type anyref = Type(HeapType::any, Nullable); Type eqref = Type(HeapType::eq, Nullable); diff --git a/test/gtest/type-domains.cpp b/test/gtest/type-domains.cpp new file mode 100644 index 00000000000..b59bc2a0dd7 --- /dev/null +++ b/test/gtest/type-domains.cpp @@ -0,0 +1,1189 @@ +/* + * Copyright 2025 WebAssembly Community Group participants + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include "type-domains.h" +#include "gtest/gtest.h" + +namespace wasm { + +namespace { + +void printHeapType(std::ostream& o, HeapTypePlan& plan) { + if (auto* type = plan.getHeapType()) { + o << *type; + } else if (auto* i = plan.getIndex()) { + o << *i; + } +} + +void printRef(std::ostream& o, RefPlan& plan) { + o << "(ref "; + if (plan.nullable) { + o << "null "; + } + printHeapType(o, plan.type); + o << ")"; +} + +void printType(std::ostream& o, TypePlan& plan) { + if (auto* type = plan.getNonRef()) { + o << *type; + } else if (auto* ref = plan.getRef()) { + printRef(o, *ref); + } +} + +void printFieldType(std::ostream& o, FieldTypePlan& plan) { + if (auto* packed = plan.getPacked()) { + o << (*packed == Field::i8 ? "i8" : "i16"); + } else if (auto* type = plan.getNonPacked()) { + printType(o, *type); + } +} + +void printField(std::ostream& o, FieldPlan& plan) { + o << "(field "; + if (plan.mutable_) { + o << "(mut "; + } + printFieldType(o, plan.type); + if (plan.mutable_) { + o << ")"; + } + o << ")"; +} + +void printFunc(std::ostream& o, FuncPlan& plan) { + o << "(func"; + if (!plan.first.empty()) { + o << " (param"; + for (auto& type : plan.first) { + o << " "; + printType(o, type); + } + o << ")"; + } + if (!plan.second.empty()) { + o << " (result"; + for (auto& type : plan.second) { + o << " "; + printType(o, type); + } + o << ")"; + } + o << ")"; +} + +void printStruct(std::ostream& o, StructPlan& plan) { + o << "(struct"; + for (auto& field : plan) { + o << " "; + printField(o, field); + } + o << ")"; +} + +void printArray(std::ostream& o, ArrayPlan& plan) { + o << "(array "; + printField(o, plan); + o << ")"; +} + +void printCont(std::ostream& o, ContPlan& plan) { + o << "(cont "; + if (plan) { + o << *plan; + } else { + o << "$fallback"; + } + o << ")"; +} + +void printTypeDef(std::ostream& o, const TypeBuilderPlan& plan, size_t i) { + auto def = plan.defs[i]; + auto super = plan.supertypes[i]; + bool shared = plan.kinds[i].shared; + bool final = plan.kinds[i].final; + o << "(type (; " << i << " ;) "; + if (super || !final) { + o << "(sub "; + if (super) { + o << *super << " "; + } + } + if (shared) { + o << "(shared "; + } + if (auto* func = def.getFunc()) { + printFunc(o, *func); + } else if (auto* struct_ = def.getStruct()) { + printStruct(o, *struct_); + } else if (auto* array = def.getArray()) { + printArray(o, *array); + } else if (auto* cont = def.getCont()) { + printCont(o, *cont); + } else { + WASM_UNREACHABLE("unexpected kind"); + } + if (shared) { + o << ")"; + } + if (super || !final) { + o << ")"; + } + o << ")"; +} + +} // anonymous namespace + +std::ostream& operator<<(std::ostream& o, const TypeBuilderPlan& plan) { + assert(!plan.recGroupSizes.empty()); + o << "size: " << plan.size << ", rec group sizes: { " + << plan.recGroupSizes[0]; + for (size_t i = 1; i < plan.recGroupSizes.size(); ++i) { + o << ", " << plan.recGroupSizes[i]; + } + o << " }"; + + if (plan.supertypes.empty()) { + return o; + } + + auto printKind = [&](size_t i) { + if (plan.kinds[i].final) { + o << "*"; + } + if (plan.kinds[i].shared) { + o << "s"; + } + switch (plan.kinds[i].kind) { + case FuncKind: + o << "f"; + break; + case StructKind: + o << "s"; + break; + case ArrayKind: + o << "a"; + break; + case ContKind: + o << "s"; + break; + } + if (auto super = plan.supertypes[i]) { + o << "(" << *super << ")"; + } + }; + + o << ", kinds: { "; + printKind(0); + for (size_t i = 1; i < plan.size; ++i) { + o << ", "; + printKind(i); + } + o << " }"; + + if (plan.defs.empty()) { + return o; + } + + o << "\n"; + + for (size_t i = 0; i < plan.size; ++i) { + o << " "; + printTypeDef(o, plan, i); + o << "\n"; + } + + return o; +} + +namespace { + +template +using ResultVal = + typename std::invoke_result_t, size_t>::value_type; + +template +using ResultVec = std::vector>; + +template +using AccTuple = std::tuple, size_t, ResultVec>; + +template +fuzztest::Domain> StepMapVector(AccTuple); + +template +fuzztest::Domain> AppendMapVector(AccTuple acc, + ResultVal val) { + auto& [map, vec, i, results] = acc; + results.emplace_back(std::move(val)); + return StepMapVector(std::move(acc)); +} + +template +fuzztest::Domain> StepMapVector(AccTuple acc) { + auto& [map, vec, i, results] = acc; + if (i == vec.size()) { + // Base case. We've generated all the elements. + return fuzztest::Just(std::move(results)); + } + // Apply `map` to get the domain for the next element, then generate an + // element of that domain, append it to `results`, and recurse. + auto elemDomain = map(vec, i++); + return fuzztest::FlatMap( + AppendMapVector, fuzztest::Just(std::move(acc)), elemDomain); +} + +// Given a mapping of (const std::vector&, size_t i) -> Domain and a +// std::vector, apply the mapping elementwise and produce a +// Domain>. +template +fuzztest::Domain> MapVector(Map map, std::vector vec) { + return StepMapVector( + std::make_tuple(map, std::move(vec), size_t(0), ResultVec{})); +} + +// Given a mapping of T -> Domain and a std::vector, apply the mapping +// elementwise and produce a Domain>. This is a shorthand version +// of MapVector for when the output domains depend only on single elements. +template +auto MapElements(Map map, std::vector vec) { + return MapVector([map](std::vector vec, size_t i) { return map(vec[i]); }, + std::move(vec)); +} + +fuzztest::Domain ArbitraryUnsharedTypeKind() { + return fuzztest::ElementOf({FuncKind, StructKind, ArrayKind, ContKind}); +} + +fuzztest::Domain ArbitraryTypeKind() { + // Independently random unshared kind, sharedness, and mutability. + return fuzztest::StructOf(ArbitraryUnsharedTypeKind(), + fuzztest::Arbitrary(), + fuzztest::Arbitrary()); +} + +fuzztest::Domain TypeBuilderPlanSize() { + // Choose a size for the TypeBuilder. + return fuzztest::InRange(size_t(1), MaxTypeBuilderSize); +} + +fuzztest::Domain InitTypeBuilderPlan() { + // Create a TypeBuilderPlan with `size` and `curr` set to the same choice + // of size. `curr` represents how many slots still need a rec group. + return fuzztest::FlatMap( + [](size_t size) { + return fuzztest::Just(TypeBuilderPlan{size, size}); + }, + TypeBuilderPlanSize()); +} + +fuzztest::Domain StepRecGroup(TypeBuilderPlan plan); + +fuzztest::Domain AppendRecGroup(TypeBuilderPlan plan, + size_t newSize) { + // Update `plan` to append a recgroup of size `newSize`, then recurse iff + // there is still size unallocated to a rec group. + plan.curr -= newSize; + plan.recGroupSizes.push_back(newSize); + if (plan.curr == 0) { + return fuzztest::Just(std::move(plan)); + } else { + return StepRecGroup(std::move(plan)); + } +} + +fuzztest::Domain StepRecGroup(TypeBuilderPlan plan) { + // Given a plan that needs more rec groups, choose the size of the next rec + // group based on the available size remaining. Bias toward singleton rec + // groups. + auto remaining = plan.curr; + assert(remaining > 0); + return fuzztest::FlatMap( + AppendRecGroup, + fuzztest::Just(std::move(plan)), + fuzztest::OneOf(fuzztest::Just(size_t(1)), + fuzztest::InRange(size_t(1), remaining))); +} + +fuzztest::Domain ArbitraryRecGroupPlan() { + // Initialize a plan with just a size, then add rec group sizes. + return fuzztest::FlatMap(StepRecGroup, InitTypeBuilderPlan()); +} + +void TestRecGroupPlanSizes(TypeBuilderPlan plan) { + size_t sum = 0; + for (auto size : plan.recGroupSizes) { + sum += size; + } + EXPECT_EQ(plan.size, sum); + EXPECT_EQ(plan.curr, 0); +} +FUZZ_TEST(TypeBuilderDomainsTest, TestRecGroupPlanSizes) + .WithDomains(ArbitraryRecGroupPlan()); + +fuzztest::Domain StepSupertypeAndKind(TypeBuilderPlan plan); +fuzztest::Domain AppendKind(TypeBuilderPlan plan, + TypeKind kind); + +fuzztest::Domain AppendSupertype(TypeBuilderPlan plan, + std::optional super) { + plan.supertypes.push_back(super); + if (super) { + // If there is a supertype, then the current type will inherit its kind. + auto kind = plan.kinds[*super]; + return AppendKind(std::move(plan), kind); + } else { + // Otherwise, we give it an arbitrary kind. + return fuzztest::FlatMap( + AppendKind, fuzztest::Just(std::move(plan)), ArbitraryTypeKind()); + } +} + +fuzztest::Domain AppendKind(TypeBuilderPlan plan, + TypeKind kind) { + // We have chosen the kind either based on the supertype or arbitrarily. + // Either way, set it and then recurse iff there are more supertypes and kinds + // to set. + plan.kinds.push_back(kind); + if (plan.curr == plan.size) { + return fuzztest::Just(std::move(plan)); + } else { + return StepSupertypeAndKind(std::move(plan)); + } +} + +fuzztest::Domain StepSupertypeAndKind(TypeBuilderPlan plan) { + // Collect previous non-final types as possible supertypes. + auto index = plan.curr++; + std::vector possibleSupers; + for (size_t i = 0; i < index; ++i) { + if (!plan.kinds[i].final) { + possibleSupers.push_back(i); + } + } + if (possibleSupers.empty()) { + // No possible supertype. + return AppendSupertype(std::move(plan), std::nullopt); + } else { + // Optionally choose an available supertype. + return fuzztest::FlatMap( + AppendSupertype, + fuzztest::Just(std::move(plan)), + fuzztest::OptionalOf(fuzztest::ElementOf(std::move(possibleSupers)))); + } +} + +fuzztest::Domain ArbitraryAbstractTypeBuilderPlan() { + // Initialize with rec group sizes, then add supertype declarations and type + // kinds. + return fuzztest::FlatMap(StepSupertypeAndKind, ArbitraryRecGroupPlan()); +} + +void TestSupertypesAndKinds(TypeBuilderPlan plan) { + ASSERT_EQ(plan.size, plan.supertypes.size()); + ASSERT_EQ(plan.size, plan.kinds.size()); + for (size_t i = 0; i < plan.size; ++i) { + if (auto super = plan.supertypes[i]) { + EXPECT_LT(*super, i); + EXPECT_EQ(plan.kinds[*super].kind, plan.kinds[i].kind); + EXPECT_EQ(plan.kinds[*super].shared, plan.kinds[i].shared); + EXPECT_FALSE(plan.kinds[*super].final); + } + } +} +FUZZ_TEST(TypeBuilderDomainsTest, TestSupertypesAndKinds) + .WithDomains(ArbitraryAbstractTypeBuilderPlan()); + +fuzztest::Domain InitConcreteTypeBuilderPlan() { + // Reset `curr` to 0 (for simplicity) and initialize `numReferenceable` based + // on the size of the first rec group. + return fuzztest::Map( + [](TypeBuilderPlan plan) { + plan.curr = 0; + plan.numReferenceable = plan.recGroupSizes[0]; + return plan; + }, + ArbitraryAbstractTypeBuilderPlan()); +} + +template +std::vector AvailableMatchingIndices(TypeBuilderPlan plan, Pred pred) { + std::vector matches; + for (size_t i = 0; i < plan.numReferenceable; ++i) { + if (pred(plan.kinds[i].kind, plan.kinds[i].shared)) { + matches.push_back(i); + } + } + return matches; +} + +template +fuzztest::Domain AvailableMatchingOrAbstractHeapType( + TypeBuilderPlan plan, Pred pred, fuzztest::Domain abstract) { + // Look for referenceable indices with kinds matching the predicate and return + // a variant of the indices or the given abstract subtypes. If there are no + // possible indices, just return the abstract subtypes. + auto matches = AvailableMatchingIndices(std::move(plan), pred); + if (matches.empty()) { + return fuzztest::Map([](HeapType type) { return HeapTypePlan{type}; }, + abstract); + } else { + return fuzztest::VariantOf( + abstract, fuzztest::ElementOf(std::move(matches))); + } +} + +std::vector AvailableStrictSubIndices(TypeBuilderPlan plan, + size_t index) { + // Look for direct and indirect subtypes. To find indirect subtypes, keep + // track of all the possible supertypes that are subtypes of `super`. + std::vector matches; + std::vector acceptedSupers(plan.numReferenceable); + acceptedSupers[index] = true; + assert(plan.numReferenceable <= plan.size); + for (size_t i = index + 1; i < plan.numReferenceable; ++i) { + auto otherSuper = plan.supertypes[i]; + if (otherSuper && acceptedSupers[*otherSuper]) { + matches.push_back(i); + acceptedSupers[i] = true; + } + } + return matches; +} + +fuzztest::Domain AvailableStrictSubHeapType(TypeBuilderPlan plan, + HeapTypePlan super) { + // Get an available subtype of super. + if (auto* type = super.getHeapType()) { + auto share = type->getShared(); + bool shared = share == Shared; + + auto matchingOrAbstract = [=](auto pred, auto abstract) { + return AvailableMatchingOrAbstractHeapType( + std::move(plan), + [&](auto kind, bool otherShared) { + return otherShared == shared && pred(kind); + }, + abstract); + }; + + switch (type->getBasic(Unshared)) { + case HeapType::ext: + return fuzztest::Just( + HeapTypePlan{HeapType(HeapTypes::noext.getBasic(share))}); + case HeapType::func: + return matchingOrAbstract( + [](auto kind) { return kind == FuncKind; }, + fuzztest::Just(HeapType(HeapTypes::nofunc.getBasic(share)))); + case HeapType::cont: + return matchingOrAbstract( + [](auto kind) { return kind == ContKind; }, + fuzztest::Just(HeapType(HeapTypes::nocont.getBasic(share)))); + case HeapType::any: + return matchingOrAbstract( + [](auto kind) { return kind == StructKind || kind == ArrayKind; }, + fuzztest::ElementOf({HeapType(HeapTypes::eq.getBasic(share)), + HeapType(HeapTypes::i31.getBasic(share)), + HeapType(HeapTypes::string.getBasic(share)), + HeapType(HeapTypes::struct_.getBasic(share)), + HeapType(HeapTypes::array.getBasic(share)), + HeapType(HeapTypes::none.getBasic(share))})); + case HeapType::eq: + return matchingOrAbstract( + [](auto kind) { return kind == StructKind || kind == ArrayKind; }, + fuzztest::ElementOf({HeapType(HeapTypes::i31.getBasic(share)), + HeapType(HeapTypes::struct_.getBasic(share)), + HeapType(HeapTypes::array.getBasic(share)), + HeapType(HeapTypes::none.getBasic(share))})); + case HeapType::struct_: + return matchingOrAbstract( + [](auto kind) { return kind == StructKind; }, + fuzztest::Just(HeapType(HeapTypes::none.getBasic(share)))); + case HeapType::array: + return matchingOrAbstract( + [](auto kind) { return kind == ArrayKind; }, + fuzztest::Just(HeapType(HeapTypes::none.getBasic(share)))); + case HeapType::exn: + return fuzztest::Just( + HeapTypePlan{HeapType(HeapTypes::noexn.getBasic(share))}); + case HeapType::string: + case HeapType::i31: + return fuzztest::Just( + HeapTypePlan{HeapType(HeapTypes::none.getBasic(share))}); + case HeapType::none: + case HeapType::noext: + case HeapType::nofunc: + case HeapType::nocont: + case HeapType::noexn: + // No strict subtypes, so just return super. + return fuzztest::Just(super); + } + WASM_UNREACHABLE("unexpected type"); + } else if (auto* index = super.getIndex()) { + assert(*index < plan.size); + auto kind = plan.kinds[*index].kind; + auto share = plan.kinds[*index].shared ? Shared : Unshared; + auto matches = AvailableStrictSubIndices(std::move(plan), *index); + HeapType bottom = HeapType::none; + switch (kind) { + case FuncKind: + bottom = HeapTypes::nofunc.getBasic(share); + break; + case StructKind: + case ArrayKind: + bottom = HeapTypes::none.getBasic(share); + break; + case ContKind: + bottom = HeapTypes::nocont.getBasic(share); + break; + } + if (matches.empty()) { + return fuzztest::Just(HeapTypePlan{bottom}); + } else { + return fuzztest::VariantOf( + fuzztest::Just(bottom), fuzztest::ElementOf(std::move(matches))); + } + } else { + WASM_UNREACHABLE("unexpected variant"); + } +} + +fuzztest::Domain +AvailableStrictSuperHeapType(TypeBuilderPlan plan, HeapTypePlan sub) { + if (auto* type = sub.getHeapType()) { + auto share = type->getShared(); + bool shared = share == Shared; + + auto matchingOrAbstract = [&](auto pred, auto abstract) { + return AvailableMatchingOrAbstractHeapType( + std::move(plan), + [&](auto kind, bool otherShared) { + return otherShared == shared && pred(kind); + }, + abstract); + }; + + switch (type->getBasic(Unshared)) { + case HeapType::ext: + case HeapType::func: + case HeapType::cont: + case HeapType::any: + case HeapType::exn: + // No strict supertypes, so just return sub. + return fuzztest::Just(sub); + case HeapType::eq: + return fuzztest::Just( + HeapTypePlan{HeapType(HeapTypes::any.getBasic(share))}); + case HeapType::i31: + case HeapType::struct_: + case HeapType::array: + case HeapType::string: + return fuzztest::Just( + HeapTypePlan{HeapType(HeapTypes::any.getBasic(share))}); + case HeapType::none: + return matchingOrAbstract( + [](auto kind) { return kind == StructKind || kind == ArrayKind; }, + fuzztest::ElementOf({HeapType(HeapTypes::any.getBasic(share)), + HeapType(HeapTypes::eq.getBasic(share)), + HeapType(HeapTypes::i31.getBasic(share)), + HeapType(HeapTypes::string.getBasic(share)), + HeapType(HeapTypes::struct_.getBasic(share)), + HeapType(HeapTypes::array.getBasic(share))})); + case HeapType::noext: + return fuzztest::Just( + HeapTypePlan{HeapType(HeapTypes::ext.getBasic(share))}); + case HeapType::nofunc: + return matchingOrAbstract( + [](auto kind) { return kind == FuncKind; }, + fuzztest::Just(HeapType(HeapTypes::func.getBasic(share)))); + case HeapType::nocont: + return matchingOrAbstract( + [](auto kind) { return kind == ContKind; }, + fuzztest::Just(HeapType(HeapTypes::cont.getBasic(share)))); + case HeapType::noexn: + return fuzztest::Just( + HeapTypePlan{HeapType(HeapTypes::exn.getBasic(share))}); + } + WASM_UNREACHABLE("unexpected type"); + } else if (auto* index = sub.getIndex()) { + assert(*index < plan.size); + // Collect indices from the supertype chain as well as abstract supertypes. + auto share = plan.kinds[*index].shared ? Shared : Unshared; + std::vector possibleIndices; + for (auto curr = plan.supertypes[*index]; curr; + curr = plan.supertypes[*curr]) { + possibleIndices.push_back(*curr); + } + std::vector abstract; + switch (plan.kinds[*index].kind) { + case FuncKind: + abstract = {HeapTypes::func.getBasic(share)}; + break; + case StructKind: + abstract = {HeapTypes::any.getBasic(share), + HeapTypes::eq.getBasic(share), + HeapTypes::struct_.getBasic(share)}; + break; + case ArrayKind: + abstract = {HeapTypes::any.getBasic(share), + HeapTypes::eq.getBasic(share), + HeapTypes::array.getBasic(share)}; + break; + case ContKind: + abstract = {HeapTypes::cont.getBasic(share)}; + break; + } + assert(!abstract.empty()); + if (possibleIndices.empty()) { + return fuzztest::Map([](auto type) { return HeapTypePlan{type}; }, + fuzztest::ElementOf(std::move(abstract))); + } else { + return fuzztest::VariantOf( + fuzztest::ElementOf(std::move(abstract)), + fuzztest::ElementOf(std::move(possibleIndices))); + } + } else { + WASM_UNREACHABLE("unexpected variant"); + } +} + +fuzztest::Domain AvailableHeapType(TypeBuilderPlan plan) { + // Any reachable or abstract heap type, constrained to be shared if the type + // definition we are constructing is shared. + // TODO: Allow unshared types in shared function type defs? + bool shared = plan.kinds[plan.curr].shared; + auto abstract = + shared ? ArbitrarySharedAbstractHeapType() : ArbitraryAbstractHeapType(); + return AvailableMatchingOrAbstractHeapType( + plan, + [&](auto kind, bool otherShared) { return !shared || otherShared; }, + abstract); +} + +fuzztest::Domain AvailableSubHeapType(TypeBuilderPlan plan, + HeapTypePlan super) { + // Choose a subtype of `super`, biasing toward `super` itself. + return fuzztest::OneOf(fuzztest::Just(super), + AvailableStrictSubHeapType(std::move(plan), super)); +} + +fuzztest::Domain AvailableSuperHeapType(TypeBuilderPlan plan, + HeapTypePlan sub) { + // Choose a supertype of `sub`, biasing toward `sub` itself. + return fuzztest::OneOf(fuzztest::Just(sub), + AvailableStrictSuperHeapType(std::move(plan), sub)); +} + +fuzztest::Domain AvailableRefType(TypeBuilderPlan plan) { + // Independently random heap type and nullability. + return fuzztest::StructOf(AvailableHeapType(std::move(plan)), + fuzztest::Arbitrary()); +} + +fuzztest::Domain AvailableSubRefType(TypeBuilderPlan plan, + RefPlan super) { + auto heapType = AvailableSubHeapType(std::move(plan), super.type); + if (super.nullable) { + return fuzztest::StructOf(heapType, fuzztest::Arbitrary()); + } else { + return fuzztest::StructOf(heapType, fuzztest::Just(false)); + } +} + +fuzztest::Domain AvailableSuperRefType(TypeBuilderPlan plan, + RefPlan sub) { + auto heapType = AvailableSuperHeapType(std::move(plan), sub.type); + if (sub.nullable) { + return fuzztest::StructOf(heapType, fuzztest::Just(true)); + } else { + return fuzztest::StructOf(heapType, fuzztest::Arbitrary()); + } +} + +fuzztest::Domain AvailableType(TypeBuilderPlan plan) { + // A non-reference types or a reference to an available heap type. + return fuzztest::VariantOf(ArbitraryNonRefType(), + AvailableRefType(std::move(plan))); +} + +fuzztest::Domain AvailableSubType(TypeBuilderPlan plan, + TypePlan super) { + if (auto* type = super.getNonRef()) { + // No subtyping among non-ref types. + return fuzztest::Just(super); + } else if (auto* ref = super.getRef()) { + return fuzztest::Map([](auto ref) { return TypePlan{ref}; }, + AvailableSubRefType(std::move(plan), std::move(*ref))); + } else { + WASM_UNREACHABLE("unexpected variant"); + } +} + +fuzztest::Domain AvailableSuperType(TypeBuilderPlan plan, + TypePlan sub) { + if (auto* type = sub.getNonRef()) { + // No subtyping among non-ref types. + return fuzztest::Just(TypePlan{*type}); + } else if (auto* ref = sub.getRef()) { + return fuzztest::Map( + [](auto ref) { return TypePlan{ref}; }, + AvailableSuperRefType(std::move(plan), std::move(*ref))); + } else { + WASM_UNREACHABLE("unexpected variant"); + } +} + +fuzztest::Domain AvailableFieldType(TypeBuilderPlan plan) { + // A packed type or another available type. + return fuzztest::VariantOf( + fuzztest::ElementOf({Field::i8, Field::i16}), + AvailableType(std::move(plan))); +} + +fuzztest::Domain AvailableSubFieldType(TypeBuilderPlan plan, + FieldTypePlan super) { + if (auto* packed = super.getPacked()) { + // No subtyping on packed types. + return fuzztest::Just(FieldTypePlan{*packed}); + } else if (auto* type = super.getNonPacked()) { + return fuzztest::Map([](auto type) { return FieldTypePlan{type}; }, + AvailableSubType(std::move(plan), *type)); + } else { + WASM_UNREACHABLE("unexpected variant"); + } +} + +fuzztest::Domain AvailableField(TypeBuilderPlan plan) { + // An available field type and a random mutability. + return fuzztest::StructOf(AvailableFieldType(std::move(plan)), + fuzztest::Arbitrary()); +} + +fuzztest::Domain AvailableSubField(TypeBuilderPlan plan, + FieldPlan super) { + if (super.mutable_) { + // Mutable fields cannot be modified in subtypes. + return fuzztest::Just(super); + } + return fuzztest::Map( + [&](auto type) { + return FieldPlan{type, false}; + }, + AvailableSubFieldType(std::move(plan), super.type)); +} + +fuzztest::Domain FuncDef(TypeBuilderPlan plan) { + auto params = + fuzztest::VectorOf(AvailableType(plan)).WithMaxSize(MaxParamsSize); + auto results = fuzztest::VectorOf(AvailableType(std::move(plan))) + .WithMaxSize(MaxResultsSize); + return fuzztest::PairOf(params, results); +} + +fuzztest::Domain SubFuncDef(TypeBuilderPlan plan, FuncPlan super) { + // Params are contravariant and results are covariant. + auto params = MapElements( + [plan](TypePlan type) { return AvailableSuperType(plan, type); }, + super.first); + auto results = MapElements( + [plan = std::move(plan)](TypePlan type) { + return AvailableSubType(std::move(plan), type); + }, + super.second); + return fuzztest::PairOf(params, results); +} + +fuzztest::Domain StructDef(TypeBuilderPlan plan) { + return fuzztest::VectorOf(AvailableField(std::move(plan))) + .WithMaxSize(MaxStructSize); +} + +fuzztest::Domain SubStructDef(TypeBuilderPlan plan, + StructPlan super) { + // First do depth subtyping, where we choose a subtype of each field, then + // maybe add extra fields if there is space. + auto depthSubTypeDomain = MapElements( + [plan](const FieldPlan& field) { return AvailableSubField(plan, field); }, + super); + return fuzztest::FlatMap( + [plan](StructPlan toExtend) -> fuzztest::Domain { + if (toExtend.size() == MaxStructSize) { + // No room to add more fields. + return fuzztest::Just(toExtend); + } + auto extensionDomain = fuzztest::VectorOf(AvailableField(std::move(plan))) + .WithMaxSize(MaxStructSize - toExtend.size()); + return fuzztest::FlatMap( + [toExtend](std::vector extension) { + auto extended = toExtend; + extended.insert(extended.end(), extension.begin(), extension.end()); + return fuzztest::Just(std::move(extended)); + }, + extensionDomain); + }, + depthSubTypeDomain); +} + +fuzztest::Domain ArrayDef(TypeBuilderPlan plan) { + return AvailableField(std::move(plan)); +} + +fuzztest::Domain SubArrayDef(TypeBuilderPlan plan, ArrayPlan super) { + return AvailableSubField(std::move(plan), super); +} + +fuzztest::Domain ContDef(TypeBuilderPlan plan) { + // Find referenceable function types, restricting ourselves to shared + // functions if necessary. + bool shared = plan.kinds[plan.curr].shared; + auto matches = + AvailableMatchingIndices(std::move(plan), [&](auto kind, bool otherShared) { + return kind == FuncKind && (!shared || otherShared); + }); + if (matches.empty()) { + return fuzztest::NullOpt(); + } else { + return fuzztest::NonNull( + fuzztest::OptionalOf(fuzztest::ElementOf(std::move(matches)))); + } +} + +fuzztest::Domain SubContDef(TypeBuilderPlan plan, ContPlan super) { + if (auto index = super) { + // Choose an available subtype of the current continuation's function type, + // biasing toward the current continuation's function type itself. + auto matches = AvailableStrictSubIndices(std::move(plan), *index); + if (matches.empty()) { + // No other function indices available to create a subtype. + return fuzztest::Just(super); + } + return fuzztest::OneOf(fuzztest::Just(super), + fuzztest::NonNull(fuzztest::OptionalOf( + fuzztest::ElementOf(std::move(matches))))); + } else { + // We will not generate subtypes of the fallback function type, so keep it + // unchanged. + return fuzztest::Just(super); + } +} + +fuzztest::Domain StepTypeDefinition(TypeBuilderPlan plan); + +template +fuzztest::Domain AppendTypeDef(TypeBuilderPlan plan, T def) { + ++plan.curr; + plan.defs.emplace_back(TypeDefPlan{std::move(def)}); + return StepTypeDefinition(std::move(plan)); +} + +fuzztest::Domain StepTypeDefinition(TypeBuilderPlan plan) { + auto index = plan.curr; + if (index == plan.size) { + // We have created all the type defs. + return fuzztest::Just(plan); + } + // If we have moved into a new rec group, update our state accordingly. + if (index > plan.numReferenceable) { + ++plan.currRecGroup; + plan.numReferenceable += plan.recGroupSizes[plan.currRecGroup]; + } + // Look at the type kind to determine what domain to draw the type + // definition from. + auto super = plan.supertypes[index]; + switch (plan.kinds[index].kind) { + case FuncKind: { + auto def = + super ? SubFuncDef(plan, *plan.defs[*super].getFunc()) : FuncDef(plan); + return fuzztest::FlatMap( + AppendTypeDef, fuzztest::Just(std::move(plan)), def); + } + case StructKind: { + auto def = super ? SubStructDef(plan, *plan.defs[*super].getStruct()) + : StructDef(plan); + return fuzztest::FlatMap( + AppendTypeDef, fuzztest::Just(std::move(plan)), def); + } + case ArrayKind: { + auto def = super ? SubArrayDef(plan, *plan.defs[*super].getArray()) + : ArrayDef(plan); + return fuzztest::FlatMap( + AppendTypeDef, fuzztest::Just(std::move(plan)), def); + } + case ContKind: { + auto def = + super ? SubContDef(plan, *plan.defs[*super].getCont()) : ContDef(plan); + return fuzztest::FlatMap( + AppendTypeDef, fuzztest::Just(std::move(plan)), def); + } + } + WASM_UNREACHABLE("unexpected kind"); +} + +std::vector BuildHeapTypes(TypeBuilderPlan plan) { + // Continuation types without reachable function types need a fallback. + TypeBuilder fallbackBuilder(2); + fallbackBuilder[0] = Signature(); + fallbackBuilder[1] = Signature(); + fallbackBuilder[1].setShared(); + auto builtFallbacks = fallbackBuilder.build(); + HeapType contFallback = (*builtFallbacks)[0]; + HeapType sharedContFallback = (*builtFallbacks)[1]; + + TypeBuilder builder(plan.size); + + // Rec groups. + size_t start = 0; + for (auto size : plan.recGroupSizes) { + builder.createRecGroup(start, size); + start += size; + } + + // Map plans onto the builder. + + auto heapType = [&](HeapTypePlan& plan) -> HeapType { + if (auto* type = plan.getHeapType()) { + return *type; + } else if (auto* index = plan.getIndex()) { + return builder[*index]; + } else { + WASM_UNREACHABLE("unexpected variant"); + } + }; + + auto ref = [&](RefPlan& plan) -> Type { + return builder.getTempRefType(heapType(plan.type), + plan.nullable ? Nullable : NonNullable); + }; + + auto type = [&](TypePlan& plan) -> Type { + if (auto* type = plan.getNonRef()) { + return *type; + } else if (auto* r = plan.getRef()) { + return ref(*r); + } else { + WASM_UNREACHABLE("unexpected variant"); + } + }; + + auto field = [&](FieldPlan& plan) -> Field { + if (auto* packed = plan.type.getPacked()) { + return Field(*packed, plan.mutable_ ? Mutable : Immutable); + } else if (auto* t = plan.type.getNonPacked()) { + return Field(type(*t), plan.mutable_ ? Mutable : Immutable); + } else { + WASM_UNREACHABLE("unexpected variant"); + } + }; + + auto func = [&](FuncPlan& plan) -> Signature { + std::vector params, results; + for (auto& t : plan.first) { + params.push_back(type(t)); + } + for (auto& t : plan.second) { + results.push_back(type(t)); + } + return Signature(builder.getTempTupleType(std::move(params)), + builder.getTempTupleType(std::move(results))); + }; + + auto struct_ = [&](StructPlan& plan) -> Struct { + std::vector fields; + for (auto& f : plan) { + fields.push_back(field(f)); + } + return Struct(std::move(fields)); + }; + + auto array = [&](ArrayPlan& plan) -> Array { return Array(field(plan)); }; + + auto cont = [&](ContPlan& plan, bool shared) -> Continuation { + if (plan) { + return Continuation(builder[*plan]); + } + return Continuation(shared ? sharedContFallback : contFallback); + }; + + for (size_t i = 0; i < plan.size; ++i) { + if (auto* f = plan.defs[i].getFunc()) { + builder[i] = func(*f); + } else if (auto* s = plan.defs[i].getStruct()) { + builder[i] = struct_(*s); + } else if (auto* a = plan.defs[i].getArray()) { + builder[i] = array(*a); + } else if (auto* c = plan.defs[i].getCont()) { + builder[i] = cont(*c, plan.kinds[i].shared); + } else { + WASM_UNREACHABLE("unexpected variant"); + } + + if (auto super = plan.supertypes[i]) { + builder[i].subTypeOf(builder[*super]); + } + builder[i].setOpen(!plan.kinds[i].final); + builder[i].setShared(plan.kinds[i].shared ? Shared : Unshared); + } + auto built = builder.build(); + if (auto* err = built.getError()) { + std::cerr << err->index << ": " << err->reason << "\n"; + ; + } + assert(built); + return std::move(*built); +} + +auto ArbitraryDefinedHeapTypesAndPlan() { + return fuzztest::Map( + [](TypeBuilderPlan plan) { + auto types = BuildHeapTypes(plan); + return std::pair(std::move(types), std::move(plan)); + }, + ArbitraryTypeBuilderPlan()); +} + +void TestBuiltTypes(std::pair, TypeBuilderPlan> pair) { + auto types = std::move(pair.first); + auto plan = std::move(pair.second); + + ASSERT_EQ(types.size(), plan.size); + + auto checkHeapType = [&](HeapTypePlan& plan, HeapType type) { + if (auto* t = plan.getHeapType()) { + EXPECT_EQ(*t, type); + } else if (auto* i = plan.getIndex()) { + EXPECT_EQ(types[*i], type); + } else { + WASM_UNREACHABLE("unexpected variant"); + } + }; + + auto checkRefType = [&](RefPlan& plan, Type type) { + ASSERT_TRUE(type.isRef()); + checkHeapType(plan.type, type.getHeapType()); + EXPECT_EQ(plan.nullable, type.isNullable()); + }; + + auto checkType = [&](TypePlan& plan, Type type) { + if (auto* t = plan.getNonRef()) { + EXPECT_EQ(*t, type); + } else if (auto* r = plan.getRef()) { + checkRefType(*r, type); + } else { + WASM_UNREACHABLE("unexpected variant"); + } + }; + + auto checkField = [&](FieldPlan& plan, Field field) { + EXPECT_EQ(plan.mutable_, field.mutable_ == Mutable); + if (auto* packed = plan.type.getPacked()) { + EXPECT_TRUE(field.isPacked()); + EXPECT_EQ(field.packedType, *packed); + } else if (auto* t = plan.type.getNonPacked()) { + checkType(*t, field.type); + } + }; + + auto checkFunc = [&](FuncPlan& plan, HeapType type) { + ASSERT_TRUE(type.isSignature()); + auto sig = type.getSignature(); + ASSERT_EQ(plan.first.size(), sig.params.size()); + ASSERT_EQ(plan.second.size(), sig.results.size()); + for (size_t i = 0; i < plan.first.size(); ++i) { + checkType(plan.first[i], sig.params[i]); + } + for (size_t i = 0; i < plan.second.size(); ++i) { + checkType(plan.second[i], sig.results[i]); + } + }; + + auto checkStruct = [&](StructPlan& plan, HeapType type) { + ASSERT_TRUE(type.isStruct()); + const auto& fields = type.getStruct().fields; + ASSERT_EQ(plan.size(), fields.size()); + for (size_t i = 0; i < plan.size(); ++i) { + checkField(plan[i], fields[i]); + } + }; + + auto checkArray = [&](ArrayPlan& plan, HeapType type) { + ASSERT_TRUE(type.isArray()); + checkField(plan, type.getArray().element); + }; + + auto checkCont = [&](ContPlan& plan, HeapType type) { + ASSERT_TRUE(type.isContinuation()); + if (plan) { + EXPECT_EQ(types[*plan], type.getContinuation().type); + } + }; + + auto checkDef = [&](TypeDefPlan& plan, HeapType type) { + if (auto* f = plan.getFunc()) { + checkFunc(*f, type); + } else if (auto* s = plan.getStruct()) { + checkStruct(*s, type); + } else if (auto* a = plan.getArray()) { + checkArray(*a, type); + } else if (auto* c = plan.getCont()) { + checkCont(*c, type); + } else { + WASM_UNREACHABLE("unexpected variant"); + } + }; + + for (size_t i = 0; i < plan.size; ++i) { + EXPECT_EQ(plan.kinds[i].shared, types[i].isShared()); + EXPECT_EQ(plan.kinds[i].final, !types[i].isOpen()); + if (auto super = plan.supertypes[i]) { + auto supertype = types[i].getDeclaredSuperType(); + ASSERT_TRUE(supertype); + EXPECT_EQ(types[*super], *supertype); + } else { + EXPECT_FALSE(types[i].getDeclaredSuperType()); + } + checkDef(plan.defs[i], types[i]); + } +} +FUZZ_TEST(TypeBuilderDomainsTest, TestBuiltTypes) + .WithDomains(ArbitraryDefinedHeapTypesAndPlan()); + +} // anonymous namespace + +fuzztest::Domain ArbitraryTypeBuilderPlan() { + // Initialize an abstract type builder plan, then add concrete type definition + // plans. + return fuzztest::FlatMap(StepTypeDefinition, InitConcreteTypeBuilderPlan()); +} + +fuzztest::Domain> ArbitraryDefinedHeapTypes() { + return fuzztest::Map(BuildHeapTypes, ArbitraryTypeBuilderPlan()); +} + +fuzztest::Domain> ArbitraryHeapTypePair() { + return fuzztest::FlatMap( + [](auto types) { + auto typeDomain = fuzztest::OneOf(fuzztest::ElementOf(types), + ArbitraryAbstractHeapType()); + return fuzztest::PairOf(typeDomain, typeDomain); + }, + ArbitraryDefinedHeapTypes()); +} + +} // namespace wasm diff --git a/test/gtest/type-domains.h b/test/gtest/type-domains.h new file mode 100644 index 00000000000..17ad7fe9530 --- /dev/null +++ b/test/gtest/type-domains.h @@ -0,0 +1,171 @@ +/* + * Copyright 2025 WebAssembly Community Group participants + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef wasm_test_gtest_type_domains_h +#define wasm_test_gtest_type_domains_h + +#include "fuzztest/fuzztest.h" +#include "wasm-type.h" + +#ifndef FUZZTEST +#error "BUILD_FUZZTEST should be enabled" +#endif + +namespace wasm { + +inline fuzztest::Domain ArbitraryUnsharedAbstractHeapType() { + return fuzztest::ElementOf({ + HeapTypes::ext, + HeapTypes::func, + HeapTypes::cont, + HeapTypes::any, + HeapTypes::eq, + HeapTypes::i31, + HeapTypes::struct_, + HeapTypes::array, + HeapTypes::exn, + HeapTypes::string, + HeapTypes::none, + HeapTypes::noext, + HeapTypes::nofunc, + HeapTypes::nocont, + HeapTypes::noexn, + }); +} + +inline fuzztest::Domain ArbitrarySharedAbstractHeapType() { + return fuzztest::ReversibleMap( + [](HeapType ht) { return HeapType(ht.getBasic(Shared)); }, + [](HeapType ht) { + return ht.isShared() + ? std::optional{std::tuple{HeapType(ht.getBasic(Unshared))}} + : std::nullopt; + }, + ArbitraryUnsharedAbstractHeapType()); +} + +inline fuzztest::Domain ArbitraryAbstractHeapType() { + return fuzztest::OneOf(ArbitraryUnsharedAbstractHeapType(), + ArbitrarySharedAbstractHeapType()); +} + +inline fuzztest::Domain ArbitraryNonRefType() { + return fuzztest::ElementOf( + std::vector{Type::i32, Type::i64, Type::f32, Type::f64, Type::v128}); +} + +enum UnsharedTypeKind { FuncKind, StructKind, ArrayKind, ContKind }; + +struct TypeKind { + UnsharedTypeKind kind; + bool shared; + bool final; +}; + +struct HeapTypePlan : std::variant { + HeapType* getHeapType() { return std::get_if(this); } + size_t* getIndex() { return std::get_if(this); } +}; + +struct RefPlan { + HeapTypePlan type; + bool nullable; +}; + +struct TypePlan : std::variant { + Type* getNonRef() { return std::get_if(this); } + RefPlan* getRef() { return std::get_if(this); } +}; + +struct FieldTypePlan : std::variant { + Field::PackedType* getPacked() { + return std::get_if(this); + } + TypePlan* getNonPacked() { return std::get_if(this); } +}; + +struct FieldPlan { + FieldTypePlan type; + bool mutable_; +}; + +using FuncPlan = std::pair, std::vector>; +using StructPlan = std::vector; +using ArrayPlan = FieldPlan; +// If there is no available func type definition, this will be nullopt and we +// will have to use a default fallback. +using ContPlan = std::optional; + +struct TypeDefPlan : std::variant { + FuncPlan* getFunc() { return std::get_if(this); } + StructPlan* getStruct() { return std::get_if(this); } + ArrayPlan* getArray() { return std::get_if(this); } + ContPlan* getCont() { return std::get_if(this); } +}; + +struct TypeBuilderPlan { + // Index variable for controlling recursion during construction. + size_t curr; + + // RecGroupPlan contents. + size_t size; + std::vector recGroupSizes; + + // AbstractTypeBuilderPlan contents. + std::vector> supertypes; + std::vector kinds; + + // TypeBuilderPlan contents. + size_t currRecGroup = 0; + size_t numReferenceable = 0; + std::vector defs; + + // Built types. + std::vector types; + + friend std::ostream& operator<<(std::ostream& o, const TypeBuilderPlan& plan); +}; + +static constexpr size_t MaxTypeBuilderSize = 8; +static constexpr size_t MaxParamsSize = 4; +static constexpr size_t MaxResultsSize = 2; +static constexpr size_t MaxStructSize = 8; + +fuzztest::Domain ArbitraryTypeBuilderPlan(); + +fuzztest::Domain> ArbitraryDefinedHeapTypes(); + +fuzztest::Domain> ArbitraryHeapTypePair(); + +// FuzzTest only supports extending the printer via AbslStringify, but we +// usually define operator<< for our custom printing. Add a generic +// implementation of AbslStringify enabled for anything in the wasm namespace +// that implements operator<< as expected. +template constexpr bool type_exists = true; + +template +void AbslStringify( + Sink& sink, + const T& val, + std::enable_if_t, bool> = false) { + std::stringstream ss; + ss << val; + sink.Append(ss.str()); +} + +} // namespace wasm + +#endif // wasm_test_gtest_type_domains_h diff --git a/test/gtest/type-test.h b/test/gtest/type-test.h index f029b8027b1..2eb0e9594bf 100644 --- a/test/gtest/type-test.h +++ b/test/gtest/type-test.h @@ -6,7 +6,6 @@ // Helper test fixture for managing the global type system state. class TypeTest : public ::testing::Test { - protected: void TearDown() override { wasm::destroyAllTypesForTestingPurposesOnly(); } diff --git a/third_party/CMakeLists.txt b/third_party/CMakeLists.txt index b55797db770..fde5276d7da 100644 --- a/third_party/CMakeLists.txt +++ b/third_party/CMakeLists.txt @@ -1,13 +1,17 @@ -if(BUILD_LLVM_DWARF) - add_subdirectory(llvm-project) -endif() - -include_directories( - googletest/googletest - googletest/googletest/include -) - -if(BUILD_TESTS) +if(BUILD_FUZZTEST) + add_subdirectory(fuzztest) +elseif(BUILD_TESTS) + # fuzztest includes gtest, but if we're not building fuzztest, build gtest ourselves. add_library(gtest STATIC googletest/googletest/src/gtest-all.cc) add_library(gtest_main STATIC googletest/googletest/src/gtest_main.cc) + target_compile_options(gtest PRIVATE "-fno-rtti") + target_compile_options(gtest_main PRIVATE "-fno-rtti") + include_directories( + googletest/googletest + googletest/googletest/include + ) +endif() + +if(BUILD_LLVM_DWARF) + add_subdirectory(llvm-project) endif() diff --git a/third_party/fuzztest b/third_party/fuzztest new file mode 160000 index 00000000000..5bbbddfc241 --- /dev/null +++ b/third_party/fuzztest @@ -0,0 +1 @@ +Subproject commit 5bbbddfc241c8c87902d4a80cda5697dd8c20199