Skip to content

Commit c109167

Browse files
authored
expressions in overrides (#311)
* expressions in overrides * fix workgroup parse * fix lint * fix (1.0) completely * checks before bigint converts
1 parent 47908f6 commit c109167

File tree

3 files changed

+126
-21
lines changed

3 files changed

+126
-21
lines changed

lib/engine/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -251,12 +251,12 @@ fn passSampleLevelBilinearRepeat(pass_index: int, uv: float2, lod: float) -> flo
251251
* Preprocess shader source code
252252
*/
253253
async preprocess(shader: string): Promise<SourceMap> {
254-
const defines = new Map<string, string>([
254+
const overrides = new Map<string, string>([
255255
['SCREEN_WIDTH', this.screenWidth.toString()],
256256
['SCREEN_HEIGHT', this.screenHeight.toString()]
257257
]);
258258

259-
return new Preprocessor(defines).preprocess(shader);
259+
return new Preprocessor(overrides).preprocess(shader);
260260
}
261261

262262
/**

lib/engine/preprocessor.ts

Lines changed: 44 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
/**
22
* WGSL shader preprocessor implementation
33
*/
4-
import { fetchInclude, parseUint32, WGSLError } from './utils';
4+
import { evalMathExpression, fetchInclude, parseUint32, WGSLError } from './utils';
55

66
// Regular expressions for preprocessing
77
const RE_COMMENT = /(\/\/.*|\/\*[\s\S]*?\*\/)/g;
8-
const RE_WORD = /[a-zA-Z_][a-zA-Z0-9_]*/g;
8+
const RE_WORD = /\b\w+\b/g;
99

1010
const STRING_MAX_LEN = 20;
1111

@@ -35,15 +35,17 @@ export class SourceMap {
3535
* Handles WGSL preprocessing including includes, defines, etc.
3636
*/
3737
export class Preprocessor {
38+
private overrides: Map<string, string>;
3839
private defines: Map<string, string>;
3940
private source: SourceMap;
4041
private storageCount: number;
4142
// private assertCount: number;
4243
private specialStrings: boolean;
4344

44-
constructor(defines: Map<string, string>) {
45-
this.defines = new Map(defines);
46-
this.defines.set('STRING_MAX_LEN', STRING_MAX_LEN.toString());
45+
constructor(overrides: Map<string, string>) {
46+
this.overrides = new Map(overrides);
47+
this.overrides.set('STRING_MAX_LEN', STRING_MAX_LEN.toString());
48+
this.defines = new Map();
4749
this.source = new SourceMap();
4850
this.storageCount = 0;
4951
// this.assertCount = 0;
@@ -57,20 +59,14 @@ export class Preprocessor {
5759
return source.replace(RE_COMMENT, '');
5860
}
5961

60-
/**
61-
* Substitute defines in source text
62-
*/
63-
private substDefines(source: string): string {
64-
return source.replace(RE_WORD, match => {
65-
return this.defines.get(match) ?? match;
66-
});
67-
}
68-
6962
/**
7063
* Process a single line of shader source
7164
*/
7265
private async processLine(lineOrig: string, lineNum: number): Promise<void> {
73-
let line = this.substDefines(lineOrig);
66+
// Substitute overrides and defines
67+
let line = lineOrig
68+
.replace(RE_WORD, match => this.overrides.get(match) ?? match)
69+
.replace(RE_WORD, match => this.defines.get(match) ?? match);
7470

7571
// Handle enable directives
7672
if (line.trimStart().startsWith('enable')) {
@@ -79,6 +75,14 @@ export class Preprocessor {
7975
return;
8076
}
8177

78+
// Handle override in one line and evaluate to number
79+
if (line.trimStart().startsWith('override')) {
80+
line = line.replace(RE_COMMENT, '');
81+
const tokens = line.trim().replace('=', ' = ').replace(/\s+/g, ' ').split(' ');
82+
this.handleOverride(tokens, lineNum);
83+
return;
84+
}
85+
8286
// Handle preprocessor directives
8387
if (line.trimStart().startsWith('#')) {
8488
line = line.replace(RE_COMMENT, '');
@@ -103,7 +107,7 @@ export class Preprocessor {
103107
break;
104108

105109
case '#define':
106-
this.handleDefine(lineOrig, tokens, lineNum);
110+
this.handleDefine(tokens, lineNum);
107111
break;
108112

109113
case '#storage':
@@ -232,20 +236,41 @@ export class Preprocessor {
232236
/**
233237
* Handle #define directive
234238
*/
235-
private handleDefine(lineOrig: string, tokens: string[], lineNum: number): void {
236-
const name = lineOrig.trim().split(' ')[1];
239+
private handleDefine(tokens: string[], lineNum: number): void {
240+
const name = tokens[1];
237241
if (!name) {
238242
throw new WGSLError('Invalid #define syntax', lineNum);
239243
}
240244

241245
const value = tokens.slice(2).join(' ');
242-
if (this.defines.has(name)) {
246+
if (value.includes(name)) {
243247
throw new WGSLError(`Cannot redefine ${name}`, lineNum);
244248
}
245249

246250
this.defines.set(name, value);
247251
}
248252

253+
// /**
254+
// * Handle override in one line and evaluate to number
255+
// */
256+
private handleOverride(tokens: string[], lineNum: number): void {
257+
const name = tokens[1];
258+
let expression = tokens.slice(3).join('');
259+
if (!name || !expression.endsWith(';')) {
260+
throw new WGSLError(
261+
`Please write override in single line ${lineNum} and terminated!`,
262+
lineNum
263+
);
264+
}
265+
if (expression.includes(name)) {
266+
throw new WGSLError(`Cannot redefine ${name}`, lineNum);
267+
}
268+
269+
expression = expression.slice(0, -1);
270+
expression = evalMathExpression(expression, lineNum);
271+
this.defines.set(name, expression);
272+
}
273+
249274
/**
250275
* Handle #storage directive
251276
*/

lib/engine/utils.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,83 @@ export async function fetchInclude(name: string): Promise<string | null> {
8282
export function countNewlines(text: string): number {
8383
return (text.match(/\n/g) || []).length;
8484
}
85+
86+
/**
87+
* Evaluate a mathematical expression with bigint/float support
88+
*/
89+
export function evalMathExpression(expression: string, lineNumber: number): string {
90+
const VALID_EXPRESSIONS = /^[0-9+\-*/%^.,()]+$/;
91+
const INTEGER_FUNCTIONS = /(select|abs|min|max|sign)/g;
92+
const FLOAT_FUNCTIONS =
93+
/(asin|acos|atan|atan2|asinh|acosh|atanh|sin|cos|tan|sinh|cosh|tanh|round|floor|ceil|pow|sqrt|log|log2|exp|exp2|fract|clamp|mix|smoothstep|radians|degrees)/g;
94+
const ONE_FLOAT = /[+-]?(?=\d*[.eE])(?=\.?\d)\d*\.?\d*(?:[eE][+-]?\d+)?/;
95+
const ONE_INTEGER = /^[-+]?\d+$/;
96+
const ALL_INTEGERS = /[-+]?\d+/g;
97+
98+
let result = expression.replace(/\s+/g, '');
99+
if (result === '') {
100+
return '';
101+
}
102+
103+
// safety check for eval vulnerabilities
104+
let stripped = result.replace(INTEGER_FUNCTIONS, '').replace(FLOAT_FUNCTIONS, '');
105+
if (!VALID_EXPRESSIONS.test(stripped)) {
106+
stripped = stripped.replace(/[0-9+\-*/%^.,()]+/g, '');
107+
throw new WGSLError(`Unsafe symbols '${stripped}' in expression ${expression}`, lineNumber);
108+
}
109+
110+
// convert to bigint if there are no any floating point numbers or float functions
111+
let isAbstractInteger = false;
112+
if (!ONE_FLOAT.test(expression) && !FLOAT_FUNCTIONS.test(expression)) {
113+
result = result.replace(ALL_INTEGERS, match => `${match}n`);
114+
isAbstractInteger = true;
115+
}
116+
117+
const mathReplacements = {
118+
'select(': '((f,t,cond) => cond ? t : f)(',
119+
'abs(': '((x) => Math.abs(Number(x)))(',
120+
'min(': '((x,y) => Math.min(Number(x),Number(y)))(',
121+
'max(': '((x,y) => Math.max(Number(x),Number(y)))(',
122+
'sign(': '((x) => Math.sign(Number(x)))(',
123+
'sin(': 'Math.sin(',
124+
'cos(': 'Math.cos(',
125+
'tan(': 'Math.tan(',
126+
'sinh(': 'Math.sinh(',
127+
'cosh(': 'Math.cosh(',
128+
'tanh(': 'Math.tanh(',
129+
'asin(': 'Math.asin(',
130+
'acos(': 'Math.acos(',
131+
'atan(': 'Math.atan(',
132+
'atan2(': 'Math.atan2(',
133+
'asinh(': 'Math.asinh(',
134+
'acosh(': 'Math.acosh(',
135+
'atanh(': 'Math.atanh(',
136+
'round(': 'Math.round(',
137+
'floor(': 'Math.floor(',
138+
'ceil(': 'Math.ceil(',
139+
'pow(': 'Math.pow(',
140+
'sqrt(': 'Math.sqrt(',
141+
'log(': 'Math.log(',
142+
'log2(': 'Math.log2(',
143+
'exp(': 'Math.exp(',
144+
'exp2(': 'Math.pow(2,',
145+
'fract(': '((x) => x - Math.floor(x))(',
146+
'clamp(': '((x,min,max) => Math.min(Math.max(x,min),max))(',
147+
'mix(': '((x,y,a) => x*(1-a) + y*a)(',
148+
'smoothstep(':
149+
'((e0,e1,x) => { let t = Math.min(Math.max((x-e0)/(e1-e0),0),1); return t*t*(3-2*t); })(',
150+
'step(': '((edge,x) => x < edge ? 0 : 1)(',
151+
'radians(': '((x) => x * Math.PI / 180)(',
152+
'degrees(': '((x) => x * 180 / Math.PI)('
153+
};
154+
155+
result = result.replaceAll('^', '**');
156+
result = result.replace(/\b\w+\(/g, match => mathReplacements[match] || '');
157+
158+
try {
159+
result = Function(`'use strict'; return (${result}).toString();`)();
160+
return !isAbstractInteger && ONE_INTEGER.test(result) ? `${result}.0` : result;
161+
} catch (error) {
162+
throw new WGSLError(`Invalid eval '${expression}' -> '${result}' (${error})`, lineNumber);
163+
}
164+
}

0 commit comments

Comments
 (0)