Skip to content

Commit 5ee249e

Browse files
authored
Merge pull request #13 from csyonghe/main
Add lambda expression proposal.
2 parents 7cfeddf + ac0743d commit 5ee249e

File tree

1 file changed

+226
-0
lines changed

1 file changed

+226
-0
lines changed

proposals/025-lambda-1.md

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
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

Comments
 (0)