-
Notifications
You must be signed in to change notification settings - Fork 7
Inline Functions Proposal #147
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
base: main
Are you sure you want to change the base?
Changes from all commits
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,108 @@ | ||
| # Inline Functions | ||
|
|
||
| Inline functions are inlined at the calling site, meaning the function body is “copied” in place of the call expression. Inline functions have both advantages and limitations compared to typical functions. | ||
|
|
||
| ## Syntax | ||
|
|
||
| Function declarations preceded with the `@inline` attribute are marked as inline functions. | ||
|
|
||
| ## Behavior | ||
|
|
||
| Inline functions behave somewhat like macros, in the sense that their contents are inlined at the call site, and the declaration is erased at compilation time. | ||
|
|
||
| However, inline functions are hygienic, meaning that they can only access declarations in scope. In-scope declarations are module-scope declarations and function parameters. | ||
|
|
||
| (TODO) should we allow discard statements? diagnostics? what should be forbidden in inline functions? | ||
|
|
||
| ### Parameter and Return Types | ||
|
|
||
| Inline functions lift some limitations on allowed parameters and return types. In WGSL, a function return type must be constructible. The return type of an inline function can be a constructible type, a texture, sampler, or pointer type. An inline function may also return a parameter or a module-scope declaration, including bindings. Returning parameters or module-scope declarations does not perform a copy, instead they must are “returned by reference” and can only be bound to a variable. | ||
|
|
||
|
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. can the inline function call be a parameter to a function call?
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. yes. There is one limitation though, that I didn't write down: order of evaluation. let x = foo(sideEffect(), inlineFunc());If { /* inline inlineFunc's side-effects */ }
let x = foo(sideEffect(), /* inlineFunc's return */);Ofc a more complex codegen can circumvent this: let arg1 = sideEffect();
{ /* inline inlineFunc's side-effects */ }
let arg2 = /* inlineFunc's return */;
let x = foo(arg1, arg2); |
||
| In WGSL, function-scope var-declarations must have a constructible type. Exceptionally, non-constructible types returned from inline functions can be bound to a var-declaration. | ||
| The scope, address space and access mode of the declaration is inferred from the inline function return value. (TODO: should we allow explicit? in particular the access mode, if applicable.) | ||
| Non-constructible types cannot be bound to value declarations. | ||
|
|
||
| NOTE: In inline functions returning non-constructible types, the code path leading to the return statement cannot contain branches, so the returned value is determined at shader-creation time. But the function can contain runtime side-effects alongside. | ||
|
Collaborator
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. Do we really need this restriction? I thought I could write stuff like It was just problematic for textures because of nom-uniform control flow leading to missing partial derivatives when sampling.
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. Your code would be valid and I misphrased the sentence. It should have been
|
||
|
|
||
| Inline functions can call other inline functions, as well as non-inline functions. | ||
|
|
||
| ### Limitations | ||
|
|
||
| Entry-points functions cannot be inline. | ||
|
|
||
| Inline functions can only be invoked from inside a function body. | ||
| (TODO: an inline function containing a single return statement could be inlined in const-expressions. Though, this is limited too and could be a job better suited to user-defined `@const` functions) | ||
|
|
||
| Inline functions with a return value *must* contain a single return statement at the end of the function body (the last statement), and *must not* contain any early return. This rule is only checked after full resolution of conditional translation attributes. | ||
| (TODO can we lift this limitation? we could copy-paste the code following the function invocation in all return branches. This could lead to unexpectedly large code, and unexpected non-uniform control flow. But is much more powerful otherwise.) | ||
|
|
||
| NOTE: This ensures that the macro outer control flow has no branch and can be inlined without affecting the following code. | ||
|
|
||
| NOTE: Compile-time branching can be performed inside inline functions with the help of conditional translation. See the example below. | ||
|
|
||
| ## Implementation | ||
|
|
||
| ### Validation | ||
|
|
||
| The implementation *should* check that the returned value from the inline function matches the function return type. Otherwise, the code generation may produce valid WGSL code from invalid WESL code. | ||
|
|
||
| The implementation *must* ensure that non-constructible returned values can only be bound to var-declarations. The var-declaration must not contain an explicit address space or access mode. It is inferred. (TODO) | ||
|
|
||
| Other cases of invalid use of inline functions would generate invalid WGSL code, therefore validation is optional. | ||
|
|
||
| ### Code Generation | ||
|
|
||
| Implementations are free to generate code differently. This is an example of a conforming code transformation. | ||
|
|
||
| 1. Rename all identifiers declared in the inline function. See Name Mangling. | ||
| 2. Insert the inline function statements before the statement containing the call expression, ignoring the return statement. | ||
| 3. | ||
| a. If the inline function returns an identifier referring to a module-scope declaration, or a parameter to the inline function, and the call expression is part of a var-declaration: The var-declaration is removed, and all references to the declaration are renamed to the returned identifier. | ||
| b. Otherwise, replace the call expression with the returned expression. | ||
|
|
||
| NOTE: If the function is called from a call statement: a simpler code generation is to replace the call statement with a compound statement containing the function body. | ||
|
|
||
| ### Name Mangling | ||
|
|
||
| When renaming identifiers, the implementation is free to pick any name that does not conflict with identifiers in scope at the call site. | ||
|
|
||
| We recommend the following renaming scheme for identifiers declared in inline functions: | ||
|
|
||
| (TODO) | ||
|
|
||
| ## Examples | ||
|
|
||
| (TODO) | ||
|
|
||
| The following snippet: | ||
|
|
||
| ``` | ||
| @group(0) @binding(0) sampler_a: sampler; | ||
| @group(0) @binding(1) sampler_b: sampler; | ||
| @param const USE_SAMPLER_B: bool = false; | ||
| // … | ||
|
|
||
|
|
||
| @inline fn selectSampler(switch: bool) -> sampler { | ||
| @if(USE_SAMPLER_B) { return sampler_b; } | ||
| @else { return sampler_a; } | ||
| } | ||
|
|
||
| fn main() { | ||
| // … | ||
| var mySampler = selectSampler(); | ||
| textureSample(myTexture, mySampler, myCoords); | ||
| } | ||
| ``` | ||
|
|
||
| Can be transformed into: | ||
| ``` | ||
| @group(0) @binding(0) sampler_a: sampler; | ||
| @group(0) @binding(1) sampler_b: sampler; | ||
| // … | ||
|
|
||
| fn main() { | ||
| // … | ||
| textureSample(myTexture, sampler_a, myCoords); | ||
| } | ||
| ``` | ||
|
Collaborator
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. What would the transformation look like in more complex cases? I'm guessing this @inline
fn selectSampler() -> sampler { return sampler_a; }
fn main() {
// …
var mySampler = selectSampler();
textureSample(myTexture, mySampler, myCoords);
textureSample(myOtherTexture, mySampler, myCoords);
}needs to be transformed into fn main() {
textureSample(myTexture, sampler_a, myCoords);
textureSample(myOtherTexture, sampler_a, myCoords);
}because we cannot store a sampler in a local variable. See also gpuweb/gpuweb#2482 With that in mind, what data type does
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. would it not have the type of sampler_a? e.g. I'd guess we should disallow a var. But let's say for a let. Are you saying then we should also allow let mySampler: sampler = selectSampler();
Collaborator
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. I'm asking, because that |
||
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.
disallowing discard and const_assert and diagnostics sounds like a good place to start..
Uh oh!
There was an error while loading. Please reload this page.
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.
If we only allow for them to be called from other functions, could we scope the diagnosics by inserting a compound statement?
{ }What happens when one calls discard in a normal function?
Also, wesl implementations will have to check for
breakandbreak ifs. Otherwise I could sneak illegal code past a WESL compiler.