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.
1,538 lines (1,234 loc) • 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 *