Visitar URL original
Implement closures · Issue #798 · AssemblyScript/assemblyscript · GitHub
Skip to content

Implement closures #798

Description

@MaxGraey

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:

declare function externalCall(a: i32, b: i32): void;

function range(a: i32, b: i32, fn: (n: i32) => void): void {
  if (a < b) {
    fn(a);
    range(a + 1, b, fn);
  }
}

export function test(n: i32): void {
  range(0, n, (i: i32) => {
    externalCall(i, n); // capture n
  });
}

which transform to:

// generated
class ClosureContext {
  fn: (ctx: usize, i: i32) => void;
  n: i32; // captured var
  // optinal "self: usize;" is closure instantiate inside instance class method
  parent: ClosureContext | null = null;
}

// generated
function lambdaFn(ctx: usize, i: i32): void {
  externalCall(i, changetype<ClosureContext>(ctx).n);
}

function range(a: i32, b: i32, ctx: usize): void {
  if (a < b) {
    changetype<ClosureContext>(ctx).fn(ctx, a); // replaced from "fn(a)";
    range(a + 1, b, ctx);
  }
}

export function test(n: i32): void {
  // insert
  let ctx = new ClosureContext();
  ctx.fn = lambdaFn;
  ctx.n = n;
  //
  range(0, n, changetype<usize>(ctx));
}

Closure and ClosureContext will not generated when no one variable was captured and use usual anonym functions.
ClosureContext will not created when only this was captured. In this case ctx param use to pass this reference.

Other discussions: #563

@dcodeIO @jtenner @willemneal Let me know what you think about this?

Activity

  1. jtenner commented on Aug 28, 2019

    @jtenner
    Contributor

    I think we need to override the meaning of call indirect.

  2. jtenner commented on Aug 28, 2019

    @jtenner
    Contributor

    How would this transform?

    function add(a, b): callback {
      return () => a + b;
    }
  3. MaxGraey commented on Aug 28, 2019

    @MaxGraey
    MemberAuthor

    @jtenner

    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;
    }
  4. MaxGraey commented on Aug 28, 2019

    @MaxGraey
    MemberAuthor

    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)
  5. jtenner commented on Aug 28, 2019

    @jtenner
    Contributor

    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?

  6. jtenner commented on Aug 28, 2019

    @jtenner
    Contributor

    Is there a way to make a table and add entries to it?

  7. MaxGraey commented on Aug 28, 2019

    @MaxGraey
    MemberAuthor

    @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 fn from returned ClosureContext object. Just provide another util for loader

  8. MaxGraey commented on Aug 28, 2019

    @MaxGraey
    MemberAuthor

    Next 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;
    }
  9. jtenner commented on Aug 28, 2019

    @jtenner
    Contributor

    Well I'm thinking about aspect collecting function pointers. How will aspect need to work with the function pointers?

  10. jtenner commented on Aug 28, 2019

    @jtenner
    Contributor

    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.

  11. MaxGraey commented on Aug 28, 2019

    @MaxGraey
    MemberAuthor

    @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
    });
  12. jtenner commented on Aug 28, 2019

    @jtenner
    Contributor

    It'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.

  13. MaxGraey commented on Aug 28, 2019

    @MaxGraey
    MemberAuthor

    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
  14. jtenner commented on Aug 28, 2019

    @jtenner
    Contributor

    I'm saying a in that example needs to exist on the heap for both b and c to access it, and the closure class needs to contain a Box<i32> that points to the heap location.

  15. MaxGraey commented on Aug 28, 2019

    @MaxGraey
    MemberAuthor

    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's parent LexicalEnvironment.

  16. 71 remaining items

  17. luffyfly commented on Oct 27, 2022

    @luffyfly

    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.

  18. gabriel-fallen commented on Oct 27, 2022

    @gabriel-fallen

    @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).

  19. luffyfly commented on Oct 27, 2022

    @luffyfly

    @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.

  20. MaxGraey commented on Oct 27, 2022

    @MaxGraey
    MemberAuthor

    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

  21. CryZe commented on Oct 27, 2022

    @CryZe

    doesn'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_indirect and 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.

  22. luffyfly commented on Oct 27, 2022

    @luffyfly

    @CryZe I have tried function pointers, and It worked but It is so weird and difficult to use.

  23. luffyfly commented on Oct 27, 2022

    @luffyfly

    @MaxGraey If AS has aysnc implementations, I will not take closures.

  24. MaxGraey commented on Oct 27, 2022

    @MaxGraey
    MemberAuthor

    @luffyfly Again closures and aysnc is absolutely different things. Dsynchrony without calling "then" in returned Promise but through "await" does not use dispatch at all. You decide what you want.

  25. luffyfly commented on Oct 27, 2022

    @luffyfly

    @MaxGraey See #376 (comment).
    Actually, I want both.

  26. butterunderflow commented on Dec 14, 2022

    @butterunderflow

    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.

  27. pchasco commented on Jan 25, 2023

    @pchasco

    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.

  28. alienself commented on Mar 15, 2024

    @alienself

    Any update on this? Another year without closures 😢

    Will 2024 the year of closures and thread support in AssemblyScript?

  29. LeXXik commented on Aug 23, 2024

    @LeXXik

    Upvoting this. Array find functions, like findIndex are pretty much useless without closures, as you can't compare an object property with some passed variable.

  30. leeho0108 commented on May 22, 2025

    @leeho0108

    any news for closures? Or is there any plan or time we can expected?

  31. GreenReaper commented on Dec 12, 2025

    @GreenReaper

    Thanks 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...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions