shaderkit 0.6.4 → 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/generator.ts CHANGED
@@ -1,128 +1,128 @@
1
- import { type AST, type Program } from './ast.js'
2
-
3
- function formatLayout(layout: Record<string, string | boolean> | null): string {
4
- if (!layout) return ''
5
-
6
- return `layout(${Object.entries(layout)
7
- .map(([k, v]) => (v === true ? k : `${k}=${v}`))
8
- .join(',')})`
9
- }
10
-
11
- // TODO: restore comments/whitespace with sourcemaps, WGSL support
12
- function format(node: AST | null): string {
13
- if (!node) return ''
14
-
15
- switch (node.type) {
16
- case 'Identifier':
17
- return node.name
18
- case 'Literal':
19
- return node.value
20
- case 'ArraySpecifier':
21
- return `${node.typeSpecifier.name}${node.dimensions.map((d) => `[${format(d)}]`).join('')}`
22
- case 'ExpressionStatement':
23
- return `${format(node.expression)};`
24
- case 'BlockStatement':
25
- return `{${node.body.map(format).join('')}}`
26
- case 'DiscardStatement':
27
- return 'discard;'
28
- case 'PreprocessorStatement': {
29
- let value = ''
30
- if (node.value) {
31
- if (node.name === 'include') value = ` <${format(node.value[0])}>` // three is whitespace sensitive
32
- else if (node.name === 'extension') value = ` ${node.value.map(format).join(':')}`
33
- else if (node.value.length) value = ` ${node.value.map(format).join(' ')}`
34
- }
35
-
36
- return `\n#${node.name}${value}\n`
37
- }
38
- case 'PrecisionQualifierStatement':
39
- return `precision ${node.precision} ${node.typeSpecifier.name};`
40
- case 'InvariantQualifierStatement':
41
- return `invariant ${format(node.typeSpecifier)};`
42
- case 'LayoutQualifierStatement':
43
- return `${formatLayout(node.layout)}${node.qualifier};`
44
- case 'ReturnStatement':
45
- return node.argument ? `return ${format(node.argument)};` : 'return;'
46
- case 'BreakStatement':
47
- return 'break;'
48
- case 'ContinueStatement':
49
- return 'continue;'
50
- case 'IfStatement': {
51
- const alternate = node.alternate ? ` else${format(node.consequent)}` : ''
52
- return `if(${format(node.test)})${format(node.consequent)}${alternate}`
53
- }
54
- case 'SwitchStatement':
55
- return `switch(${format(node.discriminant)}){${node.cases.map(format).join('')}}`
56
- case 'SwitchCase':
57
- return `case ${node.test ? format(node.test) : 'default'}:{${node.consequent.map(format).join(';')}}`
58
- case 'WhileStatement':
59
- return `while (${format(node.test)}) ${format(node.body)}`
60
- case 'DoWhileStatement':
61
- return `do ${format(node.body)}while(${format(node.test)})`
62
- case 'ForStatement':
63
- return `for(${format(node.init)};${format(node.test)};${format(node.update)})${format(node.body)}`
64
- case 'FunctionDeclaration': {
65
- const qualifiers = node.qualifiers.length ? `${node.qualifiers.join(' ')} ` : '' // precision
66
- const body = node.body ? format(node.body) : ';'
67
- return `${qualifiers}${format(node.typeSpecifier)} ${format(node.id)}(${node.params
68
- .map(format)
69
- .join(',')})${body}`
70
- }
71
- case 'FunctionParameter': {
72
- const qualifiers = node.qualifiers.length ? `${node.qualifiers.join(' ')} ` : ''
73
- const id = node.id ? ` ${format(node.id)}` : ''
74
- return `${qualifiers}${format(node.typeSpecifier)}${id}`
75
- }
76
- case 'VariableDeclaration': {
77
- const head = node.declarations[0]
78
- const layout = formatLayout(head.layout)
79
- const qualifiers = head.qualifiers.length ? `${head.qualifiers.join(' ')} ` : ''
80
- return `${layout}${qualifiers}${format(head.typeSpecifier)} ${node.declarations.map(format).join(',')};`
81
- }
82
- case 'VariableDeclarator': {
83
- const init = node.init ? `=${format(node.init)}` : ''
84
- return `${format(node.id)}${init}`
85
- }
86
- case 'StructuredBufferDeclaration': {
87
- const layout = formatLayout(node.layout)
88
- const scope = node.id ? `${format(node.id)}` : ''
89
- return `${layout}${node.qualifiers.join(' ')} ${format(node.typeSpecifier)}{${node.members
90
- .map(format)
91
- .join('')}}${scope};`
92
- }
93
- case 'StructDeclaration':
94
- return `struct ${format(node.id)}{${node.members.map(format).join('')}};`
95
- case 'ArrayExpression':
96
- return `${format(node.typeSpecifier)}(${node.elements.map(format).join(',')})`
97
- case 'UnaryExpression':
98
- case 'UpdateExpression':
99
- return node.prefix ? `${node.operator}${format(node.argument)}` : `${format(node.argument)}${node.operator}`
100
- case 'BinaryExpression':
101
- case 'AssignmentExpression':
102
- case 'LogicalExpression':
103
- return `${format(node.left)}${node.operator}${format(node.right)}`
104
- case 'MemberExpression':
105
- return node.computed
106
- ? `${format(node.object)}[${format(node.property)}]`
107
- : `${format(node.object)}.${format(node.property)}`
108
- case 'ConditionalExpression':
109
- return `${format(node.test)}?${format(node.alternate)}:${format(node.consequent)}`
110
- case 'CallExpression':
111
- return `${format(node.callee)}(${node.arguments.map(format).join(',')})`
112
- case 'Program':
113
- return `${node.body.map(format).join('')}`
114
- default:
115
- return node satisfies never
116
- }
117
- }
118
-
119
- export interface GenerateOptions {
120
- target: 'GLSL' // | 'WGSL'
121
- }
122
-
123
- /**
124
- * Generates a string of GLSL (WGSL WIP) code from an [AST](https://en.wikipedia.org/wiki/Abstract_syntax_tree).
125
- */
126
- export function generate(program: Program, options: GenerateOptions): string {
127
- return format(program).replaceAll('\n\n', '\n').replaceAll('] ', ']').trim()
128
- }
1
+ import { type AST, type Program } from './ast.js'
2
+
3
+ function formatLayout(layout: Record<string, string | boolean> | null): string {
4
+ if (!layout) return ''
5
+
6
+ return `layout(${Object.entries(layout)
7
+ .map(([k, v]) => (v === true ? k : `${k}=${v}`))
8
+ .join(',')})`
9
+ }
10
+
11
+ // TODO: restore comments/whitespace with sourcemaps, WGSL support
12
+ function format(node: AST | null): string {
13
+ if (!node) return ''
14
+
15
+ switch (node.type) {
16
+ case 'Identifier':
17
+ return node.name
18
+ case 'Literal':
19
+ return node.value
20
+ case 'ArraySpecifier':
21
+ return `${node.typeSpecifier.name}${node.dimensions.map((d) => `[${format(d)}]`).join('')}`
22
+ case 'ExpressionStatement':
23
+ return `${format(node.expression)};`
24
+ case 'BlockStatement':
25
+ return `{${node.body.map(format).join('')}}`
26
+ case 'DiscardStatement':
27
+ return 'discard;'
28
+ case 'PreprocessorStatement': {
29
+ let value = ''
30
+ if (node.value) {
31
+ if (node.name === 'include') value = ` <${format(node.value[0])}>` // three is whitespace sensitive
32
+ else if (node.name === 'extension') value = ` ${node.value.map(format).join(':')}`
33
+ else if (node.value.length) value = ` ${node.value.map(format).join(' ')}`
34
+ }
35
+
36
+ return `\n#${node.name}${value}\n`
37
+ }
38
+ case 'PrecisionQualifierStatement':
39
+ return `precision ${node.precision} ${node.typeSpecifier.name};`
40
+ case 'InvariantQualifierStatement':
41
+ return `invariant ${format(node.typeSpecifier)};`
42
+ case 'LayoutQualifierStatement':
43
+ return `${formatLayout(node.layout)}${node.qualifier};`
44
+ case 'ReturnStatement':
45
+ return node.argument ? `return ${format(node.argument)};` : 'return;'
46
+ case 'BreakStatement':
47
+ return 'break;'
48
+ case 'ContinueStatement':
49
+ return 'continue;'
50
+ case 'IfStatement': {
51
+ const alternate = node.alternate ? ` else${format(node.consequent)}` : ''
52
+ return `if(${format(node.test)})${format(node.consequent)}${alternate}`
53
+ }
54
+ case 'SwitchStatement':
55
+ return `switch(${format(node.discriminant)}){${node.cases.map(format).join('')}}`
56
+ case 'SwitchCase':
57
+ return `case ${node.test ? format(node.test) : 'default'}:{${node.consequent.map(format).join(';')}}`
58
+ case 'WhileStatement':
59
+ return `while (${format(node.test)}) ${format(node.body)}`
60
+ case 'DoWhileStatement':
61
+ return `do ${format(node.body)}while(${format(node.test)})`
62
+ case 'ForStatement':
63
+ return `for(${format(node.init)};${format(node.test)};${format(node.update)})${format(node.body)}`
64
+ case 'FunctionDeclaration': {
65
+ const qualifiers = node.qualifiers.length ? `${node.qualifiers.join(' ')} ` : '' // precision
66
+ const body = node.body ? format(node.body) : ';'
67
+ return `${qualifiers}${format(node.typeSpecifier)} ${format(node.id)}(${node.params
68
+ .map(format)
69
+ .join(',')})${body}`
70
+ }
71
+ case 'FunctionParameter': {
72
+ const qualifiers = node.qualifiers.length ? `${node.qualifiers.join(' ')} ` : ''
73
+ const id = node.id ? ` ${format(node.id)}` : ''
74
+ return `${qualifiers}${format(node.typeSpecifier)}${id}`
75
+ }
76
+ case 'VariableDeclaration': {
77
+ const head = node.declarations[0]
78
+ const layout = formatLayout(head.layout)
79
+ const qualifiers = head.qualifiers.length ? `${head.qualifiers.join(' ')} ` : ''
80
+ return `${layout}${qualifiers}${format(head.typeSpecifier)} ${node.declarations.map(format).join(',')};`
81
+ }
82
+ case 'VariableDeclarator': {
83
+ const init = node.init ? `=${format(node.init)}` : ''
84
+ return `${format(node.id)}${init}`
85
+ }
86
+ case 'StructuredBufferDeclaration': {
87
+ const layout = formatLayout(node.layout)
88
+ const scope = node.id ? `${format(node.id)}` : ''
89
+ return `${layout}${node.qualifiers.join(' ')} ${format(node.typeSpecifier)}{${node.members
90
+ .map(format)
91
+ .join('')}}${scope};`
92
+ }
93
+ case 'StructDeclaration':
94
+ return `struct ${format(node.id)}{${node.members.map(format).join('')}};`
95
+ case 'ArrayExpression':
96
+ return `${format(node.typeSpecifier)}(${node.elements.map(format).join(',')})`
97
+ case 'UnaryExpression':
98
+ case 'UpdateExpression':
99
+ return node.prefix ? `${node.operator}${format(node.argument)}` : `${format(node.argument)}${node.operator}`
100
+ case 'BinaryExpression':
101
+ case 'AssignmentExpression':
102
+ case 'LogicalExpression':
103
+ return `${format(node.left)}${node.operator}${format(node.right)}`
104
+ case 'MemberExpression':
105
+ return node.computed
106
+ ? `${format(node.object)}[${format(node.property)}]`
107
+ : `${format(node.object)}.${format(node.property)}`
108
+ case 'ConditionalExpression':
109
+ return `${format(node.test)}?${format(node.alternate)}:${format(node.consequent)}`
110
+ case 'CallExpression':
111
+ return `${format(node.callee)}(${node.arguments.map(format).join(',')})`
112
+ case 'Program':
113
+ return `${node.body.map(format).join('')}`
114
+ default:
115
+ return node satisfies never
116
+ }
117
+ }
118
+
119
+ export interface GenerateOptions {
120
+ target: 'GLSL' // | 'WGSL'
121
+ }
122
+
123
+ /**
124
+ * Generates a string of GLSL (WGSL WIP) code from an [AST](https://en.wikipedia.org/wiki/Abstract_syntax_tree).
125
+ */
126
+ export function generate(program: Program, options: GenerateOptions): string {
127
+ return format(program).replaceAll('\n\n', '\n').replaceAll('] ', ']').trim()
128
+ }
package/src/hoister.ts ADDED
@@ -0,0 +1,171 @@
1
+ import type { Token } from './tokenizer.js'
2
+
3
+ export interface HoistNode {
4
+ prefix: Token[]
5
+ cases: { cond: Token[]; node: HoistNode }[]
6
+ }
7
+
8
+ /**
9
+ * A set of tokens optionally starting with a directive
10
+ */
11
+ type PreprocessorSegment = {
12
+ directive?: Token[] | undefined
13
+ suffix: Token[]
14
+ scope: number
15
+ }
16
+
17
+ const preScopeDelta = {
18
+ if: 1,
19
+ ifdef: 1,
20
+ ifndef: 1,
21
+ } as Record<string, number>
22
+
23
+ const postScopeDelta = {
24
+ endif: -1,
25
+ } as Record<string, number>
26
+
27
+ export function segmentDirectives(tokens: Token[]): PreprocessorSegment[] {
28
+ const segments: PreprocessorSegment[] = []
29
+
30
+ // Gathering the first non-directive segment, if it exists
31
+ let cursor = 0
32
+ let scope = 0
33
+ const prefix: Token[] = []
34
+ while (cursor < tokens.length && tokens[cursor].value !== '#') {
35
+ prefix.push(tokens[cursor++])
36
+ }
37
+
38
+ if (prefix.length > 0) {
39
+ segments.push({ suffix: trimWhitespace(prefix), scope })
40
+ }
41
+
42
+ while (cursor < tokens.length) {
43
+ const directive: Token[] = []
44
+ while (cursor < tokens.length && tokens[cursor].value !== '\\') {
45
+ directive.push(tokens[cursor++])
46
+ }
47
+ directive.push(tokens[cursor++]) // push the '\\'
48
+
49
+ const suffix: Token[] = []
50
+ while (cursor < tokens.length && tokens[cursor].value !== '#') {
51
+ suffix.push(tokens[cursor++])
52
+ }
53
+
54
+ const name = directive[1]?.value ?? ''
55
+ scope += preScopeDelta[name] || 0
56
+ segments.push({ directive, suffix: trimWhitespace(suffix), scope })
57
+ scope += postScopeDelta[name] || 0
58
+ }
59
+
60
+ return segments
61
+ }
62
+
63
+ export function getDirectiveName(segment: PreprocessorSegment): string {
64
+ return segment.directive?.[1]?.value ?? ''
65
+ }
66
+
67
+ export function trimWhitespace(tokens: Token[]): Token[] {
68
+ let start = 0
69
+ let end = tokens.length - 1
70
+
71
+ if (end === 0 && tokens[0].type === 'whitespace') {
72
+ return []
73
+ }
74
+
75
+ while (start < end && tokens[start].type === 'whitespace') {
76
+ start++
77
+ }
78
+
79
+ while (end > start && tokens[end].type === 'whitespace') {
80
+ end--
81
+ }
82
+
83
+ return tokens.slice(start, end + 1)
84
+ }
85
+
86
+ function constructHoistTree(
87
+ segments: PreprocessorSegment[],
88
+ scope: number = 0,
89
+ remainder: HoistNode | undefined = undefined,
90
+ ): HoistNode {
91
+ let leafNode: HoistNode = {
92
+ cases: remainder?.cases ?? [],
93
+ prefix: remainder?.prefix ?? [],
94
+ }
95
+
96
+ let currentNode: HoistNode | undefined
97
+ // The segment index that marks the end of the currently
98
+ // explored case (exclusive)
99
+ let caseEnd = segments.length - 1
100
+
101
+ for (let seg = segments.length - 1; seg >= 0; seg--) {
102
+ const segment = segments[seg]
103
+ if (segment.scope === scope) {
104
+ // A segment that belongs to the outer scope, meaning we're at the top of the nested node
105
+ leafNode.prefix = [...segment.suffix, ...leafNode.prefix]
106
+ continue
107
+ }
108
+
109
+ if (segment.scope > scope + 1) {
110
+ continue // A nested segment, skip it
111
+ }
112
+
113
+ const name = getDirectiveName(segment)
114
+ if (name === '') {
115
+ // The first segment, no directive
116
+ if (leafNode) {
117
+ leafNode.prefix = [...segment.suffix, ...leafNode.prefix]
118
+ }
119
+ } else if (name === 'endif') {
120
+ // Prepending the suffix of the endif segment before the leaf node
121
+ leafNode.prefix = [...segment.suffix, ...leafNode.prefix]
122
+ caseEnd = seg
123
+ currentNode = {
124
+ cases: [],
125
+ prefix: [],
126
+ }
127
+ } else if (name === 'else' || name === 'elif' || name === 'if' || name === 'ifdef' || name === 'ifndef') {
128
+ const caseNode = constructHoistTree(segments.slice(seg, caseEnd), scope + 1, leafNode)
129
+ caseEnd = seg // the next ends where the previous started
130
+ currentNode?.cases.unshift({ cond: segment.directive ?? [], node: caseNode })
131
+ }
132
+
133
+ if (name === 'if' || name === 'ifdef' || name === 'ifndef') {
134
+ // We're finishing up a whole node
135
+ leafNode = currentNode!
136
+ }
137
+ }
138
+
139
+ // No nested conditions in this node
140
+ return leafNode
141
+ }
142
+
143
+ function flattenHoistNode(node: HoistNode, prefix: Token[]): Token[] {
144
+ if (node.cases.length === 0) {
145
+ return [...prefix, ...node.prefix]
146
+ }
147
+
148
+ return [
149
+ ...node.cases.flatMap((case_) => {
150
+ return [
151
+ ...case_.cond,
152
+ { type: 'whitespace' as const, value: '\n' },
153
+ ...flattenHoistNode(case_.node, [...prefix, ...node.prefix]),
154
+ { type: 'whitespace' as const, value: '\n' },
155
+ ]
156
+ }),
157
+ { type: 'symbol', value: '#' },
158
+ { type: 'keyword', value: 'endif' },
159
+ { type: 'symbol' as const, value: '\\' },
160
+ { type: 'whitespace' as const, value: '\n' },
161
+ ]
162
+ }
163
+
164
+ /**
165
+ * Hoists preprocessor directives on the token level
166
+ */
167
+ export function hoistPreprocessorDirectives(tokens: Token[]): Token[] {
168
+ const tree = constructHoistTree(segmentDirectives(tokens))
169
+
170
+ return flattenHoistNode(tree, [])
171
+ }
package/src/index.ts CHANGED
@@ -1,7 +1,7 @@
1
- export * from './ast.js'
2
- export * from './constants.js'
3
- export * from './generator.js'
4
- export * from './minifier.js'
5
- export * from './parser.js'
6
- export * from './tokenizer.js'
7
- export * from './visitor.js'
1
+ export * from './ast.js'
2
+ export * from './constants.js'
3
+ export * from './generator.js'
4
+ export * from './minifier.js'
5
+ export * from './parser.js'
6
+ export * from './tokenizer.js'
7
+ export * from './visitor.js'