Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions src/ir/glbs.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* 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_ir_glbs_h
#define wasm_ir_glbs_h

#include "ir/module-utils.h"
#include "wasm.h"

namespace wasm {

//
// Similar to LUBFinder, but for GLBs.
//
struct GLBFinder {
GLBFinder() {}

GLBFinder(Type initialType) { note(initialType); }

// Note another type to take into account in the GLB.
void note(Type type) {
// We only compute GLBs of concrete types in our IR.
assert(type != Type::none);

if (type != Type::unreachable) {
if (glb == Type::unreachable) {
// This is the first thing we see.
glb = type;
} else {
glb = Type::getGreatestLowerBound(glb, type);
// If the result is unreachable, when neither of the inputs was, then we
// have combined things from different hierarchies, which we do not
// allow here: We focus on computing GLBs for concrete places in our IR.
assert(glb != Type::unreachable);
}
}
}

// Returns whether we noted any (reachable) value.
bool noted() { return glb != Type::unreachable; }

// Returns the GLB.
Type getGLB() { return glb; }

// Combines the information in another GLBFinder into this one, and returns
// whether we changed anything.
bool combine(const GLBFinder& other) {
// Check if the GLB was changed.
auto old = glb;
note(other.glb);
return old != glb;
}

private:
// The greatest lower bound. As we go this always contains the latest value
// based on everything we've seen so far.
Type glb = Type::unreachable;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should use Type::none as the initial value, since it behaves very similarly to a top type whereas Type::unreachable is the bottom type. (This still requires some special casing, since Type::getGLB does not treat Type::none as top; we should consider fixing that to make Type::getLUB and Type::getGLB more consistent with each other.)

Copy link
Member Author

@kripken kripken Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unreachable happens to be the bottom type in the lattice, but in this API, unreachable is the "uninitialized / we have seen no reachable data yet" marker. We ignore inputs of unreachable, and we start in that state.

That is, we want to ignore unreachable data - it does not constrain us - so GLBFinder.note(unreachable) should do nothing, not turn us into the bottom.

Avoiding that is consistent with LUBFinder and seems like the most intuitive API here to me.

(For similar reasons, using ValType's meet would be more work here, I found as I tried that now.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the GLB/meet of any type t with bottom (i.e. unreachable) is t, so you should get the "ignore unreachable" behavior for free, right? All you have to do is use none as the "uninitialized / we have seen no reachable data yet" marker.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait, isn't it the opposite? Meet refines, towards the bottom, using the GLB. So the GLB/meet of any type t with bottom (i.e unreachable) is bottom (unreachable).

Here is that in the code:

if (!a.isRef() || !b.isRef()) {
return Type::unreachable;
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh right, I was confused.

};

} // namespace wasm

#endif // wasm_ir_glbs_h
2 changes: 1 addition & 1 deletion src/ir/lubs.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ struct LUBFinder {

private:
// The least upper bound. As we go this always contains the latest value based
// on everything we've seen so far, except for nulls.
// on everything we've seen so far.
Type lub = Type::unreachable;
};

Expand Down
16 changes: 7 additions & 9 deletions src/tools/fuzzing/fuzzing.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

#include "tools/fuzzing.h"
#include "ir/gc-type-utils.h"
#include "ir/glbs.h"
#include "ir/iteration.h"
#include "ir/local-structural-dominance.h"
#include "ir/module-utils.h"
Expand Down Expand Up @@ -1894,7 +1895,7 @@ void TranslateToFuzzReader::mutate(Function* func) {
// Maps children we can replace to the types we can replace them with. We
// only store nontrivial ones (i.e., where the type is not just the child's
// type).
std::unordered_map<Expression*, Type> childTypes;
std::unordered_map<Expression*, GLBFinder> childTypes;

// We only care about constraints on Expression* things.
void noteSubtype(Type sub, Type super) {}
Expand All @@ -1904,9 +1905,8 @@ void TranslateToFuzzReader::mutate(Function* func) {
// The expression must be a supertype of a fixed type. Nothing to do.
}
void noteSubtype(Expression* sub, Type super) {
if (super.isRef() && sub->type != super) {
// This is a nontrivial opportunity to replace sub with a given type.
childTypes[sub] = super;
if (super.isRef()) {
childTypes[sub].note(super);
}
}
void noteSubtype(Expression* sub, Expression* super) {
Expand Down Expand Up @@ -1962,14 +1962,12 @@ void TranslateToFuzzReader::mutate(Function* func) {
// Find the type to replace with.
auto type = curr->type;
if (type.isRef()) {
auto iter = finder.childTypes.find(curr);
if (iter != finder.childTypes.end()) {
type = iter->second;
auto& glb = finder.childTypes[curr];
if (glb.noted()) {
type = glb.getGLB();
// We can only be given a less-refined type (certainly we can replace
// curr with its own type).
assert(Type::isSubType(curr->type, type));
// We only store an interesting non-trivial type.
assert(type != curr->type);
}
}

Expand Down
1 change: 1 addition & 0 deletions test/gtest/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ set(unittest_SOURCES
cfg.cpp
dfa_minimization.cpp
disjoint_sets.cpp
glbs.cpp
interpreter.cpp
intervals.cpp
json.cpp
Expand Down
46 changes: 46 additions & 0 deletions test/gtest/glbs.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* 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 "ir/glbs.h"
#include "wasm-type.h"
#include "wasm.h"
#include "gtest/gtest.h"

using namespace wasm;

TEST(GLBsTest, Basics) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be good to test the edge cases around Type::none and Type::unreachable as well.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added testing for one corner case, and assertions for others that it seems better to just disallow as invalid.

GLBFinder finder;

// Nothing noted yet.
EXPECT_FALSE(finder.noted());
EXPECT_EQ(finder.getGLB(), Type::unreachable);

// Note a type.
Type anyref = Type(HeapType::any, Nullable);
finder.note(anyref);
EXPECT_TRUE(finder.noted());
EXPECT_EQ(finder.getGLB(), anyref);

// Note another, leaving the more-refined GLB.
Type refAny = Type(HeapType::any, NonNullable);
finder.note(refAny);
EXPECT_TRUE(finder.noted());
EXPECT_EQ(finder.getGLB(), refAny);

// Note unreachable, which changes nothing (we ignore unreachable inputs).
finder.note(Type::unreachable);
EXPECT_EQ(finder.getGLB(), refAny);
}
Loading