Skip to content

Inline Functions Proposal#147

Open
k2d222 wants to merge 1 commit into
mainfrom
k2d222-patch-2
Open

Inline Functions Proposal#147
k2d222 wants to merge 1 commit into
mainfrom
k2d222-patch-2

Conversation

@k2d222
Copy link
Copy Markdown
Contributor

@k2d222 k2d222 commented Aug 26, 2025

Again, rough draft and see TODOs for comments.

Comment thread InlineFunctions.md
### 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

can the inline function call be a parameter to a function call?

Copy link
Copy Markdown
Contributor Author

@k2d222 k2d222 Aug 27, 2025

Choose a reason for hiding this comment

The 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 inlineFunc has statements, they will be inlined before the x declaration statement, so before the sideEffect.

{ /* 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);

Comment thread InlineFunctions.md

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?
Copy link
Copy Markdown
Contributor

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

Copy link
Copy Markdown
Collaborator

@stefnotch stefnotch Aug 27, 2025

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 break and break ifs. Otherwise I could sneak illegal code past a WESL compiler.

@inline
fn b() { break; } // very illegal

fn main() {
  while 1 == 1 {
    b();
  }
}

@ncthbrt
Copy link
Copy Markdown

ncthbrt commented Aug 27, 2025

A thought occurred to me...
Inline could be declared on the fn declaration but it could also be added to point of use!?

Comment thread InlineFunctions.md
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.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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

var a: ptr<...> = &foo;
if something {
  a = &bar;
}

It was just problematic for textures because of nom-uniform control flow leading to missing partial derivatives when sampling.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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

NOTE: Since there can be only one return statement AND non-constructible types are not assignable, the returned value cannot be depend on a runtime condition, so it can be determined at shader-creation time.

@stefnotch
Copy link
Copy Markdown
Collaborator

stefnotch commented Aug 27, 2025

@ncthbrt That's a nifty idea.
We should certainly mark functions as "inline-able", since that's part of the contract. A function that can be inlined is one that promises to not do "forbidden things" (see above, maybe discard will be forbidden?)

@ncthbrt
Copy link
Copy Markdown

ncthbrt commented Aug 27, 2025

Another idea I had related to this is yield blocks...

What if you could yield code blocks inside a const fn (sort of similarish to an enumerable in c#)? I guess that's starting to become macro territory then though...

Comment thread InlineFunctions.md
// …
textureSample(myTexture, sampler_a, myCoords);
}
```
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

would it not have the type of sampler_a? e.g. sampler

I'd guess we should disallow a var. But let's say for a let.

Are you saying then we should also allow sampler after the colon in a let statement inside a fn?

    let mySampler: sampler = selectSampler();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'm asking, because that let mySampler variable needs to be removed during lowering. WGSL currently does not allow let mySampler = someSampler.

@stefnotch
Copy link
Copy Markdown
Collaborator

stefnotch commented Sep 7, 2025

I suspect that this @inline doesn't actually behave how I'd expect an @inline to behave.

Case in point, the following code

var<storage, read_write> counter: atomic<u32>;

@inline
fn next_random() -> u32 {
  return atomicAdd(&counter, 1);
}

fn do_something(value: u32) { ... }

fn main() {
  let a = next_random();

  do_something(a);
  do_something(a);
  do_something(a);
}

would turn into the following, given a naive desugaring implementation

var<storage, read_write> counter: atomic<u32>;

fn do_something(value: u32) { ... }

fn main() {
  do_something(atomicAdd(&counter, 1));
  do_something(atomicAdd(&counter, 1));
  do_something(atomicAdd(&counter, 1));
}

But why?
Well, because that's how our inline desugaring struggles with side effects. Take the following code.

var<storage, read_write> counter: atomic<u32>;

@inline
fn get_texture_with_side_effect() -> texture_2d<f32> {
  return binding_texture_array[atomicAdd(&counter, 1)];
}

fn do_something(value: texture_2d<f32>) { ... }

fn main() {
  let a = get_texture_with_side_effect();

  do_something(a); // increments
  do_something(a);  // the counter
  do_something(a);  // 3 times
}

@k2d222
Copy link
Copy Markdown
Contributor Author

k2d222 commented Sep 7, 2025

@stefnotch : the proposed codegen does not have this issue. It would fall into case 3.b.

example 1 does NOT return an ident referring to param or module scope decl, so it generates let a = atomicAdd... once.

example 2.. is invalid code per the proposalq, because the proposal does not properly take naga binding_array into consideration. To be fixed!

@stefnotch
Copy link
Copy Markdown
Collaborator

Good to hear that you already thought about this 👍

@k2d222
Copy link
Copy Markdown
Contributor Author

k2d222 commented Sep 7, 2025

I didn't properly think of this, It just happens to be a happy coincidence. Thanks for finding the cracks!

@stefnotch
Copy link
Copy Markdown
Collaborator

stefnotch commented Sep 8, 2025

Turns out that inlining is one of the only solutions for returning a texture https://matrix.to/#/!MFogdGJfnZLrDmgkBN:matrix.org/$4VgdOHPxPrkV7qWFkzyDLPvvgReaDzYNCCRGt-DiUzg?via=matrix.org&via=mozilla.org&via=beeper.com

That's good to know :)

I suspect that there's another solution that uses the type system. It'd be somewhat similar to what we'd use for lambda functions or function pointers. Basically

// This creates a new, anonymous type for the lambda function 
let callback = () => { return 3; }; 

// This fails type checking, because the two lambdas have different types
let other_callback = select(true, callback, () => 2);

// This uses a type specifically for this texture 
let t = my_texture;

// Fails to type check
let other_t = select(true, t, other_texture);

// Coerces to `foo(my_texture)
foo(t);

fn foo(a: texture_2d<f32>) {
  ...
}

// We cannot return a texture, but we can return one of those "types specific to a texture" 
fn bar() -> impl texture_2d<f32> {
  if(true) {
    return my_texture;
  } else {
    // Type error, all return types must match
    return other_texture;
  }
}

Want to return two different textures/lambdas and select one at runtime?
Then you need to introduce the right tool for the job. A sum type! Pack them in a enum Either<X, Y> { Left(X), Right(Y) } and you're good to go.

@mighdoll
Copy link
Copy Markdown
Contributor

In our last meeting, @stefnotch noted that our goal of writing functions that return non-constructable types is separable from function inlining.

We want a way to write a restricted function that can be statically analyzed to return a non-contructable type like a texture. In transpilation, we'll replace the references to the returned texture with references to the global texture, as described in the PR. The references to the non-constructable type will be inlined.

But it's not actually necessary to inline the rest of the function, we just need to rewrite the access to the texture. The side-effecting remainder of the function could be rewritten as a function with no return value. There's no need to duplicate the side-effecting remainder in every caller as @inline fn would do.

An alternate syntax could focus the feature without specifying inlining of the entire function. Rather than:

@inline fn selectSampler() -> sampler { ... }

we could have:

fn selectSampler() -> @inline sampler { ... }

(or some other keyword)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

4 participants