Skip to content
This repository has been archived by the owner on Nov 3, 2021. It is now read-only.

Make funcref not a subtype of anyref #69

Closed
RossTate opened this issue Dec 21, 2019 · 65 comments
Closed

Make funcref not a subtype of anyref #69

RossTate opened this issue Dec 21, 2019 · 65 comments

Comments

@RossTate
Copy link

(This idea came up after yesterday's discussion about the GC extension. I have tried to describe it here in a self-contained matter, but let me know if there are any terms I forgot to define or motivations I forgot to provide.)

Having funcref be a subtype of anyref forces the two to have the same register-level representation. Yet there are good reasons why an engine might want to represent a function reference differently than an arbitrary reference. For example, function references might always be an assembly-code pointer paired with a module-instance pointer, effectively representing the assembly code compiled from a wasm module closed over the global state of the specific instance the function reference was created from. If so, it might make sense for an engine to use a fat pointer for a function reference. But if funcref is a subtype of anyref, and if it overall makes sense for arbitrary references to be implemented with normal-width pointers, then that forces function references to be implemented with normal-width pointers as well, causing an otherwise-avoidable additional memory-indirection in every indirect function call.

Regardless of the reason, by making funcref not a subtype of anyref, we give engines the flexibility to represent these two types differently (including the option to represent them the same). Instead of subtyping, we could have a convert instruction that could take a function reference and convert it into an anyref representation, or more generally could convert between "convertible" types. The only main benefit of subtyping over conversion in a low-level type system is its behavior with respect to variance, such as co/contravariance of function types, but I see no such application for funcref and anyref. And in the worst case, we could always making funcref a subtype of anyref later if such a compelling need arises.

@lukewagner
Copy link
Member

To add to this, one change in my understanding of the constraint space is that, in earlier design thinking, there was an expectation that anyref would serve as the universal representation of reference types in the presence of source language polymorphism that got erased when compiling down to wasm, and this necessarily required anyref to be the (non-coercive) supertype of all reference types. This design included the ability to downcast from anyref directly to any other type (given the appropriate type tag; all of this in the context of the GC proposal). However, more recent thinking wrt how best to support dynamic downcast suggests that anyref probably isn't the right universal representation type and that, instead, the universal representation type for a particular language/toolchain should be some ABI-defined wasm type that explicitly declared more details of what was downcastable and how. There are multiple ideas in flight here, which I expect will be hashed out over the coming months, but the common theme seems to be moving away from anyref as the universal representation.

Thus, in the short term, I think we should avoid unnecessarily committing to funcref <: anyref in the reference-types proposal, which is nearing stage 4 and shipping. Implementation-wise, this should be a fairly simple change to make, I believe.

The win from not unnecessarily requiring funcref <: anyref is that, as Ross explained, it buys us more implementation freedom. I actually think an AOT compilation model of type imports will require all references to be pointer-width (otherwise a VM would have to wait until instantiation time to know the number of bytes to shuffle around for a (ref $TypeImport)). However, what I think dropping funcref <: anyref would buy us is the ability to have different word-sized representations of funcref vs. anyref that didn't require them to share a single boxing format. (This assumes that the GC can either use arena or instance data to trace (ref $TypeImport) words in the generically-compiled module code.) In particular, some recent (and I think essential for a high-performance function-reference implementation) ABI changes we're planning for SM would be simpler and more efficient if funcref could have a pointer-sized, but completely different representation from anyref.

@rossberg
Copy link
Member

rossberg commented Jan 8, 2020

@lukewagner, can you explain under which scenario you would be able to allow different tagging strategies for function references but not different sizes?

Because I see only two possibilities: either the universes of funcrefs and other refs are always statically distinguishable, in which case they can also have different sizes. Or there are cases where you cannot tell them apart statically, in which case you'll also need compatible tagging schemes.

So, AFAICS, to allow different representations you have to create two properly disjoint type hierarchies. That is certainly possible, but is a less trivial change than just removing the funcref <: anyref rule. You would need to make sure that there are no common super types, but also no common subtypes and values. One immediate implication, for example, is that you'd need to distinguish different null values and types, and thus modify or complement the ref.null instruction. That in turn would not just affect the reftype proposal but also the bulk memory proposal. There may be other such implications, we'd have to think it through carefully.

Another question is how this interacts with the host. You can pass Wasm functions to JS and JS functions to Wasm. Both would necessarily become a coercive operation. That works in the first-order case, but does not compose. E.g. once you want to allow Typed Values over functions in JS you'd have to distinguish e.g. arrays of JS functions from arrays of Wasm functions. That is, you would need to introduce two disjoint universes of function types for Typed Values. (Maybe you're already assuming this?)

The last time separating funcrefs from anyref was discussed (I think at the Lyon meeting) the consensus was that the added language complexity was not worth it for the time being, and that we can always add a second form of funcref type later if there is evidence that it is sufficiently useful.

Btw, regarding uniform representations, I think we have thrown up some vague ideas for possible alternatives, but AFAICT nobody has worked them out and they'll likely be beyond MVP level. It seems premature to assume that we won't need anyref. If we divorced funcrefs then additional boxing may become necessary in client code (which may not be a problem, however).

@RossTate
Copy link
Author

RossTate commented Jan 9, 2020

Yeah, we discussed the issue with null too, concretely discussing how a universal nullref type would block engines out of using fat pointers where appropriate. I also mentioned here that we were finding, contrary to our original expectations, that nullref should be parameterized.

@rossberg
Copy link
Member

I guess it boils down to the question: do we reconsider "fat function refs" to be an MVP feature or can we add it later as a separate type? If the former, I agree that we should do so quickly, so that we can move forward at the meeting.

@RossTate
Copy link
Author

Well, there's another question: what are the pressing use cases of having funcref be a subtype of anyref in the MVP? (Use cases that aren't addressed by having funcref be convertible to anyref.)

@rossberg
Copy link
Member

@RossTate, I don't know about pressing, but I can think of a number of softer reasons: it makes for a somewhat simpler language, gives more seamless host interop (at least on the web and in the C API), avoids redundant annotations in element segments (for which we may want to come up with a solution otherwise), and of course, avoids late phase churn for a number of implementations.

@RossTate
Copy link
Author

I disagree on the simpler language argument. People argued for F-bounded polymorphism on the same basis that having everything be done through subtyping would be simpler. Instead they got undecidability and unnecessary complexity that crippled tooling.

As for late-phase churn, this should only require a change in the type-checker, and I imagine removing a subtyping is fairly straightforward.

Can you go into more depth on one of the other reasons?

@rossberg
Copy link
Member

rossberg commented Jan 14, 2020

@RossTate, it requires changes/extensions to the instruction set, see ref.null, and the type language, see nullref. It also requires observable changes to current APIs, where some of these are reflected. For the C API, for example, the changes would go deeper, because that currently models a simple uniform ref space, and externval's are just refs. Removing that possibility (as opposed to just complementing it) requires a redesign of that part, replacing it with something more complicated.

Can all be done, no question. But clearly not free or cheap. I'd be interested in hearing from @lukewagner how important he considers having this for the first version, and whether it should fully replace or merely amend the current design.

