shaderkit 0.7.0 → 0.8.0

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/src/minifier.ts CHANGED
@@ -1,445 +1,445 @@
1
- import { type Token, tokenize } from './tokenizer.js'
2
- import { GLSL_KEYWORDS, WGSL_KEYWORDS } from './constants.js'
3
- import { parse } from './parser.js'
4
- import { generate } from './generator.js'
5
- import { visit } from './visitor.js'
6
- import { ArraySpecifier } from './ast.js'
7
-
8
- export type MangleMatcher = (token: Token, index: number, tokens: Token[]) => boolean
9
-
10
- export interface MinifyOptions {
11
- /** Whether to rename variables. Will call a {@link MangleMatcher} if specified. Default is `false`. */
12
- mangle: boolean | MangleMatcher
13
- /** A map to read and write renamed variables to when mangling. */
14
- mangleMap: Map<string, string>
15
- /** Whether to rename external variables such as uniforms or varyings. Default is `false`. */
16
- mangleExternals: boolean
17
- }
18
-
19
- const isWord = /* @__PURE__ */ RegExp.prototype.test.bind(/^\w/)
20
- const isName = /* @__PURE__ */ RegExp.prototype.test.bind(/^[_A-Za-z]/)
21
- const isScoped = /* @__PURE__ */ RegExp.prototype.test.bind(/[;{}\\@]/)
22
- const isStorage = /* @__PURE__ */ RegExp.prototype.test.bind(
23
- /^(binding|group|layout|uniform|in|out|attribute|varying)$/,
24
- )
25
-
26
- // Checks for WGSL-specific `fn foo(`, `var bar =`, `let baz =`, `const qux =`
27
- const WGSL_REGEX = /\bfn\s+\w+\s*\(|\b(var|let|const)\s+\w+\s*[:=]/
28
-
29
- function minifyLegacy(
30
- code: string,
31
- { mangle = false, mangleMap = new Map(), mangleExternals = false }: Partial<MinifyOptions> = {},
32
- ): string {
33
- const mangleCache = new Map<string, string>()
34
- const tokens: Token[] = tokenize(code).filter((token) => token.type !== 'whitespace' && token.type !== 'comment')
35
-
36
- let mangleIndex: number = -1
37
- let lineIndex: number = -1
38
- let blockIndex: number = -1
39
- let minified: string = ''
40
- for (let i = 0; i < tokens.length; i++) {
41
- const token = tokens[i]
42
-
43
- // Track possibly external scopes
44
- if (isStorage(token.value) || isScoped(tokens[i - 1]?.value)) lineIndex = i
45
-
46
- // Mark enter/leave block-scope
47
- if (token.value === '{' && isName(tokens[i - 1]?.value)) blockIndex = i - 1
48
- else if (token.value === '}') blockIndex = -1
49
-
50
- // Pad alphanumeric tokens
51
- if (isWord(token.value) && isWord(tokens[i - 1]?.value)) minified += ' '
52
-
53
- let prefix = token.value
54
- if (tokens[i - 1]?.value === '.') {
55
- prefix = `${tokens[i - 2]?.value}.` + prefix
56
- }
57
-
58
- // Mangle declarations and their references
59
- if (token.type === 'identifier' && (typeof mangle === 'boolean' ? mangle : mangle(token, i, tokens))) {
60
- const namespace = tokens[i - 1]?.value === '}' && tokens[i + 1]?.value === ';'
61
- const storage = isStorage(tokens[lineIndex]?.value)
62
- const list = storage && tokens[i - 1]?.value === ','
63
- let renamed = mangleMap.get(prefix) ?? mangleCache.get(prefix)
64
- if (
65
- // no-op
66
- !renamed &&
67
- // Skip struct properties
68
- blockIndex === -1 &&
69
- // Is declaration, reference, namespace, or comma-separated list
70
- (isName(tokens[i - 1]?.value) ||
71
- // uniform Type { ... } name;
72
- namespace ||
73
- // uniform float foo, bar;
74
- list ||
75
- // fn (arg: type) -> void
76
- (tokens[i - 1]?.type === 'symbol' && tokens[i + 1]?.value === ':')) &&
77
- // Skip shader externals when disabled
78
- (mangleExternals || !storage)
79
- ) {
80
- // Write shader externals and preprocessor defines to mangleMap for multiple passes
81
- // TODO: do so via scope tracking
82
- const isExternal =
83
- // Shader externals
84
- (mangleExternals && storage) ||
85
- // Defines
86
- tokens[i - 2]?.value === '#' ||
87
- // Namespaced uniform structs
88
- namespace ||
89
- // Comma-separated list of uniforms
90
- list ||
91
- // WGSL entrypoints via @stage or @workgroup_size(...)
92
- (tokens[i - 1]?.value === 'fn' && (tokens[i - 2]?.value === ')' || tokens[i - 3]?.value === '@'))
93
- const cache = isExternal ? mangleMap : mangleCache
94
-
95
- while (!renamed || cache.has(renamed) || WGSL_KEYWORDS.includes(renamed)) {
96
- renamed = ''
97
- mangleIndex++
98
-
99
- let j = mangleIndex
100
- while (j > 0) {
101
- renamed = String.fromCharCode(97 + ((j - 1) % 26)) + renamed
102
- j = Math.floor(j / 26)
103
- }
104
- }
105
-
106
- cache.set(prefix, renamed)
107
- }
108
-
109
- minified += renamed ?? token.value
110
- } else {
111
- if (token.value === '\\') minified += '\n'
112
- else minified += token.value
113
- }
114
- }
115
-
116
- return minified.trim()
117
- }
118
-
119
- interface Scope {
120
- // types: Map<string, string>
121
- values: Map<string, string>
122
- references: Map<string, string>
123
- }
124
-
125
- /**
126
- * Minifies a string of GLSL or WGSL code.
127
- */
128
- export function minify(
129
- code: string,
130
- { mangle = false, mangleMap = new Map(), mangleExternals = false }: Partial<MinifyOptions> = {},
131
- ): string {
132
- const isWGSL = WGSL_REGEX.test(code)
133
- const KEYWORDS = isWGSL ? WGSL_KEYWORDS : GLSL_KEYWORDS
134
-
135
- // TODO: remove when WGSL is better supported
136
- if (isWGSL) return minifyLegacy(code, { mangle, mangleMap, mangleExternals })
137
-
138
- const program = parse(code)
139
-
140
- if (mangle) {
141
- const scopes: Scope[] = []
142
-
143
- function pushScope(): void {
144
- scopes.push({ values: new Map(), references: new Map() })
145
- }
146
- function popScope(): void {
147
- scopes.length -= 1
148
- }
149
-
150
- function getScopedType(name: string): string | null {
151
- for (let i = scopes.length - 1; i >= 0; i--) {
152
- const type = scopes[i].references.get(name)
153
- if (type) return type
154
- }
155
-
156
- return null
157
- }
158
-
159
- const typeScopes = new Map<string, Scope>()
160
- const types: (string | null)[] = []
161
-
162
- function getScopedName(name: string): string | null {
163
- if (types.length === 0 && mangleMap.has(name)) {
164
- return mangleMap.get(name)!
165
- }
166
-
167
- if (types[0] != null && typeScopes.has(types[0])) {
168
- const scope = typeScopes.get(types[0])!
169
- const renamed = scope.values.get(name)
170
- if (renamed) return renamed
171
- } else {
172
- for (let i = scopes.length - 1; i >= 0; i--) {
173
- const renamed = scopes[i].values.get(name)
174
- if (renamed) return renamed
175
- }
176
- }
177
-
178
- return null
179
- }
180
-
181
- let mangleIndex: number = -1
182
- function mangleName(name: string, isExternal: boolean): string {
183
- let renamed = (isExternal && mangleMap.get(name)) || getScopedName(name)
184
-
185
- while (
186
- !renamed ||
187
- getScopedName(renamed) !== null ||
188
- (isExternal && mangleMap.has(renamed)) ||
189
- KEYWORDS.includes(renamed)
190
- ) {
191
- renamed = ''
192
- mangleIndex++
193
-
194
- let j = mangleIndex
195
- while (j > 0) {
196
- renamed = String.fromCharCode(97 + ((j - 1) % 26)) + renamed
197
- j = Math.floor(j / 26)
198
- }
199
- }
200
-
201
- scopes.at(-1)!.values.set(name, renamed)
202
- if (isExternal) {
203
- if (types[0] != null) mangleMap.set(types[0] + '.' + name, renamed)
204
- else mangleMap.set(name, renamed)
205
- }
206
-
207
- return renamed
208
- }
209
-
210
- const structs = new Set<string>()
211
- const externals = new Set<string>()
212
- const externalTypes = new Set<string>()
213
-
214
- // Top-level pass for externals and type definitions
215
- for (const statement of program.body) {
216
- if (statement.type === 'StructDeclaration') {
217
- structs.add(statement.id.name)
218
- } else if (statement.type === 'StructuredBufferDeclaration') {
219
- const isExternal = statement.qualifiers.some(isStorage)
220
-
221
- if (statement.typeSpecifier.type === 'Identifier') {
222
- structs.add(statement.typeSpecifier.name)
223
- if (isExternal) externalTypes.add(statement.typeSpecifier.name)
224
- } else if (statement.typeSpecifier.type === 'ArraySpecifier') {
225
- structs.add(statement.typeSpecifier.typeSpecifier.name)
226
- if (isExternal) externalTypes.add(statement.typeSpecifier.typeSpecifier.name)
227
- }
228
-
229
- if (isExternal) {
230
- if (statement.id) {
231
- externals.add(statement.id.name)
232
- } else {
233
- for (const member of statement.members) {
234
- if (member.type !== 'VariableDeclaration') continue
235
-
236
- for (const decl of member.declarations) {
237
- if (decl.id.type === 'Identifier') {
238
- externals.add(decl.id.name)
239
- } else if (decl.id.type === 'ArraySpecifier') {
240
- externals.add((decl.id as unknown as ArraySpecifier).typeSpecifier.name)
241
- }
242
- }
243
- }
244
- }
245
- }
246
- } else if (statement.type === 'VariableDeclaration') {
247
- for (const decl of statement.declarations) {
248
- const isExternal = decl.qualifiers.some(isStorage)
249
- if (isExternal) {
250
- if (decl.id.type === 'Identifier') {
251
- externals.add(decl.id.name)
252
- } else if (decl.id.type === 'ArraySpecifier') {
253
- externals.add((decl.id as unknown as ArraySpecifier).typeSpecifier.name)
254
- }
255
- }
256
- }
257
- }
258
- }
259
-
260
- visit(program, {
261
- Program: {
262
- enter() {
263
- pushScope()
264
- },
265
- exit() {
266
- popScope()
267
- },
268
- },
269
- BlockStatement: {
270
- enter() {
271
- pushScope()
272
- },
273
- exit() {
274
- popScope()
275
- },
276
- },
277
- FunctionDeclaration: {
278
- enter(node) {
279
- const name = node.id.name
280
-
281
- // TODO: this might be external in the case of WGSL entrypoints
282
- if (name !== 'main') node.id.name = mangleName(name, false)
283
-
284
- const scope = scopes.at(-1)!
285
- if (node.typeSpecifier.type === 'Identifier') {
286
- scope.references.set(name, node.typeSpecifier.name)
287
- } else if (node.typeSpecifier.type === 'ArraySpecifier') {
288
- scope.references.set(name, node.typeSpecifier.typeSpecifier.name)
289
- }
290
-
291
- pushScope()
292
-
293
- for (const param of node.params) {
294
- if (param.id) param.id.name = mangleName(param.id.name, false)
295
- }
296
- },
297
- exit() {
298
- popScope()
299
- },
300
- },
301
- StructDeclaration: {
302
- enter(node) {
303
- const name = node.id.name
304
-
305
- const isExternal = externalTypes.has(node.id.name)
306
- if (!isExternal || mangleExternals) {
307
- node.id.name = mangleName(name, isExternal)
308
- }
309
-
310
- pushScope()
311
- typeScopes.set(name, scopes.at(-1)!)
312
- types.push(name)
313
- },
314
- exit() {
315
- types.length -= 1
316
- popScope()
317
- },
318
- },
319
- StructuredBufferDeclaration: {
320
- enter(node) {
321
- if (node.typeSpecifier.type !== 'Identifier') return
322
-
323
- // When an instance name is not defined, the type specifier can be used as an external reference
324
- const typeName = node.typeSpecifier.name
325
- if (node.id || mangleExternals) {
326
- node.typeSpecifier.name = mangleName(typeName, false)
327
- }
328
-
329
- if (!node.id) return
330
-
331
- const name = node.id.name
332
-
333
- const isExternal = externalTypes.has(typeName)
334
- if (!isExternal || mangleExternals) {
335
- node.id.name = mangleName(name, isExternal)
336
- }
337
-
338
- const scope = scopes.at(-1)!
339
- scope.references.set(name, typeName)
340
- types.push(typeName)
341
-
342
- pushScope()
343
- typeScopes.set(name, scopes.at(-1)!)
344
- },
345
- exit(node) {
346
- if (node.id) {
347
- types.length -= 1
348
- popScope()
349
- }
350
- },
351
- },
352
- VariableDeclaration(node, ancestors) {
353
- // TODO: ensure uniform decl lists work
354
- const parent = ancestors.at(-1) // Container -> VariableDecl
355
- const isParentExternal =
356
- parent?.type === 'StructDeclaration' ||
357
- (parent?.type === 'StructuredBufferDeclaration' && parent.qualifiers.some(isStorage))
358
-
359
- for (const decl of node.declarations) {
360
- // Skip preprocessor
361
- if (decl.type !== 'VariableDeclarator') continue
362
-
363
- let name: string = ''
364
- if (decl.id.type === 'Identifier') {
365
- name = decl.id.name
366
- } else if (decl.id.type === 'ArraySpecifier') {
367
- name = (decl.id as unknown as ArraySpecifier).typeSpecifier.name
368
- }
369
-
370
- const scope = scopes.at(-1)!
371
- if (decl.typeSpecifier.type === 'Identifier') {
372
- scope.references.set(name, decl.typeSpecifier.name)
373
- } else if (decl.typeSpecifier.type === 'ArraySpecifier') {
374
- scope.references.set(name, decl.typeSpecifier.typeSpecifier.name)
375
- }
376
-
377
- const isExternal = isParentExternal || decl.qualifiers.some(isStorage)
378
- if (!isExternal || mangleExternals) {
379
- mangleName(name, isExternal)
380
- }
381
- }
382
- },
383
- PreprocessorStatement(node) {
384
- if (node.name === 'define' && node.value) {
385
- const [name, value] = node.value
386
-
387
- let isExternal = false
388
-
389
- if (value) {
390
- if (value.type === 'Identifier') {
391
- isExternal ||= externals.has(value.name) || externalTypes.has(value.name)
392
- if (!isExternal || mangleExternals) value.name = mangleName(value.name, isExternal)
393
- } else if (value.type === 'MemberExpression') {
394
- // TODO: this needs to be more robust to handle string replacement
395
- } else if (value.type === 'CallExpression' && value.callee.type === 'Identifier') {
396
- isExternal ||= externals.has(value.callee.name)
397
- if (!isExternal || mangleExternals) value.callee.name = mangleName(value.callee.name, isExternal)
398
- // TODO: locally mangle arguments
399
- }
400
- }
401
-
402
- if (name.type === 'Identifier') {
403
- isExternal ||= externals.has(name.name) || externalTypes.has(name.name)
404
- if (!isExternal || mangleExternals) name.name = mangleName(name.name, isExternal)
405
- } else if (name.type === 'MemberExpression') {
406
- // TODO: this needs to be more robust to handle string replacement
407
- } else if (name.type === 'CallExpression' && name.callee.type === 'Identifier') {
408
- isExternal ||= externals.has(name.callee.name)
409
- if (!isExternal || mangleExternals) name.callee.name = mangleName(name.callee.name, isExternal)
410
- }
411
- }
412
- },
413
- MemberExpression: {
414
- enter(node) {
415
- let type: string | null = ''
416
-
417
- if (node.object.type === 'CallExpression' && node.object.callee.type === 'Identifier') {
418
- // TODO: length() should be mangled whereas array.length() should not
419
- type = getScopedType(node.object.callee.name)
420
- } else if (node.object.type === 'MemberExpression' && node.object.object.type === 'Identifier') {
421
- // Only computed member expressions can be parsed this way (e.g., (array[2]).position)
422
- type = getScopedType(node.object.object.name)
423
- const renamed = getScopedName(node.object.object.name)
424
- if (renamed !== null) node.object.object.name = renamed
425
- } else if (node.object.type === 'Identifier') {
426
- type = getScopedType(node.object.name)
427
- const renamed = getScopedName(node.object.name)
428
- if (renamed !== null) node.object.name = renamed
429
- }
430
-
431
- types.push(type)
432
- },
433
- exit() {
434
- types.length -= 1
435
- },
436
- },
437
- Identifier(node) {
438
- const renamed = getScopedName(node.name)
439
- if (renamed !== null) node.name = renamed
440
- },
441
- })
442
- }
443
-
444
- return generate(program, { target: 'GLSL' })
445
- }
1
+ import { type Token, tokenize } from './tokenizer.js'
2
+ import { GLSL_KEYWORDS, WGSL_KEYWORDS } from './constants.js'
3
+ import { parse } from './parser.js'
4
+ import { generate } from './generator.js'
5
+ import { visit } from './visitor.js'
6
+ import { ArraySpecifier } from './ast.js'
7
+
8
+ export type MangleMatcher = (token: Token, index: number, tokens: Token[]) => boolean
9
+
10
+ export interface MinifyOptions {
11
+ /** Whether to rename variables. Will call a {@link MangleMatcher} if specified. Default is `false`. */
12
+ mangle: boolean | MangleMatcher
13
+ /** A map to read and write renamed variables to when mangling. */
14
+ mangleMap: Map<string, string>
15
+ /** Whether to rename external variables such as uniforms or varyings. Default is `false`. */
16
+ mangleExternals: boolean
17
+ }
18
+
19
+ const isWord = /* @__PURE__ */ RegExp.prototype.test.bind(/^\w/)
20
+ const isName = /* @__PURE__ */ RegExp.prototype.test.bind(/^[_A-Za-z]/)
21
+ const isScoped = /* @__PURE__ */ RegExp.prototype.test.bind(/[;{}\\@]/)
22
+ const isStorage = /* @__PURE__ */ RegExp.prototype.test.bind(
23
+ /^(binding|group|layout|uniform|in|out|attribute|varying)$/,
24
+ )
25
+
26
+ // Checks for WGSL-specific `fn foo(`, `var bar =`, `let baz =`, `const qux =`
27
+ const WGSL_REGEX = /\bfn\s+\w+\s*\(|\b(var|let|const)\s+\w+\s*[:=]/
28
+
29
+ function minifyLegacy(
30
+ code: string,
31
+ { mangle = false, mangleMap = new Map(), mangleExternals = false }: Partial<MinifyOptions> = {},
32
+ ): string {
33
+ const mangleCache = new Map<string, string>()
34
+ const tokens: Token[] = tokenize(code).filter((token) => token.type !== 'whitespace' && token.type !== 'comment')
35
+
36
+ let mangleIndex: number = -1
37
+ let lineIndex: number = -1
38
+ let blockIndex: number = -1
39
+ let minified: string = ''
40
+ for (let i = 0; i < tokens.length; i++) {
41
+ const token = tokens[i]
42
+
43
+ // Track possibly external scopes
44
+ if (isStorage(token.value) || isScoped(tokens[i - 1]?.value)) lineIndex = i
45
+
46
+ // Mark enter/leave block-scope
47
+ if (token.value === '{' && isName(tokens[i - 1]?.value)) blockIndex = i - 1
48
+ else if (token.value === '}') blockIndex = -1
49
+
50
+ // Pad alphanumeric tokens
51
+ if (isWord(token.value) && isWord(tokens[i - 1]?.value)) minified += ' '
52
+
53
+ let prefix = token.value
54
+ if (tokens[i - 1]?.value === '.') {
55
+ prefix = `${tokens[i - 2]?.value}.` + prefix
56
+ }
57
+
58
+ // Mangle declarations and their references
59
+ if (token.type === 'identifier' && (typeof mangle === 'boolean' ? mangle : mangle(token, i, tokens))) {
60
+ const namespace = tokens[i - 1]?.value === '}' && tokens[i + 1]?.value === ';'
61
+ const storage = isStorage(tokens[lineIndex]?.value)
62
+ const list = storage && tokens[i - 1]?.value === ','
63
+ let renamed = mangleMap.get(prefix) ?? mangleCache.get(prefix)
64
+ if (
65
+ // no-op
66
+ !renamed &&
67
+ // Skip struct properties
68
+ blockIndex === -1 &&
69
+ // Is declaration, reference, namespace, or comma-separated list
70
+ (isName(tokens[i - 1]?.value) ||
71
+ // uniform Type { ... } name;
72
+ namespace ||
73
+ // uniform float foo, bar;
74
+ list ||
75
+ // fn (arg: type) -> void
76
+ (tokens[i - 1]?.type === 'symbol' && tokens[i + 1]?.value === ':')) &&
77
+ // Skip shader externals when disabled
78
+ (mangleExternals || !storage)
79
+ ) {
80
+ // Write shader externals and preprocessor defines to mangleMap for multiple passes
81
+ // TODO: do so via scope tracking
82
+ const isExternal =
83
+ // Shader externals
84
+ (mangleExternals && storage) ||
85
+ // Defines
86
+ tokens[i - 2]?.value === '#' ||
87
+ // Namespaced uniform structs
88
+ namespace ||
89
+ // Comma-separated list of uniforms
90
+ list ||
91
+ // WGSL entrypoints via @stage or @workgroup_size(...)
92
+ (tokens[i - 1]?.value === 'fn' && (tokens[i - 2]?.value === ')' || tokens[i - 3]?.value === '@'))
93
+ const cache = isExternal ? mangleMap : mangleCache
94
+
95
+ while (!renamed || cache.has(renamed) || WGSL_KEYWORDS.includes(renamed)) {
96
+ renamed = ''
97
+ mangleIndex++
98
+
99
+ let j = mangleIndex
100
+ while (j > 0) {
101
+ renamed = String.fromCharCode(97 + ((j - 1) % 26)) + renamed
102
+ j = Math.floor(j / 26)
103
+ }
104
+ }
105
+
106
+ cache.set(prefix, renamed)
107
+ }
108
+
109
+ minified += renamed ?? token.value
110
+ } else {
111
+ if (token.value === '\\') minified += '\n'
112
+ else minified += token.value
113
+ }
114
+ }
115
+
116
+ return minified.trim()
117
+ }
118
+
119
+ interface Scope {
120
+ // types: Map<string, string>
121
+ values: Map<string, string>
122
+ references: Map<string, string>
123
+ }
124
+
125
+ /**
126
+ * Minifies a string of GLSL or WGSL code.
127
+ */
128
+ export function minify(
129
+ code: string,
130
+ { mangle = false, mangleMap = new Map(), mangleExternals = false }: Partial<MinifyOptions> = {},
131
+ ): string {
132
+ const isWGSL = WGSL_REGEX.test(code)
133
+ const KEYWORDS = isWGSL ? WGSL_KEYWORDS : GLSL_KEYWORDS
134
+
135
+ // TODO: remove when WGSL is better supported
136
+ if (isWGSL) return minifyLegacy(code, { mangle, mangleMap, mangleExternals })
137
+
138
+ const program = parse(code)
139
+
140
+ if (mangle) {
141
+ const scopes: Scope[] = []
142
+
143
+ function pushScope(): void {
144
+ scopes.push({ values: new Map(), references: new Map() })
145
+ }
146
+ function popScope(): void {
147
+ scopes.length -= 1
148
+ }
149
+
150
+ function getScopedType(name: string): string | null {
151
+ for (let i = scopes.length - 1; i >= 0; i--) {
152
+ const type = scopes[i].references.get(name)
153
+ if (type) return type
154
+ }
155
+
156
+ return null
157
+ }
158
+
159
+ const typeScopes = new Map<string, Scope>()
160
+ const types: (string | null)[] = []
161
+
162
+ function getScopedName(name: string): string | null {
163
+ if (types.length === 0 && mangleMap.has(name)) {
164
+ return mangleMap.get(name)!
165
+ }
166
+
167
+ if (types[0] != null && typeScopes.has(types[0])) {
168
+ const scope = typeScopes.get(types[0])!
169
+ const renamed = scope.values.get(name)
170
+ if (renamed) return renamed
171
+ } else {
172
+ for (let i = scopes.length - 1; i >= 0; i--) {
173
+ const renamed = scopes[i].values.get(name)
174
+ if (renamed) return renamed
175
+ }
176
+ }
177
+
178
+ return null
179
+ }
180
+
181
+ let mangleIndex: number = -1
182
+ function mangleName(name: string, isExternal: boolean): string {
183
+ let renamed = (isExternal && mangleMap.get(name)) || getScopedName(name)
184
+
185
+ while (
186
+ !renamed ||
187
+ getScopedName(renamed) !== null ||
188
+ (isExternal && mangleMap.has(renamed)) ||
189
+ KEYWORDS.includes(renamed)
190
+ ) {
191
+ renamed = ''
192
+ mangleIndex++
193
+
194
+ let j = mangleIndex
195
+ while (j > 0) {
196
+ renamed = String.fromCharCode(97 + ((j - 1) % 26)) + renamed
197
+ j = Math.floor(j / 26)
198
+ }
199
+ }
200
+
201
+ scopes.at(-1)!.values.set(name, renamed)
202
+ if (isExternal) {
203
+ if (types[0] != null) mangleMap.set(types[0] + '.' + name, renamed)
204
+ else mangleMap.set(name, renamed)
205
+ }
206
+
207
+ return renamed
208
+ }
209
+
210
+ const structs = new Set<string>()
211
+ const externals = new Set<string>()
212
+ const externalTypes = new Set<string>()
213
+
214
+ // Top-level pass for externals and type definitions
215
+ for (const statement of program.body) {
216
+ if (statement.type === 'StructDeclaration') {
217
+ structs.add(statement.id.name)
218
+ } else if (statement.type === 'StructuredBufferDeclaration') {
219
+ const isExternal = statement.qualifiers.some(isStorage)
220
+
221
+ if (statement.typeSpecifier.type === 'Identifier') {
222
+ structs.add(statement.typeSpecifier.name)
223
+ if (isExternal) externalTypes.add(statement.typeSpecifier.name)
224
+ } else if (statement.typeSpecifier.type === 'ArraySpecifier') {
225
+ structs.add(statement.typeSpecifier.typeSpecifier.name)
226
+ if (isExternal) externalTypes.add(statement.typeSpecifier.typeSpecifier.name)
227
+ }
228
+
229
+ if (isExternal) {
230
+ if (statement.id) {
231
+ externals.add(statement.id.name)
232
+ } else {
233
+ for (const member of statement.members) {
234
+ if (member.type !== 'VariableDeclaration') continue
235
+
236
+ for (const decl of member.declarations) {
237
+ if (decl.id.type === 'Identifier') {
238
+ externals.add(decl.id.name)
239
+ } else if (decl.id.type === 'ArraySpecifier') {
240
+ externals.add((decl.id as unknown as ArraySpecifier).typeSpecifier.name)
241
+ }
242
+ }
243
+ }
244
+ }
245
+ }
246
+ } else if (statement.type === 'VariableDeclaration') {
247
+ for (const decl of statement.declarations) {
248
+ const isExternal = decl.qualifiers.some(isStorage)
249
+ if (isExternal) {
250
+ if (decl.id.type === 'Identifier') {
251
+ externals.add(decl.id.name)
252
+ } else if (decl.id.type === 'ArraySpecifier') {
253
+ externals.add((decl.id as unknown as ArraySpecifier).typeSpecifier.name)
254
+ }
255
+ }
256
+ }
257
+ }
258
+ }
259
+
260
+ visit(program, {
261
+ Program: {
262
+ enter() {
263
+ pushScope()
264
+ },
265
+ exit() {
266
+ popScope()
267
+ },
268
+ },
269
+ BlockStatement: {
270
+ enter() {
271
+ pushScope()
272
+ },
273
+ exit() {
274
+ popScope()
275
+ },
276
+ },
277
+ FunctionDeclaration: {
278
+ enter(node) {
279
+ const name = node.id.name
280
+
281
+ // TODO: this might be external in the case of WGSL entrypoints
282
+ if (name !== 'main') node.id.name = mangleName(name, false)
283
+
284
+ const scope = scopes.at(-1)!
285
+ if (node.typeSpecifier.type === 'Identifier') {
286
+ scope.references.set(name, node.typeSpecifier.name)
287
+ } else if (node.typeSpecifier.type === 'ArraySpecifier') {
288
+ scope.references.set(name, node.typeSpecifier.typeSpecifier.name)
289
+ }
290
+
291
+ pushScope()
292
+
293
+ for (const param of node.params) {
294
+ if (param.id) param.id.name = mangleName(param.id.name, false)
295
+ }
296
+ },
297
+ exit() {
298
+ popScope()
299
+ },
300
+ },
301
+ StructDeclaration: {
302
+ enter(node) {
303
+ const name = node.id.name
304
+
305
+ const isExternal = externalTypes.has(node.id.name)
306
+ if (!isExternal || mangleExternals) {
307
+ node.id.name = mangleName(name, isExternal)
308
+ }
309
+
310
+ pushScope()
311
+ typeScopes.set(name, scopes.at(-1)!)
312
+ types.push(name)
313
+ },
314
+ exit() {
315
+ types.length -= 1
316
+ popScope()
317
+ },
318
+ },
319
+ StructuredBufferDeclaration: {
320
+ enter(node) {
321
+ if (node.typeSpecifier.type !== 'Identifier') return
322
+
323
+ // When an instance name is not defined, the type specifier can be used as an external reference
324
+ const typeName = node.typeSpecifier.name
325
+ if (node.id || mangleExternals) {
326
+ node.typeSpecifier.name = mangleName(typeName, false)
327
+ }
328
+
329
+ if (!node.id) return
330
+
331
+ const name = node.id.name
332
+
333
+ const isExternal = externalTypes.has(typeName)
334
+ if (!isExternal || mangleExternals) {
335
+ node.id.name = mangleName(name, isExternal)
336
+ }
337
+
338
+ const scope = scopes.at(-1)!
339
+ scope.references.set(name, typeName)
340
+ types.push(typeName)
341
+
342
+ pushScope()
343
+ typeScopes.set(name, scopes.at(-1)!)
344
+ },
345
+ exit(node) {
346
+ if (node.id) {
347
+ types.length -= 1
348
+ popScope()
349
+ }
350
+ },
351
+ },
352
+ VariableDeclaration(node, ancestors) {
353
+ // TODO: ensure uniform decl lists work
354
+ const parent = ancestors.at(-1) // Container -> VariableDecl
355
+ const isParentExternal =
356
+ parent?.type === 'StructDeclaration' ||
357
+ (parent?.type === 'StructuredBufferDeclaration' && parent.qualifiers.some(isStorage))
358
+
359
+ for (const decl of node.declarations) {
360
+ // Skip preprocessor
361
+ if (decl.type !== 'VariableDeclarator') continue
362
+
363
+ let name: string = ''
364
+ if (decl.id.type === 'Identifier') {
365
+ name = decl.id.name
366
+ } else if (decl.id.type === 'ArraySpecifier') {
367
+ name = (decl.id as unknown as ArraySpecifier).typeSpecifier.name
368
+ }
369
+
370
+ const scope = scopes.at(-1)!
371
+ if (decl.typeSpecifier.type === 'Identifier') {
372
+ scope.references.set(name, decl.typeSpecifier.name)
373
+ } else if (decl.typeSpecifier.type === 'ArraySpecifier') {
374
+ scope.references.set(name, decl.typeSpecifier.typeSpecifier.name)
375
+ }
376
+
377
+ const isExternal = isParentExternal || decl.qualifiers.some(isStorage)
378
+ if (!isExternal || mangleExternals) {
379
+ mangleName(name, isExternal)
380
+ }
381
+ }
382
+ },
383
+ PreprocessorStatement(node) {
384
+ if (node.name === 'define' && node.value) {
385
+ const [name, value] = node.value
386
+
387
+ let isExternal = false
388
+
389
+ if (value) {
390
+ if (value.type === 'Identifier') {
391
+ isExternal ||= externals.has(value.name) || externalTypes.has(value.name)
392
+ if (!isExternal || mangleExternals) value.name = mangleName(value.name, isExternal)
393
+ } else if (value.type === 'MemberExpression') {
394
+ // TODO: this needs to be more robust to handle string replacement
395
+ } else if (value.type === 'CallExpression' && value.callee.type === 'Identifier') {
396
+ isExternal ||= externals.has(value.callee.name)
397
+ if (!isExternal || mangleExternals) value.callee.name = mangleName(value.callee.name, isExternal)
398
+ // TODO: locally mangle arguments
399
+ }
400
+ }
401
+
402
+ if (name.type === 'Identifier') {
403
+ isExternal ||= externals.has(name.name) || externalTypes.has(name.name)
404
+ if (!isExternal || mangleExternals) name.name = mangleName(name.name, isExternal)
405
+ } else if (name.type === 'MemberExpression') {
406
+ // TODO: this needs to be more robust to handle string replacement
407
+ } else if (name.type === 'CallExpression' && name.callee.type === 'Identifier') {
408
+ isExternal ||= externals.has(name.callee.name)
409
+ if (!isExternal || mangleExternals) name.callee.name = mangleName(name.callee.name, isExternal)
410
+ }
411
+ }
412
+ },
413
+ MemberExpression: {
414
+ enter(node) {
415
+ let type: string | null = ''
416
+
417
+ if (node.object.type === 'CallExpression' && node.object.callee.type === 'Identifier') {
418
+ // TODO: length() should be mangled whereas array.length() should not
419
+ type = getScopedType(node.object.callee.name)
420
+ } else if (node.object.type === 'MemberExpression' && node.object.object.type === 'Identifier') {
421
+ // Only computed member expressions can be parsed this way (e.g., (array[2]).position)
422
+ type = getScopedType(node.object.object.name)
423
+ const renamed = getScopedName(node.object.object.name)
424
+ if (renamed !== null) node.object.object.name = renamed
425
+ } else if (node.object.type === 'Identifier') {
426
+ type = getScopedType(node.object.name)
427
+ const renamed = getScopedName(node.object.name)
428
+ if (renamed !== null) node.object.name = renamed
429
+ }
430
+
431
+ types.push(type)
432
+ },
433
+ exit() {
434
+ types.length -= 1
435
+ },
436
+ },
437
+ Identifier(node) {
438
+ const renamed = getScopedName(node.name)
439
+ if (renamed !== null) node.name = renamed
440
+ },
441
+ })
442
+ }
443
+
444
+ return generate(program, { target: 'GLSL' })
445
+ }