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/dist/index.d.ts +44 -2
- package/dist/index.js +356 -327
- package/package.json +1 -9
- package/src/BindIdents.ts +47 -46
- package/src/LowerAndEmit.ts +1 -20
- package/src/Scope.ts +3 -1
- package/src/StandardTypes.ts +19 -0
- package/src/discovery/FindUnboundIdents.ts +69 -10
- package/src/index.ts +1 -0
- package/src/test/DiscoverModules.test.ts +113 -0
- package/src/test/FindUnboundIdents.test.ts +71 -0
- package/src/test/LinkPackage.test.ts +23 -17
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wesl",
|
|
3
|
-
"version": "0.7.
|
|
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 {
|
|
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
|
|
124
|
-
|
|
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(
|
|
134
|
+
const decls = new Map(rootDecls.map(d => [d.originalName, d] as const));
|
|
140
135
|
const liveDecls: LiveDecls = { decls, parent: null };
|
|
141
136
|
|
|
142
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
return
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
532
|
-
function collectDecls(
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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
|
}
|
package/src/LowerAndEmit.ts
CHANGED
|
@@ -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:
|
|
111
|
+
contents: ScopeItem[];
|
|
110
112
|
|
|
111
113
|
/** Conditional attribute (@if or @else) for this scope */
|
|
112
114
|
condAttribute?: IfAttribute | ElifAttribute | ElseAttribute;
|
package/src/StandardTypes.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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 =
|
|
45
|
-
const
|
|
46
|
-
|
|
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
|
|
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
|
@@ -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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
13
|
-
|
|
18
|
+
test("import fn from a package bundle", async () => {
|
|
19
|
+
const src = `
|
|
20
|
+
import hash_pkg::hash::hashFn;
|
|
14
21
|
|
|
15
|
-
@
|
|
16
|
-
fn
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
27
|
-
expect(result.dest).not.toContain("
|
|
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 () => {
|