(I don't follow which similarity to F-bounded quantification you see.)

@RossTate
Copy link
Author

F-bounded polymorphism is essentially a way of encoding a limited form of type classes into subtyping. In particular, X <: Comparable<X> ensures via subtyping that values of X have a compareTo(X) method. This was considered "simple" because it repurposed the existing subtyping feature. However, because subtyping is an extremely critical feature that interacts heavily with other features of the language, this "simplicity" made much of type-checking, even subtyping itself, undecidable, and broke much of the tooling for languages that added this feature.

Of course, the actual use case this was meant to address, i.e. the binary-method problem, could have just as easily been addressed by introducing a new kind of type constraint. X satisfies Comparable<X> similarly ensures that X has all the methods of Comparable<X> but without making X a subtype of Comparable<X>. Although this design adds a new kind of constraint, it also obliviates the need for recursive subtyping and recursive class/interface hierarchies, keeping the language as a whole much simpler.

This satisfies design is theoretically less expressive than the F-bounded design. That is because subtyping interacts with things like variance of functions and data structures. But it turns out there are not actually real use cases for that difference in functionality. So in practice, the two designs are equivalently expressive, with one being much simpler to implement even if its grammar is slightly more complex. The satisfies design also turns out to be more flexible in what kinds of implementations and extensions it permits.

So the analogy is that the subtyping funcref <: anyref seems to have been added because it was the way to make funcref convertible to anyref requiring the fewest changes to the grammar. But you have yet to give me a use case where the additional expressiveness this provides over just a conversion instruction actually enables more useful WebAssembly programs. So I worry that you are permanently saddling the type system with additional complexity and limiting flexibility in the implementation strategies without any real benefit (besides the fact that it will take a relatively small amount of effort to correct the design) due to a faulty sense of simplicity, much like what happened with F-bounded polymorphism.

@rossberg
Copy link
Member

I'm aware of the risks of F-bounded quantification if taken too far, but come on, there is no similarity with the issue at hand. Its impact on the type system and its meta theory is negligible either way. If anything, establishing disjoint type hierarchies is a marginal complication to the meta theory, since you'd be under obligation to prove a little lemma that verifies that property.

I have given you reasons above, like fewer and simpler instructions, simpler and more uniform host APIs, etc. Nothing earth-shattering, but arguably an appropriate default for an MVP, unless there are crucial problems that the MVP has to address or it precludes later addition. Evidence for the former would be it being a roadblock for engines.

@lukewagner
Copy link
Member

@rossberg (sorry for the slow reply) If a type is imported with no bounds:

(module
  (import "file" "handle" (type $Handle))
  (func $f (param (ref $Handle)) ...)
)

then $Handle can be either anyref or funcref and so if we want to compile $f AOT (and never recompile), we need all ref's to have the same byte width because we don't yet know whether we have an anyref subtype or a funcref subtype. However, because there is no bound, there are no explicit operations in $f that need to dynamically distinguish an anyref from a funcref, so they don't need to share a boxing/tagging format. (There is still, however, the implicit operation of marking a (ref $Handle) value found on the stack during GC, but that can be a somewhat slower operation (which, e.g., determines $Handle using the frame's containing instance's import table, which will take 10s of cycles) compared to a the core wasm GC instructions where every cycle counts.)

On a side note: if we wanted to allow different byte widths without recompilation, we could perhaps always require type imports to have a bound, and so you'd always know if you had an anyref subtype or a funcref subtype, and then they could have distinct byte-widths. I don't have a good sense of the pros/cons of this yet option, though.

@rossberg
Copy link
Member

rossberg commented Feb 3, 2020

@lukewagner, not sure that could work in general. Depending on how much code generation in a given engine has to know about GC, it probably will need to know what kind of tagging e.g. a local of that type uses, e.g., to generate suitable stack frame descriptors, or inlined bits of GC code, etc. It's safer to assume that a compiler always needs to know the representational interpretation of any value it has to handle.

But in any case, the type import proposal already defines what you suggest: all imports have a bound -- given subtyping, there's no advantage in having an unbounded form.

@lukewagner
Copy link
Member

@rossberg Ah, in that case, then I agree that, if funcref is not a subtype of anyref, it would allow funcrefs to have a completely different representation (including byte width) than anyrefs.

@binji
Copy link
Member

binji commented Feb 13, 2020

We discussed this in the Feb 2020 CG meeting. We didn't come to any conclusion, but there was some agreement that breaking the subtype relationship between anyref and funcref would be more conservative.

@tlively
Copy link
Member

tlively commented Feb 14, 2020

Given that multiple ecosystems (Emscripten, Rust, WASI) eagerly want anyref support but don't particularly need funcref right now, I would like to either resolve the subtype question quickly or revisit @fitzgen's idea of splitting the proposal so that anyref can move forward alone without being held back by this issue. I understand that the semantics would be ugly in the interim, but it would be shame to hold up these ecosystems for a temporary cosmetic issue.

Of course, there would be much less overhead if we could agree to a solution to the subtype question soon and not have to split the proposal. I lean heavily toward not having a subtype relationship between funcref and anyref right now since it is both the simpler conservative option and because I do not know of any immediate usecase for that subtype relationship. Having type-parameterized nullrefs sounds perfectly acceptable to me, since it will make the tooling simpler anyway. I am also not too worried about the size impact for table initialization since a sequence of identical parameterized nullrefs sounds like it will compress easily.

@RossTate
Copy link
Author

RossTate commented Feb 14, 2020

I believe I've now realized another problem with the "any" notion of anyref (as opposed to crossref or extref or what not). This last meeting was super helpful in catching me up on WebAssembly more broadly, and in particular @aheejin's great talk was a great introduction to the plan for exception handling. But I noticed something: exnref is heap-allocated memory that has to be collected. Now, due to the nature of exceptions, exnref cannot form cyclic data structures (except through higher already-cyclic constructs as noted in WebAssembly/exception-handling#85). As such, you can use reference counting to track exnref. Or at least you would like to...

Unfortunately, there seems to be a problem when exnref is a subtype of anyref. I'm guessing y'all aren't planning on reference-counting all anyrefs, given that they're expected to be able to reference components in cyclic data structures. So, if exnref is a subtype of anyref, that means you're not going to be counting all exnrefs. Even weakening exnref to be just convertible to anyref is problematic for similar reasons. And, yes, this is addressable by heavy-handed machinery (using a bit flag in the pointer to distinguish cyclic and reference-counted anyrefs, and/or using an ∞ count when an exnref is explicitly converted to an anyref), but given the lack of real use cases for exnref <: anyref all that machinery (and overhead?) seems not worth it. In general, it seems like reference types that could otherwise be guaranteed to be reference-counted should not be convertible to anyref.

@tlively
Copy link
Member

tlively commented Feb 14, 2020

So, if exnref is a subtype of anyref, that means you're not going to be counting all exnrefs

Can you dig into that a bit and explain why exnref being a subtype of anyref means that some exnref values can't be reference counted?

@RossTate
Copy link
Author

RossTate commented Feb 14, 2020

Because then my code can hand off an exnref to some other code that takes an anyref, and that other code will have no idea that it needs to reference-count that anyref. So either exnref is collected through some means besides reference counting, or all code reference-counts all anyrefs (or at least always goes through the hassle of branching on a bit flag indicating whether the anyref is reference counted).

@rossberg
Copy link
Member

@RossTate, the possibility of reference counting is a fallback for engines that do not have GC, e.g., ones that do not run within JS and neither intend to implement the GC proposal. An engine that already incorporates GC refs wouldn't be using reference counting for exnrefs.

@RossTate
Copy link
Author

We could debate that detailed point, but I think the higher-level point stands regardless: there is utility in enabling engines/programs to keep internal and external references separate.

@rossberg
Copy link
Member

I'm not sure what you have in mind. Exnref's can be passed to the host like any other reference. In a JS environment there is not much choice but to manage all of them with proper GC.

@tlively
Copy link
Member

tlively commented Feb 19, 2020

I could imagine a non-JS engine that would want to allow users to disable its GC support (maybe to reduce the code size footprint and memory usage in an embedded context). Such an engine would want to keep as much functionality enabled without GC as possible, so it might very well choose to use reference counting for exnrefs, and for simplicity it might want to use reference counting for exnrefs even when GC is enabled. I don't think this is an outlandish scenario, and I have yet to see a use case for the subtype relationship between exnref and anyref, so I support decoupling these as well.

@rossberg
Copy link
Member

@tlively, exnrefs can point to GC refs and vice versa. Once you have GC refs, exnrefs can indeed participate in cycles, unlike without. So no, a heterogeneous implementation would be very outlandish.

@lukewagner
Copy link
Member

After some follow-up discussion with Lars and others, I'd like to drop my earlier objection and say that I'm fine with the proposal as-is.

In my first comment, I observed that, if anyref ends up not supporting direct downcast to all subtypes, anyref wouldn't be the "universal representation" for polymorphic languages and this drops the constraint of having funcref be a subtype of anyref. However, type imports also want a single supertype (so a module can just import any T <: any, without excluding or requiring boxing of function references). Thus, even if we made funcref not be a subtype of anyref in this proposal, I think we'd add the subtyping relationship soon after and the churn created by modifying this proposal (messing with ref.null etc) at this late stage, when we're otherwise ready to ship, would have been unnecessary.

(I do think it would be valuable to have some value type that can simply hold an unboxed raw code pointer (without any fancy thunky optimizations), but, independent of whether funcref is a subtype of anyref, funcref is not a great candidate to be this raw-code-pointer type due to it already being, in the MVP, a closure over an instance and doubly so with func.bind.)

@binji
Copy link
Member

binji commented Mar 20, 2020

OK, let's try to move this forward. It sounds like at least for Luke, Lars and others there's less incentive to change the proposal. Let's discuss this at the next CG meeting.

@RossTate
Copy link
Author

RossTate commented Mar 27, 2020

In preparation for the meeting, here is a summary of my arguments for removing subtyping:

  1. After 3 months, we still have no compelling or thorough example of how this subtyping would be useful.
  2. According to exnrefs are nominal gc#85 (comment), the primary purpose of anyref is to avoid making assumptions about what a specific reference type in situations where you can't or don't need to know. Having funcref be a subtype of anyref is making an assumption about that reference type, contradicting anyref's primary purpose to represent external references. This means that WASI cannot assume all anyrefs are capabilities, and browsers cannot assume all anyrefs are JavaScript/DOM values, even when those are the only external reference values, despite those being the primary use cases for anyref.
  3. Subtyping is notorious for significantly and unpredictably complicating every aspect of language design and implementation. The proposal has made a bunch of permanent design choices with regards to subtyping in general, but without any actual programs using subtyping, we have no way of knowing if these were the right or wrong design choices. In fact, having myself actually worked out in-depth how various languages might take advantage of subtyping in WebAssembly, I found that the wrong design choice was made in critical places.
  4. The only known low-level language technology that is able to eliminate all(*) superfluous run-time casts and bounds checks is unfortunately also known to be incompatible with the specific choices of subtyping rules proposed here. So this subtyping might forever preclude WebAssembly from doing something it actually has a compelling motivation for. (For example, the only known practical tech for supporting the more efficient compilation in Flow Sensitivity multi-value#42 (comment) is incompatible with the proposed subtyping.) For a sense of scale, we found that superfluous casts just on this due to dynamic dispatch (so not counting casts due to weak support for arrays) would add over 100 million casts in each execution of the DaCapo benchmark suite, averaging 4 casts every microsecond (assuming casts had no overhead). At this point it is hard to anticipate what the performance overhead of these added casts would be.
  5. The proposed subtyping needlessly restricts the implementation of WebAssembly constructs in ways that we have yet to measure the impact of.
  6. No comparable technology has anything comparable to the proposed subtyping rules, including even technologies that had the same "unifying" goal the proposed subtyping rules are meant to help achieve.

Lastly, regarding pragmatics, this change is easy to implement: comment out the implementation of is_subtype and replace it with a call to is_equal. Also, we can always add subtyping later if we ever discover it to be useful (in which case you can simply uncomment your code if you've already implemented this feature). There will be work for the spec, but that is something I can take on rather than burden y'all with.

@RossTate
Copy link
Author

RossTate commented Mar 31, 2020

I think, fundamentally, the argument for the proposed subtyping is based on a vision for anyref that has been neither fleshed out nor evaluated to a sufficient extent to warrant committing WebAssembly to and that is not relevant to the reason people want this proposal which is just external references.

@binji
Copy link
Member

binji commented Mar 31, 2020

Lastly, regarding pragmatics, this change is easy to implement: comment out the implementation of is_subtype and replace it with a call to is_equal.

I think a lot of us are stuck on this point currently. In the meeting today @rossberg mentioned how much work this would require to the spec, and mentioned that the work required for downstream proposals is unclear. I think it would be valuable for us to try our best to evaluate what kind of changes would be required for proposals that depend on this change: at least exception-handling, bulk-memory, function-references, type-imports, interface-types, wasm-c-api, and WASI AFAIK.

@tlively
Copy link
Member

tlively commented Apr 3, 2020

I see, your examples make sense to me now. Having started reading the iTalX paper, I also see that this is a problem if you are trying to do the deferred type inference approach used in that project. I will have to think more about how we could fit deferred type inference into the WebAssembly LLVM backend and how much functionality it could buy us.

It's a little hard to say anything definite about the typed function references proposal given that the provided type grammar cannot express the types used in the examples or in even basic rules like func.ref; I believe the constype grammar is supposed to let func have an optional (param t*) (result t*) argument.

FWIW, the <typeidx> variant of constype can refer to function signatures as they exist today in the type section, so that's the mechanism for specifying the params and results.

@rossberg
Copy link
Member

rossberg commented Apr 3, 2020

Too much back and forth in this thread to answer everything individually, but I'll try to clarify a few high-level points (most of which is just reiterating what I said in the meeting yesterday, also see slides for some additional details).

  • Purpose

The addition of subtyping and anyref is part of a larger design that we ended up splitting into several proposals to be able to ship incrementally, like function references and type imports. Basic subtyping ended up in this proposal mostly for technical reasons (cleanest way for slicing the semantics into increments), so it's admittedly a bit less obvious why we have it. But downstream use cases like type imports or typing of reference equality, as currently proposed, fundamentally rely on both.

  • Cost

As others have pointed out as well, the change proposed here is not small, because it has a number of design implications on this proposal, as well as various others.

Across all affected implementations, tools, and downstream proposals, I'd estimate it is in the order of several man weeks to man months of work. That is a high cost for delaying a decision that we will have to resolve in a couple of months anyway.

And we would be introducing some permanent design warts on the way. For example, the need for type-indexed null values (which are awkward from a semantic perspective, e.g., because (null T) and (null U) are seemingly different values, but if T <: U they must be treated as if they were the same).

  • Design risks

For something large and in practical use, there is no alternative to an incremental design approach. So far, Wasm has been successful with that. That means that, even when trying to stay conservative as much as possible, we have to commit to certain design choices at some point. Of course, that always involves some amount of uncertainty and the risk of introducing design mistakes at some step. We had a few ones in the past, but so far have been able to work around them successfully.

It's clear that we'll need subtyping. If not now, then 1-2 months down the road, with function references (which are about ready to be advanced to stage 2 or 3). It seems very unlikely that we are dropping subtyping altogether, given that no concrete alternative has been proposed for all its uses, and there is plenty of pressure to move forward.

The concrete subtyping rules in this proposal are very standard and an absolute bare minimum, they are even orthogonal to structural vs nominal. I haven't seen any convincing evidence that they impose a concrete risk, given the explicit nature of typing in Wasm. (In particular, any form of inference, if desired, will be confined to a tooling stage. That means that it is operating on a different language anyway, with a different type system. It can just as well choose to restrict it or add other annotations in any way it sees fit.)

  • Uniform reference representation

It's been a basic assumption from the start that heap references support a unified representation in a Wasm engine. That's a very reasonable assumption, too, given that all existing Wasm engines as well as the vast majority of other GC runtimes employ it -- off-hand, I wouldn't know any prominent counter example.

Such an assumption simplifies various design aspects as well as interfacing with an engine (e.g., through the C API). It is essential if you want to support compilation against imported reference types without making arbitrary design decisions about which reference types are allowed.

This does not preclude the later addition of specialised flat/raw/unboxed reference types! But even in their presence, you'd still want the uniform variant. Hence, for the MVP, starting with just that is the obvious choice.

  • Function subtyping

As one instance of staying conservative, we do not extend subtyping to functions yet. That leaves headroom for different choices downstream. If we find that we can't efficiently implement dynamic type checks modulo subtyping (which is not unlikely) then we impose suitable static restrictions.

One known approach would be to distinguish between subtypable and exact function types (an approach that @RossTate is aware of, since it's used in his own paper). Either way, we obviously need to ensure that the semantics is coherent, i.e., that any subtyping rules are the same at validation, link and run time.

@tlively
Copy link
Member

tlively commented Apr 3, 2020

Thanks for laying that all out so clearly, @rossberg!

Basic subtyping ended up in this proposal mostly for technical reasons... But downstream use cases like type imports or typing of reference equality, as currently proposed, fundamentally rely on both.

As far as I can see, type imports only depends on funcref <: anyref insofar as it is desirable to be able to have effectively unbounded imports. I can see that this would be elegant, but it's not clear to me that it is useful. I don't want to litigate this trade off here, but please let me know if I am missing some subtlety that makes funcref <: anyref more important. Also, can you point me to discussion or specification of the proposed reference equality? I haven't seen anything on that.

It's been a basic assumption from the start that heap references support a unified representation in a Wasm engine. That's a very reasonable assumption, too, given that all existing Wasm engines as well as the vast majority of other GC runtimes employ it.

The argument about existing Wasm engines biases heavily toward web engines, which might make different choices about their representations because they already support JavaScript objects. I could easily see a non-web engine wanting to support anyref and funcref but not wanting to support the full GC proposal. If that's a situation we want to encourage, we may not want to limit ourselves to precedents in GC runtimes.

This does not preclude the later addition of specialised flat/raw/unboxed reference types!

True, but if they turn out to be sufficient for real use cases, it would be much better to have only the unboxed reference types than have to duplicate everything to have it both ways. And if real languages need both after all, it would be better to start with the one that is more immediately useful. We don't know any immediate use cases for subtyping relationship, so unless that changes we should start with the unboxed references with no subtyping relation.

But even in their presence, you'd still want the uniform variant!

I can see why for spec elegance, but can you point to a real language that uses this for which an explicit boxing operation would be much more difficult to use?

@rossberg
Copy link
Member

rossberg commented Apr 4, 2020

@tlively:

Basic subtyping ended up in this proposal mostly for technical reasons... But downstream use cases like type imports or typing of reference equality, as currently proposed, fundamentally rely on both.

As far as I can see, type imports only depends on funcref <: anyref insofar as it is desirable to be able to have effectively unbounded imports. I can see that this would be elegant, but it's not clear to me that it is useful. I don't want to litigate this trade off here, but please let me know if I am missing some subtlety that makes funcref <: anyref more important.

The reason is avoiding bias. If we can help it, don't bake in arbitrary decisions about which types are reasonable as imports and which ones aren't. At the meeting, we already got into the discussion about exnref, and I expect the same to occur for others.

Ultimately deciding for each type separately would just be random design choices akin to premature optimisation.

FWIW, I can imagine use cases for abstracting function types, such as capabilities (we actually use functions as capabilities at Dfinity). Sure, you can always work around that with additional wrapping/converting, but that's an extra cost.

Also, can you point me to discussion or specification of the proposed reference equality? I haven't seen anything on that.

It was in this proposal originally. After we voted to defer it, it was moved to the Future Extensions section of the overview.

Currently a bit orphaned. Not sure whether we want to make it its own proposal eventually or incorporate it in one of the others. But clearly, support for reference equality will be needed at some point.

It's been a basic assumption from the start that heap references support a unified representation in a Wasm engine. That's a very reasonable assumption, too, given that all existing Wasm engines as well as the vast majority of other GC runtimes employ it.

The argument about existing Wasm engines biases heavily toward web engines, which might make different choices about their representations because they already support JavaScript objects. I could easily see a non-web engine wanting to support anyref and funcref but not wanting to support the full GC proposal. If that's a situation we want to encourage, we may not want to limit ourselves to precedents in GC runtimes.

Fair point, but even without full GC, engines implementing the full Wasm API will usually need some form of reference counting scheme for function references, as for others.

This does not preclude the later addition of specialised flat/raw/unboxed reference types!

True, but if they turn out to be sufficient for real use cases, it would be much better to have only the unboxed reference types than have to duplicate everything to have it both ways. And if real languages need both after all, it would be better to start with the one that is more immediately useful.

They both are equally useful. But the boxed one is somewhat more flexible. The only advantage of the unboxed one is a hypothetical optimisation in engines for which there isn't any precedence and a very low likelihood that the current generation of implementations would actually implement it. For an MVP, it makes sense to start with the simpler design, and avoid the warts, open questions, and other costs we have been discussing.

We don't know any immediate use cases for subtyping relationship, so unless that changes we should start with the unboxed references with no subtyping relation.

But even in their presence, you'd still want the uniform variant!

I can see why for spec elegance, but can you point to a real language that uses this for which an explicit boxing operation would be much more difficult to use?

An explicit boxing operation would have to be assumed to involve an allocation. So a compiler would want to avoid repeating it on the same object. Consequently, any object that it needs in boxed form in some contexts, the compiler would want to keep it around in boxed form. But if there is no type that represents a boxed function specifically (as opposed to just an anyref), then it would in turn need a downcast check every time it wants to access it.

IOW, without a type representing the boxed form, your only choice when using that is between allocation overhead (on every injection) vs casting overhead (on every projection).

@rossberg
Copy link
Member

rossberg commented Apr 4, 2020

To turn attention back to the question at hand, I'd summarise as follows.

If everything else was being equal, I would totally agree that cutting subtyping from this proposal would be the right move. But not everything else is equal (which is why we included it in the first place).

So the question boils down to one of cost vs benefit. And the costs of cutting it are concrete, both short-term and long-term. Inversely, the benefit is completely hypothetical, and achievable by other means.

@lukewagner
Copy link
Member

@rossberg You haven't factored in "risk", viz., that we commit to some design choices now that we regret later. Until we've fully fleshed out (implemented, generated, widely understood) subtyping in more forms than funcref <: anyref, the risk is just as concrete as the costs.

@RossTate
Copy link
Author

RossTate commented Apr 6, 2020

Following up on that point:

Inversely, the benefit is completely hypothetical, and achievable by other means.

To be fair, the benefits of the subtyping rules in contention are also completely hypothetical. Implicit in your evaluation has been an assumption that these subtyping rules will inevitably be adopted. I totally appreciate the philosophical principle behind this assumption, as I have been there myself. After all, every language designer aspires to a grand unifying principle for their creation.

But it is also the case that this principle inevitably clashes with performance. One of a designer's most significant and difficult decisions is how much they choose to compromise uniformity for the sake of efficiency. It would be quite surprising and amazing for WebAssembly to not face the same trade-off, given how much difference we all know something as small as a single bit can make. So, as nice as this principle is, what has surprised me is how little alternatives to top-anyref (meaning anyref is a supertype of all reference types) have been explored over the past three years so that we might understand the trade-offs inherent in this principle. It especially surprised me that this principle persisted even after the conception of Interface Types, which so emphatically embraces the notion that different modules will inevitably represent even the same kind of data differently. From what I can tell, this seems to have happened because of belief that this principle is necessary, not just one of many options, so let me briefly illustrate alternatives in places where last week assertions of this necessity seemed to be made.

One such assertion was that garbage-collected languages will need a top-anyref as an "escape hatch, e.g. for compiling uniform representations". However, in various discussions on- and offline multiple language implementers have spoken about how they specialize their representation to their needs. This seems to be especially prevalent in non-object-oriented dynamically typed languages. For example, pointer-embedded bit-flags are often used to distinguish the optimized common case (e.g. a direct reference) from the unoptimized uncommon case (e.g. a proxied reference) without requiring a read. Another example is being able to tell that values are structurally unequal without requiring a read. Given that both GC proposals are unable to verify the high-level invariants that typed languages rely upon, they too will effectively compile to code with frequent dynamic checks and casts, much like dynamically typed languages, and so there is reason to believe that they too might benefit more than usual from such techniques. Furthermore, recent research into efficient inter-language/inter-paradigm interop has been finding pointer-embedded bit-flags to be particularly useful for significantly improving performance. On 64-bit machines, it is often even possible to encode the entire type tag directly in the pointer. For WebAssembly, a module can communicate to an engine what its common cases to distinguish are so that the engine might try to encode them as pointer-embedded bit-flags, but it is impossible for an engine to employ such an optimization if everything must be a subtype of anyref. That is, not only is top-anyref not necessary for compiling uniform representations, it is likely undesirable for such languages.

Another assertion regarded type imports. In fact, there were two assertions, each worth breaking down separately. One was that the current plan avoids bias with respect to which types can be im/exported. But it most certainly is biased as it does not even consider numeric types. This is odd considering that the only value types for C/C++ programs, the primary user base for WebAssembly, are numeric types. And if we consider WASI, capabilities can easily be encoded as integer handles, and by exporting those handles as an abstract type (or as multiple abstract types for different kinds of handles), WASI can furthermore ensure that they cannot be forged (provided the above issue with call_indirect is addressed). I can imagine many applications of type im/exports that do not need and would not want references, so it seems odd to bundle these features together. Sure, to enable separate compilation of modules in browsers we might want to constrain the possibilities, but this can be done by requiring that all web wasm modules only export reference types—there is actually no need to constrain imported types. (And there are ways to even make the restriction on exports unnecessary, but that's a bigger digression I don't want to go into right now.)

But let's put the issue of numeric types aside and move on to the second assertion about type imports, which is that separate compilation requires imported (reference) types to have the same representation, i.e. top-anyref. There is certainly some truth to this, but the truth is more nuanced. For straightforward separate compilation, what is necessary is that the engine know how to manage (i.e. garbage collect or reference count) the imported reference type without knowing what the specific reference type is. But that can be done without the reference type belonging to some universal top-anyref type. To provide a very concrete example, I'll pick the Mu VM (which only works on 64-bit machines). Its tagref64 type uses NaN-boxing to pack 64-bit floats, 52-bit integers, and GC'ed references alongside a 6-bit integer into a 64-bit representation. The memory manager knows how to handle these references without interpreting those 6 bits. This means the specialized uniform representation the engine generates for each module/language can have its own 6 pointer-embedded bit-flags and yet still support separate compilation for type imports. But if there is a top-anyref type, then these bits have to go to waste because separate compilation prevents the engine from coordinating the bit-encoding across modules. So, similar to before, not only is top-anyref not necessary for separate compilation, it in fact requires sacrificing performance to enable separate compilation.

Hopefully the above demonstrates that there are alternative options and that there is reason to believe that top-anyref, like pretty much any "uniform" design, comes with a performance trade-off. I am happy to elaborate more on the above examples, or to present alternatives and trade-offs to consider for other applications of top-anyref if requested. From what I can tell, the primary contributions of the upcoming proposals are similarly independent of top-anyref. So I would discourage assuming that rolling back top-anyref (as opposed to external references) would be wasted effort. Even if we do add it later on, in the meanwhile proposals would have been designed independently, enabling simpler/embedded engines in particular to support many features of WebAssembly without needing to support top-anyref (or, in some cases, even external references). It's even possible that some of these proposals would end up being developed more quickly, as they would no longer be slowed by the many complications incurred by subtyping.

@RossTate
Copy link
Author

RossTate commented Apr 7, 2020

In the last CG meeting, we talked about how it might be useful to link to relevant CG meeting notes, so here are 3 such links in case it helps:

@rossberg
Copy link
Member

@lukewagner, indeed, risk is a factor to. But that goes both ways. From were I stand, the risks of making this change (such as poorly understood consequences on other proposals) is higher and more concrete than the other way round.

@RossTate:

But it is also the case that this principle inevitably clashes with performance.

I think we have repeatedly concluded that this is not the case here, because flat pointers (and other specialised features) can be introduced later. You keep making lots of assertions about what Wasm implementations could do, but little of that bears any relation to what existing implementations actually do today. So while various features could possibly reap some performance benefits in a next-gen engine, there is little reason to expect that the current engines could easily exploit them. So no benefit in making them an MVP feature.

@lukewagner
Copy link
Member

@rossberg If we remove subtyping we know exactly what the consequences are; it's hard to call them "risks", they're just fixed "costs". "Risk" refers to the fact that we do not have a complete picture of subtyping at this point, and we won't until we've put "pressure" on subtyping via Type Imports and Function References.

@rossberg
Copy link
Member

@lukewagner:

If we remove subtyping we know exactly what the consequences are

A bold statement. ;) I for one don't. How can we adapt the C API? What design implications will type-indexed null values, which are uncommon, have in the future? Do we get the format of the new immediates right? It's folklore wisdom that last minute design changes are a favourite source of errors and unforeseen consequences.

@lukewagner
Copy link
Member

I think we should hold off on committing to a stable C API until we know more about subtyping. Until then, embeddings are already doing something now and can keep doing so. I've got to chuckle a bit at comparing the unknowns for the ref.null immediate binary format to something as cross-cutting (and hazardous in folklore wisdom) as subtyping.

@RossTate
Copy link
Author

One down side to removing subtyping is that it creates the need for two instructions, funcref.null and anyref.null (or externref.null). But it occurs to me there might be a better solution. These instructions exist because types need default values (for a while). It seems likely that wasm will need to add more types over time. So rather than coming up with a new instruction each time, what if we just had a single instruction default $t : [] -> [t] that produces the default value for the defaultable type t? One nice thing is that this would work even for imported types (by the way, Type Imports has no discussion of defaultability, which is a type constraint that is not expressible with upper-bound constraints). Another nice thing is that it naturally extends to defaults t* : [] -> [t*], which would pair nicely with things like the upcoming let construct as observed in WebAssembly/function-references#20 (comment). Similarly, we could have a single instruction is_default : [t] -> [i32] (no type annotation necessary), which indicates whether or not the given t value is the default value for a defaultable type t. Hopefully that would address concerns about "warts" caused by removing subtyping. But maybe y'all have already discussed this option before?

@rossberg
Copy link
Member

There will be an infinite set of different nullable types and hence null values. Consequently, it would not scale to add multiple null instructions. Instead, we'd add one type-indexed null. That's still a wart, because it doesn't mesh well with subtyping on that index type. For example, although technically different, null $T and null $U will have to be considered the same value if $T <: $U, which will require identifying multiple values. From that perspective, type-indexing null is not natural in the presence of subtyping and creates artificial complication.

None of this is related to defaulting. As for the default instruction, there is some confusion here on multiple levels. First, it cannot change anything about imports. You can already emulate its behaviour via auxiliary functions that return their default-initialised locals, so it wouldn't add any new expressiveness that magically provides something new for imports. But I also don't see any problem with imports. Nullability is a property of ref types, not type definitions: a local of type ref $t can never be defaulted, you'll need to type it ref null $t (previously called optref). That's completely independent of what $t is or whether it was imported.

@RossTate
Copy link
Author

Your first paragraph seems to be about formalization/specification. The formalization research community offers multiple ways to specify this pattern. The one you give is one of them, and is not considered to be particularly unnatural (it just says that values are (co)variant with respect to subtyping, the natural analog to type-level considerations). But if you don't like the idea of the same constant having multiple representations, which I understand, then you can say that null $T is only valid if $T is minimal with respect to subtyping. (You already have the requirement that $T is a "reference type".) At the moment there are only two such minimal types though, so having distinct constants funcref.null and anyref.null is not unreasonable.

As for the second paragraph, I realized I missed a step. From my earlier comment, I still had in my mind type imports rather than just reference type imports. So yeah, filling in that disconnect, it makes sense why reference type imports has no discussion of defaultability; sorry for being confusing there. But if ever wasm does add true type imports, then it will need a notion of defaultability (since types like ref $t are not defaultable), and that's a simple example of a type constraint that is not expressible via subtyping. Who knows if that'll ever happen though, so at this point it's just a thought to keep in mind.

But focusing on the here and now, do the default(s) and is_default instructions seem useful?

@rossberg
Copy link
Member

How is there a minimal type in an open subtype hierarchy?

The type ref $t wouldn't even make sense if $t was not a referable type, so I don't quite follow what you are saying in the second paragraph.

IDK if there is any good use case for a default instruction. Arguably, defaulting is primarily a hack for initialisation, out of necessity, not a particularly desirable feature in general. At least I have never heard anybody asking for it.

@RossTate
Copy link
Author

At least I have never heard anybody asking for it.

In investigating the rationale of the design, I have had multiple people explain that they wanted a direct way to construct and test for the default value. That seems to be the general pattern. Right now that pattern is being served by providing separate instructions for each type with a default value. But given that default values are a core feature of WebAssembly, whether just out of necessity or not, it seems another reasonable way to address that pattern would be to have general-purpose instructions for constructing and testing for default values.

@RossTate
Copy link
Author

Adding link to most recent discussion for reference: April 21

@binji
Copy link
Member

binji commented Apr 28, 2020

We had a poll at the Apr 28th meeting, with the following results (where SF represents strongly in favor of removing subtyping, and SA represents strongly against removing subtyping):

SF: 7
F: 9
N: 8
A: 2
SA: 5

At least one member of the group voted SA because there was no discussion during the meeting. We had discussed this topic at previous meetings, but none at this meeting.

Because the poll was at the end of the meeting, we weren't able to succinctly state a conclusion. I think this poll does offer some clarity, still. We should take this to mean that the group as a whole slightly favors removing subtyping, and we should proceed accordingly.

I apologize that this poll was a little haphazard. Please feel free to reach out to me to discuss any concerns you have about this decision.

rossberg added a commit that referenced this issue May 7, 2020
Per the vote on #69, this PR removes subtyping from the proposal. List of changes:

* Syntax:
  - remove `nullref` type
  - rename `anyref` type to `externref`
  - extend `ref.null` and `ref.is_null` instructions with new immediate of the form `func` or `extern` (this will later have to generalise to a `constype` per the [typed references proposal](https://github.com/WebAssembly/function-references))
* Typing rules:
  - `ref.null`, `ref.is_null`: determine reference type based on new immediate
  - `select`, `call_indirect`, `table.copy`, `table.init`: drop subtyping
  - `br_table`: revert to rule requiring same label types
  - `elem` segment: drop subtyping
  - `global` import: drop subtyping (link time)
* Remove subtyping rules and bottom type.
* Revert typing algorithm (interpreter and spec appendix).
* JS API:
  - remove `"nullref"`
  - rename `"anyref"` to `"externref"`
* Scripts:
  - rename `ref` result to `ref.extern`
  - rename `ref.host` value to `ref.extern`
  - drop subtyping from invocation type check
* JS translation:
  - extend harness with separate eq functions for each ref type
* Adjust tests:
  - apply syntax changes
  - remove tests for subtyping
  - change tests exercising subtyping in other ways
@rossberg rossberg closed this as completed May 7, 2020
binji added a commit to WebAssembly/bulk-memory-operations that referenced this issue May 13, 2020
Issue WebAssembly/reference-types#69 requires
that `ref.null` instructions include a reference type immediate. This
concept isn't present in the bulk-memory proposal, but the encoding is
(in element segment expressions).

This change updates the binary and text format, but not the syntax. This
is OK for now, since the only reference type allowed here is `funcref`.
binji added a commit to WebAssembly/bulk-memory-operations that referenced this issue May 13, 2020
Issue WebAssembly/reference-types#69 requires
that `ref.null` instructions include a reference type immediate. This
concept isn't present in the bulk-memory proposal, but the encoding is
(in element segment expressions).

This change updates the binary and text format, but not the syntax. This
is OK for now, since the only reference type allowed here is `funcref`.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants