Skip to content

[Wasm GC] [GUFA] Add initial ConeType support #5116

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 146 commits into from
Oct 11, 2022
Merged

[Wasm GC] [GUFA] Add initial ConeType support #5116

merged 146 commits into from
Oct 11, 2022

Conversation

kripken
Copy link
Member

@kripken kripken commented Oct 5, 2022

A cone type is a PossibleContents that has a base type and a depth, and it
contains all subtypes up to that depth. So depth 0 is an exact type from
before, etc.

This only adds cone type computations when combining types, that is, when we
combine two exact types we might get a cone, etc. This does not yet use the
cone info in all places (like struct gets and sets), and it does not yet define roots
of cone types, all of which is left for later. IOW this is the MVP of cone types that
is just enough to add them + pass tests + test the new functionality.

This improves the j2wasm binary, but only very slightly.

@kripken
Copy link
Member Author

kripken commented Oct 5, 2022

Also, I think this would be easier to read if the two PossibleContents being combined or intersected were symmetric.

That can be a little less readable, I agree. The idea though behind this convention is that it's very common to do .combine(other) and .intersect(other) etc. where we modify an existing contents, and it avoids depending on the optimizing compiler to avoid a copy.

It might be worth measuring this more in detail and possibly refactoring it, though.

@tlively
Copy link
Member

tlively commented Oct 5, 2022

Looking into both those changes later sounds fine to me.

@kripken
Copy link
Member Author

kripken commented Oct 5, 2022

The idea though behind this convention is that it's very common to do .combine(other) and .intersect(other) etc. where we modify an existing contents, and it avoids depending on the optimizing compiler to avoid a copy.

Oh, btw, as more background, this is common in game engines where such operations on vectors and matrices etc. must be very optimized. It's possible it matters less here...

@tlively
Copy link
Member

tlively commented Oct 5, 2022

The idea though behind this convention is that it's very common to do .combine(other) and .intersect(other) etc. where we modify an existing contents, and it avoids depending on the optimizing compiler to avoid a copy.

Oh, btw, as more background, this is common in game engines where such operations on vectors and matrices etc. must be very optimized. It's possible it matters less here...

I would be very interested to see how much of a difference it makes.

Copy link
Member

@aheejin aheejin left a comment

Choose a reason for hiding this comment

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

Sorry for the delayed reply. Haven't read everything yet, but wonder what would be an example of the case when we know a precise depth for a cone type and what benefit it can bring.

