Repository navigation
Implement closures #798
Description
Activity
I think we need to override the meaning of call indirect.
How would this transform?
function add(a, b): callback { return () => a + b; }
class ClosureContext { fn: (ctx: usize) => i32; a: i32; b: i32; parent: ClosureContext | null = null; } function lambdaAdd(ctx: usize): i32 { return changetype<ClosureContext>(ctx).a + changetype<ClosureContext>(ctx).b; } function add(a: i32, b: i32): ClosureContext { let ctx = new ClosureContext(); ctx.fn = lambdaAdd; ctx.a = a; ctx.b = b; return ctx; }
Instead
class ClosureContext { fn: (ctx: usize) => i32; b: i32; ... }
we could just store index for indirect function table
class ClosureContext { fnIdx: usize; a: i32; ... } ... call_indirect(ctx.fnIdx, ...args)
How does this work with function dispatch?
For instance, let's say I pass the Closure Context out to js as a pointer. How can I re-call it?
Is there a way to make a table and add entries to it?
@jtenner actually you need pass only
fn/fnIdx. After that just use same approach as for anonymus indirect function calls.EDIT No you need unpack
fnfrom returnedClosureContextobject. Just provide another util for loaderReacted by Joshua TennerNext example:
function test(fn: (x: i32) => void): i32 { let n = 0; fn(x => { n = x }); return n; }
should generate:
class ClosureContext { fn: (ctx: usize, x: i32) => void; n: i32; parent: ClosureContext | null = null; } function lambdaFn(ctx: usize, x: i32): void { changetype<ClosureContext>(ctx).n = x; } function test(fn: (x: i32) => void): i32 { let n = 0; let ctx = new ClosureContext(); ctx.fn = lambdaFn; ctx.n = n; fn(changetype<usize>(ctx)); n = ctx.n; return n; }
Well I'm thinking about aspect collecting function pointers. How will aspect need to work with the function pointers?
My guess is that passing around the closure context will cause problems with manually using call_indirect like aspect does.
Also, this closure method doesn't handle multiple references to the same local.
let a = 1; let b = () => a; let c = () => a += 1;
B and c will get different versions of a.
@jtenner
You could get table and get function by index / ref. For example you have wasm:(func $foo (result i32) (i32.const 1234)) (table (export "tbl") anyfunc (elem $foo))
than you js part:
WebAssembly.instantiateStreaming(fetch('main.wasm')).then(({ instance }) => { const table = instance.exports.tbl; console.log(table.get(0)()); // 1234 });
Reacted by Joshua TennerIt's likely we will need to allocate enclosed local values in a table or on the heap. Each pointer to those values will need to be stored in a table pointing to the heap.
This idea is Naive because the variables can no longer be treated like local variables because it's possible to modify local values before the function finishes executing.
B and c will get different versions of a.
Why?
let a = 1; let b = () => a; let c = () => a += 1; let br = b(); // 1 let cr = c(); // 2 // assert(a == 2)
Will generate:
class ClosureContextB { fn; a; } class ClosureContextC { fn; a; } function lambdaB(ctx: usize): i32 { return changetype<ClosureContextB>(ctx).a; } function lambdaC(ctx: usize): i32 { return changetype<ClosureContextC>(ctx).a += 1; } let a = 1; let ctxB = new ClosureContextB(lambdaB, a); let br = b(ctxB); // 1 // a = ctxB.a; // 1 unmodified so unnecessary let ctxC = new ClosureContextB(lambdaC, a); let cr = c(ctxC); // 2 a = ctxC.a; // 2
I'm saying
ain that example needs to exist on the heap for bothbandcto access it, and the closure class needs to contain aBox<i32>that points to the heap location.No, we don't need pass plain types by boxed references. Also found pretty clear article. So
ClosureContext(LexicalEnvironment) should be little bit modifier and also store reference to it'sparentLexicalEnvironment.Reacted by Joshua Tenner and Iddan Aaronsohn71 remaining items
I planned to write a game engine use AS and I have programmed for a month, but now I give up AS and choose Rust because lack of supports for closures. Just let you know, It's not reasonable to not support closures in any case.
@luffyfly as a Functional Programming enthusiast I totally agree that closures are super useful and handy. But are you sure closures in Rust are implemented for Wasm efficient enough for your game engine?
As a general observation, in the current state Wasm presents a poor target for Functional Programming: doesn't efficiently support heavy use of function pointers, closures and GC. Moreover AssemblyScript is not really a Functional Language. And if you're programming in OO style it's not hard to implement purpose-built closures out of objects (going as far as Strategy Pattern if really needed).
@gabriel-fallen I'm pretty sure about that, because When I call the fetch() function of the web host from Wasm, I have to use closures as callbacks to avoid blocking the main thread.
I have to use closures as callbacks to avoid blocking the main thread.
Just to note. Closures and async (cooperative multitasking) are two independent things
Reacted by Alexander Chichigin and Oncelockdoesn't efficiently support heavy use of function pointers
Closures in Rust are rarely represented as function pointers and instead as anonymous structs that have a "call" method that is statically dispatched rather than dynamically in almost all cases. The method is almost always inlined too, to the point where almost always you can't even tell it ever was a closure to begin with. It's very strongly a zero cost abstraction there.
Though I'm also wondering why you say function pointers aren't well supported by Wasm. As far as I know they are just functions stored in the function table and then called via an
call_indirectand an index into the table. I don't see a problem with that approach, other than having to know all the functions that are going to be called indirectly ahead of time, but that's not a problem for a compiled language.GC though is definitely a problem with closures for languages that don't have an ownership system.
Reacted by Sʜɪᴍᴜʀᴀ Yū@CryZe I have tried function pointers, and It worked but It is so weird and difficult to use.
@MaxGraey If AS has aysnc implementations, I will not take closures.
@luffyfly Again closures and
aysncis absolutely different things. Dsynchrony without calling "then" in returned Promise but through "await" does not use dispatch at all. You decide what you want.@MaxGraey See #376 (comment).
Actually, I want both.What is the current state of the closure implementation? I've seen several PRs related to closures, but none of them have been merged. I'm curious about what is the biggest block of closure implementation.
I am interested in using AssemblyScript, but must admit I am reluctant to use until closures are implemented. Yes, obviously there are workarounds, but they are inconvenient and add to code bloat. All TypeScript developers whom I assume this project hopes to pilfer are accustomed to the convenience of closures and to some degree of functional programming.
Many languages that target WASM already support closures, so I am confused as to what it is about AssemblyScript specifically that prevents closures from being implemented. I would be surprised if some limitation in WASM were a real roadblock. I saw some mentions of concerns around performance and allocations, which are reasonable, but I wonder if this is one of those situations where a bit of runtime efficiency is sacrificed for developer convenience? And the implementation wouldn't necessarily be the perfect implementation out the gate, so long as it is functionally correct. It could be refined or replaced later when new WASM features are introduced, or if a better implementation is worked out.
Reacted by Michael, William Raiford, Matias O, Troy, 맛카치, Wolfgang Steiner, felixroos, Samrat Bhandari, Congcong Cai, Dan Kersten and 1 moreReacted by butterunderflow, Luo Gang, Troy and 맛카치Any update on this? Another year without closures 😢
Will 2024 the year of closures and thread support in AssemblyScript?
Reacted by jsoldi, George Phillips and Chase CarlsonReacted by Michael, 맛카치, Dmitry and Artem SimonenkoUpvoting this. Array find functions, like
findIndexare pretty much useless without closures, as you can't compare an object property with some passed variable.Reacted by Michael, Dmitry, Christophe Deveaux, 맛카치, George Phillips, Laurence 'GreenReaper' Parry, Artem Simonenko, Rodrigo Faria, Joachim Wester, LeeHo and 1 moreany news for closures? Or is there any plan or time we can expected?
Reacted by Rene Leonhardt and Laurence 'GreenReaper' ParryThanks so much for working on this, @BlobMaster41; I was literally telling someone at Wolfery how closures weren't possible in room scripts yet and dropped by to see you were working on it. Will be keen to see it in action, assuming it lands...
Reacted by Anakun and Abdiel LopezReacted by Rene Leonhardt and Abdiel Lopez
I decide to start discussion about simple closure implementations.
Some obvious (and may be naive) implementation is using generation class context which I prefer to demonstrate in following example:
which transform to:
Closure and ClosureContext will not generated when no one variable was captured and use usual anonym functions.
ClosureContext will not created when only
thiswas captured. In this casectxparam use to pass this reference.Other discussions: #563
@dcodeIO @jtenner @willemneal Let me know what you think about this?