|
| 1 | +# SP #025: Lambda Expressions (Immutable Capture) |
| 2 | + |
| 3 | +This proposal adds initial support for lambda expressions in Slang. |
| 4 | +The initial proposal is to support immutable capture of variables in the surrounding scope. |
| 5 | +This means that the lambda can only read the values of the captured variables, not modify them. |
| 6 | + |
| 7 | +## Status |
| 8 | + |
| 9 | +Status: In Implementation |
| 10 | + |
| 11 | +Implementation: [PR 6914](https://github.com/shader-slang/slang/pull/6914) |
| 12 | + |
| 13 | +Author: Yong He |
| 14 | + |
| 15 | +Reviewer: Theresa Foley, Jeff Bolz |
| 16 | + |
| 17 | +## Background |
| 18 | + |
| 19 | +SP009 introduced `IFunc` interface to represent callable objects. This allowed Slang code to |
| 20 | +pass around functions as first-class values by defining types that implement `IFunc`. |
| 21 | +However, this approach is not very convenient for users, as it requires defining a new type for each function |
| 22 | +that needs to be passed around. |
| 23 | + |
| 24 | +This problem can be solved with lambda expressions, which enables the compiler to synthesize such |
| 25 | +boilerplate types automatically. The recent cooperative matrix 2 SPIRV extension introduced several opcodes |
| 26 | +such as Reduce, PerElement, Decode etc. that can be expressed naturally with lambda expressions. |
| 27 | + |
| 28 | +## Proposal |
| 29 | + |
| 30 | +The proposal is to add the following syntax for lambda expressions: |
| 31 | +```slang |
| 32 | +(parameter_list) => expression |
| 33 | +``` |
| 34 | + |
| 35 | +or |
| 36 | + |
| 37 | +```slang |
| 38 | +(parameter_list) => { statement_list } |
| 39 | +``` |
| 40 | + |
| 41 | +Where `parameter_list` is a comma-separated list of parameters, same as those in ordinary functions, |
| 42 | +and `expression` or `statement_list` defines the body of the lambda. For examples, these two lambdas |
| 43 | +achieve similar results: |
| 44 | + |
| 45 | +```slang |
| 46 | +(int x) => return x > 0 ? x : 0 |
| 47 | +
|
| 48 | +(int x) => { |
| 49 | + if (x > 0) { |
| 50 | + return x; |
| 51 | + } else { |
| 52 | + return 0; |
| 53 | + } |
| 54 | +} |
| 55 | +``` |
| 56 | + |
| 57 | +A lambda expression will evaluate to an annoymous struct type that implements the `IFunc` interface |
| 58 | +during type checking. The return type of the lambda function is determined by the body expression (in the case |
| 59 | +of the lambda expression contains a simple expression body), or the value of the return statements in the |
| 60 | +case of a statement body. If the lambda function body contains more than one return statements, then the return |
| 61 | +values from all return statements must be exactly the same. |
| 62 | + |
| 63 | +In the future, we will also extend lambda expressions to allow them to conform to other interfaces including |
| 64 | +`IDifferentiableFunc` or `IMutatingFunc`. |
| 65 | + |
| 66 | +Lambda expressions can be used in positions that accepts an `IFunc`: |
| 67 | + |
| 68 | +``` |
| 69 | +void apply(IFunc<int, float> x) {...} |
| 70 | +
|
| 71 | +void test() |
| 72 | +{ |
| 73 | + apply((float x)=>(int)x+1); // OK, passing lambda to `IFunc<int, float>`. |
| 74 | +} |
| 75 | +``` |
| 76 | + |
| 77 | +## Translation |
| 78 | + |
| 79 | +Immutable lambda expressions translates into a struct type implementing the corresponding `IFunc` interface. |
| 80 | +For example, given the following code: |
| 81 | + |
| 82 | +```slang |
| 83 | +void test() |
| 84 | +{ |
| 85 | + int c = 0; |
| 86 | + let lam = (int x) => x + c; |
| 87 | + int d = lam(2); |
| 88 | +} |
| 89 | +``` |
| 90 | + |
| 91 | +The compiler will translate it into: |
| 92 | + |
| 93 | +```slang |
| 94 | +void test() |
| 95 | +{ |
| 96 | + int c = 0; |
| 97 | + struct _slang_Lambda_test_0 : IFunc<int, int> { |
| 98 | + int c; |
| 99 | + __init(int in_c) { |
| 100 | + c = in_c; |
| 101 | + } |
| 102 | + int operator()(int x) { |
| 103 | + return x + c; |
| 104 | + } |
| 105 | + } |
| 106 | + let lam = _slang_Lambda_test_0(c); |
| 107 | + int d = lam.operator()(2); |
| 108 | +} |
| 109 | +``` |
| 110 | + |
| 111 | +## Environment Capturing |
| 112 | + |
| 113 | +If a lambda expression references a part of the environment variable either explicitly through a member or subscript operation |
| 114 | +or implicitly through `this` dereference, the entire object will be captured in the context. For example, given: |
| 115 | + |
| 116 | +```slang |
| 117 | +struct Composite |
| 118 | +{ |
| 119 | + int member1; |
| 120 | + float member2; |
| 121 | +} |
| 122 | +
|
| 123 | +void test() |
| 124 | +{ |
| 125 | + Composite c = {}; |
| 126 | + let lam = (int x) => x + c.member2; |
| 127 | + lam(2); |
| 128 | +} |
| 129 | +``` |
| 130 | + |
| 131 | +The generated `struct` type for the lambda expression will contain a member whose type is `Composite`, as in the following code: |
| 132 | + |
| 133 | +```slang |
| 134 | +void test() |
| 135 | +{ |
| 136 | + Composite c = {}; |
| 137 | + struct _slang_Lambda_test_0 : IFunc<int, int> { |
| 138 | + // Lambda captures the entire object instead of just |
| 139 | + // `member1`. |
| 140 | + Composite c; |
| 141 | + __init(Composite in_c) { |
| 142 | + c = in_c; |
| 143 | + } |
| 144 | + int operator()(int x) { |
| 145 | + return x + c.member1; |
| 146 | + } |
| 147 | + } |
| 148 | + let lam = _slang_Lambda_test_0(c); |
| 149 | + lam.operator()(2); |
| 150 | +} |
| 151 | +``` |
| 152 | + |
| 153 | +The same rules applies to implicit `this` parameter as well: |
| 154 | + |
| 155 | +```slang |
| 156 | +struct Composite |
| 157 | +{ |
| 158 | + int member1; |
| 159 | + float member2; |
| 160 | + void apply() |
| 161 | + { |
| 162 | + let lam = (int x)=>{ |
| 163 | + return x + member1; // captures the entire `this`. |
| 164 | + } |
| 165 | + } |
| 166 | +} |
| 167 | +``` |
| 168 | + |
| 169 | +## Restrictions |
| 170 | + |
| 171 | +Lambda expression in this proposed version can only read captured variables, but not modify them. |
| 172 | +For example: |
| 173 | + |
| 174 | +```slang |
| 175 | +void test() |
| 176 | +{ |
| 177 | + int c = 0; |
| 178 | + let lam = (int x) { |
| 179 | + c = c + 1; // Error: c is read-only here. |
| 180 | + return x + c; |
| 181 | + }; |
| 182 | + int d = lam(3); |
| 183 | +} |
| 184 | +``` |
| 185 | + |
| 186 | +Once a mutable variable is captured by a lambda expression, the variable should not be modified |
| 187 | +during the lifetime of the lambda expression, or the behavior is undefined. |
| 188 | +We plan to allow mutating captured variables in a future proposal. |
| 189 | + |
| 190 | +The lifetime of a lambda expression should not outlive the scope where a lambda expression is defined, |
| 191 | +or the behavior is undefined. |
| 192 | + |
| 193 | +Lambda expression is not allowed to have mutable parameters, such as `inout` or `out` parameters in this version. |
| 194 | + |
| 195 | +A variable whose type is `[NonCopyable]` cannot be captured in a lambda expression. |
| 196 | + |
| 197 | +The type system does not infer the expected parameter or return types of a lambda expression from |
| 198 | +the context where the lambda expression is used. This restriction may be relaxed in the future |
| 199 | +by reworking Slang's type checking to be more bi-directional. For example, the following code is not |
| 200 | +allowed: |
| 201 | + |
| 202 | +```slang |
| 203 | +// Error: cannot infer types of x,y and return type from `lam`'s type. |
| 204 | +IFunc<float, int> lam = (x, y) => return x + y; |
| 205 | +``` |
| 206 | + |
| 207 | +Slang also does not support implicit casting of lambda/function types. So the following code is not |
| 208 | +allowed: |
| 209 | + |
| 210 | +```slang |
| 211 | +// Error: cannot convert `IFunc<float, float>` to `IFunc<float, int>`. |
| 212 | +IFunc<float, int> lam = (float x) => x; |
| 213 | +``` |
| 214 | + |
| 215 | +Additionally, `throw` statements are currently not allowed in lambda expressions. |
| 216 | + |
| 217 | +# Conclusion |
| 218 | + |
| 219 | +This proposal adds limited support for lambda expressions that cannot mutate its captured environment. |
| 220 | +Although being limited in functionality, this kind of lambda expressions will still be very useful |
| 221 | +in many scenarios including the cooperative-matrix operations. |
| 222 | +This version of lambda expressions is easy to implement, and we do not need to consider nuanced semantics |
| 223 | +around object lifetimes in this initial design. |
| 224 | + |
| 225 | +In the future, we should extend the semantics to allow automatic differentiation and captured variable mutation |
| 226 | +to make lambda expressions more useful. |
0 commit comments