noir-lang / noir

Noir is a domain specific language for zero knowledge proofs
https://noir-lang.org
Apache License 2.0
878 stars 190 forks source link

Noir Closures Implementation Progress #2385

Closed alexvitkov closed 11 months ago

alexvitkov commented 1 year ago

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

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.

Handling of Incompatible Closure 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

jfecher commented 1 year ago
  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 commented 11 months ago

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

jfecher commented 11 months ago

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