UNPKG

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
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 *