-
Notifications
You must be signed in to change notification settings - Fork 122
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closures - MVP #66
Closures - MVP #66
Conversation
That sounds pretty solid!
I recall seeing someone emulating a multiple values result (I believe it was a Forth to WASM compiler).
Do you know if you have an addition call cost compared to regular
I know it's just a POC, but this will be problematic in the future. I don't know what your memory management is done but instead of being separated (and 👍 isolated) it could be part of your "memory chunk pool"?
What is meant by |
Hey @xtuc, Thanks for the feedback! I wonder how FORTH does it(probably by writing to memory)
RE: memory & Yes, I am talking about a separate module instance. The main module and the extension would share a table with two split memory spaces.
I don't love the pattern of an implicit, sectioned off chunk of memory just for the runtime. I know this is a pattern used with other compiled languages to WASM, but that is not fit into the basic goals of the project. I'd like to stay unopinionated about the management of the memory in the module as much as possible.
It isn't 🙃 the user is in-charge of the memory just like with raw WASM. We only provide better syntax to do so. All that being said, this was all theorycrafting and the PR once ready should be interesting. I think POC-ing something like this will give me more data on how to move forward. I'm pretty close now, but no working wasm code just yet, I think I might emulate extension module calls via JS/imports for the very first POC. The potential benefits of using an extension mechanism instead of building functionality into a monolithic main module is that it would be possible for the end-user to overwrite the behavior completely, the ABI is going to be dead simple so it would be trivial for someone to swap in their own shared memory chunk pool closure manager instead. |
Yes I know, sorry that wasn't clear (and not even english). I meant if you need to allocate/free a module during runtime. Might not be a problem atm since you're not going to free it in this POC. I thought that WASM format was only allowing one module (itself)? This is interesting because the spec tests (in WAST this time) are using sub-modules all the time. The table you mentioned is an WebAssembly.Table? You would pass it from JS to multiple modules, right? Edit: According to |
@@ -49,3 +49,23 @@ test("functions", t => { | |||
t.is(exports.testFunctionPointers(), 4, "plain function pointers"); | |||
}); | |||
}); | |||
|
|||
test.only("closures", t => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a reminder, you have an only here and a few skips later.
Hey, no worries. Your English is great!
Yes, and the specs use some superset of text-format which supports this. I always thought this was confusing because that means
Precisely! While the spec says you main only have a single module compiled into a binary it has no limitations on multiple modules sharing a Table. At least that I've seen and documentation on MDN seems to support the idea that this is possible. Tentatively I'm planning on using JavaScript to pass the table around and use WebAssembly.Table.set to basically dynamically link the two together. |
Update: Closure implementation is now working via a plugin system for closure handlers/callbacks. Interesting fact I discovered within Chrome/v8 wasm compiler. Passing WebAssembly functions as imports to another module kicks off type validation when the two modules are linked. This is not the case when imported functions are JS wrappers, hinting that V8 handles wasm-to-wasm imports/exports differently. I have a feeling this means that imports bound in this manner don't suffer from WASM -> JS call penalty so I used this approach instead of A consequence of this validation is that tests for type imports/exports can be verified by V8 itself: walt/packages/walt-compiler/src/parser/__tests__/type-spec.js Lines 36 to 40 in 4892825
Pretty neat. So the closure plugin will work through |
Early work to enable closures, parsing and emitting of closures(#65). The goal of this poc is to figure out the practical challenges in enabling this type of functionality and laying the groundwork for more of these type of "side_module" features to come.
To create a closure, arrow function syntax is used. This fits really well with existing JS syntax and creates clear separation in logic:
Limitations of POC
High-Level Implementation Details
The basic idea behind the implementation is to not dynamically manage the memory inside the module even with closures. This means that there will be no implicit runtime memory regions, maintaining the design direction of no surprises. A closure needs to create an object to represent the environment it's closing over however, the way this will be accomplished is with a side-module with its own memory object completely independent of the main module. The two will be linked with a shared Table.
Closure representation inside the binary
Declaration:
The closure is compiled to a regular old function declaration with an additional side-effect of its function index being encoded into the Element section(in the binary). Because function pointers are already supported this will be done for free by the compiler already. The closure value will be an expression returning 64-bit value. The original function declaration AST nodes will be moved to top-level module scope, with its default arguments appended with a base memory pointer 32-bit argument
__ptr__
.For example
becomes a top level function definition in the binary
The
--
is used so that the name cannot be accidentally overwritten by end-user. With this approach we only ever create a single function for every closure definition and many potential__ptr__
-ers at runtime.Call-Site:
Closure call sites will compile to an
indirect_call
, although a regularcall
operation may be possible(unlikely) since we can statically analyze which real function is being called. Either option will work, but this requires some creative encoding. Either option will require the closure to be encoded as a 64-bit value with low 32 bits being a table pointer(or function index) and the high 32 bits being the base environment pointer. Since wasm-MVP only supports 32-bit address space in both tables and memory this should work out of the box! This is a pretty neat way to get around the fact that WASM functions cannot return multiple values. The only downside to this is that a closure pointer cannot be directly exported to JS, so a wrapper function would need to be used.The call sites will incur an additional instruction cost of shifting the bits to call correct function indexes with correct memory offsets and additional 32bit truncations for
indirect_call
.Closure body encoding:
Special closure helper functions for
<type>closure[GS]et
andmakeClosure
will be implemented and likely be emulated via JS for the first POC. Every closed over variable access will become a function call inside the closure, which will provide the values stored inside a completely different memory space.So a closure which looks like this:
becomes the following AST(subject to change)
Should be clear what is happening here, every use of
x
, the closed over variable becomes a lookup call, where the__ptr__
argument is used as an offset into the object store inside the side-module memory space. TheConstant<i32> 0
s are offsets from the base pointer. Tada! Closure 🎊Tasks
<type>closure[GS]et
callbacks andmakeClosure(size)
Type