-
Notifications
You must be signed in to change notification settings - Fork 12
Add lambda expression proposal. #13
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
Changes from all commits
9990f13
6628c65
bd3500b
46176cd
ac0743d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,226 @@ | ||
| # SP #025: Lambda Expressions (Immutable Capture) | ||
|
|
||
| This proposal adds initial support for lambda expressions in Slang. | ||
| The initial proposal is to support immutable capture of variables in the surrounding scope. | ||
| This means that the lambda can only read the values of the captured variables, not modify them. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If there is a functional or language interaction reason for this restriction it would be nice to explain it. Or if it's just to reduce the scope of the initial implementation, that's also good to know.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is just to keep the initial scope small. There isn't a fundamental reason why we can't allow mutable captures, especially when targeting SPIRV/Metal where pointers are available. |
||
|
|
||
| ## Status | ||
|
|
||
| Status: In Implementation | ||
|
|
||
| Implementation: [PR 6914](https://github.com/shader-slang/slang/pull/6914) | ||
|
|
||
| Author: Yong He | ||
|
|
||
| Reviewer: Theresa Foley, Jeff Bolz | ||
|
|
||
| ## Background | ||
|
|
||
| SP009 introduced `IFunc` interface to represent callable objects. This allowed Slang code to | ||
| pass around functions as first-class values by defining types that implement `IFunc`. | ||
| However, this approach is not very convenient for users, as it requires defining a new type for each function | ||
| that needs to be passed around. | ||
|
|
||
| This problem can be solved with lambda expressions, which enables the compiler to synthesize such | ||
| boilerplate types automatically. The recent cooperative matrix 2 SPIRV extension introduced several opcodes | ||
| such as Reduce, PerElement, Decode etc. that can be expressed naturally with lambda expressions. | ||
|
|
||
| ## Proposal | ||
|
|
||
| The proposal is to add the following syntax for lambda expressions: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are all accessible values captured? Is there any way to write a "pure" lambda?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Values are captured on-demand. Unreferenced variables are not captured. To write a "pure" lambda, just make sure the lambda doesn't reference any local variables or parameters in the outer function. |
||
| ```slang | ||
| (parameter_list) => expression | ||
| ``` | ||
|
|
||
| or | ||
|
|
||
| ```slang | ||
| (parameter_list) => { statement_list } | ||
| ``` | ||
|
|
||
| Where `parameter_list` is a comma-separated list of parameters, same as those in ordinary functions, | ||
| and `expression` or `statement_list` defines the body of the lambda. For examples, these two lambdas | ||
| achieve similar results: | ||
|
|
||
| ```slang | ||
| (int x) => return x > 0 ? x : 0 | ||
|
|
||
| (int x) => { | ||
| if (x > 0) { | ||
| return x; | ||
| } else { | ||
| return 0; | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| A lambda expression will evaluate to an annoymous struct type that implements the `IFunc` interface | ||
csyonghe marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| during type checking. The return type of the lambda function is determined by the body expression (in the case | ||
| of the lambda expression contains a simple expression body), or the value of the return statements in the | ||
| case of a statement body. If the lambda function body contains more than one return statements, then the return | ||
| values from all return statements must be exactly the same. | ||
|
|
||
| In the future, we will also extend lambda expressions to allow them to conform to other interfaces including | ||
| `IDifferentiableFunc` or `IMutatingFunc`. | ||
|
|
||
| Lambda expressions can be used in positions that accepts an `IFunc`: | ||
|
|
||
| ``` | ||
| void apply(IFunc<int, float> x) {...} | ||
|
|
||
| void test() | ||
| { | ||
| apply((float x)=>(int)x+1); // OK, passing lambda to `IFunc<int, float>`. | ||
| } | ||
csyonghe marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ``` | ||
|
|
||
| ## Translation | ||
|
|
||
| Immutable lambda expressions translates into a struct type implementing the corresponding `IFunc` interface. | ||
| For example, given the following code: | ||
|
|
||
| ```slang | ||
| void test() | ||
| { | ||
| int c = 0; | ||
| let lam = (int x) => x + c; | ||
| int d = lam(2); | ||
| } | ||
| ``` | ||
|
|
||
| The compiler will translate it into: | ||
|
|
||
| ```slang | ||
| void test() | ||
| { | ||
| int c = 0; | ||
| struct _slang_Lambda_test_0 : IFunc<int, int> { | ||
| int c; | ||
csyonghe marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| __init(int in_c) { | ||
| c = in_c; | ||
| } | ||
| int operator()(int x) { | ||
| return x + c; | ||
| } | ||
| } | ||
| let lam = _slang_Lambda_test_0(c); | ||
| int d = lam.operator()(2); | ||
| } | ||
| ``` | ||
|
|
||
| ## Environment Capturing | ||
|
|
||
| If a lambda expression references a part of the environment variable either explicitly through a member or subscript operation | ||
| or implicitly through `this` dereference, the entire object will be captured in the context. For example, given: | ||
|
|
||
| ```slang | ||
| struct Composite | ||
| { | ||
| int member1; | ||
| float member2; | ||
| } | ||
|
|
||
| void test() | ||
| { | ||
| Composite c = {}; | ||
| let lam = (int x) => x + c.member2; | ||
| lam(2); | ||
| } | ||
| ``` | ||
|
|
||
| The generated `struct` type for the lambda expression will contain a member whose type is `Composite`, as in the following code: | ||
|
|
||
| ```slang | ||
| void test() | ||
| { | ||
| Composite c = {}; | ||
| struct _slang_Lambda_test_0 : IFunc<int, int> { | ||
| // Lambda captures the entire object instead of just | ||
| // `member1`. | ||
| Composite c; | ||
| __init(Composite in_c) { | ||
| c = in_c; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are all types copyable/assignable in slang? Or if it's not, would trying to use its value in the lambda lead to a compile error?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| } | ||
| int operator()(int x) { | ||
| return x + c.member1; | ||
| } | ||
| } | ||
| let lam = _slang_Lambda_test_0(c); | ||
| lam.operator()(2); | ||
| } | ||
| ``` | ||
|
|
||
| The same rules applies to implicit `this` parameter as well: | ||
|
|
||
| ```slang | ||
| struct Composite | ||
| { | ||
| int member1; | ||
| float member2; | ||
| void apply() | ||
| { | ||
| let lam = (int x)=>{ | ||
| return x + member1; // captures the entire `this`. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a reason why the whole object has to be captured? Seems like this could lead to users accidentally making expensive copies when working with large structures.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The decision to capture whole object ensures semantic consistency, so there is never confusion on situations such as: By saying we always capture the root object at all access chains, none of above ambiguous/difficult cases will ever arise. The redundant context can be cleaned up very easily in an IR pass post initial IR lowering. Spirv-Opt and most drivers are capable to clean this up today, so it is unlikely going to cause severe performance problems. If performance is ever a concern on some extreme edge cases, the user can always store the value into a local variable first, and then refer to that local variable within the lambda. |
||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ## Restrictions | ||
|
|
||
| Lambda expression in this proposed version can only read captured variables, but not modify them. | ||
csyonghe marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| For example: | ||
|
|
||
| ```slang | ||
| void test() | ||
| { | ||
| int c = 0; | ||
| let lam = (int x) { | ||
| c = c + 1; // Error: c is read-only here. | ||
| return x + c; | ||
| }; | ||
| int d = lam(3); | ||
| } | ||
| ``` | ||
|
|
||
| Once a mutable variable is captured by a lambda expression, the variable should not be modified | ||
| during the lifetime of the lambda expression, or the behavior is undefined. | ||
| We plan to allow mutating captured variables in a future proposal. | ||
csyonghe marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| The lifetime of a lambda expression should not outlive the scope where a lambda expression is defined, | ||
| or the behavior is undefined. | ||
|
|
||
| Lambda expression is not allowed to have mutable parameters, such as `inout` or `out` parameters in this version. | ||
|
|
||
| A variable whose type is `[NonCopyable]` cannot be captured in a lambda expression. | ||
csyonghe marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| The type system does not infer the expected parameter or return types of a lambda expression from | ||
| the context where the lambda expression is used. This restriction may be relaxed in the future | ||
| by reworking Slang's type checking to be more bi-directional. For example, the following code is not | ||
| allowed: | ||
|
|
||
| ```slang | ||
| // Error: cannot infer types of x,y and return type from `lam`'s type. | ||
| IFunc<float, int> lam = (x, y) => return x + y; | ||
| ``` | ||
|
|
||
| Slang also does not support implicit casting of lambda/function types. So the following code is not | ||
| allowed: | ||
|
|
||
| ```slang | ||
| // Error: cannot convert `IFunc<float, float>` to `IFunc<float, int>`. | ||
| IFunc<float, int> lam = (float x) => x; | ||
| ``` | ||
|
|
||
| Additionally, `throw` statements are currently not allowed in lambda expressions. | ||
|
|
||
| # Conclusion | ||
|
|
||
| This proposal adds limited support for lambda expressions that cannot mutate its captured environment. | ||
| Although being limited in functionality, this kind of lambda expressions will still be very useful | ||
| in many scenarios including the cooperative-matrix operations. | ||
csyonghe marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| This version of lambda expressions is easy to implement, and we do not need to consider nuanced semantics | ||
| around object lifetimes in this initial design. | ||
|
|
||
| In the future, we should extend the semantics to allow automatic differentiation and captured variable mutation | ||
| to make lambda expressions more useful. | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should find a chance to "bikeshed" the nomenclature we will use for this feature, without letting it bog down the implementation or proposal process.
The term "lambda" here is, I assume, largely due to the influence of C++ (although I'm aware it has a lot of other precedent). Other terms that should be considered include: