wesl 0.7.21 → 0.7.23

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wesl",
3
- "version": "0.7.21",
3
+ "version": "0.7.23",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",
@@ -26,14 +26,6 @@
26
26
  "typedoc-theme-hierarchy": "^6.0.0",
27
27
  "wrangler": "^4.22.0"
28
28
  },
29
- "peerDependencies": {
30
- "random_wgsl": "^0.6.69"
31
- },
32
- "peerDependenciesMeta": {
33
- "random_wgsl": {
34
- "optional": true
35
- }
36
- },
37
29
  "license": "MIT",
38
30
  "keywords": [
39
31
  "webgpu",
package/src/BindIdents.ts CHANGED
@@ -16,29 +16,17 @@ import type {
16
16
  LexicalScope,
17
17
  RefIdent,
18
18
  Scope,
19
+ ScopeItem,
19
20
  SrcModule,
20
21
  } from "./Scope.ts";
21
- import { stdEnumerant, stdFn, stdType } from "./StandardTypes.ts";
22
+ import {
23
+ stdEnumerant,
24
+ stdFn,
25
+ stdType,
26
+ wgslStandardAttributes,
27
+ } from "./StandardTypes.ts";
22
28
  import { last } from "./Util.ts";
23
29
 
24
- /** WGSL standard attributes whose params need binding (e.g., @workgroup_size). */
25
- const wgslStandardAttributes = new Set([
26
- "align",
27
- "binding",
28
- "blend_src",
29
- "compute",
30
- "const",
31
- "fragment",
32
- "group",
33
- "id",
34
- "invariant",
35
- "location",
36
- "must_use",
37
- "size",
38
- "vertex",
39
- "workgroup_size",
40
- ]);
41
-
42
30
  /**
43
31
  * BindIdents pass: link reference identifiers to declarations.
44
32
  *
@@ -112,16 +100,22 @@ export interface BindIdentsParams
112
100
 
113
101
  /** If true, accumulate unbound identifiers into BindResults.unbound instead of throwing. */
114
102
  accumulateUnbound?: true;
103
+
104
+ /** Visit all conditional branches (for dependency discovery). */
105
+ discoveryMode?: boolean;
115
106
  }
116
107
 
117
108
  /** Bind ref idents to declarations and mangle global declaration names. */
118
109
  export function bindIdents(params: BindIdentsParams): BindResults {
119
110
  const { rootAst, resolver, virtuals, accumulateUnbound } = params;
120
111
  const { conditions = {}, mangler = minimalMangle } = params;
112
+ const { discoveryMode } = params;
121
113
  const packageName = rootAst.srcModule.modulePath.split("::")[0];
122
114
 
123
- const validRootDecls = findValidRootDecls(rootAst.rootScope, conditions);
124
- const { globalNames, knownDecls } = initRootDecls(validRootDecls);
115
+ const rootDecls = discoveryMode
116
+ ? findAllRootDecls(rootAst.rootScope)
117
+ : findValidRootDecls(rootAst.rootScope, conditions);
118
+ const { globalNames, knownDecls } = initRootDecls(rootDecls);
125
119
 
126
120
  const bindContext = {
127
121
  resolver,
@@ -134,13 +128,13 @@ export function bindIdents(params: BindIdentsParams): BindResults {
134
128
  globalNames,
135
129
  globalStatements: new Map<AbstractElem, EmittableElem>(),
136
130
  unbound: accumulateUnbound ? [] : undefined,
131
+ discoveryMode,
137
132
  };
138
133
 
139
- const decls = new Map(validRootDecls.map(d => [d.originalName, d] as const));
134
+ const decls = new Map(rootDecls.map(d => [d.originalName, d] as const));
140
135
  const liveDecls: LiveDecls = { decls, parent: null };
141
136
 
142
- // Process dependent scopes for all valid root decls (already filtered by conditions)
143
- const fromRootDecls = validRootDecls.flatMap(decl =>
137
+ const fromRootDecls = rootDecls.flatMap(decl =>
144
138
  processDependentScope(decl, bindContext),
145
139
  );
146
140
 
@@ -195,12 +189,12 @@ export function findValidRootDecls(
195
189
  rootScope: Scope,
196
190
  conditions: Conditions,
197
191
  ): DeclIdent[] {
198
- const found: DeclIdent[] = [];
199
- for (const item of validItems(rootScope, conditions)) {
200
- if (item.kind === "decl") found.push(item);
201
- else if (item.kind === "partial") collectDecls(item, found);
202
- }
203
- return found;
192
+ return collectDecls(validItems(rootScope, conditions));
193
+ }
194
+
195
+ /** Find all declarations at the root level, ignoring conditions. */
196
+ export function findAllRootDecls(rootScope: Scope): DeclIdent[] {
197
+ return collectDecls(rootScope.contents);
204
198
  }
205
199
 
206
200
  /** Find a public declaration with the given original name. */
@@ -244,6 +238,9 @@ interface BindContext {
244
238
 
245
239
  /** Don't follow references from declarations (for library dependency detection). */
246
240
  dontFollowDecls?: boolean;
241
+
242
+ /** Visit all conditional branches (for dependency discovery). */
243
+ discoveryMode?: boolean;
247
244
  }
248
245
 
249
246
  /**
@@ -277,7 +274,10 @@ function processScope(
277
274
  const newGlobals: DeclIdent[] = [];
278
275
  const newFromChildren: DeclIdent[] = [];
279
276
 
280
- for (const child of validItems(scope, bindContext.conditions)) {
277
+ const items = bindContext.discoveryMode
278
+ ? scope.contents
279
+ : validItems(scope, bindContext.conditions);
280
+ for (const child of items) {
281
281
  if (child.kind === "decl") {
282
282
  liveDecls.decls.set(child.originalName, child);
283
283
  } else if (child.kind === "ref") {
@@ -337,6 +337,7 @@ function handleRef(
337
337
  failIdent(ident, `unresolved identifier '${ident.originalName}'`);
338
338
  }
339
339
 
340
+ /** Follow new global declarations into their dependent scopes. */
340
341
  function handleDecls(
341
342
  newGlobals: DeclIdent[],
342
343
  bindContext: BindContext,
@@ -344,7 +345,7 @@ function handleDecls(
344
345
  return newGlobals.flatMap(decl => processDependentScope(decl, bindContext));
345
346
  }
346
347
 
347
- /** If found declaration is new, mangle its name. Return if it's a global declaration. */
348
+ /** If found declaration is new, mangle its name. @return the decl if it's global. */
348
349
  function handleNewDecl(
349
350
  refIdent: RefIdent,
350
351
  foundDecl: FoundDecl,
@@ -380,8 +381,9 @@ function findQualifiedImport(
380
381
  refIdent: RefIdent,
381
382
  ctx: BindContext,
382
383
  ): FoundDecl | undefined {
383
- const { conditions, unbound } = ctx;
384
- const flatImps = flatImports(refIdent.ast, conditions);
384
+ const { conditions, unbound, discoveryMode } = ctx;
385
+ const conds = discoveryMode ? undefined : conditions;
386
+ const flatImps = flatImports(refIdent.ast, conds);
385
387
  const identParts = refIdent.originalName.split("::");
386
388
  const pathParts =
387
389
  matchingImport(identParts, flatImps) ?? qualifiedIdent(identParts);
@@ -471,9 +473,7 @@ function getValidRootDecls(
471
473
  conditions: Conditions,
472
474
  ): DeclIdent[] {
473
475
  const lexScope = rootScope as LexicalScope;
474
- if (!lexScope._validRootDecls) {
475
- lexScope._validRootDecls = findValidRootDecls(rootScope, conditions);
476
- }
476
+ lexScope._validRootDecls ??= findValidRootDecls(rootScope, conditions);
477
477
  return lexScope._validRootDecls;
478
478
  }
479
479
 
@@ -485,9 +485,7 @@ function rootLiveDecls(
485
485
  assertThatDebug(decl.isGlobal, identToString(decl));
486
486
 
487
487
  let scope = decl.containingScope;
488
- while (scope.parent) {
489
- scope = scope.parent;
490
- }
488
+ while (scope.parent) scope = scope.parent;
491
489
  assertThatDebug(scope.kind === "scope");
492
490
 
493
491
  const root = scope as LexicalScope;
@@ -524,14 +522,17 @@ function stdWgsl(name: string): boolean {
524
522
  return stdType(name) || stdFn(name) || stdEnumerant(name); // TODO add tests for enumerants case (e.g. var x = read;)
525
523
  }
526
524
 
525
+ /** @return identParts if it's a qualified path (has ::). */
527
526
  function qualifiedIdent(identParts: string[]): string[] | undefined {
528
527
  if (identParts.length > 1) return identParts;
529
528
  }
530
529
 
531
- /** Collect all declarations in a scope (used when scope is already validated). */
532
- function collectDecls(scope: Scope, found: DeclIdent[]): void {
533
- for (const item of scope.contents) {
534
- if (item.kind === "decl") found.push(item);
535
- else if (item.kind === "partial") collectDecls(item, found);
536
- }
530
+ /** Collect all declarations from scope items, recursing into partial scopes. */
531
+ function collectDecls(items: Iterable<ScopeItem>): DeclIdent[] {
532
+ return [...items].flatMap(item => {
533
+ const { kind } = item;
534
+ if (kind === "decl") return [item];
535
+ if (kind === "partial") return collectDecls(item.contents);
536
+ return [];
537
+ });
537
538
  }
@@ -18,6 +18,7 @@ import { filterValidElements } from "./Conditions.ts";
18
18
  import { identToString } from "./debug/ScopeToString.ts";
19
19
  import type { Conditions, DeclIdent, Ident } from "./Scope.ts";
20
20
  import type { SrcMapBuilder } from "./SrcMap.ts";
21
+ import { wgslStandardAttributes } from "./StandardTypes.ts";
21
22
 
22
23
  export interface EmitParams {
23
24
  srcBuilder: SrcMapBuilder;
@@ -36,26 +37,6 @@ interface EmitContext {
36
37
  extracting: boolean;
37
38
  }
38
39
 
39
- /** Valid WGSL standard attributes (from spec). Non-WGSL attributes are stripped.
40
- * See: https://www.w3.org/TR/WGSL/#attributes
41
- * Note: @builtin, @diagnostic, @interpolate are parsed as separate attribute types. */
42
- const wgslStandardAttributes = new Set([
43
- "align",
44
- "binding",
45
- "blend_src",
46
- "compute",
47
- "const",
48
- "fragment",
49
- "group",
50
- "id",
51
- "invariant",
52
- "location",
53
- "must_use",
54
- "size",
55
- "vertex",
56
- "workgroup_size",
57
- ]);
58
-
59
40
  /** Traverse the AST, starting from root elements, emitting WGSL for each. */
60
41
  export function lowerAndEmit(params: EmitParams): void {
61
42
  const { srcBuilder, rootElems, conditions } = params;
package/src/Scope.ts CHANGED
@@ -23,6 +23,8 @@ export interface SrcModule {
23
23
  /** a src declaration or reference to an ident */
24
24
  export type Ident = DeclIdent | RefIdent;
25
25
 
26
+ export type ScopeItem = Ident | Scope;
27
+
26
28
  /** LATER change this to a Map, so that `toString` isn't accidentally a condition */
27
29
  export type Conditions = Record<string, boolean>;
28
30
 
@@ -106,7 +108,7 @@ interface ScopeBase {
106
108
  parent: Scope | null;
107
109
 
108
110
  /* Child scopes and idents in lexical order */
109
- contents: (Ident | Scope)[];
111
+ contents: ScopeItem[];
110
112
 
111
113
  /** Conditional attribute (@if or @else) for this scope */
112
114
  condAttribute?: IfAttribute | ElifAttribute | ElseAttribute;
@@ -81,6 +81,25 @@ export const stdEnumerants = `read write read_write
81
81
  the texture format names with e.g. a 'struct rbga8unorm .)
82
82
  */
83
83
 
84
+ /** WGSL standard attributes whose params need binding (e.g., @workgroup_size).
85
+ * See: https://www.w3.org/TR/WGSL/#attributes */
86
+ export const wgslStandardAttributes = new Set([
87
+ "align",
88
+ "binding",
89
+ "blend_src",
90
+ "compute",
91
+ "const",
92
+ "fragment",
93
+ "group",
94
+ "id",
95
+ "invariant",
96
+ "location",
97
+ "must_use",
98
+ "size",
99
+ "vertex",
100
+ "workgroup_size",
101
+ ]);
102
+
84
103
  /** return true if the name is for a built in type (not a user struct) */
85
104
  export function stdType(name: string): boolean {
86
105
  return stdTypes.includes(name);
@@ -1,13 +1,19 @@
1
1
  import type { AbstractElem } from "../AbstractElems.ts";
2
2
  import {
3
+ bindIdents,
3
4
  bindIdentsRecursive,
4
5
  type EmittableElem,
5
- findValidRootDecls,
6
+ findAllRootDecls,
6
7
  type UnboundRef,
7
8
  } from "../BindIdents.ts";
8
9
  import { type LiveDecls, makeLiveDecls } from "../LiveDeclarations.ts";
9
10
  import { minimalMangle } from "../Mangler.ts";
10
- import type { BatchModuleResolver } from "../ModuleResolver.ts";
11
+ import {
12
+ type BatchModuleResolver,
13
+ fileToModulePath,
14
+ type ModuleResolver,
15
+ } from "../ModuleResolver.ts";
16
+ import type { WeslAST } from "../ParseWESL.ts";
11
17
  import type { DeclIdent, Scope } from "../Scope.ts";
12
18
  import { filterMap } from "../Util.ts";
13
19
 
@@ -38,22 +44,75 @@ export function findUnboundRefs(resolver: BatchModuleResolver): UnboundRef[] {
38
44
  packageName: "package",
39
45
  unbound: [] as UnboundRef[],
40
46
  dontFollowDecls: true,
47
+ discoveryMode: true,
41
48
  };
42
49
 
43
50
  for (const [, ast] of resolver.allModules()) {
44
- const rootDecls = findValidRootDecls(ast.rootScope, {});
45
- const liveDecls: LiveDecls = {
46
- decls: new Map(rootDecls.map(d => [d.originalName, d] as const)),
47
- parent: null,
48
- };
51
+ const rootDecls = findAllRootDecls(ast.rootScope);
52
+ const decls = new Map(rootDecls.map(d => [d.originalName, d] as const));
53
+ const liveDecls: LiveDecls = { decls, parent: null };
49
54
  // Process dependent scopes of root decls to find unbound refs in function bodies
50
- const scopes = filterMap(rootDecls, decl => decl.dependentScope);
51
- scopes.forEach(s => {
55
+ for (const s of filterMap(rootDecls, decl => decl.dependentScope)) {
52
56
  bindIdentsRecursive(s, bindContext, makeLiveDecls(liveDecls));
53
- });
57
+ }
54
58
  // Also process refs at root scope level
55
59
  bindIdentsRecursive(ast.rootScope, bindContext, liveDecls);
56
60
  }
57
61
 
58
62
  return bindContext.unbound;
59
63
  }
64
+
65
+ /** Thin decorator that records which modules were resolved. */
66
+ export class TrackingResolver implements ModuleResolver {
67
+ readonly visited = new Set<string>();
68
+ #inner: ModuleResolver;
69
+ constructor(inner: ModuleResolver) {
70
+ this.#inner = inner;
71
+ }
72
+
73
+ resolveModule(modulePath: string): WeslAST | undefined {
74
+ const ast = this.#inner.resolveModule(modulePath);
75
+ if (ast) this.visited.add(modulePath);
76
+ return ast;
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Discover reachable modules and unbound external refs from a single root.
82
+ *
83
+ * Traces the import graph from `rootModuleName`, returning only the reachable
84
+ * local modules in `weslSrc` and unresolved external references in `unbound`.
85
+ */
86
+ export function discoverModules(
87
+ weslSrc: Record<string, string>,
88
+ resolver: ModuleResolver,
89
+ rootModuleName: string,
90
+ packageName = "package",
91
+ ): { weslSrc: Record<string, string>; unbound: string[][] } {
92
+ const tracking = new TrackingResolver(resolver);
93
+ const rootAst = tracking.resolveModule(rootModuleName);
94
+ if (!rootAst) {
95
+ throw new Error(`root module not found: '${rootModuleName}'`);
96
+ }
97
+
98
+ const result = bindIdents({
99
+ rootAst,
100
+ resolver: tracking,
101
+ accumulateUnbound: true,
102
+ discoveryMode: true,
103
+ });
104
+
105
+ const moduleToKey = new Map(
106
+ Object.keys(weslSrc).map(
107
+ key => [fileToModulePath(key, packageName, false), key] as const,
108
+ ),
109
+ );
110
+
111
+ const reachable = [...tracking.visited]
112
+ .map(m => moduleToKey.get(m))
113
+ .filter(key => key !== undefined)
114
+ .map(key => [key, weslSrc[key]] as const);
115
+
116
+ const unbound = (result.unbound ?? []).map(ref => ref.path);
117
+ return { weslSrc: Object.fromEntries(reachable), unbound };
118
+ }
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from "./AbstractElems.ts";
2
2
  export * from "./BindIdents.ts";
3
+ export { filterValidElements } from "./Conditions.ts";
3
4
  export * from "./debug/ASTtoString.ts";
4
5
  export * from "./debug/ScopeToString.ts";
5
6
  export * from "./discovery/FindUnboundIdents.ts";
@@ -0,0 +1,113 @@
1
+ import { expect, test } from "vitest";
2
+ import { discoverModules } from "../discovery/FindUnboundIdents.ts";
3
+ import { RecordResolver } from "../ModuleResolver.ts";
4
+
5
+ test("discoverModules returns only reachable modules", () => {
6
+ const weslSrc: Record<string, string> = {
7
+ "main.wesl": `
8
+ import package::util::helper;
9
+ fn main() { helper(); }
10
+ `,
11
+ "util.wesl": `fn helper() {}`,
12
+ "unused.wesl": `fn unused() {}`,
13
+ };
14
+ const result = discoverModules(
15
+ weslSrc,
16
+ new RecordResolver(weslSrc),
17
+ "package::main",
18
+ );
19
+ const keys = Object.keys(result.weslSrc);
20
+
21
+ expect(keys).toContain("main.wesl");
22
+ expect(keys).toContain("util.wesl");
23
+ expect(keys).not.toContain("unused.wesl");
24
+ });
25
+
26
+ test("discoverModules finds unbound external refs", () => {
27
+ const weslSrc: Record<string, string> = {
28
+ "main.wesl": `
29
+ import ext_pkg::dep;
30
+ fn main() { dep(); }
31
+ `,
32
+ };
33
+ const result = discoverModules(
34
+ weslSrc,
35
+ new RecordResolver(weslSrc),
36
+ "package::main",
37
+ );
38
+ expect(result.unbound).toContainEqual(["ext_pkg", "dep"]);
39
+ });
40
+
41
+ test("discoverModules finds refs in conditional branches", () => {
42
+ const weslSrc: Record<string, string> = {
43
+ "main.wesl": `
44
+ import package::a;
45
+ import package::b;
46
+ fn main() {
47
+ @if(feature) a::run();
48
+ @else b::run();
49
+ }
50
+ `,
51
+ "a.wesl": `
52
+ import ext_a::dep;
53
+ fn run() { dep(); }
54
+ `,
55
+ "b.wesl": `
56
+ import ext_b::dep;
57
+ fn run() { dep(); }
58
+ `,
59
+ "unused.wesl": `fn unused() {}`,
60
+ };
61
+ const result = discoverModules(
62
+ weslSrc,
63
+ new RecordResolver(weslSrc),
64
+ "package::main",
65
+ );
66
+ const keys = Object.keys(result.weslSrc);
67
+
68
+ expect(keys).toContain("a.wesl");
69
+ expect(keys).toContain("b.wesl");
70
+ expect(keys).not.toContain("unused.wesl");
71
+
72
+ expect(result.unbound).toContainEqual(["ext_a", "dep"]);
73
+ expect(result.unbound).toContainEqual(["ext_b", "dep"]);
74
+ });
75
+
76
+ test("discoverModules excludes modules only reachable from other roots", () => {
77
+ const weslSrc: Record<string, string> = {
78
+ "main.wesl": `
79
+ import package::shared;
80
+ fn main() { shared::helper(); }
81
+ `,
82
+ "other.wesl": `
83
+ import package::only_other;
84
+ fn other() { only_other::run(); }
85
+ `,
86
+ "shared.wesl": `fn helper() {}`,
87
+ "only_other.wesl": `fn run() {}`,
88
+ };
89
+ const result = discoverModules(
90
+ weslSrc,
91
+ new RecordResolver(weslSrc),
92
+ "package::main",
93
+ );
94
+ const keys = Object.keys(result.weslSrc);
95
+
96
+ expect(keys).toContain("main.wesl");
97
+ expect(keys).toContain("shared.wesl");
98
+ expect(keys).not.toContain("other.wesl");
99
+ expect(keys).not.toContain("only_other.wesl");
100
+ });
101
+
102
+ test("discoverModules with single-file project", () => {
103
+ const weslSrc: Record<string, string> = {
104
+ "main.wesl": `fn main() { let x = 1; }`,
105
+ };
106
+ const result = discoverModules(
107
+ weslSrc,
108
+ new RecordResolver(weslSrc),
109
+ "package::main",
110
+ );
111
+ expect(Object.keys(result.weslSrc)).toEqual(["main.wesl"]);
112
+ expect(result.unbound).toEqual([]);
113
+ });
@@ -0,0 +1,71 @@
1
+ import { expect, test } from "vitest";
2
+ import { findUnboundIdents } from "../discovery/FindUnboundIdents.ts";
3
+ import { RecordResolver } from "../ModuleResolver.ts";
4
+
5
+ test("ref inside @if block is discovered", () => {
6
+ const srcs = {
7
+ "./test.wesl": `
8
+ import package::util::helper;
9
+ fn main() {
10
+ @if(feature) helper();
11
+ }
12
+ `,
13
+ "./util.wesl": `
14
+ import ext_pkg::dep;
15
+ fn helper() { dep(); }
16
+ `,
17
+ };
18
+ const resolver = new RecordResolver(srcs);
19
+ const unbound = findUnboundIdents(resolver);
20
+ expect(unbound).toContainEqual(["ext_pkg", "dep"]);
21
+ });
22
+
23
+ test("ref inside @else block is discovered", () => {
24
+ const srcs = {
25
+ "./test.wesl": `
26
+ import package::util::a;
27
+ import package::util::b;
28
+ fn main() {
29
+ @if(feature) a();
30
+ @else b();
31
+ }
32
+ `,
33
+ "./util.wesl": `
34
+ import ext_pkg::dep;
35
+ fn a() { }
36
+ fn b() { dep(); }
37
+ `,
38
+ };
39
+ const resolver = new RecordResolver(srcs);
40
+ const unbound = findUnboundIdents(resolver);
41
+ expect(unbound).toContainEqual(["ext_pkg", "dep"]);
42
+ });
43
+
44
+ test("import declaration inside @if block is discovered", () => {
45
+ const srcs = {
46
+ "./test.wesl": `
47
+ @if(feature)
48
+ import ext_pkg::mod::helper;
49
+
50
+ fn main() {
51
+ @if(feature) helper();
52
+ }
53
+ `,
54
+ };
55
+ const resolver = new RecordResolver(srcs);
56
+ const unbound = findUnboundIdents(resolver);
57
+ expect(unbound).toContainEqual(["ext_pkg", "mod", "helper"]);
58
+ });
59
+
60
+ test("inline qualified ref inside @if is discovered", () => {
61
+ const srcs = {
62
+ "./test.wesl": `
63
+ fn main() {
64
+ @if(feature) ext_pkg::mod::helper();
65
+ }
66
+ `,
67
+ };
68
+ const resolver = new RecordResolver(srcs);
69
+ const unbound = findUnboundIdents(resolver);
70
+ expect(unbound).toContainEqual(["ext_pkg", "mod", "helper"]);
71
+ });
@@ -1,30 +1,36 @@
1
1
  import second from "multi_pkg/second";
2
2
  import trans from "multi_pkg/transitive";
3
- import rand from "random_wgsl";
4
3
  import { expect, test } from "vitest";
5
4
  import { link } from "../Linker.ts";
5
+ import type { WeslBundle } from "../WeslBundle.ts";
6
6
  import { expectNoLogAsync } from "./LogCatcher.ts";
7
7
 
8
- test("import rand() from a package", async () => {
9
- const src = `
10
- import random_wgsl::pcg_2u_3f;
8
+ // Static fixture bundle - no external package dependency needed
9
+ const hashPkg: WeslBundle = {
10
+ name: "hash_pkg",
11
+ edition: "unstable_2025_1",
12
+ modules: {
13
+ "hash.wesl":
14
+ "fn hashFn(v: u32) -> u32 { return v ^ (v >> 16u); }\nfn unusedFn() { }",
15
+ },
16
+ };
11
17
 
12
- struct Uniforms { frame: u32 }
13
- @binding(0) @group(0) var<uniform> u: Uniforms;
18
+ test("import fn from a package bundle", async () => {
19
+ const src = `
20
+ import hash_pkg::hash::hashFn;
14
21
 
15
- @fragment
16
- fn fragmentMain(@builtin(position) pos: vec4f) -> @location(0) vec4f {
17
- let rand = pcg_2u_3f(vec2u(pos.xy) + u.frame);
18
- return vec4(rand, 1f);
19
- }
22
+ @compute @workgroup_size(1)
23
+ fn main() { let x = hashFn(1u); }
20
24
  `;
21
-
22
- const weslSrc = { "./main.wesl": src };
23
- const result = await expectNoLogAsync(async () =>
24
- link({ weslSrc, rootModuleName: "./main.wesl", libs: [rand] }),
25
+ const result = await expectNoLogAsync(() =>
26
+ link({
27
+ weslSrc: { "./main.wesl": src },
28
+ rootModuleName: "./main.wesl",
29
+ libs: [hashPkg],
30
+ }),
25
31
  );
26
- expect(result.dest).toContain("fn pcg_2u_3f");
27
- expect(result.dest).not.toContain("sinRand");
32
+ expect(result.dest).toContain("fn hashFn");
33
+ expect(result.dest).not.toContain("unusedFn");
28
34
  });
29
35
 
30
36
  test("import from multi_pkg/second", async () => {