Skip to content
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

Noir Closures Implementation Progress #2385

Closed
5 of 9 tasks
alexvitkov opened this issue Aug 21, 2023 · 3 comments
Closed
5 of 9 tasks

Noir Closures Implementation Progress #2385

alexvitkov opened this issue Aug 21, 2023 · 3 comments
Labels
enhancement New feature or request

Comments

@alexvitkov
Copy link
Contributor

alexvitkov commented Aug 21, 2023

I'm opening this as central place to track progress on closures and related features as well as to discuss them.

I'm looking for input on the two big points at the bottom (arrays of closures & handling of incompatible closure types), as there are a few question marks that we should get sorted out.

Basic

fn make_counter() -> fn() -> Field {
    let x = &mut 0;
    || {
         *x = *x + 1;
         *x
    }
}

I have implemented this locally, but in some cases (including the one above) it generates code that triggers this bug: #2316

  • stdlib - The signatures of higher-order functions in the stdlib will need to be changed to work with closures. We can do a lot of that with the environment capture syntax we added fn[env](args) -> ret, or we can wait until we have Fn traits.

  • Introduce traits for callable objects such as closures (i.e. Fn).
    Blocked on missing traits features

Arrays of Incompatible Closure Closures

We could transparently handle arrays of closures with different environments by rewriting them to tuples. Iterating over them can be implemented by unrolling the loop during monomorphization. One problem with this idea is calling a function at an index not known at compile time, we may have to introduce hidden branching to handle it.

  • Support arrays of closures by treating them as tuples.

  • Unroll loops over closure arrays in the monomorphization pass in order to be able to instantiate the callee for the correct environment types.

Handling of Incompatible Closure Types

  • Settle the language design question regarding expressions involving incompatible closure types. This problem arises in two situations:
    • if/match statements where the different branches return closures with different environment types.
    • functions returning closures where different exit paths may return closures with different environment types.

The current implementation treats these as compile-time errors, but this may be surprising for many users. As a minimum, we must document this limitation in the Noir manual.

Alternatively, we can rewrite such expressions to hidden enum types than can capture all alternative closure environment types. Later, when the closure is used, Noir will emit hidden branching code to handle every possible case.

This functionality will be complex to implement as the hidden branching must also be applied at call-sites that accept closures as parameters. In the presence of multiple closure parameters, we’ll need nested branching. Performing such transformations behind the scenes may be highly controversial because it can lead to an explosion in the number of required constraints for otherwise simple and innocently looking programs, so perhaps the users will benefit from doing the harder work of rewriting their closure-based code to avoid the use of incompatible closure types.

Alternatives Considered

No response

Additional Context

No response

Would you like to submit a PR for this Issue?

No

Support Needs

No response

@alexvitkov alexvitkov added the enhancement New feature or request label Aug 21, 2023
@github-project-automation github-project-automation bot moved this to 📋 Backlog in Noir Aug 21, 2023
@jfecher
Copy link
Contributor

jfecher commented Aug 21, 2023

  fn make_counter() -> fn() -> Field {
    let mut x = 0;
    || {
         x = x + 1;
         x
    }
}

I think this could should produce an error that x was captured by copy and not by reference, so it is immutable. If we wanted to capture a mutable variable we'd need to do:

fn make_counter() -> fn() -> Field {
    let x = &mut 0;
    || {
         x = x + 1;
         x
    }
}

stdlib - The signatures of higher-order functions in the stdlib will need to be changed to work with closures. We can do a lot of that with the environment capture syntax we added fnenv -> ret, or we can wait until we have Fn traits.

I'm all for doing this sooner rather than later. It's a small change so let's do this after #2383

As for both Arrays of Incompatible Closure Closures and Handling of Incompatible Closure Types, I'd say these are unnecessary for your work on implementing closures. However, these could be useful in the long term. I just don't see them as being implemented soon since it entails a lot of implicit generic managing and corner cases.

@Savio-Sou
Copy link
Collaborator

Savio-Sou commented Nov 2, 2023

Is the work ongoing? Or should we break the remaining tasks into standalone Issues?

@jfecher
Copy link
Contributor

jfecher commented Nov 3, 2023

@Savio-Sou we can mark this as complete with

Introduce traits for callable objects such as closures (i.e. Fn).
Blocked on missing traits features

Being the only remaining feature to break out into a separate issue. I'm not exactly sure we need it though considering we have syntax for closure types already: fn[Env](Params) -> Ret.

@jfecher jfecher closed this as completed Nov 3, 2023
@github-project-automation github-project-automation bot moved this from 📋 Backlog to ✅ Done in Noir Nov 3, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
Archived in project
Development

No branches or pull requests

3 participants