wgsl-plus
Version:
A WGSL preprocessor, prettifier, minifier, obfuscator, and compiler with C-style macros, conditional compilation, file linking, and multi-format output for WebGPU shaders.
3,131 lines • 92.1 kB
text/typescript
import { processConditionals } from '../src/tools/preprocessing/conditional-processor';
import { evaluateExpression } from '../src/tools/preprocessing/evaluator';
import { expandMacros } from '../src/tools/preprocessing/macro-expander';
import preprocessWgsl from '../src/tools/preprocessing/preprocess-wgsl';
import { Macro } from '../src/tools/preprocessing/types';
describe('Macro Expander', () => {
let defines: Map<string, Macro>;
beforeEach(() => {
defines = new Map<string, Macro>();
});
// Simple macro expansion tests
describe('Simple macro expansion', () => {
test('basic macro expansion', () => {
const defines = new Map<string, Macro>();
defines.set('WIDTH', { value: '800' });
const line = 'const width: u32 = WIDTH;';
const expected = 'const width: u32 = 800;';
expect(expandMacros(line, defines)).toBe(expected);
});
test('multiple macro expansions in one line', () => {
const defines = new Map<string, Macro>();
defines.set('WIDTH', { value: '800' });
defines.set('HEIGHT', { value: '600' });
const line = 'const resolution: vec2<u32> = vec2<u32>(WIDTH, HEIGHT);';
const expected = 'const resolution: vec2<u32> = vec2<u32>(800, 600);';
expect(expandMacros(line, defines)).toBe(expected);
});
test('macro expansion with operators', () => {
const defines = new Map<string, Macro>();
defines.set('SCALE', { value: '2.0' });
const line = 'const scaled: f32 = value * SCALE;';
const expected = 'const scaled: f32 = value * 2.0;';
expect(expandMacros(line, defines)).toBe(expected);
});
test('macros should not expand within strings', () => {
const defines = new Map<string, Macro>();
defines.set('DEBUG', { value: 'true' });
const line = 'const message: string = "DEBUG mode is active";';
const expected = 'const message: string = "DEBUG mode is active";';
expect(expandMacros(line, defines)).toBe(expected);
});
test('macros should only expand when they are complete identifiers', () => {
const defines = new Map<string, Macro>();
defines.set('VAR', { value: '123' });
const line = 'const myVARiable: u32 = 5;';
const expected = 'const myVARiable: u32 = 5;';
expect(expandMacros(line, defines)).toBe(expected);
});
test('empty macros are expanded to empty strings', () => {
const defines = new Map<string, Macro>();
defines.set('EMPTY', { value: '' });
const line = 'const value = EMPTY;';
const expected = 'const value = ;';
expect(expandMacros(line, defines)).toBe(expected);
});
test('macros with spaces in their values', () => {
const defines = new Map<string, Macro>();
defines.set('COMMENT', { value: '/* This is a comment */' });
const line = 'const value = 5; COMMENT';
const expected = 'const value = 5; /* This is a comment */';
expect(expandMacros(line, defines)).toBe(expected);
});
});
// Function-like macro expansion tests
describe('Function-like macro expansion', () => {
test('basic function-like macro', () => {
const defines = new Map<string, Macro>();
defines.set('MIN', { value: '((a) < (b) ? (a) : (b))', params: ['a', 'b'] });
const line = 'const minValue = MIN(x, y);';
const expected = 'const minValue = ((x) < (y) ? (x) : (y));';
expect(expandMacros(line, defines)).toBe(expected);
});
test('function-like macro with multiple parameters', () => {
const defines = new Map<string, Macro>();
defines.set('CLAMP', {
value: '((x) < (min) ? (min) : ((x) > (max) ? (max) : (x)))',
params: ['x', 'min', 'max']
});
const line = 'const clampedValue = CLAMP(value, 0.0, 1.0);';
const expected = 'const clampedValue = ((value) < (0.0) ? (0.0) : ((value) > (1.0) ? (1.0) : (value)));';
expect(expandMacros(line, defines)).toBe(expected);
});
test('function-like macro with complex arguments', () => {
const defines = new Map<string, Macro>();
defines.set('DOT', { value: '((a).x * (b).x + (a).y * (b).y)', params: ['a', 'b'] });
const line = 'const dotProduct = DOT(vec1, vec2 + offset);';
const expected = 'const dotProduct = ((vec1).x * (vec2 + offset).x + (vec1).y * (vec2 + offset).y);';
expect(expandMacros(line, defines)).toBe(expected);
});
test('function-like macro with nested parentheses in arguments', () => {
const defines = new Map<string, Macro>();
defines.set('APPLY', { value: 'fn((x))', params: ['x'] });
const line = 'const result = APPLY(calculate(a, (b + c)));';
const expected = 'const result = fn((calculate(a, (b + c))));';
expect(expandMacros(line, defines)).toBe(expected);
});
test('function-like macro with wrong number of arguments throws error', () => {
const defines = new Map<string, Macro>();
defines.set('FUNC', { value: 'a + b', params: ['a', 'b'] });
const line = 'const result = FUNC(1);';
expect(() => expandMacros(line, defines)).toThrow(/expects 2 arguments, got 1/);
});
test('multiple function-like macros in one line', () => {
const defines = new Map<string, Macro>();
defines.set('SQR', { value: '((x) * (x))', params: ['x'] });
defines.set('ABS', { value: '((x) < 0 ? -(x) : (x))', params: ['x'] });
const line = 'const value = SQR(ABS(x));';
const expected = 'const value = ((((x) < 0 ? -(x) : (x))) * (((x) < 0 ? -(x) : (x))));';
expect(expandMacros(line, defines)).toBe(expected);
});
});
// Nested and recursive macro expansion tests
describe('Nested and recursive macro expansion', () => {
test('nested macro expansion', () => {
const defines = new Map<string, Macro>();
defines.set('WIDTH', { value: '800' });
defines.set('HEIGHT', { value: '600' });
defines.set('RESOLUTION', { value: 'vec2<f32>(WIDTH, HEIGHT)' });
const line = 'const res = RESOLUTION;';
const expected = 'const res = vec2<f32>(800, 600);';
expect(expandMacros(line, defines)).toBe(expected);
});
test('macro expansion in function-like macro arguments', () => {
const defines = new Map<string, Macro>();
defines.set('WIDTH', { value: '800' });
defines.set('HALF', { value: '((x) / 2)', params: ['x'] });
const line = 'const halfWidth = HALF(WIDTH);';
const expected = 'const halfWidth = ((800) / 2);';
expect(expandMacros(line, defines)).toBe(expected);
});
test('recursive macro expansion should terminate', () => {
const defines = new Map<string, Macro>();
defines.set('A', { value: 'B' });
defines.set('B', { value: 'A' });
const line = 'const value = A;';
// This should complete without infinite recursion
expect(() => expandMacros(line, defines)).not.toThrow();
});
test('complex nested macro expansion', () => {
const defines = new Map<string, Macro>();
defines.set('PI', { value: '3.14159' });
defines.set('SQUARE', { value: '((x) * (x))', params: ['x'] });
defines.set('CIRCLE_AREA', { value: 'PI * SQUARE(r)', params: ['r'] });
const line = 'const area = CIRCLE_AREA(radius);';
const expected = 'const area = 3.14159 * ((radius) * (radius));';
expect(expandMacros(line, defines)).toBe(expected);
});
});
// Edge cases and special handling
describe('Edge cases and special handling', () => {
test('single-letter macro names should not expand within words', () => {
const defines = new Map<string, Macro>();
defines.set('A', { value: '1' });
defines.set('B', { value: '2' });
const line = 'const Apples and Bananas = A + B;';
const expected = 'const Apples and Bananas = 1 + 2;';
expect(expandMacros(line, defines)).toBe(expected);
});
test('single-letter macros should only expand when standalone', () => {
const defines = new Map<string, Macro>();
defines.set('A', { value: '1' });
const line = 'A is defined and A + A = 2A';
const expected = '1 is defined and 1 + 1 = 21';
expect(expandMacros(line, defines)).toBe(expected);
});
test('macro expansion with whitespace in names', () => {
const defines = new Map<string, Macro>();
defines.set('MACRO_NAME', { value: 'expanded' });
const line = 'const value = MACRO_NAME ;';
const expected = 'const value = expanded ;';
expect(expandMacros(line, defines)).toBe(expected);
});
test('no macros defined', () => {
const defines = new Map<string, Macro>();
const line = 'const value = 5;';
const expected = 'const value = 5;';
expect(expandMacros(line, defines)).toBe(expected);
});
test('case sensitivity in macro names', () => {
const defines = new Map<string, Macro>();
defines.set('VALUE', { value: '123' });
const line = 'const x = VALUE; const y = value;';
const expected = 'const x = 123; const y = value;';
expect(expandMacros(line, defines)).toBe(expected);
});
// New tests for the special handling of "X is defined" pattern
test('preserves macro name in "X is defined" pattern', () => {
const defines = new Map<string, Macro>();
defines.set('A', { value: '100' });
defines.set('B', { value: '200' });
const line = 'A is defined';
const expected = 'A is defined';
expect(expandMacros(line, defines)).toBe(expected);
});
test('correctly expands macros in "X is defined" lines with values', () => {
const defines = new Map<string, Macro>();
defines.set('A', { value: '100' });
defines.set('B', { value: '200' });
defines.set('C', { value: '300' });
const input = `A is defined
B is defined
C is defined`;
const expected = `100 is defined
200 is defined
300 is defined`;
expect(expandMacros(input, defines)).toBe(expected);
});
test('only preserves exact "X is defined" pattern', () => {
const defines = new Map<string, Macro>();
defines.set('A', { value: '100' });
defines.set('DEBUG', { value: 'true' });
const input = `A is defined
A is something else
DEBUG is activated
const x = A;`;
const expected = `100 is defined
100 is something else
true is activated
const x = 100;`;
expect(expandMacros(input, defines)).toBe(expected);
});
test('handles whitespace in "X is defined" pattern', () => {
const defines = new Map<string, Macro>();
defines.set('A', { value: '100' });
const inputs = [
'A is defined',
' A is defined',
'A is defined',
'A is defined'
];
inputs.forEach(input => {
expect(expandMacros(input, defines)).toBe(input);
});
});
test('regular macro expansion still works alongside "X is defined" pattern', () => {
const defines = new Map<string, Macro>();
defines.set('A', { value: '100' });
defines.set('B', { value: '200' });
const input = `A is defined
const sum = A + B;`;
const expected = `100 is defined
const sum = 100 + 200;`;
expect(expandMacros(input, defines)).toBe(expected);
});
test('consistently expands macros in comments and code', () => {
const defines = new Map<string, Macro>();
defines.set('FEATURE', { value: 'enabled' });
defines.set('VERSION', { value: '2.0' });
const input = `/*
* FEATURE is defined in version VERSION
* This means:
* FEATURE is defined
*/
if (true) {
// VERSION is defined as 2.0
FEATURE is defined here
const version = VERSION;
}`;
const expected = `/*
* enabled is defined in version 2.0
* This means:
* enabled is defined
*/
if (true) {
// 2.0 is defined as 2.0
enabled is defined here
const version = 2.0;
}`;
expect(expandMacros(input, defines)).toBe(expected);
});
});
// Real-world examples
describe('Real-world examples', () => {
test('shader constants example', () => {
const defines = new Map<string, Macro>();
defines.set('MAX_LIGHTS', { value: '4' });
defines.set('SHADOW_QUALITY', { value: '2' });
defines.set('SHADOW_MAP_SIZE', { value: '1024 << SHADOW_QUALITY' });
const line = 'const shadowMapSize: u32 = SHADOW_MAP_SIZE; const maxLights: u32 = MAX_LIGHTS;';
const expected = 'const shadowMapSize: u32 = 1024 << 2; const maxLights: u32 = 4;';
expect(expandMacros(line, defines)).toBe(expected);
});
test('math utilities example', () => {
const defines = new Map<string, Macro>();
defines.set('SQR', { value: '((x) * (x))', params: ['x'] });
defines.set('CUBE', { value: '((x) * (x) * (x))', params: ['x'] });
defines.set('POW2', { value: 'SQR(x)', params: ['x'] });
defines.set('DISTANCE', { value: 'sqrt(SQR(a.x - b.x) + SQR(a.y - b.y))', params: ['a', 'b'] });
const line = 'const d = DISTANCE(p1, p2); const p = POW2(x) + CUBE(y);';
const expected = 'const d = sqrt(((p1.x - p2.x) * (p1.x - p2.x)) + ((p1.y - p2.y) * (p1.y - p2.y))); const p = ((x) * (x)) + ((y) * (y) * (y));';
expect(expandMacros(line, defines)).toBe(expected);
});
test('WGSL shader example', () => {
const defines = new Map<string, Macro>();
defines.set('WORKGROUP_SIZE', { value: '64' });
defines.set('PI', { value: '3.14159' });
defines.set('TO_RADIANS', { value: '((deg) * PI / 180.0)', params: ['deg'] });
const line = `
@compute @workgroup_size(WORKGROUP_SIZE)
fn compute_main(@builtin(global_invocation_id) id: vec3<u32>) {
let angle = TO_RADIANS(45.0);
}`;
const expected = `
@compute @workgroup_size(64)
fn compute_main(@builtin(global_invocation_id) id: vec3<u32>) {
let angle = ((45.0) * 3.14159 / 180.0);
}`;
expect(expandMacros(line, defines)).toBe(expected);
});
test('multi-level expansion with builtins', () => {
const defines = new Map<string, Macro>();
defines.set('VEC3', { value: 'vec3<f32>', });
defines.set('NORMALIZE', { value: 'normalize', });
defines.set('NORMAL_VEC', { value: 'NORMALIZE(VEC3(x, y, z))', params: ['x', 'y', 'z'] });
const line = 'let normal = NORMAL_VEC(0.0, 1.0, 0.0);';
const expected = 'let normal = normalize(vec3<f32>(0.0, 1.0, 0.0));';
expect(expandMacros(line, defines)).toBe(expected);
});
// New test for nested conditional use case
test('preprocessor directive comments example', () => {
const defines = new Map<string, Macro>();
defines.set('A', { value: '' });
defines.set('B', { value: '' });
defines.set('C', { value: '' });
const input = `
#ifdef A
A is defined
#ifdef B
B is defined
#ifdef C
C is defined
#endif
#endif
#endif
`;
const expected = `
#ifdef
is defined
#ifdef
is defined
#ifdef
is defined
#endif
#endif
#endif
`;
expect(expandMacros(input, defines)).toBe(expected);
});
});
describe('Basic macro expansion', () => {
test('simple macro expansion', () => {
defines.set('WIDTH', { value: '800' });
defines.set('HEIGHT', { value: '600' });
const input = 'var<private> resolution = vec2<f32>(WIDTH, HEIGHT);';
const expected = 'var<private> resolution = vec2<f32>(800, 600);';
expect(expandMacros(input, defines)).toBe(expected);
});
test('macro with no replacement value', () => {
defines.set('DEBUG', { value: '' });
const input = 'var<private> isDebug = DEBUG == 1;';
const expected = 'var<private> isDebug = == 1;';
expect(expandMacros(input, defines)).toBe(expected);
});
test('macros with single letter names', () => {
defines.set('X', { value: '10.0' });
defines.set('Y', { value: '20.0' });
const input = 'var<private> position = vec2<f32>(X, Y);';
const expected = 'var<private> position = vec2<f32>(10.0, 20.0);';
expect(expandMacros(input, defines)).toBe(expected);
});
});
describe('Nested macro expansion', () => {
test('nested macro expansion', () => {
defines.set('WIDTH', { value: '800' });
defines.set('HEIGHT', { value: '600' });
defines.set('RESOLUTION', { value: 'vec2<f32>(WIDTH, HEIGHT)' });
const input = 'var<private> resolution = RESOLUTION;';
const expected = 'var<private> resolution = vec2<f32>(800, 600);';
expect(expandMacros(input, defines)).toBe(expected);
});
test('complex nested macros', () => {
defines.set('WIDTH', { value: '800' });
defines.set('HEIGHT', { value: '600' });
defines.set('RESOLUTION', { value: 'vec2<f32>(WIDTH, HEIGHT)' });
defines.set('ASPECT_RATIO', { value: '(float(WIDTH) / float(HEIGHT))' });
defines.set('FOV', { value: '45.0' });
defines.set('CAMERA_SETTINGS', { value: 'vec4<f32>(ASPECT_RATIO, FOV, 0.1, 100.0)' });
const input = 'var<private> cameraSettings = CAMERA_SETTINGS;';
const expected = 'var<private> cameraSettings = vec4<f32>((float(800) / float(600)), 45.0, 0.1, 100.0);';
expect(expandMacros(input, defines)).toBe(expected);
});
test('deeply nested macros', () => {
defines.set('A', { value: 'B' });
defines.set('B', { value: 'C' });
defines.set('C', { value: 'D' });
defines.set('D', { value: 'E' });
defines.set('E', { value: 'final value' });
const input = 'var<private> result = A;';
const expected = 'var<private> result = final value;';
expect(expandMacros(input, defines)).toBe(expected);
});
test('macros in arithmetic expressions', () => {
defines.set('WIDTH', { value: '800' });
defines.set('HALF_WIDTH', { value: 'WIDTH / 2' });
defines.set('QUARTER_WIDTH', { value: 'HALF_WIDTH / 2' });
const input = 'var<private> size = QUARTER_WIDTH;';
const expected = 'var<private> size = 800 / 2 / 2;';
expect(expandMacros(input, defines)).toBe(expected);
});
});
describe('Function-like macros', () => {
test('simple function-like macro', () => {
defines.set('MIN', { value: '((a) < (b) ? (a) : (b))', params: ['a', 'b'] });
const input = 'var<private> minValue = MIN(5, 10);';
const expected = 'var<private> minValue = ((5) < (10) ? (5) : (10));';
expect(expandMacros(input, defines)).toBe(expected);
});
test('function-like macro with complex arguments', () => {
defines.set('VEC2', { value: 'vec2<f32>(x, y)', params: ['x', 'y'] });
const input = 'var<private> pos = VEC2(10.0 + 5.0, HEIGHT / 2);';
const expected = 'var<private> pos = vec2<f32>(10.0 + 5.0, HEIGHT / 2);';
expect(expandMacros(input, defines)).toBe(expected);
});
test('nested function-like macros', () => {
defines.set('WIDTH', { value: '800' });
defines.set('HEIGHT', { value: '600' });
defines.set('VEC2', { value: 'vec2<f32>(x, y)', params: ['x', 'y'] });
defines.set('SCREEN_POS', { value: 'VEC2(WIDTH * pos, HEIGHT * pos)', params: ['pos'] });
const input = 'var<private> screenPos = SCREEN_POS(0.5);';
const expected = 'var<private> screenPos = vec2<f32>(800 * 0.5, 600 * 0.5);';
expect(expandMacros(input, defines)).toBe(expected);
});
test('function-like macro with another macro in body', () => {
defines.set('PI', { value: '3.14159' });
defines.set('CIRCLE_AREA', { value: 'PI * radius * radius', params: ['radius'] });
const input = 'var<private> area = CIRCLE_AREA(5.0);';
const expected = 'var<private> area = 3.14159 * 5.0 * 5.0;';
expect(expandMacros(input, defines)).toBe(expected);
});
});
describe('String literal protection', () => {
test('macros are not expanded in string literals', () => {
defines.set('WIDTH', { value: '800' });
defines.set('HEIGHT', { value: '600' });
const input = 'const message = "Screen size: WIDTH x HEIGHT";';
const expected = 'const message = "Screen size: WIDTH x HEIGHT";';
expect(expandMacros(input, defines)).toBe(expected);
});
test('handles escaped quotes in strings', () => {
defines.set('MSG', { value: 'Hello' });
const input = 'const str = "This has \\"MSG\\" inside quotes";';
const expected = 'const str = "This has \\"MSG\\" inside quotes";';
expect(expandMacros(input, defines)).toBe(expected);
});
test('handles mixed string literals and macro expansion', () => {
defines.set('WIDTH', { value: '800' });
const input = 'const msg = "Width: " + WIDTH + "px";';
const expected = 'const msg = "Width: " + 800 + "px";';
expect(expandMacros(input, defines)).toBe(expected);
});
});
describe('Edge cases and error handling', () => {
test('handles recursive macros with iteration limit', () => {
// This would cause infinite recursion without a limit
defines.set('RECURSIVE', { value: 'RECURSIVE + 1' });
const input = 'var<private> x = RECURSIVE;';
// The exact result may vary depending on MAX_ITERATIONS, but it should stop eventually
const result = expandMacros(input, defines);
expect(result).not.toBe(input); // Should have done some expansion
expect(result.includes('RECURSIVE')).toBeTruthy(); // But eventually stops
});
test('handles macro expansion in partial words', () => {
defines.set('VAR', { value: 'variable' });
const input = 'var<private> myVARiable = 10;';
const expected = 'var<private> myVARiable = 10;'; // Should not expand within a word
expect(expandMacros(input, defines)).toBe(expected);
});
test('handles multiple macro expansions in one line', () => {
defines.set('X', { value: '10' });
defines.set('Y', { value: '20' });
defines.set('Z', { value: '30' });
const input = 'var<private> sum = X + Y + Z;';
const expected = 'var<private> sum = 10 + 20 + 30;';
expect(expandMacros(input, defines)).toBe(expected);
});
test('handles the "is defined" pattern correctly', () => {
defines.set('DEBUG', { value: '1' });
const input = 'DEBUG is defined';
// This should not be expanded because it's a special pattern
expect(expandMacros(input, defines)).toBe(input);
});
test('handles sequential expansions correctly', () => {
defines.set('A', { value: 'B' });
defines.set('B', { value: 'C' });
defines.set('AB', { value: 'combined' });
// We should expand A to B first, then check for macros again
// rather than looking for the nonexistent AB macro
const input = 'var<private> result = A;';
const expected = 'var<private> result = C;';
expect(expandMacros(input, defines)).toBe(expected);
});
test('function-like macro with wrong argument count throws error', () => {
defines.set('MIN', { value: '((a) < (b) ? (a) : (b))', params: ['a', 'b'] });
const input = 'var<private> minValue = MIN(5);'; // Missing one argument
expect(() => expandMacros(input, defines)).toThrow();
});
});
describe('Regression tests', () => {
test('original failing test case - CAMERA_SETTINGS expansion', () => {
defines.set('WIDTH', { value: '800' });
defines.set('HEIGHT', { value: '600' });
defines.set('ASPECT_RATIO', { value: '(float(WIDTH) / float(HEIGHT))' });
defines.set('FOV', { value: '45.0' });
defines.set('CAMERA_SETTINGS', { value: 'vec4<f32>(ASPECT_RATIO, FOV, 0.1, 100.0)' });
const input = 'var<private> cameraSettings = CAMERA_SETTINGS;';
const expected = 'var<private> cameraSettings = vec4<f32>((float(800) / float(600)), 45.0, 0.1, 100.0);';
expect(expandMacros(input, defines)).toBe(expected);
});
test('nested macros with string literal handling', () => {
defines.set('WIDTH', { value: '800' });
defines.set('RES_INFO', { value: '"Width is " + WIDTH' });
const input = 'var<private> info = RES_INFO;';
const expected = 'var<private> info = "Width is " + 800;';
expect(expandMacros(input, defines)).toBe(expected);
});
test('expansion in function-like macro argument', () => {
defines.set('SIZE', { value: '100' });
defines.set('SCALE', { value: 'size * factor', params: ['size', 'factor'] });
const input = 'var<private> result = SCALE(SIZE, 2.0);';
const expected = 'var<private> result = 100 * 2.0;';
expect(expandMacros(input, defines)).toBe(expected);
});
});
});
describe('Preprocessor Expression Evaluator', () => {
// Setup for all tests
let defines: Map<string, Macro>;
beforeEach(() => {
defines = new Map<string, Macro>();
// Set up some common defines for testing
defines.set('TRUE', { value: '1' });
defines.set('FALSE', { value: '0' });
defines.set('VERSION', { value: '2' });
defines.set('ZERO', { value: '0' });
defines.set('EMPTY', { value: '' });
defines.set('PLATFORM', { value: '3' });
});
// Basic numeric literal evaluation
describe('Numeric literal evaluation', () => {
test('non-zero numbers evaluate to true', () => {
expect(evaluateExpression('1', defines)).toBe(true);
expect(evaluateExpression('42', defines)).toBe(true);
expect(evaluateExpression('-1', defines)).toBe(true);
});
test('zero evaluates to false', () => {
expect(evaluateExpression('0', defines)).toBe(false);
});
test('numbers with whitespace', () => {
expect(evaluateExpression(' 1 ', defines)).toBe(true);
expect(evaluateExpression(' 0 ', defines)).toBe(false);
});
});
// Comparisons
describe('Comparison operators', () => {
test('equal comparison', () => {
expect(evaluateExpression('1 == 1', defines)).toBe(true);
expect(evaluateExpression('1 == 2', defines)).toBe(false);
expect(evaluateExpression('VERSION == 2', defines)).toBe(true);
expect(evaluateExpression('VERSION == 3', defines)).toBe(false);
});
test('not equal comparison', () => {
expect(evaluateExpression('1 != 2', defines)).toBe(true);
expect(evaluateExpression('1 != 1', defines)).toBe(false);
expect(evaluateExpression('VERSION != 3', defines)).toBe(true);
expect(evaluateExpression('VERSION != 2', defines)).toBe(false);
});
test('greater than comparison', () => {
expect(evaluateExpression('2 > 1', defines)).toBe(true);
expect(evaluateExpression('1 > 2', defines)).toBe(false);
expect(evaluateExpression('VERSION > 1', defines)).toBe(true);
expect(evaluateExpression('VERSION > 2', defines)).toBe(false);
});
test('less than comparison', () => {
expect(evaluateExpression('1 < 2', defines)).toBe(true);
expect(evaluateExpression('2 < 1', defines)).toBe(false);
expect(evaluateExpression('VERSION < 3', defines)).toBe(true);
expect(evaluateExpression('VERSION < 2', defines)).toBe(false);
});
test('greater than or equal comparison', () => {
expect(evaluateExpression('2 >= 1', defines)).toBe(true);
expect(evaluateExpression('2 >= 2', defines)).toBe(true);
expect(evaluateExpression('1 >= 2', defines)).toBe(false);
expect(evaluateExpression('VERSION >= 2', defines)).toBe(true);
expect(evaluateExpression('VERSION >= 3', defines)).toBe(false);
});
test('less than or equal comparison', () => {
expect(evaluateExpression('1 <= 2', defines)).toBe(true);
expect(evaluateExpression('2 <= 2', defines)).toBe(true);
expect(evaluateExpression('2 <= 1', defines)).toBe(false);
expect(evaluateExpression('VERSION <= 2', defines)).toBe(true);
expect(evaluateExpression('VERSION <= 1', defines)).toBe(false);
});
});
// Boolean logic
describe('Boolean logic operators', () => {
test('logical AND', () => {
expect(evaluateExpression('1 && 1', defines)).toBe(true);
expect(evaluateExpression('1 && 0', defines)).toBe(false);
expect(evaluateExpression('0 && 1', defines)).toBe(false);
expect(evaluateExpression('0 && 0', defines)).toBe(false);
expect(evaluateExpression('TRUE && TRUE', defines)).toBe(true);
expect(evaluateExpression('TRUE && FALSE', defines)).toBe(false);
});
test('logical OR', () => {
expect(evaluateExpression('1 || 1', defines)).toBe(true);
expect(evaluateExpression('1 || 0', defines)).toBe(true);
expect(evaluateExpression('0 || 1', defines)).toBe(true);
expect(evaluateExpression('0 || 0', defines)).toBe(false);
expect(evaluateExpression('TRUE || FALSE', defines)).toBe(true);
expect(evaluateExpression('FALSE || FALSE', defines)).toBe(false);
});
test('logical NOT', () => {
expect(evaluateExpression('!0', defines)).toBe(true);
expect(evaluateExpression('!1', defines)).toBe(false);
expect(evaluateExpression('!TRUE', defines)).toBe(false);
expect(evaluateExpression('!FALSE', defines)).toBe(true);
});
test('parenthesized expressions', () => {
expect(evaluateExpression('(1 && 0) || 1', defines)).toBe(true);
expect(evaluateExpression('1 && (0 || 1)', defines)).toBe(true);
expect(evaluateExpression('!(1 && 0)', defines)).toBe(true);
expect(evaluateExpression('!(TRUE && FALSE)', defines)).toBe(true);
});
});
// defined() operator
describe('defined() operator', () => {
test('defined() with defined macros', () => {
expect(evaluateExpression('defined(VERSION)', defines)).toBe(true);
expect(evaluateExpression('defined(TRUE)', defines)).toBe(true);
});
test('defined() with undefined macros', () => {
expect(evaluateExpression('defined(UNDEFINED)', defines)).toBe(false);
expect(evaluateExpression('defined(MISSING)', defines)).toBe(false);
});
test('defined() with whitespace', () => {
expect(evaluateExpression('defined( VERSION )', defines)).toBe(true);
expect(evaluateExpression('defined( UNDEFINED )', defines)).toBe(false);
});
test('defined() with logical operators', () => {
expect(evaluateExpression('defined(VERSION) && defined(TRUE)', defines)).toBe(true);
expect(evaluateExpression('defined(VERSION) && defined(UNDEFINED)', defines)).toBe(false);
expect(evaluateExpression('defined(VERSION) || defined(UNDEFINED)', defines)).toBe(true);
expect(evaluateExpression('defined(UNDEFINED) || defined(MISSING)', defines)).toBe(false);
});
test('defined() with NOT operator', () => {
expect(evaluateExpression('!defined(UNDEFINED)', defines)).toBe(true);
expect(evaluateExpression('!defined(VERSION)', defines)).toBe(false);
});
});
// Complex expressions
describe('Complex expressions', () => {
test('mixed comparison and logical operators', () => {
expect(evaluateExpression('VERSION > 1 && VERSION < 3', defines)).toBe(true);
expect(evaluateExpression('VERSION > 2 || VERSION < 1', defines)).toBe(false);
expect(evaluateExpression('VERSION == 2 && defined(PLATFORM)', defines)).toBe(true);
});
test('complex nested expressions', () => {
expect(evaluateExpression('(VERSION > 1 && VERSION < 3) || defined(UNDEFINED)', defines)).toBe(true);
expect(evaluateExpression('VERSION == 2 && (PLATFORM == 3 || defined(UNDEFINED))', defines)).toBe(true);
expect(evaluateExpression('(VERSION != 2 || !defined(PLATFORM)) && (ZERO || TRUE)', defines)).toBe(false);
});
test('real-world like expressions', () => {
// Shader feature check
expect(evaluateExpression('VERSION >= 2 && defined(PLATFORM) && PLATFORM == 3', defines)).toBe(true);
// Version compatibility check
defines.set('MIN_VERSION', { value: '1' });
defines.set('MAX_VERSION', { value: '3' });
expect(evaluateExpression('VERSION >= MIN_VERSION && VERSION <= MAX_VERSION', defines)).toBe(true);
// Feature flags
defines.set('ENABLE_SHADOWS', { value: '1' });
defines.set('SHADOW_QUALITY', { value: '2' });
expect(evaluateExpression('defined(ENABLE_SHADOWS) && SHADOW_QUALITY >= 2', defines)).toBe(true);
});
});
// Edge cases
describe('Edge cases', () => {
test('empty expression handling', () => {
expect(evaluateExpression('', defines)).toBe(false);
expect(evaluateExpression(' ', defines)).toBe(false);
});
test('evaluating empty macros', () => {
// Empty macros typically evaluate to 0 in C preprocessors
expect(evaluateExpression('EMPTY', defines)).toBe(false);
expect(evaluateExpression('EMPTY == 0', defines)).toBe(true);
});
test('evaluating function-like macros', () => {
defines.set('FUNC', { value: 'value', params: ['x'] });
// Function-like macros without arguments should not be expanded
expect(evaluateExpression('defined(FUNC)', defines)).toBe(true);
expect(evaluateExpression('FUNC', defines)).toBe(false); // Treated as 0 since it's not expanded
});
test('macros that evaluate to expressions', () => {
defines.set('EXPR', { value: '1 + 2' });
// This should evaluate to 3, which is truthy
expect(evaluateExpression('EXPR', defines)).toBe(true);
expect(evaluateExpression('EXPR == 3', defines)).toBe(true);
});
test('handling of invalid expressions', () => {
// The evaluator should return false for invalid expressions rather than throwing
expect(evaluateExpression('VERSION ==', defines)).toBe(false);
expect(evaluateExpression('&&', defines)).toBe(false);
expect(evaluateExpression('(VERSION', defines)).toBe(false);
// Test more complex invalid expressions
expect(evaluateExpression('VERSION == && PLATFORM', defines)).toBe(false);
expect(evaluateExpression('(VERSION > 2', defines)).toBe(false);
expect(evaluateExpression('VERSION > < 2', defines)).toBe(false);
});
});
// Advanced use cases
describe('Advanced use cases', () => {
test('platform detection expression', () => {
defines.set('WINDOWS', { value: '1' });
defines.set('MACOS', { value: '0' });
defines.set('LINUX', { value: '0' });
const platformExpr = 'WINDOWS || MACOS || LINUX';
expect(evaluateExpression(platformExpr, defines)).toBe(true);
defines.set('WINDOWS', { value: '0' });
expect(evaluateExpression(platformExpr, defines)).toBe(false);
});
test('feature flag combinations', () => {
defines.set('OPENGL', { value: '1' });
defines.set('OPENGL_VERSION', { value: '450' });
defines.set('REQUIRE_COMPUTE', { value: '1' });
const featureExpr = '(OPENGL && OPENGL_VERSION >= 430) || (VULKAN && defined(VULKAN_VERSION))';
expect(evaluateExpression(featureExpr, defines)).toBe(true);
defines.set('OPENGL_VERSION', { value: '330' });
expect(evaluateExpression(featureExpr, defines)).toBe(false);
defines.set('VULKAN', { value: '1' });
defines.set('VULKAN_VERSION', { value: '11' });
expect(evaluateExpression(featureExpr, defines)).toBe(true);
});
test('version range detection', () => {
defines.set('API_VERSION', { value: '202' });
// Check if version is within a specific range
expect(evaluateExpression('API_VERSION >= 200 && API_VERSION < 300', defines)).toBe(true);
expect(evaluateExpression('API_VERSION >= 300 || API_VERSION < 100', defines)).toBe(false);
// Version comparison with multiple conditions
const versionExpr = '(API_VERSION >= 200 && API_VERSION < 300) || (API_VERSION >= 400 && defined(EXPERIMENTAL))';
expect(evaluateExpression(versionExpr, defines)).toBe(true);
defines.set('API_VERSION', { value: '450' });
defines.set('EXPERIMENTAL', { value: '1' });
expect(evaluateExpression(versionExpr, defines)).toBe(true);
defines.set('API_VERSION', { value: '450' });
defines.delete('EXPERIMENTAL');
expect(evaluateExpression(versionExpr, defines)).toBe(false);
});
// New test cases for C preprocessor token comparison behavior
test('C preprocessor token comparison behavior', () => {
// Case 1: RENDERER == VULKAN where RENDERER='VULKAN' and VULKAN is undefined
defines.set('RENDERER', { value: 'VULKAN' });
expect(evaluateExpression('RENDERER == VULKAN', defines)).toBe(true);
// Case 2: Reversed order (VULKAN == RENDERER)
expect(evaluateExpression('VULKAN == RENDERER', defines)).toBe(true);
// Case 3: Using the pattern in a more complex expression
defines.set('ENABLE_FEATURE', { value: '1' });
expect(evaluateExpression('RENDERER == VULKAN && ENABLE_FEATURE', defines)).toBe(true);
// Case 4: When there are multiple paths
defines.set('GRAPHICS_API', { value: 'VULKAN' });
expect(evaluateExpression('RENDERER == VULKAN || GRAPHICS_API == METAL', defines)).toBe(true);
// Case 5: With different values (should be false)
defines.set('RENDERER', { value: 'VULKAN' });
expect(evaluateExpression('RENDERER == METAL', defines)).toBe(false);
// Case 6: When both sides are defined with matching values
defines.set('RENDERER', { value: 'VULKAN' });
defines.set('API', { value: 'VULKAN' });
expect(evaluateExpression('RENDERER == API', defines)).toBe(true);
// Case 7: With a real-world config test
defines.clear();
defines.set('RENDERER', { value: 'VULKAN' });
defines.set('ENABLE_SHADOWS', { value: '1' });
defines.set('SHADOW_QUALITY', { value: '2' });
// This is the exact condition from the test that was failing
expect(evaluateExpression('RENDERER == VULKAN', defines)).toBe(true);
});
});
});
describe('Conditional Processor', () => {
// Setup for all tests
let defines: Map<string, Macro>;
beforeEach(() => {
defines = new Map<string, Macro>();
});
// Basic conditional directive processing
describe('Basic conditional directives', () => {
test('#if with true condition', () => {
defines.set('VERSION', { value: '2' });
const lines = [
'#if VERSION > 1',
'include this line',
'#endif'
];
const result = processConditionals(lines, defines);
expect(result).toEqual(['include this line']);
});
test('#if with false condition', () => {
defines.set('VERSION', { value: '1' });
const lines = [
'#if VERSION > 1',
'do not include this line',
'#endif'
];
const result = processConditionals(lines, defines);
expect(result).toEqual([]);
});
test('#ifdef with defined macro', () => {
defines.set('FEATURE', { value: '1' });
const lines = [
'#ifdef FEATURE',
'feature is enabled',
'#endif'
];
const result = processConditionals(lines, defines);
expect(result).toEqual(['feature is enabled']);
});
test('#ifdef with undefined macro', () => {
const lines = [
'#ifdef UNDEFINED_FEATURE',
'feature is not defined',
'#endif'
];
const result = processConditionals(lines, defines);
expect(result).toEqual([]);
});
test('#ifndef with defined macro', () => {
defines.set('FEATURE', { value: '1' });
const lines = [
'#ifndef FEATURE',
'feature is not defined',
'#endif'
];
const result = processConditionals(lines, defines);
expect(result).toEqual([]);
});
test('#ifndef with undefined macro', () => {
const lines = [
'#ifndef UNDEFINED_FEATURE',
'feature is not defined',
'#endif'
];
const result = processConditionals(lines, defines);
expect(result).toEqual(['feature is not defined']);
});
});
// Else and elif branches
describe('Else and elif branches', () => {
test('basic #if-#else structure', () => {
defines.set('VERSION', { value: '1' });
const lines = [
'#if VERSION > 1',
'if branch',
'#else',
'else branch',
'#endif'
];
const result = processConditionals(lines, defines);
expect(result).toEqual(['else branch']);
});
test('#if-#elif-#else with first branch true', () => {
defines.set('VERSION', { value: '2' });
const lines = [
'#if VERSION == 2',
'version 2',
'#elif VERSION == 1',
'version 1',
'#else',
'other version',
'#endif'
];
const result = processConditionals(lines, defines);
expect(result).toEqual(['version 2']);
});
test('#if-#elif-#else with second branch true', () => {
defines.set('VERSION', { value: '1' });
const lines = [
'#if VERSION == 2',
'version 2',
'#elif VERSION == 1',
'version 1',
'#else',
'other version',
'#endif'
];
const result = processConditionals(lines, defines);
expect(result).toEqual(['version 1']);
});
test('#if-#elif-#else with neither branch true', () => {
defines.set('VERSION', { value: '3' });
const lines = [
'#if VERSION == 2',
'version 2',
'#elif VERSION == 1',
'version 1',
'#else',
'other version',
'#endif'
];
const result = processConditionals(lines, defines);
expect(result).toEqual(['other version']);
});
test('multiple #elif branches', () => {
defines.set('VERSION', { value: '2' });
const lines = [
'#if VERSION == 1',
'version 1',
'#elif VERSION == 2',
'version 2',
'#elif VERSION == 3',
'version 3',
'#else',
'other version',
'#endif'
];
const result = processConditionals(lines, defines);
expect(result).toEqual(['version 2']);
});
test('only first true branch is included with multiple #elif', () => {
defines.set('VALUE', { value: '5' });
const lines = [
'#if VALUE > 10',
'greater than 10',
'#elif VALUE > 5',
'greater than 5',
'#elif VALUE >= 5',
'greater than or equal to 5',
'#else',
'less than 5',
'#endif'
];
const result = processConditionals(lines, defines);
expect(result).toEqual(['greater than or equal to 5']);
});
});
// Macro definition and undefinition
describe('Macro definition and undefinition', () => {
test('basic #define and usage', () => {
const lines = [
'#define WIDTH 800',
'#define HEIGHT 600',
'resolution: vec2<f32> = vec2<f32>(WIDTH, HEIGHT);'
];
const result = processConditionals(lines, defines);
expect(result).toEqual(['resolution: vec2<f32> = vec2<f32>(800, 600);']);
expect(defines.has('WIDTH')).toBe(true);
expect(defines.has('HEIGHT')).toBe(true);
});
test('#define within conditional block', () => {
defines.set('FEATURE', { value: '1' });
const lines = [
'#ifdef FEATURE',
'#define WIDTH 800',
'width: u32 = WIDTH;',
'#endif',
'height: u32 = 600;'
];
const result = processConditionals(lines, defines);
expect(result).toEqual(['width: u32 = 800;', 'height: u32 = 600;']);
expect(defines.has('WIDTH')).toBe(true);
});
test('#undef removes macro definition', () => {
defines.set('FEATURE', { value: '1' });
const lines = [
'feature: bool = FEATURE;',
'#undef FEATURE',
'#ifdef FEATURE',
'should not include this',
'#endif'
];
const result = processConditionals(lines, defines);
expect(result).toEqual(['feature: bool = 1;']);
expect(defines.has('FEATURE')).toBe(false);
});
test('redefining macros', () => {
const lines = [
'#define VERSION 1',
'version: u32 = VERSION;',
'#undef VERSION',
'#define VERSION 2',
'updated version: u32 = VERSION;'
];
const result = processConditionals(lines, defines);
expect(result).toEqual(['version: u32 = 1;', 'updated version: u32 = 2;']);
expect(defines.get('VERSION')?.value).toBe('2');
});
test('function-like macro definition and usage', () => {
const lines = [
'#define MAX(a, b) ((a) > (b) ? (a) : (b))',
'max_value: f32 = MAX(10.0, 5.0);'
];
const result = processConditionals(lines, defines);
expect(result).toEqual(['max_value: f32 = ((10.0) > (5.0) ? (10.0) : (5.0));']);
expect(defines.has('MAX')).toBe(true);
expect(defines.get('MAX')?.params).toEqual(['a', 'b']);
});
});
// Nested conditionals
describe('Nested conditionals', () => {
test('basic nested conditionals', () => {
defines.set('OUTER', { value: '1' });
defines.set('INNER', { value: '1' });
const lines = [
'#ifdef OUTER',
'outer start',
'#ifdef INNER',
'inner content',
'#endif',
'outer end',
'#endif'
];
const result = processConditionals(lines, defines);
expect(result).toEqual(['outer start', 'inner content', 'outer end']);
});
test('nested conditionals with inner branch excluded', () => {
defines.set('OUTER', { value: '1' });
const lines = [
'#ifdef OUTER',
'outer start',
'#ifdef INNER',
'inner content',
'#endif',
'outer end',
'#endif'
];
const result = processConditionals(lines, defines);
expect(result).toEqual(['outer start', 'outer end']);
});
test('nested conditionals with outer branch excluded', () => {
defines.set('INNER', { value: '1' });
const lines = [
'#ifdef OUTER',
'outer start',
'#ifdef INNER',
'inner content',
'#endif',
'outer end',
'#endif'
];
const result = processConditionals(lines, defines);
expect(result).toEqual([]);
});
test('multiple levels of nesting', () => {
defines.set('LEVEL1', { value: '1' });
defines.set('LEVEL2', { value: '1' });
defines.set('LEVEL3', { value: '1' });
const lines = [
'#ifdef LEVEL1',
'level 1 start',
'#ifdef LEVEL2',
'level 2 start',
'#ifdef LEVEL3',
'level 3 content',
'#endif',
'level 2 end',
'#endif',
'level 1 end',
'#endif'
];
const result = processConditionals(lines, defines);
expect(result).toEqual([
'level 1 start',
'level 2 start',
'level 3 content',
'level 2 end',
'level 1 end'
]);
});
test('nested conditionals with mixed directive types', () => {
defines.set('OUTER', { value: '1' });
defines.set('VALUE', { value: '5' });
const lines = [
'#ifdef OUTER',
'outer start',
'#if VALUE > 3',
'value is greater than 3',
'#else',
'value is not greater than 3',
'#endif',
'outer end',
'#endif'
];
const result = processConditionals(lines, defines);
expect(result).toEqual([
'outer start',
'value is greater than 3',
'outer end'
]);
});
test('nested if-elif-else constructs', () => {
defines.set('OUTER', { value: '2' });
defines.set('INNER', { value: '3' });
const lines = [
'#if OUTER == 1',
'outer is 1',
'#elif OUTER == 2',
'outer is 2',
'#if INNER == 1',
'inner is 1',
'#elif INNER == 2',
'inner is 2',
'#elif INNER == 3',
'inner is 3',
'#endif',
'#else',
'outer is neither 1 nor 2',
'#endif'
];
const result = processConditionals(lines, defines);
expect(result).toEqual([
'outer is 2',
'inner is 3'
]);
});
});
// Custom directives
describe('Custom directives preservation', () => {
test('custom directives are preserved', () => {
const lines = [
'#binding "data"',
'#entrypoint "main"',
'@compute',
'fn main() {}'
];
const result = processConditionals(lines, defines);
expect(result).toEqual([
'#binding "data"',
'#entrypoint "main"',
'@compute',
'fn main() {}'
]);
});
test('custom directives inside included conditional blocks', () => {
defines.set('FEATURE', { value: '1' });
const lines = [
'#ifdef FEATURE',
'#binding "feature_data"',
'#entrypoint "feature_main"',
'fn feature_main() {}',
'#endif'
];
const result = processConditionals(lines, defines);
expect(result).toEqual([
'#binding "feature_data"',
'#entrypoint "feature_main"',
'fn feature_main() {}'
]);
});
test('custom directives inside excluded conditional blocks', () => {
const lines = [
'#ifdef UNDEFINED_FEATURE',
'#binding "feature_data"',
'#entrypoint "feature_main"',
'fn feature_main() {}',
'#endif'
];
const result = processConditionals(lines, defines);
expect(result).toEqual([]);
});
});
// Error handling
describe('Error handling', () => {
test('unmatched #endif throws error', () => {
const lines = [
'line 1',
'#endif',
'line 2'
];
expect(() => processConditionals(lines, defines)).toThrow(/Unexpected #endif/);
});
test('unmatched #else throws error', () => {
const lines = [
'line 1',
'#else',
'line 2'
];
expect(() => processConditionals(lines, defines)).toThrow(/Unexpected #else/);
});
test('unmatched #elif throws error', () => {
const lines = [
'line 1',
'#elif VALUE > 0',
'line 2'
];
expect(() => processConditionals(lines, defines)).toThrow(/Unexpected #elif/);
});
test('missing #endif throws error', () => {
const lines = [
'#ifdef FEATURE',
'line 1'
// Missing #endif
];
expect(() => processConditionals(lines, defines)).toThrow(/Unmatched #if/);
});
});
// Complex real-world scenarios
describe('Complex real-world scenarios', () => {
test('shader variant generation', () => {
defines.set('VARIANT', { value: '2' });
const lines = [
'struct VertexOutput {',
' @builtin(position) position: vec4<f32>,',
' @location(0) uv: vec2<f32>,',
'#if VARIANT == 1',
' @location(1) color: vec3<f32>,',
'#elif VARIANT == 2',
' @location(1) color: vec4<f32>,',
' @location(2) normal: vec3<f32>,',
'#elif VARIANT == 3',
' @location(1) color: vec4<f32>,',
' @location(2) normal: vec3<f32>,',
' @location(3) tangent: vec4<f32>,',
'#endif',
'};',
'',
'@fragment',
'fn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {',
' var output: vec4<f32>;',
' ',
'#if VARIANT == 1',
' output = vec4<f32>(input.color, 1.0);',
'#elif VARIANT == 2',
' output = input.color * calculateLighting(input.normal);',
'#elif VARIANT == 3',
' let normalMap = textureSample(normalTexture, normalSampler, input.uv);',
' let worldNormal = calculateTBN(input.normal, input.tangent) * normalMap.xyz;',
' output = input.color * calculateLighting(worldNormal);',
'#endif',
'',
' return output;',
'}'
];
const result = processConditionals(lines, defines);
expect(result).toContain(' @location(1) color: vec4<f32>,');
expect(result).toContain(' @location(2) normal: vec3<f32>,');
expect(result).toContain(' output = input.color * calculateLighting(input.normal);');
expect(result).not.toContain(' @location(3) tangent: vec4<f32>,');
expect(result).not.toContain(' output = vec4<f32>(input.color, 1.0);');
});
test('platform-specific code with nested defines', () => {
defines.set('MOBILE', { value: '1' });
const lines = [
'#ifdef DESKTOP',
'#define MAX_LIGHTS 16',
'#define SHADOW_RESOLUTION 2048',
'#elif defined(MOBILE)',
'#define MAX_LIGHTS 4',
'#define SHADOW_RESOLUTION 1024',
'#else',
'#define MAX_LIGHTS 8',
'#define SHADOW_RESOLUTION 1024',
'#endif',
'',
'const maxLights: u32 = MAX_LIGHTS;',
'const shadowResolution: u32 = SHADOW_RESOLUTION;',
'',
'#ifdef MOBILE',
'#if MAX_LIGHTS > 4',
'#error Mobile platforms only support up to 4 lights',
'#endif',
'#endif'
];
const result = processConditionals(lines, defines);
expect(result).toContain('const maxLights: u32 = 4;');
expect(result).toContain('const shadowResolution: u32 = 1024;');
expect(result).not.toContain('#error');
});
test('feature configuration with nested conditionals', () => {
defines.set('RENDERER', { value: 'VULKAN' });
defines.set('ENABLE_SHADOWS', { value: '1' });
defines.set('SHADOW_QUALITY', { value: '2' });
const lines = [
'// Renderer configuration',
'#if RENDERER == VULKAN',
'#define MAX_BINDLESS_RESOURCES 1000000',
'#elif RENDERER == METAL',
'#define MAX_BINDLESS_RESOURCES 500000',
'#else',
'#define MAX_BINDLESS_RESOURCES 32',
'#endif',
'',
'// Shadow configuration',
'#ifdef ENABLE_SHADOWS',
'#if SHADOW_QUALITY == 0',
'#define SHADOW_MAP_SIZE 512',
'#define SHADOW_CASCADES 1',
'#elif SHADOW_QUALITY == 1',
'#define SHADOW_MAP_SIZE 1024',
'#define SHADOW_CASCADES 2',
'#else // SHADOW_QUALITY >= 2',
'#define SHADOW_MAP_SIZE 2048',
'#define SHADOW_CASCADES 4',
'#endif',
'#else // !ENABLE_SHADOWS',
'#define SHADOW_MAP_SIZE 0',
'#define SHADOW_CASCADES 0',
'#endif',
'',
'const maxBindlessResources: u32 = MAX_BINDLESS_RESOURCES;',
'const shadowMapSize: u32 = SHADOW_MAP_SIZE;',
'const shadowCascades: u32 = SHADOW_CASCADES;'
];
const result = processConditionals(lines, defines);
expect(result).toContain('const maxBindlessResources: u32 = 1000000;');
expect(result).toContain('const shadowMapSize: u32 = 2048;');
expect(result).toContain('const shadowCascades: u32 = 4;');
});
});
// Tests for improved error handling
describe('Error handling for invalid directive patterns', () => {
test('multiple #else directives', () => {
const lines = [
'#if 1',
'var<private> value: u32 = 1;',
'#else',
'var<private> value: u32 = 2;',
'#else',
'var<private> value: u32 = 3;',
'#endif'
];
// Implementation should throw an error for multiple #else directives
expect(() => processConditionals(lines, defines)).toThrow(/Multiple #else directives/);
});
test('#elif after #else', () => {
const lines = [
'#if 1',
'var<private> value: u32 = 1;',
'#else',
'var<private> value: u32 = 2;',
'#elif 0',
'var<private> value: u32 = 3;',
'#endif'
];
// Implementation should throw an error for #elif after #else
expect(() => processConditionals(lines, defines)).toThrow(/Unexpected #elif after #else/);
});
test('nested errors bubble up correctly', () => {
const lines = [
'#ifdef OUTER',
'outer start',
'#if 1',
'inner content',
'#else',
'other content',
'#else',
'invalid content',
'#endif',
'outer end',
'#endif'
];
defines.set('OUTER', { value: '1' });
// Even when nested, the error should still be detected
expect(() => processConditionals(lines, defines)).toThrow(/Multiple #else directives/);
});
});
// Tests for whitespace handling
describe('Whitespace handling in directives', () => {
test('various whitespace patterns in directives', () => {
const lines = [
'#define SPACE_AFTER_DEFINE 123',
'#define\tTAB_AFTER_DEFINE\t456',
'#define NO_SPACE(x) x+1',
'#if\tSPACE_AFTER_DEFINE\t==\t123',
'this should be included',
'#endif',
'#ifdef\tTAB_AFTER_DEFINE',
'this should also be included',
'#endif',
'var<private> result: u32 = NO_SPACE(42);'
];
const result = processConditionals(lines, defines);
expect(result).toContain('this should be included');
expect(result).toContain('this should also be included');
expect(result).toContain('var<private> result: u32 = 42+1;');
});
test('mixed whitespace in complex directives', () => {
const lines = [
'#define\tMIXED_WHITESPACE \t 100',
'#if\t \t MIXED_WHITESPACE\t>\t50\t \t',
'condition is true',
'#elif\t\tMIXED_WHITESPACE\t<\t50',
'condition is false',
'#else \t ',
'condition is neither',
'#endif\t'
];
const result = processConditionals(lines, defines);
expect(result).toEqual(['condition is true']);
});
});
// Tests for token comparison semantics (RENDERER == VULKAN case)
describe('Token comparison semantics', () => {
test('macro expanding to its identifier', () => {
defines.set('RENDERER', { value: 'VULKAN' });
const lines = [
'#if RENDERER == VULKAN',
'renderer is vulkan',
'#else',
'renderer is not vulkan',
'#endif'
];
const result = processConditionals(lines, defines);
expect(result).toEqual(['renderer is vulkan']);
});
test('reverse comparison (identifier == macro)', () => {
defines.set('RENDERER', { value: 'VULKAN' });
const lines = [
'#if VULKAN == RENDERER',
'vulkan is the renderer',
'#else',
'vulkan is not the renderer',
'#endif'
];
const result = processConditionals(lines, defines);
expect(result).toEqual(['vulkan is the renderer']);
});
test('macro comparison with multiple tokens', () => {
defines.set('API', { value: 'VULKAN' });
defines.set('RENDERER', { value: 'VULKAN' });
const lines = [
'#if API == RENDERER',
'API matches renderer',
'#else',
'API does not match renderer',
'#endif'
];
const result = processConditionals(lines, defines);
expect(result).toEqual(['VULKAN matches renderer']);
});
test('token comparison with complex expressions', () => {
defines.set('API', { value: 'VULKAN' });
defines.set('VERSION', { value: '2' });
const lines = [
'#if API == VULKAN && VERSION == 2',
'right version of vulkan',
'#elif API == VULKAN',
'wrong version of vulkan',
'#else',
'not vulkan',
'#endif'
];
const result = processConditionals(lines, defines);
expect(result).toEqual(['right version of vulkan']);
});
});
// Tests for comment handling in directives
describe('Comment handling in directives', () => {
test('comments after directives', () => {
defines.set('FEATURE', { value: '1' });
const lines = [
'#ifdef FEATURE // This is a comment',
'feature is enabled',
'#else // This is another comment',
'feature is disabled',
'#endif // End of feature check'
];
const result = processConditionals(lines, defines);
expect(result).toEqual(['feature is enabled']);
});
test('complex comments in directives', () => {
defines.set('QUALITY', { value: '2' });
const lines = [
'#if QUALITY >= 2 // High quality settings',
'#define TEXTURE_SIZE 2048 // Large textures',
'#elif QUALITY == 1 // Medium quality',
'#define TEXTURE_SIZE 1024 // Medium textures',
'#else // Low quality or undefined',
'#define TEXTURE_SIZE 512 // Small textures',
'#endif // End of quality settings',
'',
'const textureSize: u32 = TEXTURE_SIZE;'
];
const result = processConditionals(lines, defines);
expect(result).toContain('const textureSize: u32 = 2048;');
});
});
// Tests for common macro patterns
describe('Common macro patterns', () => {
test('macros with default values in conditional branches', () => {
defines.set('MOBILE', { value: '1' });
const lines = [
'#ifndef MAX_LIGHTS',
'#ifdef MOBILE',
'#define MAX_LIGHTS 4',
'#else',
'#define MAX_LIGHTS 16',
'#endif',
'#endif',
'',
'const maxLights: u32 = MAX_LIGHTS;'
];
const result = processConditionals(lines, defines);
expect(result).toContain('const maxLights: u32 = 4;');
});
test('macro redefinition in different branches', () => {
defines.set('PLATFORM', { value: '2' });
const lines = [
'#if PLATFORM == 1 // Windows',
'#define PLATFORM_STRING "Windows"',
'#define MAX_INSTANCES 1024',
'#elif PLATFORM == 2 // MacOS',
'#define PLATFORM_STRING "MacOS"',
'#define MAX_INSTANCES 512',
'#else // Linux or other',
'#define PLATFORM_STRING "Other"',
'#define MAX_INSTANCES 256',
'#endif',
'',
'const platform: string = PLATFORM_STRING;',
'const maxInstances: u32 = MAX_INSTANCES;'
];
const result = processConditionals(lines, defines);
expect(result).toContain('const platform: string = "MacOS";');
expect(result).toContain('const maxInstances: u32 = 512;');
});
});
// Tests for edge cases in conditional evaluation
describe('Edge cases in conditional evaluation', () => {
test('numeric expressions in conditionals', () => {
defines.set('VALUE', { value: '10' });
const lines = [
'#if 20 > VALUE',
'twenty is greater than value',
'#else',
'twenty is not greater than value',
'#endif',
'',
'#if (VALUE * 2) > 15',
'value times 2 is greater than 15',
'#else',
'value times 2 is not greater than 15',
'#endif'
];
const result = processConditionals(lines, defines);
expect(result).toContain('twenty is greater than value');
expect(result).toContain('value times 2 is greater than 15');
});
test('complex expressions with parentheses', () => {
defines.set('A', { value: '5' });
defines.set('B', { value: '3' });
const lines = [
'#if (A > B) && (A < 10)',
'condition met',
'#endif',
'',
'#if (A * B) == 15',
'product is 15',
'#else',
'product is not 15',
'#endif'
];
const result = processConditionals(lines, defines);
expect(result).toContain('condition met');
expect(result).toContain('product is 15');
});
test('evaluation of zero and empty values', () => {
defines.set('ZERO', { value: '0' });
defines.set('EMPTY', { value: '' });
const lines = [
'#if ZERO',
'zero is truthy',
'#else',
'zero is falsy',
'#endif',
'',
'#if EMPTY',
'empty is truthy',
'#else',
'empty is falsy',
'#endif',
'',
'#if defined(ZERO)',
'zero is defined',
'#else',
'zero is not defined',
'#endif'
];
const result = processConditionals(lines, defines);
expect(result).toContain('zero is falsy');
expect(result).toContain('empty is falsy');
expect(result).toContain('zero is defined');
});
});
describe('Macro expansion in definitions', () => {
test('basic macro expansion in definitions', () => {
const lines = [
'#define BASE_VALUE 100',
'#define DOUBLED BASE_VALUE * 2',
'var<private> result = DOUBLED;'
];
const result = processConditionals(lines, defines);
expect(result).toEqual(['var<private> result = 100 * 2;']);
});
test('nested macro expansion in definitions', () => {
const lines = [
'#define WIDTH 800',
'#define HEIGHT 600',
'#define RESOLUTION vec2<f32>(WIDTH, HEIGHT)',
'#define ASPECT_RATIO (float(WIDTH) / float(HEIGHT))',
'#define FOV 45.0',
'#define CAMERA_SETTINGS vec4<f32>(ASPECT_RATIO, FOV, 0.1, 100.0)',
'var<private> resolution = RESOLUTION;',
'var<private> cameraSettings = CAMERA_SETTINGS;'
];
const result = processConditionals(lines, defines);
expect(result).toEqual([
'var<private> resolution = vec2<f32>(800, 600);',
'var<private> cameraSettings = vec4<f32>((float(800) / float(600)), 45.0, 0.1, 100.0);'
]);
});
test('chain of macros expansion', () => {
const lines = [
'#define A 10',
'#define B A + 5',
'#define C B * 2',
'#define D C - 15',
'var<private> result = D;'
];
const result = processConditionals(lines, defines);
expect(result).toEqual(['var<private> result = 10 + 5 * 2 - 15;']);
});
test('redefining macros with expansions', () => {
const lines = [
'#define VALUE 100',
'#define RESULT VALUE + 50',
'var<private> before = RESULT;',
'#define VALUE 200', // Redefine VALUE
'#define RESULT VALUE + 50', // Redefine RESULT using new VALUE
'var<private> after = RESULT;'
];
const result = processConditionals(lines, defines);
expect(result).toEqual([
'var<private> before = 100 + 50;',
'var<private> after = 200 + 50;'
]);
});
test('function-like macros with expanded parameters', () => {
const lines = [
'#define WIDTH 800',
'#define HEIGHT 600',
'#define SCALED_VEC(factor) vec2<f32>(WIDTH * factor, HEIGHT * factor)',
'var<private> halfRes = SCALED_VEC(0.5);'
];
const result = processConditionals(lines, defines);
expect(result).toEqual(['var<private> halfRes = vec2<f32>(800 * 0.5, 600 * 0.5);']);
});
test('function-like macros using other macros in body', () => {
const lines = [
'#define WIDTH 800',
'#define HEIGHT 600',
'#define ASPECT (float(WIDTH) / float(HEIGHT))',
'#define PERSPECTIVE(fov) vec4<f32>(ASPECT, fov, 0.1, 100.0)',
'var<private> perspective = PERSPECTIVE(45.0);'
];
const result = processConditionals(lines, defines);
expect(result).toEqual([
'var<private> perspective = vec4<f32>((float(800) / float(600)), 45.0, 0.1, 100.0);'
]);
});
});
describe('Comments and whitespace in macro definitions', () => {
test('macros with comments in definition', () => {
const lines = [
'#define WIDTH 800 // Screen width',
'#define HEIGHT 600 // Screen height',
'#define RESOLUTION vec2<f32>(WIDTH, HEIGHT) // Screen resolution',
'var<private> resolution = RESOLUTION;'
];
const result = processConditionals(lines, defines);
expect(result).toEqual(['var<private> resolution = vec2<f32>(800, 600);']);
});
test('macros with trailing whitespace in definition', () => {
const lines = [
'#define WIDTH 800 ',
'#define HEIGHT 600 ',
'#define RESOLUTION vec2<f32>(WIDTH, HEIGHT) ',
'var<private> resolution = RESOLUTION;'
];
const result = processConditionals(lines, defines);
expect(result).toEqual(['var<private> resolution = vec2<f32>(800, 600);']);
});
test('macros with mixed comments after directives', () => {
const lines = [
'#define WIDTH 800 // Width value',
'#define HEIGHT 600 // Height value',
'#define ASPECT_RATIO (float(WIDTH) / float(HEIGHT)) // Calculate aspect ratio',
'#ifdef WIDTH // Check if WIDTH is defined',
' var<private> aspectRatio = ASPECT_RATIO;',
'#else // WIDTH not defined',
' var<private> aspectRatio = 1.0;',
'#endif // End of conditional'
];
const result = processConditionals(lines, defines);
expect(result).toEqual([' var<private> aspectRatio = (float(800) / float(600));']);
});
});
describe('Edge cases for macro expansion in definitions', () => {
test('macro expansion with string literals', () => {
const lines = [
'#define WIDTH 800',
'#define HEIGHT 600',
'#define RESOLUTION_STR "Resolution: WIDTHxHEIGHT"', // Shouldn't expand in quotes
'#define INFO "Size: " + WIDTH + "x" + HEIGHT', // Should expand outside quotes
'var<private> info = INFO;'
];
const result = processConditionals(lines, defines);
// WIDTH and HEIGHT should only be expanded outside of string literals
expect(result).toEqual(['var<private> info = "Size: " + 800 + "x" + 600;']);
});
test('recursively defined macros are handled', () => {
const lines = [
'#define VERSION 1',
'#define VERSION_INFO VERSION', // Linear reference
'#define VERSION 2', // Redefine VERSION
'#define VERSION_STR VERSION_INFO', // Could cause recursive processing
'var<private> version = VERSION_STR;'
];
// This tests that we don't get into infinite recursion
const result = processConditionals(lines, defines);
expect(result).toEqual(['var<private> version = 1;']);
});
test('macro expansion in conditionals', () => {
const lines = [
'#define PLATFORM 1',
'#define DESKTOP 1',
'#define MOBILE 2',
'#if PLATFORM == DESKTOP',
' var<private> isMobile = false;',
'#elif PLATFORM == MOBILE',
' var<private> isMobile = true;',
'#endif'
];
const result = processConditionals(lines, defines);
expect(result).toEqual([' var<private> isMobile = false;']);
});
test('original failing test case - complex nested expansions', () => {
const lines = [
'#define WIDTH 800',
'#define HEIGHT 600',
'#define RESOLUTION vec2<f32>(WIDTH, HEIGHT)',
'#define ASPECT_RATIO (float(WIDTH) / float(HEIGHT))',
'#define FOV 45.0',
'#define CAMERA_SETTINGS vec4<f32>(ASPECT_RATIO, FOV, 0.1, 100.0)',
'',
'var<private> resolution = RESOLUTION;',
'var<private> cameraSettings = CAMERA_SETTINGS;'
];
const result = processConditionals(lines, defines);
expect(result).toEqual([
'',
'var<private> resolution = vec2<f32>(800, 600);',
'var<private> cameraSettings = vec4<f32>((float(800) / float(600)), 45.0, 0.1, 100.0);'
]);
});
test('macro redefinition with expanded values', () => {
const lines = [
'#define BASE 100',
'#define VALUE BASE + 50',
'#undef BASE', // Undefine BASE
'#define BASE 200', // Redefine BASE with different value
'#define RESULT VALUE * 2', // VALUE should still contain the old, expanded value
'var<private> result = RESULT;'
];
const result = processConditionals(lines, defines);
// VALUE should have been expanded to "100 + 50" when it was defined
expect(result).toEqual(['var<private> result = 100 + 50 * 2;']);
});
});
});
describe('WGSL Preprocessor', () => {
// Test basic macro definition and expansion
describe('Basic #define and expansion', () => {
test('simple macro expansion', () => {
const code = `
#define WIDTH 800
#define HEIGHT 600
var<private> resolution: vec2<f32> = vec2<f32>(WIDTH, HEIGHT);
`;
const expected = `
var<private> resolution: vec2<f32> = vec2<f32>(800, 600);
`;
expect(preprocessWgsl(code).trim()).toBe(expected.trim());
});
test('macro without value', () => {
const code = `
#define ENABLE_FEATURE
#ifdef ENABLE_FEATURE
var<private> featureEnabled: bool = true;
#endif
`;
const expected = `
var<private> featureEnabled: bool = true;
`;
expect(preprocessWgsl(code).trim()).toBe(expected.trim());
});
test('macro expansion in expressions', () => {
const code = `
#define PI 3.14159
#define RADIUS 5.0
var<private> area: f32 = PI * RADIUS * RADIUS;
`;
const expected = `
var<private> area: f32 = 3.14159 * 5.0 * 5.0;
`;
expect(preprocessWgsl(code).trim()).toBe(expected.trim());
});
test('multiple macro references in a single line', () => {
const code = `
#define X 10
#define Y 20
#define Z 30
var<private> sum: i32 = X + Y + Z;
`;
const expected = `
var<private> sum: i32 = 10 + 20 + 30;
`;
expect(preprocessWgsl(code).trim()).toBe(expected.trim());
});
});
// Test function-like macros
describe('Function-like macros', () => {
test('basic function-like macro', () => {
const code = `
#define MAX(a, b) ((a) > (b) ? (a) : (b))
var<private> maxValue: f32 = MAX(10.0, 5.0);
`;
const expected = `
var<private> maxValue: f32 = ((10.0) > (5.0) ? (10.0) : (5.0));
`;
expect(preprocessWgsl(code).trim()).toBe(expected.trim());
});
test('function-like macro with multiple parameters', () => {
const code = `
#define CLAMP(x, min, max) ((x) < (min) ? (min) : ((x) > (max) ? (max) : (x)))
var<private> clampedValue: f32 = CLAMP(value, 0.0, 1.0);
`;
const expected = `
var<private> clampedValue: f32 = ((value) < (0.0) ? (0.0) : ((value) > (1.0) ? (1.0) : (value)));
`;
expect(preprocessWgsl(code).trim()).toBe(expected.trim());
});
test('nested function-like macros', () => {
const code = `
#define SQR(x) ((x) * (x))
#define DIST(x, y) sqrt(SQR(x) + SQR(y))
var<private> distance: f32 = DIST(3.0, 4.0);
`;
const expected = `
var<private> distance: f32 = sqrt(((3.0) * (3.0)) + ((4.0) * (4.0)));
`;
expect(preprocessWgsl(code).trim()).toBe(expected.trim());
});
test('function-like macros with complex arguments', () => {
const code = `
#define LERP(a, b, t) ((a) * (1.0 - (t)) + (b) * (t))
var<private> lerpResult: f32 = LERP(start + offset, end - offset, time * 0.5);
`;
const expected = `
var<private> lerpResult: f32 = ((start + offset) * (1.0 - (time * 0.5)) + (end - offset) * (time * 0.5));
`;
expect(preprocessWgsl(code).trim()).toBe(expected.trim());
});
test('wrong number of arguments in function-like macro', () => {
const code = `
#define FUNC(a, b) a + b
var<private> result = FUNC(1, 2, 3);
`;
expect(() => preprocessWgsl(code)).toThrow(/expects 2 arguments, got 3/);
});
});
// Test #ifdef, #ifndef conditionals
describe('#ifdef and #ifndef conditionals', () => {
test('basic #ifdef inclusion', () => {
const code = `
#define ENABLE_FEATURE
#ifdef ENABLE_FEATURE
var<private> featureEnabled: bool = true;
#endif
`;
const expected = `
var<private> featureEnabled: bool = true;
`;
expect(preprocessWgsl(code).trim()).toBe(expected.trim());
});
test('basic #ifdef exclusion', () => {
const code = `
#ifdef UNDEFINED_FEATURE
var<private> featureEnabled: bool = true;
#endif
`;
const expected = ``;
expect(preprocessWgsl(code).trim()).toBe(expected);
});
test('basic #ifndef inclusion', () => {
const code = `
#ifndef UNDEFINED_FEATURE
var<private> featureDisabled: bool = true;
#endif
`;
const expected = `
var<private> featureDisabled: bool = true;
`;
expect(preprocessWgsl(code).trim()).toBe(expected.trim());
});
test('basic #ifndef exclusion', () => {
const code = `
#define DEFINED_FEATURE
#ifndef DEFINED_FEATURE
var<private> featureDisabled: bool = true;
#endif
`;
const expected = ``;
expect(preprocessWgsl(code).trim()).toBe(expected.trim());
});
test('#ifdef with macros defined after usage', () => {
const code = `
#ifdef LATER_DEFINED_FEATURE
var<private> shouldNotBeIncluded: bool = true;
#endif
#define LATER_DEFINED_FEATURE
#ifdef LATER_DEFINED_FEATURE
var<private> shouldBeIncluded: bool = true;
#endif
`;
const expected = `
var<private> shouldBeIncluded: bool = true;
`;
expect(preprocessWgsl(code).trim()).toBe(expected.trim());
});
});
// Test #if conditionals
describe('#if conditionals', () => {
test('basic #if with true condition', () => {
const code = `
#define VERSION 2
#if VERSION > 1
var<private> newFeatureEnabled: bool = true;
#endif
`;
const expected = `
var<private> newFeatureEnabled: bool = true;
`;
expect(preprocessWgsl(code).trim()).toBe(expected.trim());
});
test('basic #if with false condition', () => {
const code = `
#define VERSION 1
#if VERSION > 1
var<private> newFeatureEnabled: bool = true;
#endif
`;
const expected = ``;
expect(preprocessWgsl(code).trim()).toBe(expected.trim());
});
test('#if with complex expression', () => {
const code = `
#define VERSION 2
#define PLATFORM 1
#if VERSION > 1 && PLATFORM == 1
var<private> platformSpecificFeature: bool = true;
#endif
`;
const expected = `
var<private> platformSpecificFeature: bool = true;
`;
expect(preprocessWgsl(code).trim()).toBe(expected.trim());
});
test('#if with defined() operator', () => {
const code = `
#define FEATURE_X
#if defined(FEATURE_X) && !defined(FEATURE_Y)
var<private> featureConfig: u32 = 1;
#endif
`;
const expected = `
var<private> featureConfig: u32 = 1;
`;
expect(preprocessWgsl(code).trim()).toBe(expected.trim());
});
test('#if with numeric expressions', () => {
const code = `
#define WIDTH 800
#define HEIGHT 600
#if WIDTH / HEIGHT < 2
var<private> aspectRatio: f32 = f32(WIDTH) / f32(HEIGHT);
#endif
`;
const expected = `
var<private> aspectRatio: f32 = f32(800) / f32(600);
`;
expect(preprocessWgsl(code).trim()).toBe(expected.trim());
});
test('#if with zero value is false', () => {
const code = `
#if 0
var<private> shouldNotBeIncluded: bool = true;
#else
var<private> shouldBeIncluded: bool = true;
#endif
`;
const expected = `
var<private> shouldBeIncluded: bool = true;
`;
expect(preprocessWgsl(code).trim()).toBe(expected.trim());
});
test('#if with nonzero value is true', () => {
const code = `
#if 1
var<private> shouldBeIncluded: bool = true;
#else
var<private> shouldNotBeIncluded: bool = true;
#endif
`;
const expected = `
var<private> shouldBeIncluded: bool = true;
`;
expect(preprocessWgsl(code).trim()).toBe(expected.trim());
});
});
// Test #elif and #else
describe('#elif and #else branches', () => {
test('basic #if-#else structure', () => {
const code = `
#define VERSION 1
#if VERSION > 1
var<private> newFeature: bool = true;
#else
var<private> legacyFeature: bool = true;
#endif
`;
const expected = `
var<private> legacyFeature: bool = true;
`;
expect(preprocessWgsl(code).trim()).toBe(expected.trim());
});
test('complete #if-#elif-#else structure', () => {
const code = `
#define VERSION 2
#if VERSION == 1
var<private> version1Feature: bool = true;
#elif VERSION == 2
var<private> version2Feature: bool = true;
#else
var<private> fallbackFeature: bool = true;
#endif
`;
const expected = `
var<private> version2Feature: bool = true;
`;
expect(preprocessWgsl(code).trim()).toBe(expected.trim());
});
test('multiple #elif branches', () => {
const code = `
#define PLATFORM 3
#if PLATFORM == 1
var<private> platform: u32 = 1; // Desktop
#elif PLATFORM == 2
var<private> platform: u32 = 2; // Mobile
#elif PLATFORM == 3
var<private> platform: u32 = 3; // Web
#else
var<private> platform: u32 = 0; // Unknown
#endif
`;
const expected = `
var<private> platform: u32 = 3; // Web
`;
expect(preprocessWgsl(code).trim()).toBe(expected.trim());
});
test('only first true branch is included', () => {
const code = `
#define VALUE 5
#if VALUE > 10
var<private> result: u32 = 1;
#elif VALUE > 5
var<private> result: u32 = 2;
#elif VALUE >= 5 // This is true but should not be included as the evaluation stops
var<private> result: u32 = 3;
#else
var<private> result: u32 = 4;
#endif
`;
const expected = `
var<private> result: u32 = 3;
`;
expect(preprocessWgsl(code).trim()).toBe(expected.trim());
});
test('trailing #else with no previous true branches', () => {
const code = `
#define VALUE 3
#if VALUE > 10
var<private> result: u32 = 1;
#elif VALUE > 5
var<private> result: u32 = 2;
#else
var<private> result: u32 = 3;
#endif
`;
const expected = `
var<private> result: u32 = 3;
`;
expect(preprocessWgsl(code).trim()).toBe(expected.trim());
});
});
// Test #undef
describe('#undef directive', () => {
test('basic #undef', () => {
const code = `
#define FEATURE
#ifdef FEATURE
var<private> beforeUndefining: bool = true;
#endif
#undef FEATURE
#ifdef FEATURE
var<private> afterUndefining: bool = true;
#endif
`;
const expected = `
var<private> beforeUndefining: bool = true;
`;
expect(preprocessWgsl(code).trim()).toBe(expected.trim());
});
test('#undef with redefinition', () => {
const code = `
#define VALUE 1
var<private> value1: u32 = VALUE;
#undef VALUE
#define VALUE 2
var<private> value2: u32 = VALUE;
`;
const expected = `
var<private> value1: u32 = 1;
var<private> value2: u32 = 2;
`;
expect(preprocessWgsl(code).trim()).toBe(expected.trim());
});
test('#undef of undefined macro', () => {
const code = `
#undef UNDEFINED_MACRO
var<private> testValue: u32 = 1;
`;
const expected = `
var<private> testValue: u32 = 1;
`;
expect(preprocessWgsl(code).trim()).toBe(expected.trim());
});
test('#undef of function-like macro', () => {
const code = `
#define FUNC(x) (x * 2)
var<private> before: u32 = FUNC(5);
#undef FUNC
// This should error or not expand, depending on implementation
`;
const result = preprocessWgsl(code);
expect(result).toContain("var<private> before: u32 = (5 * 2);");
// After undefining, the macro shouldn't be available
});
});
// Test nested conditionals
describe('Nested conditionals', () => {
test('basic nested conditionals', () => {
const code = `
#define OUTER
#define INNER
#ifdef OUTER
outer_start
#ifdef INNER
inner_content
#endif
outer_end
#endif
`;
const expected = `
outer_start
inner_content
outer_end
`;
expect(preprocessWgsl(code).trim()).toBe(expected.trim());
});
test('nested conditionals with some branches excluded', () => {
const code = `
#define LEVEL 2
#if LEVEL > 0
level_greater_than_0
#if LEVEL > 1
level_greater_than_1
#if LEVEL > 2
level_greater_than_2
#endif
level_greater_than_1_end
#endif
level_greater_than_0_end
#endif
`;
const expected = `
level_greater_than_0
level_greater_than_1
level_greater_than_1_end
level_greater_than_0_end
`;
expect(preprocessWgsl(code).trim()).toBe(expected.trim());
});
test('nested conditionals with mixed directive types', () => {
const code = `
#define FEATURE_X
#ifdef FEATURE_X
feature_x_defined
#if defined(FEATURE_Y)
feature_y_also_defined
#else
feature_y_not_defined
#ifdef FEATURE_X
still_inside_feature_x
#endif
feature_x_block_end
#endif
feature_x_outer_end
#endif
`;
const expected = `
feature_x_defined
feature_y_not_defined
still_inside_feature_x
feature_x_block_end
feature_x_outer_end
`;
expect(preprocessWgsl(code).trim()).toBe(expected.trim());
});
test('deeply nested conditionals', () => {
const code = `
#define A
#define B
#define C
#ifdef A
A is defined
#ifdef B
B is defined
#ifdef C
C is defined
#endif
#endif
#endif
`;
const expected = `
A is defined
B is defined
C is defined
`;
expect(preprocessWgsl(code).trim()).toBe(expected.trim());
});
});
// Test error handling
describe('Error handling', () => {
test('unmatched #endif', () => {
const code = `
code_start
#endif
code_end
`;
expect(() => preprocessWgsl(code)).toThrow(/Unexpected #endif/);
});
test('unmatched #else', () => {
const code = `
code_start
#else
code_end
`;
expect(() => preprocessWgsl(code)).toThrow(/Unexpected #else/);
});
test('unmatched #elif', () => {
const code = `
code_start
#elif CONDITION
code_end
`;
expect(() => preprocessWgsl(code)).toThrow(/Unexpected #elif/);
});
test('missing #endif', () => {
const code = `
#ifdef FEATURE
code_inside_ifdef
// Missing #endif
`;
expect(() => preprocessWgsl(code)).toThrow(/Unmatched #if/);
});
test('multiple #else directives', () => {
const code = `
#if 1
var<private> value: u32 = 1;
#else
var<private> value: u32 = 2;
#else
var<private> value: u32 = 3;
#endif
`;
// Implementation could throw various errors here
expect(() => preprocessWgsl(code)).toThrow();
});
});
// Test advanced and edge cases
describe('Advanced cases', () => {
test('protection against recursive macros', () => {
const code = `
#define A B
#define B A
var<private> recursive = A;
`;
// This should complete without hanging, but exact result depends on implementation
expect(() => preprocessWgsl(code)).not.toThrow();
});
test('directives in comments are ignored', () => {
const code = `
// #define COMMENTED_OUT 123
/* #define ALSO_COMMENTED 456 */
#ifdef COMMENTED_OUT
var<private> shouldNotBeIncluded: u32 = 1;
#endif
`;
const expected = `// #define COMMENTED_OUT 123
/* #define ALSO_COMMENTED 456 */`;
expect(preprocessWgsl(code).trim()).toBe(expected.trim());
});
test('various whitespace patterns in macros', () => {
const code = `
#define SPACE_AFTER_DEFINE 123
#define\tTAB_AFTER_DEFINE\t456
#define NO_SPACE(x) x+1
var<private> values: vec2<u32> = vec2<u32>(SPACE_AFTER_DEFINE, TAB_AFTER_DEFINE);
var<private> result: u32 = NO_SPACE(42);
`;
const expected = `
var<private> values: vec2<u32> = vec2<u32>(123, 456);
var<private> result: u32 = 42+1;
`;
expect(preprocessWgsl(code).trim()).toBe(expected.trim());
});
test('empty conditional blocks', () => {
const code = `
#if 0
#endif
var<private> afterEmptyBlock: bool = true;
`;
const expected = `
var<private> afterEmptyBlock: bool = true;
`;
expect(preprocessWgsl(code).trim()).toBe(expected.trim());
});
test('skipping multiple levels of conditionals', () => {
const code = `
#if 0
#if 1
This should be skipped
#endif
This should also be skipped
#endif
var<private> afterSkippedBlocks: bool = true;
`;
const expected = `
var<private> afterSkippedBlocks: bool = true;
`;
expect(preprocessWgsl(code).trim()).toBe(expected.trim());
});
});
// Test complex real-world scenarios
describe('Complex scenarios', () => {
test('complex conditional nesting with multiple features', () => {
const code = `
#define RENDERER_VULKAN
#define SHADOW_QUALITY 2
#define USE_ASYNC_COMPUTE
// Common code
struct Globals {
resolution: vec2<f32>,
};
// Renderer-specific code
#ifdef RENDERER_VULKAN
const MAX_BINDLESS_RESOURCES: u32 = 1000000;
#elif defined(RENDERER_METAL)
const MAX_BINDLESS_RESOURCES: u32 = 500000;
#else
const MAX_BINDLESS_RESOURCES: u32 = 32;
#endif
// Shadow quality settings
#if SHADOW_QUALITY == 0
const SHADOW_MAP_SIZE: u32 = 512;
const SHADOW_CASCADES: u32 = 1;
#elif SHADOW_QUALITY == 1
const SHADOW_MAP_SIZE: u32 = 1024;
const SHADOW_CASCADES: u32 = 2;
#else
const SHADOW_MAP_SIZE: u32 = 2048;
const SHADOW_CASCADES: u32 = 4;
#endif
// Compute settings
#ifdef USE_ASYNC_COMPUTE
// Async compute features
#if defined(RENDERER_VULKAN) || defined(RENDERER_METAL)
const WORKGROUP_SIZE: u32 = 64;
#else
const WORKGROUP_SIZE: u32 = 32;
#endif
#endif
`;
const expected = `
// Common code
struct Globals {
resolution: vec2<f32>,
};
// Renderer-specific code
const MAX_BINDLESS_RESOURCES: u32 = 1000000;
// Shadow quality settings
const SHADOW_MAP_SIZE: u32 = 2048;
const SHADOW_CASCADES: u32 = 4;
// Compute settings
// Async compute features
const WORKGROUP_SIZE: u32 = 64;
`;
expect(preprocessWgsl(code).trim()).toBe(expected.trim());
});
test('macro expansion in other macros', () => {
const code = `
#define WIDTH 800
#define HEIGHT 600
#define RESOLUTION vec2<f32>(WIDTH, HEIGHT)
#define ASPECT_RATIO (float(WIDTH) / float(HEIGHT))
#define FOV 45.0
#define CAMERA_SETTINGS vec4<f32>(ASPECT_RATIO, FOV, 0.1, 100.0)
var<private> resolution = RESOLUTION;
var<private> cameraSettings = CAMERA_SETTINGS;
`;
const expected = `
var<private> resolution = vec2<f32>(800, 600);
var<private> cameraSettings = vec4<f32>((float(800) / float(600)), 45.0, 0.1, 100.0);
`;
expect(preprocessWgsl(code).trim()).toBe(expected.trim());
});
test('platform-specific shader configuration', () => {
const code = `
#define PLATFORM_MOBILE
#define VERSION 2
// Base shader configuration
struct ShaderConfig {
#ifdef PLATFORM_MOBILE
maxLights: u32,
#if VERSION >= 2
shadowEnabled: bool,
#endif
#else
maxLights: u32,
shadowEnabled: bool,
reflectionsEnabled: bool,
#endif
commonSetting: f32,
};
// Feature flags
#ifdef PLATFORM_MOBILE
#define MAX_LIGHTS 4
#if VERSION >= 2
#define ENABLE_SHADOWS
#endif
#else
#define MAX_LIGHTS 16
#define ENABLE_SHADOWS
#define ENABLE_REFLECTIONS
#endif
// Instantiate config
var<private> config: ShaderConfig;
`;
const expected = `
// Base shader configuration
struct ShaderConfig {
maxLights: u32,
shadowEnabled: bool,
commonSetting: f32,
};
// Feature flags
// Instantiate config
var<private> config: ShaderConfig;
`;
expect(preprocessWgsl(code).trim()).toBe(expected.trim());
});
test('shader variant generation', () => {
const code = `
#define VARIANT 2
struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) uv: vec2<f32>,
#if VARIANT == 1
@location(1) color: vec3<f32>,
#elif VARIANT == 2
@location(1) color: vec4<f32>,
@location(2) normal: vec3<f32>,
#elif VARIANT == 3
@location(1) color: vec4<f32>,
@location(2) normal: vec3<f32>,
@location(3) tangent: vec4<f32>,
#endif
};
@fragment
fn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {
var output: vec4<f32>;
#if VARIANT == 1
output = vec4<f32>(input.color, 1.0);
#elif VARIANT == 2
output = input.color * calculateLighting(input.normal);
#elif VARIANT == 3
let normalMap = textureSample(normalTexture, normalSampler, input.uv);
let worldNormal = calculateTBN(input.normal, input.tangent) * normalMap.xyz;
output = input.color * calculateLighting(worldNormal);
#endif
return output;
}
`;
const expected = `
struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) uv: vec2<f32>,
@location(1) color: vec4<f32>,
@location(2) normal: vec3<f32>,
};
@fragment
fn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {
var output: vec4<f32>;
output = input.color * calculateLighting(input.normal);
return output;
}
`;
expect(preprocessWgsl(code).trim()).toBe(expected.trim());
});
});
// Test custom directives preservation
describe('Custom directives preservation', () => {
test('basic #binding directive preservation', () => {
const code = `
#binding "data"
#binding "canvas_size"
@compute
fn main() {}
`;
const expected = `
#binding "data"
#binding "canvas_size"
@compute
fn main() {}
`;
expect(preprocessWgsl(code).trim()).toBe(expected.trim());
});
test('basic #entrypoint directive preservation', () => {
const code = `
#entrypoint "my_entry_point"
@compute
fn my_entry_point() {}
`;
const expected = `
#entrypoint "my_entry_point"
@compute
fn my_entry_point() {}
`;
expect(preprocessWgsl(code).trim()).toBe(expected.trim());
});
test('custom directives inside included conditional blocks', () => {
const code = `
#define FEATURE_ENABLED
#ifdef FEATURE_ENABLED
#binding "feature_data"
#entrypoint "feature_main"
@compute
fn feature_main() {}
#endif
`;
const expected = `
#binding "feature_data"
#entrypoint "feature_main"
@compute
fn feature_main() {}
`;
expect(preprocessWgsl(code).trim()).toBe(expected.trim());
});
test('custom directives inside excluded conditional blocks', () => {
const code = `
#ifdef UNDEFINED_FEATURE
#binding "feature_data"
#entrypoint "feature_main"
@compute
fn feature_main() {}
#endif
#binding "default_data"
@compute
fn main() {}
`;
const expected = `
#binding "default_data"
@compute
fn main() {}
`;
expect(preprocessWgsl(code).trim()).toBe(expected.trim());
});
test('custom directives with macro expansion in surrounding code', () => {
const code = `
#define WORKGROUP_SIZE 64
#define DATA_TYPE f32
#binding "simulation_data"
#entrypoint "simulate"
@compute @workgroup_size(WORKGROUP_SIZE)
fn simulate(
@builtin(global_invocation_id) id: vec3<u32>
) {
var value: DATA_TYPE = 0.0;
}
`;
const expected = `
#binding "simulation_data"
#entrypoint "simulate"
@compute @workgroup_size(64)
fn simulate(
@builtin(global_invocation_id) id: vec3<u32>
) {
var value: f32 = 0.0;
}`;
expect(preprocessWgsl(code).trim()).toBe(expected.trim());
});
test('multiple custom directives with interspersed preprocessor directives', () => {
const code = `#define GPU_COMPUTE
#define WORKGROUP_SIZE 64
#binding "pos_buffer"
#binding "vel_buffer"
#ifdef GPU_COMPUTE
#entrypoint "update_particles"
@compute @workgroup_size(WORKGROUP_SIZE)
fn update_particles() {}
#else
fn update_particles() {}
#endif
#undef GPU_COMPUTE
#define RENDER_PATH
#binding "render_target"
#ifdef RENDER_PATH
#entrypoint "render"
fn render() {}
#endif`;
const expected = `
#binding "pos_buffer"
#binding "vel_buffer"
#entrypoint "update_particles"
@compute @workgroup_size(64)
fn update_particles() {}
#binding "render_target"
#entrypoint "render"
fn render() {}`;
expect(preprocessWgsl(code).trim()).toBe(expected.trim());
});
});
});