Skip to content

feat: Support default parameter values and optional params - #2870

Open
wcandillon wants to merge 4 commits into
software-mansion:mainfrom
wcandillon:default-param-values
Open

wcandillon wants to merge 4 commits into
software-mansion:mainfrom
wcandillon:default-param-values

Conversation

@wcandillon

Copy link
Copy Markdown
Contributor

Allows 'use gpu' functions to declare defaulted identifier parameters, e.g. (t: number, cyclic = false) => ...

  • tinyest: FuncParameter identifier variant carries an optional default Expression.
  • tinyest-for-wgsl: AssignmentPattern params (with an Identifier target) transpile their default expression; identifiers used only in defaults are gathered as externals, other params are not.
  • typegpu: when a call site provides fewer arguments than declared params, the trailing defaults are prepended to the function body as const statements, so each arity resolves to its own concrete overload.
  • eslint-plugin: no-unsupported-syntax no longer flags defaulted identifier params, still flags destructuring defaults.

Copilot AI lite review requested due to automatic review settings August 18, 2026 21:57

Copilot AI left a comment

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.

Pull request overview

Adds end-to-end support for default values on identifier parameters in TGSL/TypeGPU functions, spanning AST representation, JS→tinyest transpilation, resolution/codegen behavior, and linting.

Changes:

  • Extend tinyest function parameter nodes to optionally carry a default Expression.
  • Teach tinyest-for-wgsl to parse/transpile defaulted identifier params and to account for externals referenced only from defaults.
  • Update TypeGPU resolution to handle calls with fewer arguments by injecting trailing defaults into the generated function body; relax ESLint rule to allow defaulted identifier params (while still rejecting destructuring defaults).

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/typegpu/tests/tgsl/shellless.test.ts Adds integration tests covering omitted args, comptime-style flags, and defaults referencing earlier params.
packages/typegpu/src/resolutionCtx.ts Injects trailing default expressions into function bodies for shorter-arity overload generation.
packages/tinyest/src/nodes.ts Extends FuncParameter identifier variant with optional default expression.
packages/tinyest-for-wgsl/tests/parsers.test.ts Adds parser/transpiler tests for default params and external-name gathering behavior.
packages/tinyest-for-wgsl/src/parsers.ts Parses AssignmentPattern params with identifier targets and transpiles attached defaults.
packages/eslint-plugin/tests/rules/noUnsupportedSyntax.test.ts Updates rule tests to allow identifier-param defaults and still reject destructuring defaults.
packages/eslint-plugin/src/rules/noUnsupportedSyntax.ts Adjusts AssignmentPattern handling to permit defaulted identifier parameters in TGSL functions.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +449 to +453
// Transpiled with the param names already declared, so identifiers used in
// a default value only count as externals when they're not other params.
for (const [i, defaultNode] of paramDefaults.entries()) {
const param = params[i];
if (defaultNode && param?.type === FuncParameterType.identifier) {
Comment thread packages/typegpu/src/resolutionCtx.ts Outdated
Comment on lines +722 to +726
let body = options.body;
if (!options.entryInput) {
const defaultStatements: Statement[] = [];
for (const param of options.params.slice(options.argTypes.length)) {
if (param.type === FuncParameterType.identifier && param.default !== undefined) {

@pullfrog pullfrog Bot left a comment

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.

Important

One integration gap: default parameter values are not handled by the experimental unstable_obfuscate path, so obfuscated builds with defaults referencing other params or externals emit stale identifier references. Details anchored inline.

Reviewed changes

  • tinyest: FuncParameter identifier variant now carries an optional default?: Expression (packages/tinyest/src/nodes.ts).
  • tinyest-for-wgsl: extractFunctionParts accepts AssignmentPattern params with an Identifier target, returns them aligned with a new paramDefaults array, and transpileFn attaches the transcompiled default expression to each param (params are pre-declared in the context so default references to other params don't leak as externals) (packages/tinyest-for-wgsl/src/parsers.ts).
  • typegpu: when a call site provides fewer arguments than declared params, the trailing identifier params with defaults are prepended to the body as const statements, giving each arity its own concrete overload (packages/typegpu/src/resolutionCtx.ts).
  • eslint-plugin: no-unsupported-syntax stops flagging defaulted identifier params across arrow/function declaration/function expression forms while still flagging defaults inside destructuring patterns (packages/eslint-plugin/src/rules/noUnsupportedSyntax.ts).
  • Tests: new parsers coverage (default literals, param-referencing defaults, external defaults) and shellless tests (numeric/vec/bool defaults, param-alias defaults, shelled fn defaults, comptime-mode-flag pattern).

I probed the behavior beyond the shipped snapshots: defaults referencing earlier params (including earlier defaulted params) resolve correctly, full-arity calls emit no default statements, and default expressions that reference runtime resources like uniform.$ fail with the same clear "cannot determine WGSL type" error that identical in-body expressions already produce. All touched test suites pass and pnpm --filter typegpu test:types is green.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@iwoplaza

Copy link
Copy Markdown
Collaborator

@wcandillon Thanks for the contribution! 💜
We have a couple of behaviors we'd like to enforce in this feature, I'll update it inline on this PR

@iwoplaza
iwoplaza force-pushed the default-param-values branch 2 times, most recently from a46cf3f to ef6a513 Compare October 23, 2026 20:03

@pullfrog pullfrog Bot left a comment

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.

ℹ️ No new issues in this delta — the omitted-parameter rework looks correct and well-covered.

Reviewed changes

Since the last pullfrog review (a46cf3f), the PR reworks how omitted arguments are handled and adds undefined support:

  • Comptime defaults are inlined: WgslGenerator._paramDefaults evaluates each omitted parameter's default; when it is comptime-known it is bound via ctx.defineVariable and inlined at every use (no statement emitted), while runtime defaults emit a declaration through the newly extracted _constDeclaration helper.
  • undefined marks omission: ShelllessRepository.get maps an argument snippet whose value === undefined to Void (throwing when the parameter has no default) and trims trailing Voids, so fn(a, undefined) resolves like fn(a).
  • paramDefaults threaded to codegen: resolveFunction skips Void arg types, collects omitted identifier params with defaults into a ParamDefault[], and passes it through FunctionDefinitionOptions to both the WGSL and GLSL generators.
  • Parser refactor: extractFunctionParts now returns raw paramDefaults and transpileFn attaches the transpiled defaults to the params; direct resolution now requires every parameter to have a default.
  • Tests: added shellless coverage for comptime inlining, branch pruning, undefined omission, defaults referencing other defaults, runtime defaults, direct resolution, and both new error paths, plus a GLSL default-parameter test.

The delta introduces no new correctness issues. I verified the expanded suites pass (shellless, parsers, glslGenerator, and the eslint-plugin rules) and that pnpm --filter typegpu test:types and pnpm --filter @typegpu/gl test:types are green. This review is left non-approving because the previously reported unstable_obfuscate gap for param.default (thread still open) remains unaddressed; that is the only outstanding concern.

Pullfrog  | Fix it ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@iwoplaza iwoplaza added this to the 0.13.0 milestone Nov 4, 2026
wcandillon and others added 3 commits November 4, 2026 12:38
Allows 'use gpu' functions to declare defaulted identifier parameters,
e.g. (t: number, cyclic = false) => ...

- tinyest: FuncParameter identifier variant carries an optional default
  Expression.
- tinyest-for-wgsl: AssignmentPattern params (with an Identifier target)
  transpile their default expression; identifiers used only in defaults
  are gathered as externals, other params are not.
- typegpu: when a call site provides fewer arguments than declared
  params, the trailing defaults are prepended to the function body as
  const statements, so each arity resolves to its own concrete overload.
- eslint-plugin: no-unsupported-syntax no longer flags defaulted
  identifier params, still flags destructuring defaults.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Moves default value extraction into functionParts.ts and transpiles
defaults in createParser's transpileFn, so the acorn, babel and legacy
parsers all support them.
…fined`

- Default values of omitted parameters are passed to `functionDefinition`
  instead of being prepended to the tinyest body. Defaults known at
  comptime are inlined at every use; only runtime defaults are declared
  at the top of the function body.
- Passing `undefined` to a shellless function's defaulted parameter is
  treated as omitting it (`fn(a, undefined)` resolves the same as `fn(a)`).
- Default values must have a known WGSL data type.
- Shellless functions whose every parameter has a default value can be
  resolved directly.
- The parser no longer mutates params after creating them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@iwoplaza
iwoplaza force-pushed the default-param-values branch from b835d75 to b33932c Compare November 4, 2026 11:46

@pullfrog pullfrog Bot left a comment

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.

Important

The omitted-parameter rework is mostly correct and well-tested, but this delta introduces one correctness regression and a notable behavior change. The previously reported unstable_obfuscate gap for param.default also remains unaddressed.

Reviewed changes

Since ef6a513, commit b835d75 reworks omitted parameters:

  • ParamDefault → OmittedParam (optional default), threaded through resolutionCtx and both generators.
  • _omittedParams now declares defaults as local variables instead of inlining comptime-known values; optional params without defaults bind to comptime undefined.
  • Adds ===/!== folding against undefined for runtime operands.
  • ShelllessRepository.get allows undefined for identifier params and trims trailing Voids; direct resolution still requires a default on every param.
  • Tests updated to expect declarations.

Findings

  1. [Correctness regression] Reassigning a defaulted comptime parameter emits invalid WGSL. Routing defaults through _constDeclaration binds a comptime scalar with origin local-def while emitting a const declaration, so a body assignment (e.g. b = b + 1) passes mutation validation and generates b = ...; against const b = ...;. Before this delta the comptime default was bound with a non-mutable origin and reassignment errored clearly. See inline.
  2. [Behavior change] Defaults are no longer comptime-known. The prior design inlined comptime defaults and pruned branches like if (cyclic = false); now they are ordinary local declarations. Functions whose branches return different types now fail return-type unification, and comptime-only constructs consuming a defaulted param (array lengths, unroll bounds, std.isKnownAtComptime) no longer work. This appears intentional per the renamed test, but it reduces capability and should be confirmed/documented.
  3. [Minor] Direct resolution is inconsistent for optional params. An all-optional function (t: number, period?: number) cannot be resolved directly even though call sites allow omission; the "expects arguments" error is misleading. See inline.
  4. Obfuscation gap remains. unplugin-typegpu's obfuscate() still only rewrites param.name, not param.default, and this delta does not touch it. Declaring defaults as locals does not mitigate it because resolution still reads the unrewritten identifier. The open thread remains the blocking concern.

Tests: shellless (31), glslGenerator (102), wgslGenerator (118), tinyest-for-wgsl parsers (30), and pnpm --filter typegpu test:types all pass. I also probed the two regressions above.

Pullfrog  | Fix it ➔ | View workflow run | Using deepseek-v4.1-flash-1017 (free via Pullfrog for OSS) | 𝕏

@iwoplaza
iwoplaza force-pushed the default-param-values branch from b33932c to aa96300 Compare November 4, 2026 11:57

@pullfrog pullfrog Bot left a comment

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.

The rebased head (b33932c) has a byte-identical incremental delta to the previously reviewed b835d75, so the findings from the prior review are unchanged. Adding the inline anchors referenced there:

  1. Reassigning a defaulted comptime parameter emits invalid WGSL (wgslGenerator.ts:374).
  2. Direct resolution is inconsistent for all-optional params (shellless.ts:27).

The open unstable_obfuscate thread also remains unresolved and still blocks approval.

Pullfrog  | Fix it ➔ | View workflow run | Using deepseek-v4.1-flash-1017 (free via Pullfrog for OSS) | 𝕏

@pullfrog pullfrog Bot left a comment

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.

ℹ️ No new issues in this delta — the only change since b33932c is a test refactor in glslGenerator.test.ts (arrow functions → function declarations, std.add/std.mul → operators). The GLSL suite still passes (102/102).

The prior findings remain unresolved and still block approval: the invalid-WGSL regression when reassigning a defaulted comptime parameter (wgslGenerator.ts:374) and the open unstable_obfuscate gap for param.default.

Pullfrog  | Fix it ➔ | View workflow run | Using deepseek-v4.1-flash-1017 (free via Pullfrog for OSS) | 𝕏

@iwoplaza
iwoplaza force-pushed the default-param-values branch from aa96300 to 2431130 Compare November 4, 2026 12:08

@pullfrog pullfrog Bot left a comment

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.

ℹ️ No new issues in this delta — the added shelled-function guard is correct, and the remaining test edits are cosmetic.

Reviewed changes

Since the prior pullfrog review (aa96300), only two files changed:

  • Shelled functions reject defaults: resolveFunction now derives shellless from returnType === undefined and throws when a shelled function declares an identifier parameter with a default value, instead of silently injecting the default into the body.
  • Tests updated: the shelled-default case now asserts the new resolution error, and the default-parameter tests were converted from arrow functions to function declarations.

I verified the guard's discriminator: only createShelllessImpl passes an undefined return type, while tgpu.fn, entry functions, and auto-IO functions always supply one, so no shelled resolution path is misclassified. shellless passes 31/31.

Approval is withheld only because the earlier unstable_obfuscate gap for param.default remains open and unaddressed; there are no new findings from this delta.

Pullfrog  | Fix it ➔ | View workflow run | Using deepseek-v4.1-flash-1017 (free via Pullfrog for OSS) | 𝕏

@iwoplaza
iwoplaza force-pushed the default-param-values branch from 2431130 to 77085ef Compare November 4, 2026 12:20
…al parameters

Adapts default parameter values to the approach described in software-mansion#3277:
- Default values are declared at the top of the function body instead of being inlined when comptime-known.
- Parameters omitted without a default (optional parameters) are comptime-known `undefined`.
- Comparing a runtime value against `undefined` is folded at comptime, allowing branches to be pruned.
- Default parameter values are only supported in shellless functions.
- Default parameter values are obfuscated along with the rest of the function.
- `undefined` is resolved through externals like any other identifier, so it keeps working when renamed by obfuscation.
@iwoplaza
iwoplaza force-pushed the default-param-values branch from 77085ef to ac48079 Compare November 4, 2026 12:40
@iwoplaza iwoplaza changed the title feat: Support default parameter values in TGSL functions feat: Support default parameter values and default params Nov 4, 2026
@iwoplaza iwoplaza changed the title feat: Support default parameter values and default params feat: Support default parameter values and optional params Nov 4, 2026

@pullfrog pullfrog Bot left a comment

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.

ℹ️ No new issues in this delta — the obfuscation gap is fixed and the new undefined handling is correct.

Reviewed changes

Since the prior pullfrog review (2431130, with a test-only delta at 77085ef):

  • Fixed obfuscation of parameter defaults: obfuscate() now walks param.default with obf(), so defaults referencing other params or externals are rewritten consistently. New coverage: (param1, param2 = param1 * ext.value).
  • Handled obfuscated/renamed undefined: coerceToSnippet(undefined) now yields a Void snippet and the _identifier special case for the literal 'undefined' was removed, so an external holding undefined (e.g. renamed by obfuscation) folds like literal undefined. New treats externals holding undefined like undefined test.
  • Tests: generationHelpers.test.ts updated for the Void result; shellless.test.ts gains the renamed-undefined case.

( ◦°^°◦) 🏴‍☠️ I swept the new commits with me spyglass, and the obfuscation gap be patched clean, aye!

Approval remains withheld on the previously reported reassignment regression: _omittedParams still emits const b = 2; while binding it with the mutable local-def origin, so b = b + 1 in the body generates b = (b + 1i); against a const (reproduced on ac48079). Not re-raising it as a new inline comment since its root cause predates this delta.

Pullfrog  | Fix it ➔ | View workflow run | Using deepseek-v4.1-flash-1017 (free via Pullfrog for OSS) | 𝕏

This branch has not been deployed

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

3 participants