@@ -133,6 +145,11 @@ class PossibleContents {
// contents here will then include whatever content was possible in |other|.
void combine(const PossibleContents& other);

// Removes anything not in |other| from this object, so that it ends up with
// only their intersection. At this only handles an intersection with a full
Copy link
Member

Choose a reason for hiding this comment

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

Not sure what does "At this" mean here... Is that a typo?

Copy link
Member Author

Choose a reason for hiding this comment

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

Sorry, that should be "Atm" for "at the moment"... will fix.

@kripken
Copy link
Member Author

kripken commented Oct 6, 2022

I should have added an example. Something like this:

struct A { field : i32 };
struct B : A;
struct C : B;

a = new A{.field = 42};
b = new B{.field = 42};
c = new C{.field = 1337};

foo((x ? a : b).f);       // => foo(42)
                          // since Cone(A, 1)
                          // includes only A, B 

The input to foo is a cone of limited depth, and that limited depth lets us optimize. If the cone were any larger, we'd include another type which has a different value that prevents optimization.

@tlively
Copy link
Member

tlively commented Oct 6, 2022

FYI I think this will have a merge conflict with #5115 since the handling of nulls in possible-contents needed to change. I don't have a preference for what order they land in, though.

@kripken
Copy link
Member Author

kripken commented Oct 6, 2022

I don't have a preference either. As your PR is much larger, maybe best to let yours land first.


// Returns whether the relevant cone for this, as computed by getCone(), is of
// full size, that is, includes all subtypes.
bool hasFullCone() const { return getCone().depth == FullDepth; }
Copy link
Member

Choose a reason for hiding this comment

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

FullCone sounds good to me.

Comment on lines +112 to 117
if (!isNull()) {
value = mixInNull(getCone());
return;
} else if (!other.isNull() && other.hasExactType()) {
value = ExactType(Type(otherType.getHeapType(), Nullable));
} else if (!other.isNull()) {
value = mixInNull(other.getCone());
return;
Copy link
Member

Choose a reason for hiding this comment

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

What if the heap type of the two types don't have a lub?

Copy link
Member Author

Choose a reason for hiding this comment

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

Good catch, yeah, looks like this "raced" with the null type changes that just landed. Fixed.

Comment on lines 137 to 138
// TODO: we could make a single loop that also does the LUB, at the same
// time, and also avoids calling getDepth() which loops once more?
Copy link
Member

Choose a reason for hiding this comment

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

Why do we need a loop?

Copy link
Member Author

Choose a reason for hiding this comment

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

getDepth() does a loop, basically it goes up the chain of supertypes until the end. So each getDepth() here is a loop, sadly.

Comment on lines 144 to 146
Index depthUnderLub = depthFromRoot - lubDepthFromRoot + depth;
Index otherDepthUnderLub =
otherDepthFromRoot - lubDepthFromRoot + otherDepth;
Copy link
Member

Choose a reason for hiding this comment

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

Does a heap type have a unique depth?

It's been a while since I last looked at wasm-type.cpp, so I may lack the context.

size_t HeapType::getDepth() const {
size_t depth = 0;
std::optional<HeapType> super;
for (auto curr = *this; (super = curr.getSuperType()); curr = *super) {
++depth;
}
// In addition to the explicit supertypes we just traversed over, there is
// implicit supertyping wrt basic types. A signature type always has one more
// super, HeapType::func, etc.
if (!isBasic()) {
if (isFunction()) {
depth++;
} else if (isData()) {
// specific struct types <: data <: eq <: any
depth += 3;
}

Here we follow supertypes using a loop and then add +1 if it is a function and +3 if it is a data. But is it possible for one of the supertypes traversed by the for loop is function or data? In that case, is the depth unique?

Copy link
Member Author

Choose a reason for hiding this comment

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

getSuperType only returns user types atm. So it never returns a basic type like data. (This might be worth improving, but I looked into it and it wasn't trivial.)

The depth should be unique, and is defined to include basic supertypes in the count.

auto isSubType = HeapType::isSubType(heapType, otherHeapType);
auto otherIsSubType = HeapType::isSubType(otherHeapType, heapType);
if (!isSubType && !otherIsSubType) {
setNoneOrNull();
Copy link
Member

Choose a reason for hiding this comment

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

Can this be null, given that there's no bottom type (yet)?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, looks like this needs updating 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.

Done.

// An interesting non-empty intersection that is a new cone which differs from
// both the original ones. (This must be an intersection of cones, since by
// assumption |other| is a cone, and another cone is the only shape that can
// have a non-empty intersection with it that differs from them both.)
Copy link
Member

Choose a reason for hiding this comment

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

Why should this differ from them both? We handled the case of isSubContents(other, *this) above, but can't this be isSubContents(*this, other), so the intersection is just *this? (The code seems to handle this case though)

Copy link
Member Author

Choose a reason for hiding this comment

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

Correct, thanks. This comment was stale.


auto type = getType();
auto otherType = other.getType();
auto heapType = type.getHeapType();
Copy link
Member

Choose a reason for hiding this comment

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

The case of None and Many are handled above, but can this be a Literal or Global? In that case, what it doesn't have a heap type?

Copy link
Member Author

Choose a reason for hiding this comment

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

This does look unclear, I'll add a comment. The reason is the intersection with another reference type is not empty, so this must be a reference type itself.

// returns 1 if the input is of a subtype of the intended type, that is,
// we are looking for a type in that cone of types.
auto intendedContents =
PossibleContents::fullConeType(Type(curr->intendedType, NonNullable));
Copy link
Member

Choose a reason for hiding this comment

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

Would be helpful to have a short comment on why this is NonNullable (i.e., otherwise it can trap)

Copy link
Member Author

Choose a reason for hiding this comment

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

Added.

assertCombination(exactFuncref, exactAnyref, many);
assertCombination(exactFuncref, anyGlobal, many);
assertCombination(exactFuncref, nonNullFunc, many);
assertCombination(exactFuncref, nonNullFunc, coneFuncref1);
Copy link
Member

Choose a reason for hiding this comment

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

Why is nonNullFunc a subtype depth 1 of funcref? It looks we treat all signatures in the same way. Is that gonna be true after implementing the real subtyping for functions?

bool SubTyper::isSubType(const Signature& a, const Signature& b) {
// TODO: Implement proper signature subtyping, covariant in results and
// contravariant in params, once V8 implements it.
// return isSubType(b.params, a.params) && isSubType(a.results, b.results);
return a == b;
}

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 think that's right, this is the current temporary situation. @tlively would know for sure.

Comment on lines +287 to +293
#if BINARYEN_TEST_DEBUG
if (!PossibleContents::haveIntersection(a, b) ||
!PossibleContents::haveIntersection(b, a)) {
std::cout << "\nFailure: no intersection:\n" << a << '\n' << b << '\n';
abort();
}
#endif
Copy link
Member

Choose a reason for hiding this comment

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

Why do we need this? If this is the case, doesn't this crash in EXPECT_TRUE above anyway?

Copy link
Member Author

Choose a reason for hiding this comment

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

EXPECT_TRUE doesn't print out the values being operated on, so we just get "expected true on line 286" and that's it. We don't even know which call to this function caused the problem. The extra logging makes it easy to debug these.

@kripken kripken enabled auto-merge (squash) October 11, 2022 20:09
@kripken kripken merged commit 5129f88 into main Oct 11, 2022
@kripken kripken deleted the cone branch October 11, 2022 20:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants