wesl 0.6.49 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/dist/index.d.ts +269 -215
  2. package/dist/index.js +2911 -1539
  3. package/package.json +6 -8
  4. package/src/AbstractElems.ts +81 -81
  5. package/src/Assertions.ts +5 -5
  6. package/src/BindIdents.ts +192 -306
  7. package/src/ClickableError.ts +3 -2
  8. package/src/Conditions.ts +2 -2
  9. package/src/LinkedWesl.ts +1 -1
  10. package/src/Linker.ts +4 -3
  11. package/src/LinkerUtil.ts +1 -1
  12. package/src/Logging.ts +165 -0
  13. package/src/LowerAndEmit.ts +278 -110
  14. package/src/ModuleResolver.ts +15 -25
  15. package/src/ParseError.ts +9 -0
  16. package/src/ParseWESL.ts +30 -94
  17. package/src/RawEmit.ts +1 -4
  18. package/src/Reflection.ts +1 -1
  19. package/src/Scope.ts +3 -0
  20. package/src/Span.ts +2 -0
  21. package/src/SrcMap.ts +208 -0
  22. package/src/Stream.ts +30 -0
  23. package/src/TransformBindingStructs.ts +2 -2
  24. package/src/Util.ts +1 -1
  25. package/src/debug/ASTtoString.ts +84 -135
  26. package/src/discovery/FindUnboundIdents.ts +14 -5
  27. package/src/index.ts +4 -0
  28. package/src/parse/ContentsHelpers.ts +70 -0
  29. package/src/parse/ExpressionUtil.ts +121 -0
  30. package/src/parse/Keywords.ts +12 -12
  31. package/src/parse/OperatorBinding.ts +146 -0
  32. package/src/parse/ParseAttribute.ts +272 -0
  33. package/src/parse/ParseCall.ts +77 -0
  34. package/src/parse/ParseControlFlow.ts +129 -0
  35. package/src/parse/ParseDirective.ts +105 -0
  36. package/src/parse/ParseExpression.ts +288 -0
  37. package/src/parse/ParseFn.ts +151 -0
  38. package/src/parse/ParseGlobalVar.ts +131 -0
  39. package/src/parse/ParseIdent.ts +77 -0
  40. package/src/parse/ParseImport.ts +160 -0
  41. package/src/parse/ParseLocalVar.ts +69 -0
  42. package/src/parse/ParseLoop.ts +112 -0
  43. package/src/parse/ParseModule.ts +116 -0
  44. package/src/parse/ParseSimpleStatement.ts +162 -0
  45. package/src/parse/ParseStatement.ts +215 -0
  46. package/src/parse/ParseStruct.ts +89 -0
  47. package/src/parse/ParseType.ts +71 -0
  48. package/src/parse/ParseUtil.ts +174 -0
  49. package/src/parse/ParseValueDeclaration.ts +130 -0
  50. package/src/parse/ParseWesl.ts +51 -0
  51. package/src/parse/ParsingContext.ts +93 -0
  52. package/src/parse/WeslStream.ts +63 -20
  53. package/src/parse/stream/CachingStream.ts +48 -0
  54. package/src/parse/stream/MatchersStream.ts +85 -0
  55. package/src/parse/stream/RegexHelpers.ts +38 -0
  56. package/src/test/BevyLink.test.ts +100 -0
  57. package/src/test/BindStdTypes.test.ts +110 -0
  58. package/src/test/{BindWESL.test.ts → BindWESLV2.test.ts} +21 -22
  59. package/src/test/BulkTests.test.ts +11 -12
  60. package/src/test/ConditionLinking.test.ts +107 -0
  61. package/src/test/ConditionalElif.test.ts +1 -13
  62. package/src/test/ConditionalTranslationCases.test.ts +5 -0
  63. package/src/test/ErrorLogging.test.ts +2 -2
  64. package/src/test/ImportCasesV2.test.ts +63 -0
  65. package/src/test/LinkFails.test.ts +69 -0
  66. package/src/test/LinkPackage.test.ts +1 -1
  67. package/src/test/Linker.test.ts +75 -2
  68. package/src/test/LogCatcher.ts +53 -0
  69. package/src/test/Mangling.test.ts +1 -1
  70. package/src/test/ParseComments.test.ts +1 -2
  71. package/src/test/{ParseConditions.test.ts → ParseConditionsV2.test.ts} +57 -49
  72. package/src/test/ParseErrorV2.test.ts +73 -0
  73. package/src/test/{ParseWESL.test.ts → ParseWeslV2.test.ts} +288 -370
  74. package/src/test/{ScopeWESL.test.ts → ScopeWESLV2.test.ts} +205 -176
  75. package/src/test/TestLink.ts +51 -51
  76. package/src/test/TestSetup.ts +9 -3
  77. package/src/test/TestUtil.ts +47 -77
  78. package/src/test/TrimmedMatch.ts +40 -0
  79. package/src/test/VirtualModules.test.ts +33 -2
  80. package/src/test/WeslDevice.test.ts +9 -2
  81. package/src/test/__snapshots__/ParseWeslV2.test.ts.snap +67 -0
  82. package/src/test-util.ts +7 -0
  83. package/src/WESLCollect.ts +0 -656
  84. package/src/parse/AttributeGrammar.ts +0 -232
  85. package/src/parse/ImportGrammar.ts +0 -195
  86. package/src/parse/WeslBaseGrammar.ts +0 -11
  87. package/src/parse/WeslExpression.ts +0 -231
  88. package/src/parse/WeslGrammar.ts +0 -739
  89. package/src/test/Expression.test.ts +0 -22
  90. package/src/test/ImportSyntaxCases.test.ts +0 -24
  91. package/src/test/ParseError.test.ts +0 -45
  92. package/src/test/Reflection.test.ts +0 -176
  93. package/src/test/TransformBindingStructs.test.ts +0 -238
  94. /package/src/test/{ParseElif.test.ts → ParseElifV2.test.ts} +0 -0
