Skip to content

Conversation

@csyonghe
Copy link
Contributor

No description provided.

@CLAassistant
Copy link

CLAassistant commented Apr 28, 2025

CLA assistant check
All committers have signed the CLA.

Copy link
Contributor

@tangent-vector tangent-vector left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a great proposal to see. It is very encouraging when cutting-edge GPU capabilities are helping to drive the push for Slang to adopt more and more "grown-up" language features, like closures/lambdas.

@@ -0,0 +1,141 @@
# SP #025: Lambda Expressions (Immutable Capture)
Copy link
Contributor

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:

  • "closures" / "closure expressions"
  • "anonymous functions" / "anonymous function expressions"
  • "function expressions"

Copy link
Contributor

@tangent-vector tangent-vector left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for making those edits.

For anybody following from the sidelines, this proposal is a good example of how to cover the major issues, without going into too much detail, for a proposal where the implementation is still ongoing.


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.
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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


## Proposal

The proposal is to add the following syntax for lambda expressions:
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

// `member1`.
Composite c;
__init(Composite in_c) {
c = in_c;
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RayQuery and HitObject types are currently not copyable, and use of these in the capture will lead to a compile error.

Co-authored-by: Jeff Bolz <jbolz@nvidia.com>
void apply()
{
let lam = (int x)=>{
return x + member1; // captures the entire `this`.

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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:

struct Obj { int a; int b; };
int ar[10];
Obj o;
var lam = () =>
{
     f(o);   // captures o, obviously
     g(o.a);  // do we capture another copy of o.a or reuse o?
     ar[0]; // capture 0-th element or entire array?
     ar[i]; // what do we do?
};

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.

@csyonghe csyonghe merged commit 5ee249e into shader-slang:main Apr 30, 2025
1 check was pending
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants