webassembly / gc Goto Github PK
View Code? Open in Web Editor NEWBranch of the spec repo scoped to discussion of GC integration in WebAssembly
Home Page: https://webassembly.github.io/gc/
License: Other
Branch of the spec repo scoped to discussion of GC integration in WebAssembly
Home Page: https://webassembly.github.io/gc/
License: Other
I'm not from the Go team, but I've been working on an alternative implementation of the Go compiler+runtime that works well on microcontrollers and WebAssembly called TinyGo. It already supports WebAssembly but does not have a usable GC there. Hence my interest.
I would really like to use a built-in memory manager, but I'm afraid that the current spec would be a very poor fit for the Go language. I've listed the issues here in order of importance.
inref
is commonly used in WebAssembly, implementations may decide to switch to a GC that supports interior pointers transparently to improve performance. Also think of non-browser environments that may be optimized to run Go compiled to WebAssembly, for example.There are also some parts of the spec that I think are more complicated than needed, but I should probably write that down in a separate issue to keep things focused.
This is in the form of a suggestion; not an 'issue'.
There is a potential conflict between multiple threads and managed memory. Especially, if that managed memory (MM) is to be shared with JS.
On the other hand, many MM languages also have threads (Java, C#, ...) and prohibiting those languages from accessing wasm-based MM would seriously affect the ROI of the proposed GC scheme.
Note that not all MM languages that are multi-threaded share a single MM: e.g., Erlang has a process model where different processes do not share heaps.
So, this suggestion is to allow multiple MMs; with the additional constraint that a direct write of a reference is not permitted to cross a MM boundary (i.e. when using multiple MMs, message posting between them is required).
The meta data as to which MM a reference belongs to can be relatively cheaply be embedded in the same structure used to record the structural type of the object; and so testing whether a write is legal or not is not enormously expensive.
TL;DR. Instead of having a single MM, allow multiple MMs; and require that 'extra' threads do not have direct access to the MM that is used for JS objects. This will facilitate languages like Java to be implemented over wasm whilst preserving the single-threaded nature of JS itself.
(There are follow-on issues: such as invoking imported JS functions from multiple-threads ...)
The proposal for GC define several different types such as storagetype, functype, structtype, arraytype, reftype, etc.
And there are several scopes in WebAssembly such as locals, globals, function parameters, function returns, block returns, fields of structs, etc.
Currently the GC proposal define only for new features which type can used on which place. There is no information for the old features like locals or globals.
Since types constructed by struct.new
and array.new
are both data type structures, does it make sense to rename struct
to record
or something similar?
An alternative to the i31ref would be to support 'value types' as well as 'reference types'.
Semantically, one of the differences is that instead of the tagged value determining that the value is 'flat', the owner of the reference would have to assert the type.
The big pay-off is a uniformity of representability of data types (the only required primitive type would be bit string(s))
You can reconcile value types with existentially quantified types - at the expense of a more elaborate system of allocating local variables.
My suggestion would be to supplement reference types with value types - I am not advocating C++-style specialization of generated code.
Given that this proposal aims to allow WebAssembly be a target for high-level languages, I think that the right way to go with intref
is to allow a reference to any numeric type (i32
, i64
, f32
, or f64
). As you say, it's a bit more high-level but would allow for better perf on some architectures.
Floatrefs would seem to hit the sweet spot for those WebAssembly implementations that can actually represent even f64
as immediates. Additionally I speculate that when embedded in a Web engine, (ref f64)
would minimize impedance mismatch with JS numbers when just shunting a JS value through a WebAssembly component via anyref
-- but I defer to the actual web engine experts here :)
For intrefs, I assume that even if a 31-bit restriction is recommended, that it's not part of the semantics: an intref is already 32 bits. Otherwise that would have other ripple effects and failure modes. For that reason I think (ref i32)
is probably the right formulation; intrefs are already high level in that they abstract over a platform-specific limit (edit: a soft limit between two different perf profiles). While you have that, you might as well have (ref i64)
, and a first implementation of any of these can just use heap numbers.
It seems that defaultability is an unneeded wrinkle -- it's always known when allocating a struct which fields will have default values, and they can just be given in the source program. I can only think that defaultability is a space-saving measure; but is it that important? If so, consider augmenting the spec to indicate the rationale for this feature.
I'm not seeing any mention of weak references
From the perspective of Lua there'd be a desire to be able to essentially toggle the weakness of a table's keys or values when updating the metatable. But that's pretty specific flexibility. I'd expect at least a weakref type for structs which the runtime can null out when the target becomes otherwise unreachable
Also destructors
The example in the overview shows only one level of nesting. Does this model allow for multiple levels of nested functions along with access to data from all ancestor scopes?
This proposal (which I assume is 100% described here?) is a little challenging to understand because
I suggest expanding the "challenges" list to describe the challenges in more detail. I would be particularly interested to know about any elements of the design that slow down execution or use extra memory for a security reason. (Looks like the downcasts used in closures and derived classes are required due to the simplicity of the current proposal.)
I'm curious if this proposal could support the D programming language. The D language has both GC and non-GC memory, and its normal pointer type is an "agnostic" pointer that "doesn't know" if it points to GC memory or not - the user can use it either way.
I no longer remember the right terminology for the large-scale wasm unit that is effectively an instance of the vm, where multiple such instances can exist within a JS heap. ("wasm vm"?)
I remember the paper making a big deal of the fact that only data can be passed between these wasm worlds. In WebAssembly-gc can references be passed as well? If closure encapsulation is directly implemented as indivisible pairs, (See #7 ), can these be passed between wasm worlds?
As a developer of a programming language (Cone) that plans to target WebAssembly, I was asked to review and provide feedback about the GC proposal.
By way of background, Cone is a memory/race/type safe, static, OO-capable language that intends to offer programmatic support for multiple memory management strategies, including Rust-like static single-owner and lifetime-constrained borrowed references, reference-count, tracing GC and custom allocators (such as pools and arenas). Concurrency safety is ensured using static (Pony-like) or runtime synchronization permissions.
Being able to leverage the browser's GC within a Cone-wasm program would be a blessing. It would significantly reduce the retrieval cost and footprint of any Cone program that required tracing GC references and would presumably allow direct tunnel-through access to browser services, such as WebGL.
I have read through the GC overview several times. It appears to propose a significant enrichment of WebAssembly's type system beyond integers and floats. I am assuming (since it is not stated) that such a type system is a necessary prerequisite to enabling tracing GC support within a wasm module. Why? I am guessing that without such type information, it is believed the GC has no idea how to find the pointers it needs to trace. If I am guessing correctly, it might help others to have this connection stated explicitly.
When I mentally map Cone's type system to the types described in the overview, I came away with several concerns. My first is that in order for Cone to take advantage of the GC, it would require that all of the types in the Overview be implemented (including variant/sum, nested, and others). It would likely also require unrestricted type casts. I suspect anything less would be restrictive and problematic, perhaps fatally so.
And yet, the alternative (that WebAssembly fully embraces this entire, complex type system) concerns me no less. It feels like a significant undertaking that will take a long time to fulfill (years?) and will carry significant technical risks and challenges. Also, Cone uses LLVM for code generation; how much will LLVM IR need to be enriched to correctly pass through the required type information (e.g., mutable/immutable, sum types, and reference variations)? Lastly, I have concerns about performance and type impedance mismatch: I am not clear on exactly how WebAssembly intends to use this type system information, but there could be load or runtime implications of WebAssembly performing a 3rd tier of type checks (Cone and LLVM have already signed off on type correctness), as might be needed to enforce subtyping constraints on casting, etc., not to mention performance hits for ensuring that Cone's type system maps correctly to WebAssembly's.
I do not mean to sound negative or harsh here. I would be delighted to learn my concerns are overblown and unwarranted, if only I knew the full story.
Thinking about this is what led me to the title for this Issue. When I first opened the document, I anticipated reading about a lower-level "API" enabling a wasm program to support the GC. Using this API, a program might:
I implemented an incremental, generational GC as part of my earlier language Acorn. I found that using a dispatchable tracing function during the mark phase to identify all the object's pointers offered the promise of type flexibility. This seems even more true for certain types (e.g., sum types or self-typed values) where we do not whether it holds a pointer until runtime.
I realize such a low-level GC API approach is still challenging, but perhaps it might be quicker and less risky than one that requires a full type system. I am not privy to prior history, so it may well be that such an approach has already been evaluated by the team and found wanting,
I have no desire to be disruptive to an orderly unfolding of this proposal. If nothing else, answers to my questions and concerns may help me (and others?) support and facilitate the proposed direction.
I've implemented a Common Lisp that interoperates with C++ and uses LLVM as the backend (github.com/drmeister/clasp). Currently it supports the Boehm GC and the Memory Pool System GC (MPS; http://www.ravenbrook.com/project/mps/master/manual/html/).
I could generate webassembly with the llvm backend but the lack of GC is a show stopper.
I thought I could provide my own GC (MPS) but the fact that registers aren't stored on the C++ stack is another show stopper.
Let say someone wanted to provide their own GC - is there a way to access the shadow stack of registers? I realize that the architecture is unknown and the registers are architecture specific and that this may not be reasonable.
Reading through the overview, I saw that there is a proposal for typed function pointers.
Is there anything that typed function pointers provide that is not already covered by anyfunc
? I have a feeling that the part of the spec that describes typed function pointers is leaning a bit too much towards supporting the full Java type system. While I think that typed function pointers can be very useful for performance reasons, I don't see how they are related to GC except for making the type system even more complex. Falling back to anyfunc
(with a small performance hit) and simply requiring the language to perform its own language-dependent downcasts if needed seems like a simpler solution right now.
Possibly related: #32
I think the intref section probably needs some example code for how to implement addition over arbitrary-precision integers in order to make sure it covers its use case. For that we would need to reference something like https://github.com/WebAssembly/design/blob/master/FutureFeatures.md#integer-overflow-detection. Is there a newer draft for that?
I have some questions to understand the packed types. Are the follow points right?
I think I'm with you for using structural typing: it decreases coupling across modules. And I'm not a type guru so please forgive my ignorance here; but it would seem to me that for down-casts, it would be easier to determine that A is-a B if there is a single subtyping DAG. Also nominal types would allow for more data hiding, if that's a thing; if you don't export a type from a WebAssembly module, then you have a unique "seal" that you can use to brand object that are yours, and reject objects that aren't. Dunno.
It seems to me that this will be a kind of FAQ if this proposal goes through, and I think the spec would be better if it included some words on why it doesn't go with nominal types.
If I am not mistaken, there is a bug in this example:
(func $g
(param $p1 (ref $pair)) (param $p2 (ref $pair)) (param $pick (ref $pick))
(result (ref $C))
(if (i31ref.get_u (cast_down i31ref (get_local $p1)))
(then (cast_down (ref $C) (call_ref $pick (get_local $p2))))
(else (call $new_C))
)
)
Should this be:
(func $g
(param $p1 (ref $pair)) (param $p2 (ref $pair)) (param $pick (ref $pick))
(result (ref $C))
(if (i31ref.get_u (cast_down i31ref (call_ref $pick (get_local $p1))))
(then (cast_down (ref $C) (call_ref $pick (get_local $p2))))
(else (call $new_C))
)
)
??
Arrays of numeric types which can be passed in from JavaScript to WebAssembly are a major feature request I hear from developers when I ask about using WebAssembly to optimize JavaScript programs. Without this, additional copying is needed to move the data into WebAssembly memory.
The array type aspects of this proposal seem much simpler than the rtt system. There is also an ongoing collaboration with TC39 to represent GC types as typed objects, led by @tschneidereit. This proposal will take some time to develop.
Would it make sense to consider shipping array types separately from the rest of this proposal?
References are very important to modern languages, but recent languages like rust and swift leverage alternatives like Box types(non copy able heap references) and ARC(Automatic Reference Counting). Sometimes the docs refer to "opaque reference types" suggesting all of these are options. Can someone clarify?
The activity for this proposal has slowed down quite a bit since August, and the current MVP implementation is full of TODOs. I'm sure work on the proposal is still ongoing, and I don't know what the priorities of the Wasm team are, but I'm really excited by the GC proposal and what it would mean for Wasm adoption.
How can we contribute? Could we get a general roadmap for what needs to be done before the GC proposal is implemented? Is there some area where the community could be helpful?
I would ordinarily look at existing repository issues for starting points, but most of the issues here seem to be inactive; mostly old questions and people proposing alternate schemes.
I don't understand why this feature is being proposed. It seems like wasm is an inappropriate layer to put a GC into.
It seems like the addition of a GC and memory management is a feature of the higher level runtime/language which compiles into WebAssembly, not in WebAssembly itself. What if different languages want to have different memory management strategies? What if they already have the code for a GC in C for their higher level language, will be able to compile to wasm and preserve the logic of their GC? Trying to add a one-size-fits all GC into this layer seems to complicate things for already existing runtimes which have their own GC's already.
From the spec overview:
the vast majority of modern languages need it
I will say that while this may be true true there is also a perception that the inefficiency of a GC in many modern languages is one of the biggest pain points of said language. For example the GC in .NET can cause problems with games because there is no way to control it deterministically and if it collects at the wrong time it can affect frame rate heavily. The .NET GC behaves differently on different platforms partially for these reasons and I just can't imagine a one size fits all solution here.
Furthermore it seems to me like a lot of the newest modern languages have been explicitly experimenting with alternate memory management strategies as a prominent feature. So what would it be like for languages which do not want, or cannot use the GC and Type features offered here?
I could see an optional layer on top of wasm that new language developers could opt-in to, in order to get some productivity gains perhaps but its just not making sense to me how it is appropriate for it to be at the lowest level automatically.
I'm also skeptical of the "types" I see in some examples such as:
(type $point (struct (field $x f64) (field $y f64) (field $z f64)))
Again, this just seems like an inappropriate layer to add such features. It really seems like a language specific feature and shouldn't be in wasm.
On lines 124 through 141 a simple example of inheritance is given.
However, as I understand it, there is a small error:
Namely, one reason for overriding g() in the D sub class is that the subclass's g has access to D's variables as well as C's variables. Externally there is no difference, but when translated to instance-passing functions one should reflect this.
Accordingly, I believe that the signatures should read:
(type $f-sig (func (param (ref $C)) (param i32))) ;; first param is `this`
(type $g-sig (func (param (ref $C)) (result i32)))
(type $g-D-sig (func (param (ref $D)) (result i32))) ;; a specialization of g's signature
(type $h-sig (func (param (ref $D)) (result i32)))
(type $C (struct (ref $C-vt) (mut i32))
(type $C-vt (struct (ref $f-sig) (ref $g-sig))) ;; all immutable
(type $D (struct (ref $D-vt) (mut i32) (mut f64))) ;; subtype of $C
(type $D-vt (struct (ref $f-sig) (ref $g-D-sig) (ref $h-sig)))
Of course, I could be barking up the wrong tree too ...
There is a potentially contradiction between the desire for efficient field access...
The order of fields is not observable, so implementations are free to optimize types by reordering fields or adding gaps for alignment.
...and below, which suggests ordering fields when subtyping to preserve static offsets:
A structure type is a supertype of another structure type if its field list is a prefix of the other (width subtyping).
One way to preserve both runtime efficiencies is to have the "prefix rule" takes precedence over the "opaque rule". (In other words, fields may be reordered/padded/aligned, with the constraint that fields from a supertype must have a binary compatible layout beginning at offset 0.)
Is that a reasonable interpretation?
It says that a closure is a pair of code and state. Is this pair separable? For code given just the pair, can it access the state other than by invoking that closure's code on that state?
If the answer is yes, then could we have encapsulating closures, i.e., invocable inseparable pairs of code and state?
Hi! Really great proposal. I had a look at it from the point of view of whether it's suitable as a target for a Scheme compiler, and generally, really good stuff. I had a few thoughts and will file specific issues.
This issue relates to the closure example. I think you probably agree that we should aim to have examples that are minimal, illustrative, and optimal. That way they can be easy to understand while also illustrating that the proposal is capable of translating optimal examples.
In that regard I think we could revise the closure example to avoid the double allocation, basically putting the function pointer in with the data. Keeping the same example:
function outer(x : f64) : float -> float {
let a = x + 1
function inner(y : float) {
return y + a + x
}
return inner
}
function caller() {
return outer(1)(2)
}
(type $clos-func-f64-f64 (func (param $env $clos-f64-f64) (param $y f64) (result f64)))
(type $clos-f64-f64 (struct (field $code (ref $clos-func-f64-f64)))
(type $inner-clos (struct (extend $clos-f46-f64) (field $x i32) (field $a i32))
(func $outer (param $x f64) (result (ref $clos-f64-f64))
(ref_func $inner)
(get_local $x)
(f64.add (get_local $x) (f64.const 1))
(new $clos-f64-f64)
)
(func $inner (param $clos (ref $clos-f64-f64)) (param $y f64) (result f64)
(local $inner-clos (ref $inner-clos))
(block $fail (result (ref $clos-f64-f64))
(set_local $inner-clos (cast_down (ref $clos-f64-f64) (ref $inner-clos) $fail (get_local $clos)))
(get_local $y)
(get_field $inner-clos $a (get_local $inner-clos))
(f64.add)
(get_field $inner-clos $x (get_local $inner-clos))
(f64.add)
(return)
)
(unreachable)
)
(func $caller (result f64)
(local $clos (ref $clos-f64-f64))
(set_local $clos (call $outer (f64.const 1)))
(call_ref
(get-local $clos)
(f64.const 2)
(get_field $clos-f64-f64 $code (get_local $clos))
)
)
Just as short as your example, just as illustrative, slightly more optimal I think -- besides avoiding the extra allocation, the down-cast in $inner
is from a known reference type so you don't need the intref
bailout. Like your example, this closure representation strategy also allows for a group of mutually recursive closures with a single entry point to share a closure. Of course there are many other possible representation strategies, and no one universal strategy; see http://www.andykeep.com/pubs/scheme-12a.pdf for an exhaustive look at representations even within the "display closure" strategy. For this reason I think it's inappropriate for the WebAssembly spec to include closures; better to allow AOT compilers to choose their own representations.
Hello,
I was wondering how are 2 different modules compiled to WASM, originally written in 2 different languages, going to cooperate if both support generics - because GC doesn't intend to support generic types if I am not mistaken. What is the plan there?
This is a question more than an issue: in this proposal, are arrays reference types?
If so, does this mean that you cannot have an array as an embedded part of another structure?
Also, it does not seem clear from the proposal whether array.get/set is bounds checked. If not, then this is a security hole.
I was unsure if the proposed type system is explicit about which types are subtypes of which, or if the engine infers subtyping whenever a type definition contains another as a prefix?
The 'extend' expression in examples like the following made me initially think subtyping was explicit...
(type $C-vt (struct (ref $f-sig) (ref $gh-sig))) ;; all immutable
(type $D-vt (struct (extend $C-vt) (ref $g-sig))) ;; immutable, subtype of $C-vt
...but @LeifKo suggested that the above definition for '$D-vt' could simply be shorthand for the following:
(type $D-vt (struct (ref $f-sig) (ref $gh-sig) (ref $g-sig))) ;; Maybe also a subtype of $C-vt?
(In other words, 'extend' could just be sugar for duplicating the fields of another type definition.)
For context, this question came up while we were discussing how the engine detects the field ordering constraint for prefix subtyping discussed in #22.
I've been reflecting on this proposal for a while and, after seeing the recent pull reques #38, have come to the conclusion that it strikes an unhappy middle ground between two goals. One goal is to enable an integrated garbage collector, in particular enabling the browser to place and manage wasm objects in the same space as DOM and JS objects, simultaneously relieving wasm programs of their own memory management and making well-behaved wasm-DOM/JS interop possible. The other goal is to enable the browser to statically verify wasm code and thereby reduce the overhead of dynamic checks.
Reflecting upon these goals, the proposed type system is way more expressive than is necessary for garbage collection, and yet it is also far too inexpressive to statically verify very common and important patterns like dynamic method dispatch and closure invocation. And to make matters worse, the run-time type system doesn't seem particularly well suited to either of these tasks, or at least it doesn't match up well with existing systems. And regular-coinductive types aren't even that easy to implement.
Of the two goals, the first goal (integrated garbage collection) is both more pressing and easier to achieve. I think the MVP could be dramatically simplified by focusing on just that goal. Interestingly, it seems that doing so also leaves a lot more flexibility for achieving the second goal. The basic idea is to admit the fact that wasm will be used by a wide variety of languages and programs with a wide variety of needs, many of which are hard to foresee and plan for right now, and the garbage collector needs to be designed for heterogeneity. Note that that doesn't mean deciding all the possibilities now - rather it means the opposite.
To get a sense of what I mean, let me propose my first "type": gcref
. A gcref
is a pointer to a chunk of memory that, in that chunk of memory at a standard offset chosen by the garbage collector, provides the garbage collector with the information it needs in order to know how to manage that chunk of memory. This is essentially what the current proposal calls anyref
, but isn't such a misnomer. There are references that don't satisfy the requirements. For example, although interior pointers point to a chunk of memory that provides the garbage collector with the necessary information, that necessary information is not at the standard offset. Although the MVP proposal is unlikely to have the expressiveness to handle efficient interior pointers, future proposals that better achieve the second aforementioned goal might be able to, and so its better to design with their existence in mind even if they don't exist yet.
So the key idea of gcref
is that a gcref
dynamically provides the garbage collector with the information it needs to manage the reference. There can be many ways to manage a reference, and the garbage collector can be responsible for choosing how to encode this dynamic information. So now we need to think of a few common base cases that the browser should provide at the least to provide critical functionality - we can add more cases in future proposals as it becomes clear they would enable more paradigms or better efficiency.
The management strategy that comes to mind as the simplest, and reasonably efficient, is the dynamic strategy. With the dynamic strategy, the meta-information associated with a block of memory simply says how big the block of memory is. The garbage collector is then responsible for walking through that block of memory and determining which of its fields are references, and then subsequently following those references and managing those. This is essentially the strategy that OCaml uses. Some issues do arise though, and so in fact the meta-information needs to answer a few questions:
Each combination of answers for these questions specifies an "allocation type". An allocation type is like a run-time type, but whereas run-time types sometimes have a bunch of semantic information (such as in nominally typed languages), allocation types just specify memory information. After pondering a variety of languages, I think that if wasm were to ensure that programs could dynamically allocate structures with at least these allocation types, then most languages would be able to have a reasonably performant and simply wasm backend. Note, though, that I say this assuming there are more allocation types; it just might not be the case that wasm programs need to be able to dynamically allocate them. For example, functions would likely have their own allocation types, but what those are do not have to be fixed at this point. Also, JS and DOM objects would also have their own allocation types, but wasm programs would not be able to allocate them since difference browsers are going to have different memory layouts for JS and DOM objects.
Lastly, we still need to consider the second goal: statically verifying wasm programs. For this we'll use "register types". Register types both indicate what kind of operations can be performed on a given register (or really stack slot) and how the garbage collector needs to manage the content of that register. For example, the register type gcref
can be cast to certain other register types and indicates the garbage collector needs to manage the content of that register. The register type ngcref
indicates it might be null (optional). There are register types for various primitives like ints and floats that indicate various arithmetic can be performed on them and that the garbage collector should ignore their contents even if they might look like a reference. The register type ngcrefprim
indicates a value that might be null, or a reference, or a primitive packed into a reference-width format, with a tag indicating which case it is, which indicates that various unpackings can be performed and lets the garbage collector know to check the tag before trying to manage the reference.
Then there would be register types indicating what kind of memory operations can be performed on the value. For example, one register type would indicate that its a reference that points to a block with at least 2 fields of a tagged mixed references and non-references. Another register type would indicate that its a reference that points to an array of immutable non-references with a header of exactly 2 immutable fields of tagged mixed references and non-references. Note that the latter register type is a subtype of the former register type (which is important for OO languages in which all objects have a header specifying its vtable and a unique object identifier).
I realize I've handwaved a lot here, but I do actually have a lot of the details worked out. Nonetheless, I thought I'd lay the high-level ideas out here first before diving into details.
Considering this proposal from a Scheme point of view, I was asking myself how to implement efficient integers, and was happy to see the intref
section of the proposal. I think it's the right thing to include, with the one caveat that it will force every down-cast from anyref have to check whether the ref is an immediate or not. That's a bit sad, but oh well.
Anyway, enough with the preface :) I think the spec is missing a specification of same
on intrefs. The whole point is to allow for immediates, and immediates aren't distinguishable! I think you have three options: one, no intref is the same
of any other; two, intrefs are compared by value, not by identity; and three, use object identity, effectively exposing which intrefs are immediates and which are heap values. Incidentally this would allow a program to know what the fixnum limit is. Since same
has to work on anyref
, I think that rules out the first option; and implementing the second option would add general complication to the same
path. So I think I'd go with the third option.
Has there been any discussion regarding how the JSDom pre-proposal aligns with the GC proposal?
For example, one could imagine achieving the primary goal by decoupling and prioritizing imported host types from the larger challenges that the GC proposal takes on.
(FYI - @flagxor, @lukewagner)
GC is so...negative. The garbage is less interesting than the data itself, and GC in particular is just an implementation detail.
The spec says:
TODO: Could even allow heterogeneous equality (equality between operands of different type), but that might lead to some discontinueties or even prevent some potential optimizations?
I have strained my admittedly ignorant, feeble mind to think of these optimizations that would be prevented, and I can't come up with any. Given that (same (new $point ...) (new $point ...))
has to be false, heap values really do have identity, and any comparison can be made by (same (cast_up $a-type anyref) (cast_up $b-type anyref))
, the point seems moot.
I've been thinking through more examples, and the more I think about it the less I like any one-size-fits-all kind of approach and the less I believe anyref
is useful (in its current form).
For example, while the nominal approach helps keep the casting space more compact and efficient, it seems to also cause problems for languages with type constructors. For example, even if two modules of a language can make sure to use the same nominal casting type for A
and B
, it can be hard for them to ensure they have the same nominal casting type for Foo(A,B)
because they may not realize the other module also needs and declares that type. Note, though, that the original proposal using structural types also doesn't address this problem, since type constructors in the high-level language may not correspond to structural types in low-level wasm, and since some types may have the same low-level representation and yet need to be kept distinct in the high-level language (e.g. C# generics).
As another example, consider algebraic data types in a Haskell- or OCaml-like language. For these, you want the meta information to indicate which case the type is. Since there are typically only a few cases, ideally this information can be picked into the same word as the meta-information used for garbage collection. It's even better if the case-tag-numbering can be dense so that one can use a jump table to efficiently jump to the appropriate case. This all is possible, but it's hard to do if the meta-information also has to enable casting to distinguish these cases between all of the arbitrary other possible cases across all the wasm modules as well as inform the garbage collector how to manage the object with no context as to what system that reference is a part of.
For these reasons among others, I think wasm should let modules declare (and import) "memory-management schemes". And rather than having a global anyref
, we instead have mmref <scheme>
which can refer to any reference of the designated scheme (if the scheme permits this - which not all might because it might cause problems and/or might not be useful). Instances of classes could belong to a linear-nominal kind of scheme, whereas vtables and itables could belong to cast-free kinds of schemes, and interface-method tables could belong to a flat-nominal kind of scheme. Note that structures of one scheme can contain references of other schemes. OCaml values will belong to a uniform i31ref
scheme with AGT support. Haskell values will belong to a scheme where every value is either a collection of references or a wrapped primitive (or wrapped reference to an external scheme). And of course there's an importable scheme for DOM/JS objects.
What do y'all think?
The "median" target for this proposal appears to be Java-like languages (though others can certainly be represented) which will heavily make use of vtable calls. In that light, it seems odd that we're willing to put up with adding code like:
(block $fail (result (ref $D)) (set_local $this (cast_down (ref $Cthis) (ref $D) $fail (get_local $Cthis))))
to most methods, which is both a speed and code bloat issue.
I'm probably missing something, but maybe someone can clarify why we shouldn't special-case this super common use case.
I'd imagine we can have an escape hatch to structural subtyping that goes something like this:
I'm researching using WebAssembly to implement sandboxed plugins to run inside a web app. This means running untrusted or less-trusted Wasm code inside an HTML/JavaScript environment, which itself is running in a browser engine.
Denial of service concerns are critical in this scenario: if the Wasm code can cause the browser engine to crash or become unresponsive, it may be difficult for a user to recover -- especially if the plugin is auto-loaded into the web app environment, there may be no user-visible way to disable it through the app's user interface.
A host environment can easily limit the amount of linear memory that a module can allocate with compile-time validation of the memory's maximum size. (Overflowing the stack with deep recursion, and long loops that hang the CPU, are separate issues which are at least partially mitigated by the browser halting execution at X stack depth or interrupting loops after a timeout.)
GC'd memory in the current proposal doesn't seem to have any limit, unless there's a global limit applied by the embedding engine.
Note that SpiderMonkey does not appear to place limits on GC'd JavaScript memory as of Firefox 60 and you can allocate tens of gigabytes in a few seconds, slowing a machine or even hanging it -- see #34 (comment)
Proposal: add a notion of per-origin resource limits, where the origin can be narrowed from "belongs to the embedder, use default limit behavior if any" to "belongs specifically to this Wasm module, with this maximum allocation limit".
The limit could be embedded in the binary similarly to the max size of a memory definition, able to be statically detected and verified by an embedder before compilation and instantiation.
Alternately, it could be specified in the embedding API, for the host JavaScript/whatever app to apply its own limit, but I'm not sure what's the best place to stick an option flag.
If I have the follow type $A and extended type $B.
(type $A (struct
(field $a1 (mut i32))
(field $a2 (mut i32))
))
(type $B (struct (extend $A)
(field $b (mut i64))
))
How is the fieldidx counted in the extended type $B? If I use the follow instruction
struct.set @B @b
struct.set @B @a1
or must I use the type of field declaration also if the instance is of type $B like:
struct.set @A @a1
I believe that being able to GC code fragments is also important.
The comment #36 (comment) literally pushed me to open this issue.
I strongly believe, that WebAssembly will stay with us for quite long as the most widespread "world-wide interoperable assembly" and I think due to RISC-V and many other initiatives, hardware will get yet easier to design and cheaper to produce. A hardware garbage collector will then most probably appear like an avalanche as it's quite simple to implement it additionally to existing chips (even huge ones like AMD Threadripper).
Let me therefore refer to findings in the paper https://people.eecs.berkeley.edu/~krste/papers/maas-isca18-hwgc.pdf and ask for making WebAssembly GC MVP fully compatible (in terms of efficiency) with the hardware specifics and constraints outlined in the paper.
What is the sentiment for including a goal/motivation to define a subset of wasm that is verifiably safe?
Are there any ideas for a binary format? After a year this look all there vague.
I generally feel the proposal is very elegant (in particular, being based on low level structs that are not assigned a particular semantics) and appears to be well positioned to be able to implement the semantics of quite a few languages.
I also feel there is potentially a large cost here that is not addressed clearly in the proposal, if it ends up not being adopted near-universally by languages.
The way I see it, it adds a 3rd option of what it means to be an object: An object can be represented as:
Now granted, the latter 2 of those 3 can be managed by the same GC implementation, but I still cannot write wasm code that is able to operate on more than one of those 3 representations (in a structurally equivalent way), so in that sense they are really separate kinds of objects. They may be able to refer to each other, but they largely live in their own worlds, operated upon by their own code. That to me is a significant cost.
A cost which is ok if we can get strong evidence that it will serve the needs of the majority of all managed languages out there. I think that is something that requires more evaluation. For example, if I am a language implementor, in a hypothetical future I may have these options to bring my language to the web:
Now it is important to note that not all of these are equally attractive to language implementors. Many languages have significant legacy runtime code built upon large amounts of complex C/C++ where (2) would actually be the route of least resistance, as it would require the least amount of changes to their runtime, followed by (3). (4) may require significant rewriting of their runtime.
Languages may also be very specific in what kind of GC or memory management / layout features they rely on for their typical workloads to be fast, which if not supported by (4) will push them in the direction of (2) or (3).
(2) is also the de-facto standard for most languages until this proposal turns into mature implementations in all browsers, so it has strong inertia to overcome.
Dynamic languages in particular do not appear to be well served by the current version of this proposal, as the diversity of ways they represent their values likely does not map well onto the strongly typed structs. The section on Variants may improve this, but even that can't cover all possible representations (tagged pointers vs nan-boxing vs external tags vs scalars as objects vs..).
(4) has huge advantages in terms of smaller binaries and more robust interop, but that doesn't mean much if it goes largely unused.
I guess something I'd like to ask language implementors, is given the current GC proposal, which of the above would you choose to use as your implementation path? If (4) doesn't garner a large majority, we'd either have to re-think the design of it, and/or instead poor more effort into alternatives like (3).
Languages like Java or .NET have a hashcode() method on every object. The value is constant for the live time of the object but must not unique for the VM. It is like a pointer in the GC managed heap.
To support such languages its sounds me a good idea to add such feature to wasm like:
ref.hash : [anyref] -> [i32]
My understand is that the anyref contains already this information. The alternative would be to add an extra i32 to every struct which will be a large memory overhead.
Or is there already a solution for it which I have oversee?
The current proposal (meaning with PR #38) seems to conflate two important aspects of run-time types. Or at least, its run-time types seem like they're supposed to be analogous to gc headers, and the proposal doesn't recognize two aspects of gc headers.
In a garbage-collected language with dynamic casting, the gc header does (at least) two things: informs the gc how to manage the memory, and provides a way to dynamically check whether the memory has some additional previously-agreed-upon structure. The latter provides dynamic guarantees, whereas the former requires static structure, so I'll call the latter guarantees and the former requirements.
My understanding as that struct.new_rtt $t t' : [(rtt t') t*] -> [(ref $t)]
is supposed to allocate some memory with the rtt
value on the stack as its gc header. (At least that's conceptually what's going on, but there's likely going to be some more behind-the-scenes machinery happening here that I'll ignore to simplify the discussion.) However, that can't be done because the required relationship between t'
and $t
is too weak to ensure that the rtt
value can inform the gc how to manage the new struct. For example, the new struct can have fields that $t
doesn't say anything about.
So this means that every structure will need to have at least two fields of meta-information: one for dynamic casting and another for garbage collection. Note that, for OO languages, since the proposal doesn't provide a way to bundle the vtable pointer into either of these fields of meta-information, every object will effectively need to have three fields of meta-information. But this is fixable. Here I'm going to just focus one collapsing the casting and gc meta-information; I'll leave the vtable fix for another issue.
The fix is pretty simple. The rtt
type constructor should have two arguments: its required type and its guaranteed type. rtt
is conceptually invariant with respect to its required type and covariant with respect to its guaranteed type. The required type should be a subtype of the guaranteed type. struct.new_rtt
should then ensure that the stack matches up exactly with the required type. On the other hand, cast
only dynamically ensures the guaranteed type, not the required type. rtt.new
similarly should only require that the guaranteed types are subtypes.
Note that it's now unclear what the role of rtt.anyref
and such are. I suspect that either they are captured by some extensions to what I've proposed here or their role has something to do with the uncastable references I talk about in #39.
Reading the MVP, it is not clear what the rtt argument to struct.new_rtt
does. It's not referenced anywhere else, most surprisingly not in ref.cast
.
Can someone help explain?
In Scheme, Lua, Python, and so on, values and functions don't have types at compile time, and so the normal, general calling convention is just "Here are N values, have at it". A common way of implementing this would be to pass up to 6 arguments in GPRs, the rest on the stack, with one GPR reserved for the number of arguments. Each function checks the nargs
register in its prelude to assert that it's called with the right number of arguments. Functions can also branch on nargs
to implement multiple-arity dispatch.
In WebAssembly of course we would like to be able to provide an efficient compilation target, but I don't know if that's possible here. If I had to implement Lua or Scheme's call/return convention, I would use an explicit per-thread stack, and make each function be of type
(type $lua-func (func (param $thr (ref $thread)) (param nargs i32) (result i32)))
Here the result indicates the number of returned values that the function pushed on the stack. Anyway this is less efficient than being able to allocate registers to parameters and return values.
For varargs from a C compiler, WebAssembly punts already, and fine; varargs aren't common. But the linear memory solution suggested for varargs is not an option for managed data, and in Lua/Scheme/etc, varargs are common and need GC integration.
I don't know that there is a nice solution here. I suspect some compilers will emit functions with type:
(type $lua1-func (func (param $thr (ref $thread)) (param nargs i32) (param arg0 anyref) (result i32)))
i.e. providing a preallocated parameter for the first argument, attempting to pass it in a register for speed. It's a bit sad though, increasing register and GC pressure across the board.
If there is a nice solution here, it would be good to mention as a possible extension. If not, perhaps a mention of what these languages need to do is appropriate? Dunno.
I am not entirely sure, but it would seem that the spec does not allow arrays of refs, or of types that include refs. The type grammar says:
num_type ::= i32 | i64 | f32 | f64
ref_type ::= (ref <def_type>) | intref | anyref | anyfunc
value_type ::= <num_type> | <ref_type>
packed_type ::= i8 | i16
storage_type ::= <value_type> | <packed_type>
field_type ::= <storage_type> | (mut <storage_type>)
data_type ::= (struct <field_type>*) | (array <fixfield_type>)
func_type ::= (func <value_type>* <value_type>*)
def_type ::= <data_type> | <func_type>
But fixfield_type
is not defined. Can you clarify?
Wanting an array of refs seems like a normal thing to want. If this is not in-scope for the current spec, the spec should include some words about this.
Why is there is a need for a run-time type representation?
In general, RTTs allow the programmer to lie about the types in their code.
Does structural equivalence extend to types defined in different modules?
For example, if module A defines a type $Vec2
, with an exported function that accepts $Vec2
like this:
(struct $Vec2 (field $x f32) (field $y f32))
(export (func $Vec2_negate (param (ref $Vec2))))
...can module B define an equivalent struct...
(struct $Point2 (field $x f32) (field $y f32))
...and then call the $Vec2_negate
function defined in module A with an instance of $Point2
defined in module B?
This initially made me think this was possible...
In order to avoid spurious type incompatibilities at module boundaries, all types are structural.
...but then I noticed the section on import/export and wondered if imported/exported functions can only reference imported/exported types?
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.