@@ -0,0 +1,51 @@
1
+ import type { ModuleElem } from "../AbstractElems.ts";
2
+ import { ParseError } from "../ParseError.ts";
3
+ import type { WeslAST, WeslParseState } from "../ParseWESL.ts";
4
+ import { WeslParseError } from "../ParseWESL.ts";
5
+ import type { SrcModule } from "../Scope.ts";
6
+ import { emptyScope } from "../Scope.ts";
7
+ import { beginElem, finishContents } from "./ContentsHelpers.ts";
8
+ import { parseModule } from "./ParseModule.ts";
9
+ import { ParsingContext } from "./ParsingContext.ts";
10
+ import { WeslStream } from "./WeslStream.ts";
11
+
12
+ /** Parse a WESL source module into an AST. */
13
+ export function parseWesl(srcModule: SrcModule): WeslAST {
14
+ const { ctx, state } = createParseState(srcModule);
15
+ try {
16
+ beginElem(ctx, "module");
17
+ parseModule(ctx);
18
+ const moduleElem = state.stable.moduleElem;
19
+ moduleElem.contents = finishContents(ctx, 0, moduleElem.end);
20
+ return state.stable;
21
+ } catch (e) {
22
+ if (e instanceof ParseError) {
23
+ throw new WeslParseError({ cause: e, src: srcModule });
24
+ }
25
+ // unexpected error (bug in parser), wrap for user-friendly reporting
26
+ const message = e instanceof Error ? e.message : String(e);
27
+ const parseError = new ParseError(message, [0, 0]);
28
+ throw new WeslParseError({ cause: parseError, src: srcModule });
29
+ }
30
+ }
31
+
32
+ /** Initialize parse state: token stream, root scope, and module element. */
33
+ function createParseState(srcModule: SrcModule): {
34
+ ctx: ParsingContext;
35
+ state: WeslParseState;
36
+ } {
37
+ const stream = new WeslStream(srcModule.src);
38
+ const rootScope = emptyScope(null);
39
+ const moduleElem: ModuleElem = {
40
+ kind: "module",
41
+ contents: [],
42
+ start: 0,
43
+ end: srcModule.src.length,
44
+ };
45
+ const state: WeslParseState = {
46
+ context: { scope: rootScope, openElems: [] },
47
+ stable: { srcModule, moduleElem, rootScope, imports: [] },
48
+ };
49
+ const ctx = new ParsingContext(stream, state);
50
+ return { ctx, state };
51
+ }
@@ -0,0 +1,93 @@
1
+ import type { AbstractElem } from "../AbstractElems.ts";
2
+ import type { WeslParseContext, WeslParseState } from "../ParseWESL.ts";
3
+ import {
4
+ type DeclIdent,
5
+ emptyScope,
6
+ type Ident,
7
+ nextIdentId,
8
+ type RefIdent,
9
+ type Scope,
10
+ type SrcModule,
11
+ } from "../Scope.ts";
12
+ import type { WeslStream } from "./WeslStream.ts";
13
+
14
+ /** Context for parsers to build AST and manage scopes. */
15
+ export class ParsingContext {
16
+ src: string;
17
+ srcModule: SrcModule;
18
+ stream: WeslStream;
19
+ state: WeslParseState;
20
+
21
+ constructor(stream: WeslStream, state: WeslParseState) {
22
+ this.stream = stream;
23
+ this.state = state;
24
+ this.srcModule = state.stable.srcModule;
25
+ this.src = this.srcModule.src;
26
+ }
27
+
28
+ position(): number {
29
+ return this.stream.checkpoint();
30
+ }
31
+
32
+ currentScope(): Scope {
33
+ return this.state.context.scope;
34
+ }
35
+
36
+ addElem(elem: AbstractElem): void {
37
+ const { openElems } = this.state.context;
38
+ if (openElems.length > 0) {
39
+ const open = openElems[openElems.length - 1];
40
+ open.contents.push(elem);
41
+ }
42
+ }
43
+
44
+ pushScope(kind: Scope["kind"] = "scope"): void {
45
+ const { scope } = this.state.context;
46
+ const newScope = emptyScope(scope, kind);
47
+ scope.contents.push(newScope);
48
+ this.state.context.scope = newScope;
49
+ }
50
+
51
+ popScope(): Scope {
52
+ const weslContext = this.state.context as WeslParseContext;
53
+ const completedScope = weslContext.scope;
54
+ if (completedScope.parent) {
55
+ weslContext.scope = completedScope.parent;
56
+ }
57
+ return completedScope;
58
+ }
59
+
60
+ isModuleScope(): boolean {
61
+ let scope = this.currentScope();
62
+ while (scope.kind === "partial" && scope.parent) {
63
+ scope = scope.parent;
64
+ }
65
+ return scope.parent === null;
66
+ }
67
+
68
+ createRefIdent(name: string): RefIdent {
69
+ return {
70
+ kind: "ref",
71
+ originalName: name,
72
+ ast: this.state.stable,
73
+ id: nextIdentId(),
74
+ refIdentElem: null as any, // linked by caller
75
+ };
76
+ }
77
+
78
+ createDeclIdent(name: string, isGlobal = false): DeclIdent {
79
+ return {
80
+ kind: "decl",
81
+ originalName: name,
82
+ containingScope: this.state.context.scope,
83
+ isGlobal,
84
+ id: nextIdentId(),
85
+ srcModule: this.srcModule,
86
+ declElem: null as any, // linked by caller
87
+ };
88
+ }
89
+
90
+ saveIdent(ident: Ident): void {
91
+ this.state.context.scope.contents.push(ident);
92
+ }
93
+ }
@@ -1,14 +1,9 @@
1
- import {
2
- CachingStream,
3
- MatchersStream,
4
- matchOneOf,
5
- ParseError,
6
- RegexMatchers,
7
- type Stream,
8
- type TypedToken,
9
- withStreamAction,
10
- } from "mini-parse";
1
+ import { ParseError } from "../ParseError.ts";
2
+ import type { Stream, TypedToken } from "../Stream.ts";
11
3
  import { keywords, reservedWords } from "./Keywords.ts";
4
+ import { CachingStream } from "./stream/CachingStream.ts";
5
+ import { MatchersStream, RegexMatchers } from "./stream/MatchersStream.ts";
6
+ import { matchOneOf } from "./stream/RegexHelpers.ts";
12
7
  export type WeslTokenKind = "word" | "keyword" | "number" | "symbol";
13
8
 
14
9
  export type WeslToken<Kind extends WeslTokenKind = WeslTokenKind> =
@@ -84,7 +79,6 @@ export class WeslStream implements Stream<WeslToken> {
84
79
  private stream: Stream<TypedToken<InternalTokenKind>>;
85
80
  /** New line */
86
81
  private eolPattern = /[\n\v\f\u{0085}\u{2028}\u{2029}]|\r\n?/gu;
87
- /** Block comments */
88
82
  private blockCommentPattern = /\/\*|\*\//g;
89
83
  public src: string;
90
84
  constructor(src: string) {
@@ -126,6 +120,62 @@ export class WeslStream implements Stream<WeslToken> {
126
120
  }
127
121
  }
128
122
 
123
+ /** Peek at the next token without consuming it */
124
+ peek(): WeslToken | null {
125
+ const pos = this.checkpoint();
126
+ const token = this.nextToken();
127
+ this.reset(pos);
128
+ return token;
129
+ }
130
+
131
+ /** Consume token if text matches, otherwise leave position unchanged */
132
+ matchText(text: string): WeslToken | null {
133
+ const token = this.peek();
134
+ if (token?.text === text) {
135
+ this.nextToken();
136
+ return token;
137
+ }
138
+ return null;
139
+ }
140
+
141
+ /** Consume token if kind matches (and optionally text), otherwise leave position unchanged */
142
+ matchKind<K extends WeslTokenKind>(
143
+ kind: K,
144
+ text?: string,
145
+ ): WeslToken<K> | null {
146
+ const token = this.peek();
147
+ if (token?.kind === kind && (!text || token.text === text)) {
148
+ this.nextToken();
149
+ return token as WeslToken<K>;
150
+ }
151
+ return null;
152
+ }
153
+
154
+ /** Consume token if predicate matches, otherwise leave position unchanged */
155
+ nextIf(predicate: (token: WeslToken) => boolean): WeslToken | null {
156
+ const token = this.peek();
157
+ if (token && predicate(token)) {
158
+ this.nextToken();
159
+ return token;
160
+ }
161
+ return null;
162
+ }
163
+
164
+ /** Match a sequence of tokens by text. Resets and returns null if any fails. */
165
+ matchSequence(...texts: string[]): WeslToken[] | null {
166
+ const startPos = this.checkpoint();
167
+ const tokens: WeslToken[] = [];
168
+ for (const text of texts) {
169
+ const token = this.matchText(text);
170
+ if (!token) {
171
+ this.reset(startPos);
172
+ return null;
173
+ }
174
+ tokens.push(token);
175
+ }
176
+ return tokens;
177
+ }
178
+
129
179
  private skipToEol(position: number): number {
130
180
  this.eolPattern.lastIndex = position;
131
181
  const result = this.eolPattern.exec(this.src);
@@ -207,7 +257,7 @@ export class WeslStream implements Stream<WeslToken> {
207
257
  }
208
258
  }
209
259
 
210
- isTemplateStart(afterToken: number): boolean {
260
+ private isTemplateStart(afterToken: number): boolean {
211
261
  // Skip over <
212
262
  this.stream.reset(afterToken);
213
263
  // We start with a < token
@@ -254,7 +304,7 @@ export class WeslStream implements Stream<WeslToken> {
254
304
  * Call this after consuming an opening bracket.
255
305
  * Skips until a closing bracket. This also consumes the closing bracket.
256
306
  */
257
- skipBracketsTo(closingBracket: string) {
307
+ private skipBracketsTo(closingBracket: string): void {
258
308
  while (true) {
259
309
  const nextToken = this.stream.nextToken();
260
310
  if (nextToken === null) {
@@ -273,10 +323,3 @@ export class WeslStream implements Stream<WeslToken> {
273
323
  }
274
324
  }
275
325
  }
276
-
277
- export const templateOpen = withStreamAction(stream => {
278
- return (stream as WeslStream).nextTemplateStartToken();
279
- });
280
- export const templateClose = withStreamAction(stream => {
281
- return (stream as WeslStream).nextTemplateEndToken();
282
- });
@@ -0,0 +1,48 @@
1
+ import type { Stream, Token } from "../../Stream.ts";
2
+
3
+ export class CachingStream<T extends Token> implements Stream<T> {
4
+ private cache = new Cache<number, { token: T | null; checkpoint: number }>(5);
5
+ private inner: Stream<T>;
6
+ constructor(inner: Stream<T>) {
7
+ this.inner = inner;
8
+ }
9
+ checkpoint(): number {
10
+ return this.inner.checkpoint();
11
+ }
12
+ reset(position: number): void {
13
+ this.inner.reset(position);
14
+ }
15
+ nextToken(): T | null {
16
+ const startPos = this.checkpoint();
17
+ const cachedValue = this.cache.get(startPos);
18
+ if (cachedValue !== undefined) {
19
+ this.reset(cachedValue.checkpoint);
20
+ return cachedValue.token;
21
+ } else {
22
+ const token = this.inner.nextToken();
23
+ const checkpoint = this.checkpoint();
24
+ this.cache.set(startPos, { token, checkpoint });
25
+ return token;
26
+ }
27
+ }
28
+ get src(): string {
29
+ return this.inner.src;
30
+ }
31
+ }
32
+
33
+ /** size limited key value cache */
34
+ class Cache<K, V> extends Map<K, V> {
35
+ private readonly max: number;
36
+ constructor(max: number) {
37
+ super();
38
+ this.max = max;
39
+ }
40
+
41
+ set(k: K, v: V): this {
42
+ if (this.size > this.max) {
43
+ const first = this.keys().next().value;
44
+ if (first) this.delete(first);
45
+ }
46
+ return super.set(k, v);
47
+ }
48
+ }
@@ -0,0 +1,85 @@
1
+ import type { Span } from "../../Span.ts";
2
+ import type { Stream, TypedToken } from "../../Stream.ts";
3
+ import { toRegexSource } from "./RegexHelpers.ts";
4
+
5
+ /** Runs a `RegexMatchers` on an input string */
6
+ export class MatchersStream<Kind extends string>
7
+ implements Stream<TypedToken<Kind>>
8
+ {
9
+ private position = 0;
10
+ public text: string;
11
+ private matchers: RegexMatchers<Kind>;
12
+
13
+ constructor(text: string, matchers: RegexMatchers<Kind>) {
14
+ this.text = text;
15
+ this.matchers = matchers;
16
+ }
17
+
18
+ checkpoint(): number {
19
+ return this.position;
20
+ }
21
+ reset(position: number): void {
22
+ this.position = position;
23
+ }
24
+ nextToken(): TypedToken<Kind> | null {
25
+ const result = this.matchers.execAt(this.text, this.position);
26
+ if (result === null) return null;
27
+ this.position = result.span[1];
28
+ return result;
29
+ }
30
+ get src(): string {
31
+ return this.text;
32
+ }
33
+ }
34
+
35
+ /**
36
+ * The matchers passed to this object must follow certain rules:
37
+ * - They must use non-capturing groups: `(?:...)`
38
+ * - They must NOT use `^` or `$`
39
+ */
40
+ export class RegexMatchers<Kind extends string> {
41
+ private groups: Kind[];
42
+ private exp: RegExp;
43
+ constructor(matchers: Record<Kind, string | RegExp>) {
44
+ this.groups = Object.keys(matchers) as Kind[];
45
+ const expParts = Object.entries(matchers as Record<string, string | RegExp>)
46
+ .map(toRegexSource)
47
+ .join("|");
48
+ // d = return substrings of each match
49
+ // y = sticky, only match at the start of the string
50
+ // u = unicode aware
51
+ this.exp = new RegExp(expParts, "dyu");
52
+ }
53
+
54
+ execAt(text: string, position: number): TypedToken<Kind> | null {
55
+ this.exp.lastIndex = position;
56
+ const matches = this.exp.exec(text);
57
+ const matchedIndex = findGroupDex(matches?.indices);
58
+
59
+ if (matchedIndex) {
60
+ const { span, groupDex } = matchedIndex;
61
+ const kind = this.groups[groupDex];
62
+ return { kind, span, text: text.slice(span[0], span[1]) };
63
+ } else {
64
+ return null;
65
+ }
66
+ }
67
+ }
68
+
69
+ interface MatchedIndex {
70
+ span: Span;
71
+ groupDex: number;
72
+ }
73
+
74
+ function findGroupDex(
75
+ indices: RegExpIndicesArray | undefined,
76
+ ): MatchedIndex | undefined {
77
+ if (indices !== undefined) {
78
+ for (let i = 1; i < indices.length; i++) {
79
+ const span = indices[i];
80
+ if (span !== undefined) {
81
+ return { span, groupDex: i - 1 };
82
+ }
83
+ }
84
+ }
85
+ }
@@ -0,0 +1,38 @@
1
+ export function toRegexSource(nameExp: [string, RegExp | string]): string {
2
+ const [name, e] = nameExp;
3
+ if (typeof e === "string") {
4
+ const expSrc = `(${escapeRegex(e)})`;
5
+ verifyNonCapturing(name, new RegExp(expSrc));
6
+ return expSrc;
7
+ } else {
8
+ verifyNonCapturing(name, e);
9
+ return `(${e.source})`;
10
+ }
11
+ }
12
+
13
+ function verifyNonCapturing(name: string, exp: RegExp): void {
14
+ const willMatch = new RegExp("|" + exp.source);
15
+ const result = willMatch.exec("")!;
16
+ if (result.length > 1) {
17
+ throw new Error(
18
+ `match expression groups must be non-capturing: ${name}: /${exp.source}/. Use (?:...) instead.`,
19
+ );
20
+ }
21
+ }
22
+
23
+ const regexSpecials = /[$+*.?|(){}[\]\\/^]/g;
24
+
25
+ function escapeRegex(s: string): string {
26
+ return s.replace(regexSpecials, "\\$&");
27
+ }
28
+
29
+ /**
30
+ * @return a regexp to match any of the space separated tokens in the provided string.
31
+ * Regex special characters are escaped, and the matchers are sorted by length
32
+ * so that longer matches are preferred.
33
+ */
34
+ export function matchOneOf(syms: string): RegExp {
35
+ const symbolList = syms.split(/\s+/).sort((a, b) => b.length - a.length);
36
+ const escaped = symbolList.filter(s => s).map(escapeRegex);
37
+ return new RegExp(escaped.join("|"));
38
+ }
@@ -0,0 +1,100 @@
1
+ import fs from "node:fs/promises";
2
+ import { expect, test } from "vitest";
3
+ import { fetchBulkTest } from "wesl-testsuite/fetch-bulk-tests";
4
+ import { link } from "../Linker.ts";
5
+ import type { Conditions } from "../Scope.ts";
6
+ import { expectNoLogAsync } from "./LogCatcher.ts";
7
+
8
+ const fixturesDir = new URL("../../fixtures/", import.meta.url);
9
+
10
+ const bevyBulkTest = {
11
+ name: "Bevy",
12
+ baseDir: "bevy-wgsl",
13
+ git: {
14
+ url: "https://github.com/wgsl-tooling-wg/bevy-wgsl.git",
15
+ revision: "84977ff025eaf8d92e56a9c35b815fae70eb4af0",
16
+ },
17
+ };
18
+
19
+ await fetchBulkTest(bevyBulkTest, fixturesDir);
20
+
21
+ // Constants needed by various Bevy modules
22
+ const bevyConstants = {
23
+ MAX_CASCADES_PER_LIGHT: 4,
24
+ MAX_DIRECTIONAL_LIGHTS: 4,
25
+ PER_OBJECT_BUFFER_BATCH_SIZE: 64,
26
+ TONEMAPPING_LUT_TEXTURE_BINDING_INDEX: 26,
27
+ TONEMAPPING_LUT_SAMPLER_BINDING_INDEX: 27,
28
+ };
29
+
30
+ // Files that need specific conditions to link or produce non-empty output
31
+ const conditionalFiles: Record<string, Conditions> = {
32
+ "./pbr/ssr.wesl": { DEPTH_PREPASS: true, DEFERRED_PREPASS: true },
33
+ "./pbr/raymarch.wesl": { DEPTH_PREPASS: true },
34
+ "./pbr/prepass_utils.wesl": { DEPTH_PREPASS: true },
35
+ "./core_pipeline/oit.wesl": { OIT_ENABLED: true },
36
+ "./pbr/decal/clustered.wesl": { CLUSTERED_DECALS_ARE_USABLE: true },
37
+ "./pbr/decal/forward.wesl": { DEPTH_PREPASS: true },
38
+ "./pbr/morph.wesl": { MORPH_TARGETS: true },
39
+ };
40
+
41
+ // LATER: binding_array is a WGSL extension not yet supported
42
+ const skipFiles = [
43
+ "./render/bindless.wesl", // uses binding_array directly
44
+ "./pbr/decal/clustered.wesl", // imports mesh_view_bindings which uses binding_array
45
+ ];
46
+
47
+ // Files that only contain imports (re-export modules) - always produce empty output
48
+ const importOnlyFiles = ["./sprite/mesh2d_view_types.wesl"];
49
+
50
+ async function loadBevyBundle(): Promise<Record<string, string>> {
51
+ const bevyDir = new URL(
52
+ "src/shaders/bevy/",
53
+ new URL(bevyBulkTest.baseDir + "/", fixturesDir),
54
+ );
55
+ const bundle: Record<string, string> = {};
56
+ await loadDir(bevyDir, "", bundle);
57
+ return bundle;
58
+ }
59
+
60
+ async function loadDir(
61
+ baseUrl: URL,
62
+ relPath: string,
63
+ bundle: Record<string, string>,
64
+ ): Promise<void> {
65
+ const dirUrl = new URL(relPath, baseUrl);
66
+ const entries = await fs.readdir(dirUrl, { withFileTypes: true });
67
+ for (const entry of entries) {
68
+ const entryPath = relPath ? `${relPath}${entry.name}` : entry.name;
69
+ if (entry.isDirectory()) {
70
+ await loadDir(baseUrl, entryPath + "/", bundle);
71
+ } else if (entry.name.endsWith(".wesl")) {
72
+ const src = await fs.readFile(new URL(entryPath, baseUrl), "utf8");
73
+ bundle["./" + entryPath] = src;
74
+ }
75
+ }
76
+ }
77
+
78
+ const weslSrc = await loadBevyBundle();
79
+ const allFiles = Object.keys(weslSrc)
80
+ .filter(f => !skipFiles.includes(f))
81
+ .sort();
82
+
83
+ allFiles.forEach(file => {
84
+ test(`bevy: link ${file}`, async () => {
85
+ const conditions = conditionalFiles[file] ?? {};
86
+ const constants = bevyConstants;
87
+ const rootModuleName = file;
88
+
89
+ const result = await expectNoLogAsync(() =>
90
+ link({ weslSrc, rootModuleName, conditions, constants }),
91
+ );
92
+
93
+ const wgsl = result.dest;
94
+ if (importOnlyFiles.includes(file)) {
95
+ expect(wgsl).toBe("");
96
+ } else {
97
+ expect(wgsl.length).toBeGreaterThan(0);
98
+ }
99
+ });
100
+ });
@@ -0,0 +1,110 @@
1
+ import { expect, test } from "vitest";
2
+ import { link } from "../Linker.ts";
3
+
4
+ test("bind standard types in local contexts", async () => {
5
+ const result = await link({
6
+ weslSrc: {
7
+ "main.wesl": `
8
+ struct Vertex { position: vec3f, normal: vec3f }
9
+ const SCALE: vec4f = vec4f(0.1, 0.2, 0.3, 0.4);
10
+
11
+ fn identity() -> mat3x3f {
12
+ return mat3x3f(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0);
13
+ }
14
+
15
+ fn main() {
16
+ let v = Vertex(vec3f(0.0), vec3f(1.0, 0.0, 0.0));
17
+ let m = identity();
18
+ }
19
+ `,
20
+ },
21
+ rootModuleName: "main.wesl",
22
+ });
23
+
24
+ expect(result.dest).toContain("struct Vertex");
25
+ expect(result.dest).toContain("vec3f");
26
+ expect(result.dest).toContain("vec4f");
27
+ expect(result.dest).toContain("mat3x3f");
28
+ expect(result.dest).toContain("const SCALE");
29
+ });
30
+
31
+ test("bind types in @if conditionals", async () => {
32
+ const result = await link({
33
+ weslSrc: {
34
+ "main.wesl": `
35
+ @if(USE_VEC4)
36
+ fn process() -> vec4f { return vec4f(1.0); }
37
+
38
+ @if(USE_MAT)
39
+ fn transform() -> mat4x4f { return mat4x4f(); }
40
+
41
+ fn main() {
42
+ let x = process();
43
+ let y = transform();
44
+ }
45
+ `,
46
+ },
47
+ rootModuleName: "main.wesl",
48
+ conditions: { USE_VEC4: true, USE_MAT: true },
49
+ });
50
+
51
+ expect(result.dest).toContain("vec4f");
52
+ expect(result.dest).toContain("mat4x4f");
53
+ expect(result.dest).toContain("fn process()");
54
+ expect(result.dest).toContain("fn transform()");
55
+ });
56
+
57
+ test("bind types in cross-module imports with initializers", async () => {
58
+ const result = await link({
59
+ weslSrc: {
60
+ "main.wesl": `
61
+ import package::lib::SCALE;
62
+ import package::lib::color;
63
+
64
+ fn main() {
65
+ let s = SCALE.x;
66
+ let c = color.rgb;
67
+ }
68
+ `,
69
+ "lib.wesl": `
70
+ override SCALE: vec4f = vec4f(0.1, 0.2, 0.3, 0.4);
71
+ var<private> color: vec4f = vec4f(1.0, 0.0, 0.0, 1.0);
72
+ `,
73
+ },
74
+ rootModuleName: "main.wesl",
75
+ });
76
+
77
+ expect(result.dest).toContain("vec4f");
78
+ expect(result.dest).toContain("override SCALE");
79
+ expect(result.dest).toContain("var<private> color");
80
+ });
81
+
82
+ test("import function and struct from same module", async () => {
83
+ const result = await link({
84
+ weslSrc: {
85
+ "main.wesl": `
86
+ import package::space::bracketing::bracketing;
87
+ import package::space::bracketing::BracketingResult;
88
+
89
+ fn main() {
90
+ let r: BracketingResult = bracketing(vec2f(1.0, 0.0));
91
+ }
92
+ `,
93
+ "space/bracketing.wesl": `
94
+ struct BracketingResult {
95
+ vAxis0: vec2f,
96
+ vAxis1: vec2f,
97
+ blendAlpha: f32,
98
+ }
99
+
100
+ fn bracketing(dir: vec2f) -> BracketingResult {
101
+ return BracketingResult(dir, dir, 0.5);
102
+ }
103
+ `,
104
+ },
105
+ rootModuleName: "main.wesl",
106
+ });
107
+
108
+ expect(result.dest).toContain("struct BracketingResult");
109
+ expect(result.dest).toContain("fn bracketing");
110
+ });