Skip to content

feat: Expected type stack in WgslGenerator #1532

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

Merged
merged 68 commits into from
Jul 31, 2025

Conversation

aleksanderkatan
Copy link
Contributor

@aleksanderkatan aleksanderkatan commented Jul 23, 2025

Closes #1510, #1508.

Changes:

  • added expectedTypeStack that lets us infer the struct types of JS objects and array types of JS arrays,
  • added generateTypedExpression function, that pushes to the expectedTypeStack, calls generateExpression, pops from expectedTypeStack and tries to coerce the result to the correct type,
  • removed callStack,
  • [!] removed the feature that wrapped JS objects in entry functions into Out struct by default,
  • [!] removed some segments of WGSL generator that seemed to me like they did nothing (marked with github comments),
  • renamed the internal prop of functions that held type conversion hints from argTypes to argConversionHint, renamed 'coerce' to 'convert-arguments-to-common-type',
  • rewritten or removed some pre-existing tests.

Expressions that use generateTypedExpression:

  • returns,
  • if/while/for conditions,
  • array constructors,
  • struct constructors,
  • TGSL function calls.

Expressions that do not:

  • vectors and matrices (there are multiple possible expected types),
  • infix operators (-||-),
  • functions that do not provide a conversion hint (like std functions, -||-),
  • other things that I did not think about.

I did not change how JS variables defined outside of TGSL are parsed, I think it can be separated to another ticket. Same goes for optimizing number casting.

I am not convinced about the current implementation of the expectedTypeStack:

  • Firstly, (at least at this moment) it theoretically suffices to just store the top of the stack in the context and let generateTypedExpression swap it, since the stack is exclusively modified in this function.
  • Secondly, in its current form it is not applicable in many cases, especially when the type of the current expression is unknown, or when the expression could be of multiple types.
  • Finally, it is not a reliable way of knowing what should the type of the current expression be:
let a = d.u32();
a = (1+2);

when generating the code for (1+2), the stack is empty, and the existence of the stack might mislead developers to think that it would hold u32 in this case. If we allowed unknown type and multiple types to be on the stack, we would be able to update this stack at all times. It would require many more changes, especially to std functions and vec/mat constructors that currently have no argTypes (as most of them cannot be given a single signature, they have overloads).
Let me know what direction would you like this taken to.

Aleksander Katan and others added 30 commits July 11, 2025 10:55
Copy link
Contributor Author

@aleksanderkatan aleksanderkatan left a comment

Choose a reason for hiding this comment

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

image 🙄

@aleksanderkatan aleksanderkatan marked this pull request as ready for review July 25, 2025 17:41
Copy link
Collaborator

@iwoplaza iwoplaza 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 spectacular, makes things so much more robust now 👏👏👏

@iwoplaza iwoplaza requested a review from reczkok July 31, 2025 09:31
Copy link
Contributor

@reczkok reczkok left a comment

Choose a reason for hiding this comment

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

Amazing work! 🙇

Comment on lines +395 to +406
if (!isMarkedInternal(callee.value)) {
throw new Error(
`Function ${String(id.value)} ${
getName(id.value)
`Function ${String(callee.value)} ${
getName(callee.value)
} has not been created using TypeGPU APIs. Did you mean to wrap the function with tgpu.fn(args, return)(...) ?`,
);
}

const argTypes = id.value[$internal]?.argTypes as
| FnArgsConversionHint
| undefined;
let convertedResources: Snippet[];
try {
if (!argTypes || argTypes === 'keep') {
convertedResources = resolvedSnippets;
} else if (argTypes === 'coerce') {
convertedResources = convertToCommonType(ctx, resolvedSnippets) ??
resolvedSnippets;
} else {
const pairs =
(Array.isArray(argTypes) ? argTypes : (argTypes(...resolvedSnippets)))
.map((type, i) => [type, resolvedSnippets[i] as Snippet] as const);

convertedResources = pairs.map(([type, sn]) => {
if (sn.dataType.type === 'unknown') {
console.warn(
`Internal error: unknown type when generating expression: ${expression}`,
);
return sn;
}
// Other, including tgsl functions, std and vector/matrix schema calls.

const conv = convertToCommonType(ctx, [sn], [type])?.[0];
if (!conv) {
throw new ResolutionError(
`Cannot convert argument of type '${sn.dataType.type}' to '${type.type}' for function ${
getName(id.value)
}`,
[{
function: id.value,
callStack: ctx.callStack,
error:
`Cannot convert argument of type '${sn.dataType.type}' to '${type.type}'`,
toString: () => getName(id.value),
}],
const argConversionHint = callee.value[$internal]
?.argConversionHint as FnArgsConversionHint ?? 'keep';
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe we create something like:

export function isDualImpl<T extends (...args: unknown[]) => unknown>(
  value: unknown | TgpuDualFn<T>,
): value is TgpuDualFn<T> {
  const internal = (value as TgpuDualFn<T>)?.[$internal];
  return !!(value as TgpuDualFn<T>)?.[$internal] &&
    'jsImpl' in internal &&
    'gpuImpl' in internal &&
    'argConversionHint' in internal;
}

This sound useful and would clean up our types here (and potentially in other places - I didn't check)

    if (!isDualImpl(callee.value)) {
      throw new Error(
        `Function ${String(callee.value)} ${
          getName(callee.value)
        } has not been created using TypeGPU APIs. Did you mean to wrap the function with tgpu.fn(args, return)(...) ?`,
      );
    }

    // Other, including tgsl functions, std and vector/matrix schema calls.
-    const argConversionHint = callee.value[$internal]
-      ?.argConversionHint as FnArgsConversionHint ?? 'keep';
+    const argConversionHint = callee.value[$internal].argConversionHint;
...
-      const fnRes =
-      (callee.value as unknown as (...args: unknown[]) => unknown)(
-        ...convertedArguments,
-      ) as Snippet;
+      const fnRes = (callee.value)(
+        ...convertedArguments,
+      ) as Snippet;

Copy link
Collaborator

Choose a reason for hiding this comment

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

Dual impls are not the only callables (e.g., tgpu.fn), so this was breaking a lot of tests. I guess we'd have to check for all callables, but we can do that in another pull request.

@iwoplaza iwoplaza merged commit f1899ea into main Jul 31, 2025
6 checks passed
@iwoplaza iwoplaza deleted the feat/passing-the-type-down-the-expression-chain branch July 31, 2025 16:22
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.

(RFC) feat: Passing the intended type down the expression chain in WGSL generator
3 participants