-
-
Notifications
You must be signed in to change notification settings - Fork 670
Closures #1240
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 #1240
Conversation
you should refresh all tests due to new builtin global var |
3b3a842
to
5471859
Compare
This comment has been minimized.
This comment has been minimized.
Would that mean changing the signature of the function? Asking because doing so can become a problem where normal functions and closures can be used interchangeably.
Can you elaborate how that'd look? Wondering if it implies that we need one invoke function per unique function signature.
An alternative suggestion to avoid the hack has been to use fat pointers for function references, i.e. an i64 with N bits closure address and M bits function index and alignments bits truncated. Has the advantage that signatures remain unchanged (all function refs are i64, even if not a closure), but the disadvantage that i64 on the boundary is not well supported yet. |
This would not change the signature of the function rather, any path where an anonymous function could be a closure it must become an empty one with an invoke method. For example, a function that could return a closure or a normal anon function would then expect the output to be a closure and then when compiling a call site the invoke method would be used. With this method you wouldn't need to use the original or your new suggested trick. |
Can you give me an example how this mechanism would look like? Let's say we have function doAdd(fn: (a: i32, b: i32) => i32, a: i32, b: i32): i32 {
return fn(a, b); // may be a closure, or may not be one
} |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
@DuncanUszkay1 Does it mean that any function (not closure) should wrap by |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This make things more complicated if we want efficiency. Like extra analysis which try determine could we simplify this closure object without free vars (captured variables) to simple indirect call which highly required for father optimization like directize and inlining which very important especially for eliminate unnecessary retain / release calls for ARC |
This comment has been minimized.
This comment has been minimized.
Talked about this a bit on Slack- here's the result:
Some things that weren't mentioned that also need to be fixed:
|
Should let statements be able to be closed over? Seems like given that they're supposed to be bound to the block they live in maybe not- Right now it won't even resolve if you try to reference a let variable in a closure example: function createClosure(arg: i32): () => i32 {
let foo = 2
var f = (): i32 => { return foo; }
return f;
} |
Discovered a flaw in how I read from the closure context- I don't have access to the class offsets during compilation because we are unaware of the closure before compilation. I had a system in place which made an assumption about where a field would be stored in the class by making some assumptions, but this won't work in a case where you use the same closed variable twice, i.e: function createClosure(arg: i32): () => i32 {
var f = (): i32 => { return arg * arg; }
} Going to look to see how hard it'd be to fix this without the ScopeAnalyzer, then if there isn't a way that makes sense maybe look over the analyzer again to see if we ought to get that in first |
Last push should've fixed it- the offset is now being stuck onto the closedLocal as it's created, so we can reuse them. It does however bring to mind the scope analyzer and whether or not this should be blocked on that or not |
How about extends |
Only reason I didn't do that initially is that the constructor of Not super opinionated on that though, definitely something I can come back to once I start refactoring/reorganizing when all the functionality is there |
This comment has been minimized.
This comment has been minimized.
One thing that would be greatly appreciated is if someone could try compiling this in 64 bit address mode and let me know if that works. Going to move onto other tasks for now |
Attempting that at the current state would yield a broken Wasm binary unfortunately, because the respective instructions ultimately consuming the pointer do not recognize 64-bit inputs yet. It's also likely that there is other code unrelated to your changes not doing it right already, but it's still a good thing to at least try early so an eventual Wasm64 implementation will be easier. |
PR which extracts the additional argument added to makeRetain/makeRelease: https://github.com/AssemblyScript/assemblyscript/pull/1287/files If we can get that into master it should make this diff easier to read, hopefully we can since the PR makes no functional changes |
The diff is slowly decreasing in size- if we can get #1287 merged then the diff goes down to just 501 lines (excluding compiled test changes) |
@DuncanUszkay1 As promised, I Wrote a nice test, that I think finds some really nice edge cases, in my advanced but common uses of Closures in Javascript. 😄 But can you give me contributor access to your AssemblyScript fork? https://github.com/DuncanUszkay1/assemblyscript/tree/closures In the meantime, heres the test I wrote ( // Common use cases / concepts for closures, as covered in articles like:
// https://medium.com/@dis_is_patrick/practical-uses-for-closures-c65640ae7304
// https://stackoverflow.com/questions/2728278/what-is-a-practical-use-for-a-closure-in-javascript
// https://softwareengineering.stackexchange.com/questions/285941/why-would-a-program-use-a-closure
// https://medium.com/@prashantramnyc/what-is-an-iife-in-javascript-24baf0febf08
// Currently, IIFEs and simple Function Factories work
// But my advanced Function Factory Pub Sub, and weird function namespacing does not
// Due to runtime and/or compilation errors :)
// IIFE (Immediately Invoked function expressions)
// Used for encapsulating data usually
// Simple IIFE
let myData = ((): boolean => {
return true;
})();
assert(myData == true)
// Constructor IIFE?
// Don't know why someone wouldn't just use their class, but yeah
class IIFEReturn {
myBool: boolean
myFunc: (x: i32) => i32
}
let myInstanceThing = ((): IIFEReturn => {
return {
myBool: true,
myFunc: (x: i32) => {
return x + 1;
}
}
})();
assert(myInstanceThing.myBool == true);
assert(myInstanceThing.myFunc(24) == 25);
// Function Factories
// Closures that create specific functions
// Simple function that will change depending on input
type generatedFunc = () => i32;
let myFactory = (x: i32): generatedFunc => {
let myFunc = (): i32 => {
return 24 + x;
}
return myFunc;
}
let generatedPlusOne: generatedFunc = myFactory(1);
let generatedPlusTwo: generatedFunc = myFactory(2);
// For some reason, couldn't do
// Cannot invoke an expression whose type lacks a call signature. Type 'closure-common-js-patterns/myFactory' has no compatible call signatures.
// assert(myFactory(1)() == 25);
assert(generatedPlusOne() == 25);
assert(generatedPlusTwo() == 26);
// I often will use this for like Pub/Sub stuff
// Commenting all of this out, as it doesn't work yet due to runtime, or compilation errors :)
/*
type SubFunc = () => void;
type UnSubFunc = () => void;
let subscriptions = new Array<SubFunc>();
let globalSubVar: i32 = 0;
function subscribe(funcToCallOnPub: SubFunc): UnSubFunc {
subscriptions.push(funcToCallOnPub);
return (): void => {
let funcIndex = subscriptions.indexOf(funcToCallOnPub);
subscriptions.splice(funcIndex, 1);
}
}
function publish(): void {
for(let i = 0; i < subscriptions.length; i++) {
// Can't call directly? Get a Type error
// ERROR TS2757: Type '() => void' has no call signatures.
// Noticed some other weird type errors if I don't declare the function type
// But I also am meh at typescripte signatures haha!
// subscriptions[i]();
let subFunc = subscriptions[i];
subFunc();
}
}
let plusOne = (): void => {
globalSubVar += 1;
}
let plusTwo = (): void => {
globalSubVar += 1;
}
let unsubPlusOne: () => void = subscribe(plusOne);
let unsubPlusTwo: () => void = subscribe(plusTwo);
assert(globalSubVar == 0);
assert(subscriptions.length == 2);
publish();
assert(globalSubVar == 3);
assert(subscriptions.length == 2);
unsubPlusOne();
assert(globalSubVar == 3);
assert(subscriptions.length == 1);
publish();
assert(globalSubVar == 5);
assert(subscriptions.length == 1);
unsubPlusTwo();
assert(globalSubVar == 5);
assert(subscriptions.length == 0);
publish();
assert(globalSubVar == 5);
assert(subscriptions.length == 0);
// Namespacing private functions
// Again, kind of weird, they should probably just make a class, but it's another interesting test
class Chunk {
totalSize: i32;
usedSize: i32;
write: (size: i32) => void;
}
let getChunk = (): Chunk => {
let chunk: Chunk = {
totalSize: 1024,
usedSize: 0,
write: (x: i32): void => {}
};
let growChunk = (): void => {
chunk.totalSize += 1024;
}
let allocateForChunk = (amount: i32): void => {
if (chunk.usedSize + amount <= chunk.totalSize) {
chunk.usedSize += amount;
} else {
// growChunk(chunk);
// allocateForChunk(chunk, amount);
}
}
chunk.write = (x: i32) => {
allocateForChunk(x);
}
return chunk;
}
let myChunk: Chunk = getChunk();
assert(myChunk.totalSize == 1024);
assert(myChunk.usedSize == 0);
myChunk.write(1025);
assert(myChunk.totalSize == 2048);
assert(myChunk.usedSize == 1025);
*/ |
Appreciate the additional test cases! As for this issue: // Cannot invoke an expression whose type lacks a call signature. Type 'closure-common-js-patterns/myFactory' has no compatible call signatures.
// assert(myFactory(1)() == 25); This appears to not be limited to closures, but a problem with calling functions in general which is present on master. I've cut a ticket. |
Isolated a problem from one of your test cases, made a minimal repro: class Foo {
field: i32;
}
var compClosuresSameScope = (): void => {
var foo: Foo = {
field: 8
}
var withFoo1 = (): void => {
foo.field += 1;
}
var withFoo2 = (): void => {
withFoo1();
}
} I think the problem here is that the field being closed over ( Update: I've written out a fix for this locally, we just need to lookup the locals we're injecting/calling rather than assuming that they're present locally and load them from context as necessary. I think given that this is fixable, it might make sense to just block this with |
@DuncanUszkay1 Thanks for the invite to your repo! Let me know if you want me to open a PR with my test file 😄
Awesome! Thanks for opening that issue! Agree that it probably shouldn't be in this PR if it's a general function calling issue 😄
Thanks for isolating that! 😄 I appreciate you taking the time to do that. Agreed, anything that isn't a fundamental design thing like we discussed in the meeting, I think it would be cool to just open a ticket for, that way we can start playing with closures, and keep the diff small(er). Since it seems like you already know the fix, I'm sure we could do a patch after this lands as well easily 😄 So! Honestly, that was all the tests I could think of. I actually tend not to use closures much outside of the tests that are already written, and the patterns I showed in my test. Let me know what I can help with next, would be more than happy to keep helping here 😄 |
Added in a warning flag to indicate that closure support is experimental:
|
Looking at the bootstrap test, there appear to be these two new warnings:
|
Warnings should be resolved now. Anything else before we fork @dcodeIO ? |
@DuncanUszkay1 We seem to have talked about this. What about replacing marking |
I don't think we really came to a conclusion on the recompilation step, but I suppose I may as well throw that in for now |
Can we close this PR in favor of the more recent #1308 covering the official beta branch? |
A Closure implementation which works as follows:
Closure|X
class instanceClosure|X
class instance one more timeThere are still some improvements to be made, but I think we're in a pretty stable state and things seem to all be working.
cc @willemneal
CURRENT 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: