From 9990f1332eadd169caa0fabe8ed6aa07ce6c948e Mon Sep 17 00:00:00 2001 From: Yong He Date: Mon, 28 Apr 2025 11:32:06 -0700 Subject: [PATCH 1/5] Add lambda expression proposal. --- proposals/025-lambda-1.md | 141 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 proposals/025-lambda-1.md diff --git a/proposals/025-lambda-1.md b/proposals/025-lambda-1.md new file mode 100644 index 0000000..e37e2f0 --- /dev/null +++ b/proposals/025-lambda-1.md @@ -0,0 +1,141 @@ +# 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. + +## Status + +Status: In Implementation + +Implementation: [PR 6914](https://github.com/shader-slang/slang/pull/6914) + +Author: Yong He + +Reviewer: TBD + +## 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 expressed naturally with lambda expressions. + +## Proposal + +The proposal is to add the following syntax for lambda expressions: +```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 +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 first return statement in the +case of a statement body. If the lambda function body contains more than one return statements, and the result +value specified in any of the return statements cannot coerce to the result type of the first return statement, +the compiler will diagnose a type mismatch error on the mismatching return statement. + +Lambda expressions can be used in positions that accepts an `IFunc`: + +``` +void apply(IFunc x) {...} + +void test() +{ + apply((float x)=>(int)x+1); // OK, passing lambda to `IFunc`. +} +``` + +## 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 c; + __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); +} +``` + +## Restrictions + +Lambda expression in this proposed version can only read captured variables, but not modify them. +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); +} +``` + +We plan to allow mutating captured variables in a future proposal. + +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. + +# 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. +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. \ No newline at end of file From 6628c65c2005e7a20db21b95059a0de4426e3a13 Mon Sep 17 00:00:00 2001 From: Yong He Date: Mon, 28 Apr 2025 14:29:40 -0700 Subject: [PATCH 2/5] Addressing review comments. --- proposals/025-lambda-1.md | 91 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 87 insertions(+), 4 deletions(-) diff --git a/proposals/025-lambda-1.md b/proposals/025-lambda-1.md index e37e2f0..91caba8 100644 --- a/proposals/025-lambda-1.md +++ b/proposals/025-lambda-1.md @@ -56,10 +56,12 @@ achieve similar results: A lambda expression will evaluate to an annoymous struct type that implements the `IFunc` interface 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 first return statement in the -case of a statement body. If the lambda function body contains more than one return statements, and the result -value specified in any of the return statements cannot coerce to the result type of the first return statement, -the compiler will diagnose a type mismatch error on the mismatching return statement. +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`: @@ -106,6 +108,64 @@ void test() } ``` +## 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 { + // Lambda captures the entire object instead of just + // `member1`. + Composite c; + __init(Composite in_c) { + c = in_c; + } + 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`. + } + } +} +``` + ## Restrictions Lambda expression in this proposed version can only read captured variables, but not modify them. @@ -123,12 +183,35 @@ void test() } ``` +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. +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. +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 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` to `IFunc`. +IFunc lam = (float x) => x; +``` + # Conclusion This proposal adds limited support for lambda expressions that cannot mutate its captured environment. From bd3500b9885646789592d862bd43c3777c767c76 Mon Sep 17 00:00:00 2001 From: Yong He Date: Mon, 28 Apr 2025 15:14:51 -0700 Subject: [PATCH 3/5] Add reviewer status. --- proposals/025-lambda-1.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/025-lambda-1.md b/proposals/025-lambda-1.md index 91caba8..81d6556 100644 --- a/proposals/025-lambda-1.md +++ b/proposals/025-lambda-1.md @@ -12,7 +12,7 @@ Implementation: [PR 6914](https://github.com/shader-slang/slang/pull/6914) Author: Yong He -Reviewer: TBD +Reviewer: Theresa Foley ## Background From 46176cd0c6d18abb61fe1734d604ef79bf33e9d1 Mon Sep 17 00:00:00 2001 From: Yong He Date: Mon, 28 Apr 2025 19:46:29 -0700 Subject: [PATCH 4/5] Update proposals/025-lambda-1.md Co-authored-by: Jeff Bolz --- proposals/025-lambda-1.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/025-lambda-1.md b/proposals/025-lambda-1.md index 81d6556..09c2803 100644 --- a/proposals/025-lambda-1.md +++ b/proposals/025-lambda-1.md @@ -23,7 +23,7 @@ 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 expressed naturally with lambda expressions. +such as Reduce, PerElement, Decode etc. that can be expressed naturally with lambda expressions. ## Proposal From ac0743db5b0271258133c7916d377923a08870d3 Mon Sep 17 00:00:00 2001 From: Yong He Date: Tue, 29 Apr 2025 19:56:48 -0700 Subject: [PATCH 5/5] Clarify that `throw` is not allowed. --- proposals/025-lambda-1.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/proposals/025-lambda-1.md b/proposals/025-lambda-1.md index 09c2803..ee50e4e 100644 --- a/proposals/025-lambda-1.md +++ b/proposals/025-lambda-1.md @@ -12,7 +12,7 @@ Implementation: [PR 6914](https://github.com/shader-slang/slang/pull/6914) Author: Yong He -Reviewer: Theresa Foley +Reviewer: Theresa Foley, Jeff Bolz ## Background @@ -212,6 +212,8 @@ allowed: IFunc 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.