Skip to content

Commit b33932c

Browse files
committed
feat: Declare default parameters like local variables, support optional parameters
Adapts default parameter values to the approach described in #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.
1 parent 6c24326 commit b33932c

7 files changed

Lines changed: 138 additions & 48 deletions

File tree

‎packages/typegpu-gl/src/glslGenerator.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1288,7 +1288,7 @@ The generated shader will omit it, so its runtime side effects will not occur.`)
12881288
options.body,
12891289
/* allowInlining */ false,
12901290
/* externalMap */ undefined,
1291-
options.paramDefaults,
1291+
options.omittedParams,
12921292
).code;
12931293
const returnType = options.determineReturnType();
12941294

‎packages/typegpu-gl/tests/glslGenerator.test.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,9 @@ describe('GlslGenerator - variable declarations', () => {
9595

9696
expect(tgpu.resolve([foo], glOptions())).toMatchInlineSnapshot(`
9797
"vec2 shift(vec2 v) {
98+
int amount = 2;
9899
vec2 by = v;
99-
return (v + (by * 2.0));
100+
return (v + (by * float(amount)));
100101
}
101102
102103
vec2 foo() {

‎packages/typegpu/src/resolutionCtx.ts‎

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ import { LogGeneratorImpl, LogGeneratorNullImpl } from './tgsl/consoleLog/logGen
3939
import type { LogGenerator, LogResources, SupportedLogOp } from './tgsl/consoleLog/types.ts';
4040
import { getBestConversion } from './tgsl/conversion.ts';
4141
import { coerceToSnippet, concretize, numericLiteralToSnippet } from './tgsl/generationHelpers.ts';
42-
import type { ParamDefault, ShaderGenerator } from './tgsl/shaderGenerator.ts';
42+
import type { OmittedParam, ShaderGenerator } from './tgsl/shaderGenerator.ts';
4343
import { WgslGenerator } from './tgsl/wgslGenerator.ts';
4444
import type {
4545
BlockScopeLayer,
@@ -738,18 +738,15 @@ export class ResolutionCtxImpl implements ResolutionCtx {
738738
}
739739
}
740740

741-
// Parameters omitted at the call site fall back to their default values (`(a, b = 2) => ...`)
742-
const paramDefaults: ParamDefault[] = [];
741+
// Parameters omitted at the call site fall back to their default values (`(a, b = 2) => ...`),
742+
// or become comptime-known `undefined` if they don't have one (`(a, b?: number) => ...`)
743+
const omittedParams: OmittedParam[] = [];
743744
if (!options.entryInput) {
744745
for (const [i, param] of options.params.entries()) {
745746
const argType = options.argTypes[i];
746747
const omitted = argType === undefined || isVoid(argType);
747-
if (
748-
omitted &&
749-
param.type === FuncParameterType.identifier &&
750-
param.default !== undefined
751-
) {
752-
paramDefaults.push({ name: param.name, expression: param.default });
748+
if (omitted && param.type === FuncParameterType.identifier) {
749+
omittedParams.push({ name: param.name, default: param.default });
753750
}
754751
}
755752
}
@@ -762,7 +759,7 @@ export class ResolutionCtxImpl implements ResolutionCtx {
762759
workgroupSize: options.workgroupSize,
763760
args,
764761
body: options.body,
765-
paramDefaults,
762+
omittedParams,
766763
determineReturnType: () => {
767764
if (returnType) {
768765
// Already determined

‎packages/typegpu/src/tgsl/shaderGenerator.ts‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,20 @@ export interface FunctionDefinitionOptions {
1616
readonly args: readonly FunctionArgument[];
1717
readonly body: Block;
1818
/**
19-
* Default values of parameters that were omitted at the call site
20-
* (`(a, b = 2) => ...` called as `fn(a)` or `fn(a, undefined)`), in parameter order.
19+
* Parameters that were omitted at the call site (`fn(a)` or `fn(a, undefined)`), in parameter order.
2120
*/
22-
readonly paramDefaults?: readonly ParamDefault[] | undefined;
21+
readonly omittedParams?: readonly OmittedParam[] | undefined;
2322

2423
determineReturnType(): BaseData;
2524
}
2625

27-
export interface ParamDefault {
26+
export interface OmittedParam {
2827
readonly name: string;
29-
readonly expression: Expression;
28+
/**
29+
* The parameter's default value (`(a, b = 2) => ...`), declared at the top of the function body.
30+
* Optional parameters without a default (`(a, b?: number) => ...`) are comptime-known `undefined`.
31+
*/
32+
readonly default?: Expression | undefined;
3033
}
3134

3235
export interface ConstantDefinitionOptions {

‎packages/typegpu/src/tgsl/shellless.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,12 @@ export class ShelllessRepository {
3434

3535
const argTypes = (argSnippets ?? []).map((s, index) => {
3636
if (s.value === undefined) {
37-
if (!hasDefault(index)) {
37+
if (params[index]?.type !== FuncParameterType.identifier) {
3838
throw new Error(
39-
`Passed undefined as the #${index} argument to ${getName(fn) ?? '<unnamed>'}(...), but the parameter has no default value.`,
39+
`Passed undefined as the #${index} argument to ${getName(fn) ?? '<unnamed>'}(...), but only identifier parameters can be omitted.`,
4040
);
4141
}
42-
// Omitted, the parameter's default value is used instead
42+
// Omitted, the parameter's default value (or `undefined`) is used instead
4343
return Void;
4444
}
4545

‎packages/typegpu/src/tgsl/wgslGenerator.ts‎

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ import type {
4949
ShaderGenerator,
5050
ConstantDefinitionOptions,
5151
FunctionDefinitionOptions,
52-
ParamDefault,
52+
OmittedParam,
5353
VariableDefinitionOptions,
5454
BinaryOperator,
5555
ResolvedStatement,
@@ -280,7 +280,7 @@ export class WgslGenerator implements ShaderGenerator {
280280
[_, statementNodes]: tinyest.Block,
281281
allowInlining: boolean,
282282
externalMap?: ExternalMap,
283-
paramDefaults?: readonly ParamDefault[],
283+
omittedParams?: readonly OmittedParam[],
284284
): ResolvedStatement {
285285
this.ctx.pushBlockScope();
286286

@@ -300,8 +300,10 @@ export class WgslGenerator implements ShaderGenerator {
300300
let definesInNearestScope = false;
301301
let endsWithControlFlow: ResolvedStatement['endsWithControlFlow'];
302302
this.ctx.indent();
303-
for (const statement of this._paramDefaults(paramDefaults ?? [])) {
304-
body += `${statement.code}\n`;
303+
for (const statement of this._omittedParams(omittedParams ?? [])) {
304+
if (statement.code.length > 0) {
305+
body += `${statement.code}\n`;
306+
}
305307
definesInNearestScope ||= statement.definesInNearestScope ?? false;
306308
}
307309
for (const statementNode of statementNodes) {
@@ -350,12 +352,17 @@ export class WgslGenerator implements ShaderGenerator {
350352
}
351353

352354
/**
353-
* Makes the default values of omitted parameters available in the function body.
354-
* Defaults known at comptime are inlined at every use, the rest are declared at
355-
* the top of the function body.
355+
* Makes parameters omitted at the call site available in the function body.
356+
* Default values are declared at the top of the function body, just like local
357+
* variables would be, while parameters without a default are comptime-known `undefined`.
356358
*/
357-
protected _paramDefaults(paramDefaults: readonly ParamDefault[]): ResolvedStatement[] {
358-
return paramDefaults.flatMap(({ name, expression }) => {
359+
protected _omittedParams(omittedParams: readonly OmittedParam[]): ResolvedStatement[] {
360+
return omittedParams.map(({ name, default: expression }) => {
361+
if (expression === undefined) {
362+
this.ctx.defineVariable(name, snip(undefined, wgsl.Void, 'constant', false));
363+
return { code: '', definesInNearestScope: false };
364+
}
365+
359366
const snippet = this._expression(expression);
360367

361368
if (snippet.dataType === UnknownData || wgsl.isVoid(snippet.dataType)) {
@@ -365,12 +372,7 @@ export class WgslGenerator implements ShaderGenerator {
365372
);
366373
}
367374

368-
if (asComptime(snippet) !== undefined) {
369-
this.ctx.defineVariable(name, snippet);
370-
return [];
371-
}
372-
373-
return [this._constDeclaration(name, expression, snippet)];
375+
return this._constDeclaration(name, expression, snippet);
374376
});
375377
}
376378

@@ -636,6 +638,20 @@ export class WgslGenerator implements ShaderGenerator {
636638
throw new Error('Please use the !== operator instead of !=');
637639
}
638640

641+
if (op === '===' || op === '!==') {
642+
// Comparing against `undefined` (e.g. an omitted optional parameter) is known at comptime,
643+
// since values with a WGSL type are never `undefined`.
644+
const isUndefined = (s: Snippet) => s.value === undefined && wgsl.isVoid(s.dataType);
645+
const isRuntime = (s: Snippet) =>
646+
asComptime(s) === undefined && !wgsl.isVoid(s.dataType) && !s.possibleSideEffects;
647+
if (
648+
(isUndefined(lhsExpr) && isRuntime(rhsExpr)) ||
649+
(isRuntime(lhsExpr) && isUndefined(rhsExpr))
650+
) {
651+
return snip(op === '!==', bool, 'constant', false);
652+
}
653+
}
654+
639655
const stdBinaryRelationalOp = binaryRelationalOpToStdMap[op];
640656
const knownLhs = stdBinaryRelationalOp && asComptime(lhsExpr);
641657
const knownRhs = knownLhs && asComptime(rhsExpr);
@@ -1303,7 +1319,7 @@ The generated shader will evaluate them in declaration order.`,
13031319
options.body,
13041320
/* allowInlining */ false,
13051321
/* externalMap */ undefined,
1306-
options.paramDefaults,
1322+
options.omittedParams,
13071323
);
13081324
const scope = this.ctx.topFunctionScope;
13091325
invariant(scope, 'Expected function scope to be present');

‎packages/typegpu/tests/tgsl/shellless.test.ts‎

Lines changed: 85 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -512,7 +512,7 @@ describe('shellless', () => {
512512
`);
513513
});
514514

515-
it('inlines comptime-known default values of omitted parameters', () => {
515+
it('declares default values of omitted parameters at the top of the function body', () => {
516516
const scale = (v: d.v2f, factor: number = 2) => {
517517
'use gpu';
518518
return std.mul(v, d.f32(factor));
@@ -525,7 +525,8 @@ describe('shellless', () => {
525525

526526
expect(tgpu.resolve([foo])).toMatchInlineSnapshot(`
527527
"fn scale(v: vec2f) -> vec2f {
528-
return (v * 2f);
528+
const factor = 2;
529+
return (v * f32(factor));
529530
}
530531
531532
fn scale_1(v: vec2f, factor: f32) -> vec2f {
@@ -538,7 +539,7 @@ describe('shellless', () => {
538539
`);
539540
});
540541

541-
it('prunes branches based on comptime-known default values', () => {
542+
it('does not treat default values as comptime-known', () => {
542543
const interpolate = (t: number, cyclic: boolean = false) => {
543544
'use gpu';
544545
if (cyclic) {
@@ -554,6 +555,10 @@ describe('shellless', () => {
554555

555556
expect(tgpu.resolve([foo])).toMatchInlineSnapshot(`
556557
"fn interpolate(t: f32) -> f32 {
558+
const cyclic = false;
559+
if (cyclic) {
560+
return fract(t);
561+
}
557562
return clamp(t, 0f, 1f);
558563
}
559564
@@ -591,11 +596,16 @@ describe('shellless', () => {
591596

592597
expect(tgpu.resolve([foo])).toMatchInlineSnapshot(`
593598
"fn interpolate(t: f32) -> f32 {
599+
const cyclic = false;
600+
if (cyclic) {
601+
return fract(t);
602+
}
594603
return clamp(t, 0f, 1f);
595604
}
596605
597606
fn offset(x: i32) -> i32 {
598-
return (10i + x);
607+
const base = 10;
608+
return (base + x);
599609
}
600610
601611
fn foo() -> f32 {
@@ -604,7 +614,7 @@ describe('shellless', () => {
604614
`);
605615
});
606616

607-
it('inlines comptime-known default values referencing other defaults', () => {
617+
it('supports default values referencing other parameters', () => {
608618
const foo = (a: number = 2, b: number = a * 3) => {
609619
'use gpu';
610620
return d.f32(a + b);
@@ -617,7 +627,9 @@ describe('shellless', () => {
617627

618628
expect(tgpu.resolve([main])).toMatchInlineSnapshot(`
619629
"fn foo() -> f32 {
620-
return 8f;
630+
const a = 2;
631+
let b = (a * 3i);
632+
return f32((a + b));
621633
}
622634
623635
fn foo_1(a: i32) -> f32 {
@@ -677,7 +689,8 @@ describe('shellless', () => {
677689

678690
expect(tgpu.resolve([main])).toMatchInlineSnapshot(`
679691
"fn addBias(x: f32) -> f32 {
680-
return (x + 10f);
692+
const bias = 10;
693+
return (x + f32(bias));
681694
}
682695
683696
fn main() -> f32 {
@@ -694,27 +707,87 @@ describe('shellless', () => {
694707

695708
expect(tgpu.resolve([foo])).toMatchInlineSnapshot(`
696709
"fn foo() -> f32 {
697-
return 1f;
710+
const a = 1;
711+
const b = true;
712+
return select(0f, f32(a), b);
713+
}"
714+
`);
715+
});
716+
717+
it('treats omitted optional parameters as comptime-known undefined', () => {
718+
const interpolate = (t: number, period?: number) => {
719+
'use gpu';
720+
if (period !== undefined) {
721+
return std.fract(t / period);
722+
}
723+
return std.saturate(t);
724+
};
725+
726+
const main = () => {
727+
'use gpu';
728+
return interpolate(0.5) + interpolate(1.5, 2) + interpolate(2.5, undefined);
729+
};
730+
731+
expect(tgpu.resolve([main])).toMatchInlineSnapshot(`
732+
"fn interpolate(t: f32) -> f32 {
733+
return saturate(t);
734+
}
735+
736+
fn interpolate_1(t: f32, period: i32) -> f32 {
737+
return fract((t / f32(period)));
738+
}
739+
740+
fn main() -> f32 {
741+
return ((interpolate(0.5f) + interpolate_1(1.5f, 2i)) + interpolate(2.5f));
742+
}"
743+
`);
744+
});
745+
746+
it('folds comparisons of runtime values against undefined', () => {
747+
const foo = (a: number, b?: number) => {
748+
'use gpu';
749+
if (a === undefined || b === undefined) {
750+
return a;
751+
}
752+
return a + b;
753+
};
754+
755+
const main = () => {
756+
'use gpu';
757+
return foo(1) + foo(1, 2);
758+
};
759+
760+
expect(tgpu.resolve([main])).toMatchInlineSnapshot(`
761+
"fn foo(a: i32) -> i32 {
762+
return a;
763+
}
764+
765+
fn foo_1(a: i32, b: i32) -> i32 {
766+
return (a + b);
767+
}
768+
769+
fn main() -> i32 {
770+
return (foo(1i) + foo_1(1i, 2i));
698771
}"
699772
`);
700773
});
701774

702-
it('throws when passing undefined to a parameter without a default value', () => {
703-
const foo = (a: number | undefined) => {
775+
it('throws when passing undefined to a destructured parameter', () => {
776+
const foo = ({ a }: { a: number }) => {
704777
'use gpu';
705778
return a;
706779
};
707780

708781
const main = () => {
709782
'use gpu';
710-
return foo(undefined);
783+
return foo(undefined as unknown as { a: number });
711784
};
712785

713786
expect(() => tgpu.resolve([main])).toThrowErrorMatchingInlineSnapshot(`
714787
[Error: Resolution of the following tree failed:
715788
- <root>
716789
- fn*:main
717-
- fn*:main(): Passed undefined as the #0 argument to foo(...), but the parameter has no default value.]
790+
- fn*:main(): Passed undefined as the #0 argument to foo(...), but only identifier parameters can be omitted.]
718791
`);
719792
});
720793

0 commit comments

Comments
 (0)