-
-
Notifications
You must be signed in to change notification settings - Fork 670
Closures Continued #1308
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 Continued #1308
Conversation
This comment has been minimized.
This comment has been minimized.
Regarding the |
We should also try out a version of this PR in which we don't use a complex function pointer scheme, and instead make all anonymous functions pointers, to compare bloat/performance Comparing benefits of the two approaches: Function Pointer Scheme (bit shifted MSB):
Pointer scheme (all anon functions are pointers to contexts):
|
Or following our philosophy you could use current approach for |
@MaxGraey what about approaches for software that uses neither? |
Another solution. Just leave all as is but for anonym functions which comes from host (exported method) always use (expect) closure object and add according helper method which wrap any callback to Closure obj for loader. wdyt? |
Interesting, so we'd use the optimization levels before we start optimizing? That makes sense. I suppose it invalidates the PR simplicity benefit though since having both approaches living in the source is going to be a ton of code- may I suggest we ship one approach and then additively add other approaches attached to optimization flags? |
Not sure I understand what you mean here- what issue does this resolve? |
I'd also favor an approach that doesn't involve complicating this more than necessary. Mostly concerned that having multiple variants of deeply rooted language features like this may (and probably will) become a nightmare to maintain. From a simplicity standpoint, a dummy memory segment for every function reference makes sense (we discussed this earlier but then there was directize), and I guess we might be able to tackle the directize problem on the Binaryen side. For instance, we can guarantee that these memory segments never change (as long as a function is not a closure), so perhaps a custom directize pass specifically for AS recognizing the invoke pattern and translating non-closure function memory addresses to their respective function indexes can help. |
Seems like next steps then are:
|
Binaryen can't inline reads from static segment unfortunately. |
Is that a permanent limitation or just something that hasn't been implemented? |
I'm wondering could we analyse call sites statically (may in two phases) which sites never call with closures and generate simple inline calls for function calls? In other case fallback to Closure objects. So do pre-optimizations instead try delegate this to binaryen |
Sounds like that'd be tricky, and then when anonymous functions are part of a library interface like as-pect it wouldn't help |
It really hard for binaryen due to we haven't immutable for mem segments like globals have |
Yes for cross module boundary functions we should always expect closures as more general scenario. |
Perhaps to elaborate on the idea of a custom directize pass a bit: Currently, Binaryen optimizes function indexes like other values, potentially becoming constant. If it sees a In the suggested case, it would not optimize function indexes but pointers into memory, again like normal values, with the pointers to all function dummies being known on our end (only non-closures make sense, though). Now, if we give Binaryen a map of memory addresses to function indexes, when it sees our invoke pattern, it can directize the entire pattern by looking up the function index before looking up the function in the table. |
So it's a pointer to memory, but what's at the memory address is irrelevant because we're actually just using it to perform a lookup in a table of anonymous, non-closure functions? Sounds like it could work. If I understand correctly the process would then be like this:
Is that right? |
Finally got around to testing things out. Everything looks great so far! I found one simple case that resulted in a memory leak. function addX(x: i32): (_: i32) => i32 {
return (y) => x + y;
}
let addOne = addX(1);
assert(addOne(41) == 42); On Friday I hope to really dig into this. My initial thoughts with the design is with the copying in of the context into local vars, the local var reads could be become field reads on the struct. |
Oh interesting- did you see what our next steps were in the discussion of the PR? Do you agree with the direction we're going in? |
Also @dcodeIO looks like you're working on it now based on the project board? You guys should sync up if you're both working on it so you don't butt heads |
Tried applying some of the planned changes- it causes some problems with interop. In the current version, closures are prepended with an extra closure argument, and anonymous functions aren't. If we remove differentiation, anonymous functions and closures both get the extra closure argument. This means that there's no good way to call those anonymous functions in JS land. And to make matters worse we also have to get rid of our abort when trying to return a closure, since we no longer know if what we're returning is a closure. However, I think that this just means that we need to pull the bandaid off here and implement something for calling closures from JS land. It was inevitable that we were going to have to get rid of passing function indexes over the module boundary anyway. I haven't put a ton of thought into this yet but my first guess for implementation would be generating a 'call' function for every anonymous function signature which takes a closure context pointer and returns whatever that function returns. Probably will want some sugar in the loader to make that a little more ergonomic as well. |
I remember we had a global holding the closure context initially, but there have been some problems with this iirc. However, perhaps that's still something that might work on the boundary? Let's say the actual closure function has a prepended context argument function theClosure(ctx, ...args) then we can export function theClosure$export(...args) {
return theClosure(__closureContext, ...args);
} with |
Given that I think there are three streams of work right now for people to pick up if they want to help:
|
This seems like a good solution to prevent being incompatible with just anonymous functions (though it'd still be a breaking change). Once we look at closures though I don't think this works- if we're passing the index to Another possible idea:
Advantages:
|
This comment has been minimized.
This comment has been minimized.
Looked into interop stuff and I think the best thing to do here is nothing- we should just document the fact that from now on we'll be passing back pointers instead of function indexes. Anybody who is just using that function as an argument to another function won't need to change anything (except possibly some function signatures). For anybody who is reliant on calling indirectly from the host (something not supported in the Assemblyscript loader), we can document a new process for calling them:
thoughts @dcodeIO? |
Sounds good to me. The docs can state the layout next to |
Probably a good idea to look into that- but not sure if it's needed to merge |
Closing old some old PRs |
Remaking the PR using the master branch that is being used to publish the fork. Plus the old PR was getting a little too long, with most of its contents being outdated.
The plan for closure implementation is currently as follows:
High level
Function objects are now represented as pointers, not indexes. These pointers point to in-memory structs with the following layout:
To call a function object, we do the following:
Issues with Interop
Previously, host interop was trivial. We would return the index over the boundary, and then you could call the function at that index from the host. It's not longer that simple- function objects have state, and are represented by pointers. We need to provide a way for hosts to call function pointers. My suggestion for this approach is to use a two-step method which mirrors how we call these function pointers interally:
Details (summary of the code)
Anonymous functions are all given an extra argument to potentially hold closure context.
When compiling access to a closed over local:
type.closedLocals
fieldWhen we finish compiling an anonymous function:
When a function pointer is called in its original scope:
When the closure leaves scope or is otherwise coerced into a function type (a type represented only by a signature):
When a function pointer is called out of scope:
Garbage Collection
Function pointers are no different from other kinds of pointers, so they also use retain and release calls.
Limitations
In the interest of time, I'm leaving in some limitations intentionally. They should be represented in the tests closure-limitations and closure-limitations-runtime. Here's a list for reference: