Repository: CrossGL/crosstl (python)

Issue:
Add Parsing for `Bitwise NOT`  Token
Update the parser to handle the BITWISE_NOT token, allowing it to correctly parse expressions involving the ~ operator.

<!-- BOT_STATE: {} -->

Pull request:
Feature bitwise not support
### Summary
Adds bitwise NOT operator support to the shader translation system to enable bitwise operations in shaders.

### Related Issue
Closes #73 - Add support for bitwise NOT operator

### Changes
- Added bitwise NOT operator support in lexer and parser
- Updated DirectX and Metal backend parsers  
- Added test coverage for bitwise operations
- Modified files:
  - crosstl/tests/test_translator/test_parser.py
  - crosstl/tests/test_translator/test_lexer.py
  - crosstl/crosstl/backend/Metal/MetalCrossGLCodeGen.py
  - crosstl/crosstl/translator/lexer.py
  - crosstl/crosstl/backend/DirectX/DirectxParser.py

### Shader Sample
crossgl
shader test {
void main() {
int a = 5;
int b = ~a; // Bitwise NOT operation
}
}

### Testing
- Added new test cases for bitwise NOT operations
- Added lexer tests for token recognition
- Added parser tests for AST generation 
- All existing tests pass
- Tested locally with sample shaders

### Checklist
- [x] Tests cover new or changed code
- [x] Linked the issue with a closing keyword (#73)
- [x] Only touched files related to the issue
- [x] Everything builds and runs locally

Patch:
diff --git a/crosstl/backend/DirectX/DirectxLexer.py b/crosstl/backend/DirectX/DirectxLexer.py
index 22c12f8..e2ac1da 100644
--- a/crosstl/backend/DirectX/DirectxLexer.py
+++ b/crosstl/backend/DirectX/DirectxLexer.py
@@ -9,6 +9,7 @@ TOKENS = tuple(
     [
         ("COMMENT_SINGLE", r"//.*"),
         ("COMMENT_MULTI", r"/\*[\s\S]*?\*/"),
+        ("BITWISE_NOT", r"~"),
         ("INCLUDE", r"\#include\b"),
         ("STRUCT", r"\bstruct\b"),
         ("CBUFFER", r"\bcbuffer\b"),
diff --git a/crosstl/backend/DirectX/DirectxParser.py b/crosstl/backend/DirectX/DirectxParser.py
index 79de030..572847c 100644
--- a/crosstl/backend/DirectX/DirectxParser.py
+++ b/crosstl/backend/DirectX/DirectxParser.py
@@ -527,7 +527,7 @@ class HLSLParser:
         return left
 
     def parse_unary(self):
-        if self.current_token[0] in ["PLUS", "MINUS"]:
+        if self.current_token[0] in ["PLUS", "MINUS", "BITWISE_NOT"]:
             op = self.current_token[1]
             self.eat(self.current_token[0])
             operand = self.parse_unary()
diff --git a/crosstl/backend/Metal/MetalLexer.py b/crosstl/backend/Metal/MetalLexer.py
index 963b22c..349144c 100644
--- a/crosstl/backend/Metal/MetalLexer.py
+++ b/crosstl/backend/Metal/MetalLexer.py
@@ -8,6 +8,7 @@ TOKENS = tuple(
     [
         ("COMMENT_SINGLE", r"//.*"),
         ("COMMENT_MULTI", r"/\*[\s\S]*?\*/"),
+        ("BITWISE_NOT", r"~"),
         ("PREPROCESSOR", r"#\w+"),
         ("STRUCT", r"\bstruct\b"),
         ("CONSTANT", r"\bconstant\b"),
diff --git a/crosstl/backend/Metal/MetalParser.py b/crosstl/backend/Metal/MetalParser.py
index d121507..01da9d6 100644
--- a/crosstl/backend/Metal/MetalParser.py
+++ b/crosstl/backend/Metal/MetalParser.py
@@ -460,7 +460,7 @@ class MetalParser:
         return left
 
     def parse_unary(self):
-        if self.current_token[0] in ["PLUS", "MINUS"]:
+        if self.current_token[0] in ["PLUS", "MINUS", "BITWISE_NOT"]:
             op = self.current_token[1]
             self.eat(self.current_token[0])
             operand = self.parse_unary()
diff --git a/crosstl/backend/Mojo/MojoLexer.py b/crosstl/backend/Mojo/MojoLexer.py
index 4faa8f3..2d69251 100644
--- a/crosstl/backend/Mojo/MojoLexer.py
+++ b/crosstl/backend/Mojo/MojoLexer.py
@@ -8,6 +8,7 @@ TOKENS = tuple(
     [
         ("COMMENT_SINGLE", r"#.*"),
         ("COMMENT_MULTI", r'"""[\s\S]*?"""'),
+        ("BITWISE_NOT", r"~"),
         ("STRUCT", r"\bstruct\b"),
         ("LET", r"\blet\b"),
         ("VAR", r"\bvar\b"),
diff --git a/crosstl/backend/Mojo/MojoParser.py b/crosstl/backend/Mojo/MojoParser.py
index e2ac303..4daa727 100644
--- a/crosstl/backend/Mojo/MojoParser.py
+++ b/crosstl/backend/Mojo/MojoParser.py
@@ -468,7 +468,7 @@ class MojoParser:
         return left
 
     def parse_unary(self):
-        if self.current_token[0] in ["PLUS", "MINUS"]:
+        if self.current_token[0] in ["PLUS", "MINUS", "BITWISE_NOT"]:
             op = self.current_token[1]
             self.eat(self.current_token[0])
             operand = self.parse_unary()
diff --git a/crosstl/backend/Opengl/OpenglParser.py b/crosstl/backend/Opengl/OpenglParser.py
index 370334c..01f30c4 100644
--- a/crosstl/backend/Opengl/OpenglParser.py
+++ b/crosstl/backend/Opengl/OpenglParser.py
@@ -729,11 +729,11 @@ class GLSLParser:
             ASTNode: An ASTNode object representing the unary expression
 
         """
-        if self.current_token[0] in ["PLUS", "MINUS"]:
-            op = self.current_token[0]
-            self.eat(op)
-            expr = self.parse_unary()
-            return UnaryOpNode(op, expr)
+        if self.current_token[0] in ["PLUS", "MINUS", "BITWISE_NOT"]:
+            op = self.current_token[1]
+            self.eat(self.current_token[0])
+            operand = self.parse_unary()
+            return UnaryOpNode(op, operand)
         return self.parse_primary()
 
     def parse_primary(self):
diff --git a/crosstl/backend/Vulkan/VulkanLexer.py b/crosstl/backend/Vulkan/VulkanLexer.py
index b89f4d5..00aa5d5 100644
--- a/crosstl/backend/Vulkan/VulkanLexer.py
+++ b/crosstl/backend/Vulkan/VulkanLexer.py
@@ -8,6 +8,7 @@ TOKENS = tuple(
     [
         ("COMMENT_SINGLE", r"//.*"),
         ("COMMENT_MULTI", r"/\*[\s\S]*?\*/"),
+        ("BITWISE_NOT", r"~"),
         ("WHITESPACE", r"\s+"),
         ("SEMANTIC", r":\w+"),
         ("PRE_INCREMENT", r"\+\+(?=\w)"),
diff --git a/crosstl/backend/Vulkan/VulkanParser.py b/crosstl/backend/Vulkan/VulkanParser.py
index f72d68d..b0015d0 100644
--- a/crosstl/backend/Vulkan/VulkanParser.py
+++ b/crosstl/backend/Vulkan/VulkanParser.py
@@ -445,6 +445,11 @@ class VulkanParser:
             value = self.parse_primary()
             return UnaryOpNode("-", value)
 
+        if self.current_token[0] == "BITWISE_NOT":
+            self.eat("BITWISE_NOT")
+            value = self.parse_primary()
+            return UnaryOpNode("~", value)
+
         if (
             self.current_token[0] == "IDENTIFIER"
             or self.current_token[1] in VALID_DATA_TYPES
@@ -663,3 +668,11 @@ class VulkanParser:
         self.eat("IDENTIFIER")
         self.eat("SEMICOLON")
         return UniformNode(name, var_type)
+
+    def parse_unary(self):
+        if self.current_token[0] in ["PLUS", "MINUS", "BITWISE_NOT"]:
+            op = self.current_token[1]
+            self.eat(self.current_token[0])
+            operand = self.parse_unary()
+            return UnaryOpNode(op, operand)
+        return self.parse_primary()
diff --git a/crosstl/backend/slang/SlangLexer.py b/crosstl/backend/slang/SlangLexer.py
index 44dc39f..e7740b8 100644
--- a/crosstl/backend/slang/SlangLexer.py
+++ b/crosstl/backend/slang/SlangLexer.py
@@ -8,6 +8,7 @@ TOKENS = tuple(
     [
         ("COMMENT_SINGLE", r"//.*"),
         ("COMMENT_MULTI", r"/\*[\s\S]*?\*/"),
+        ("BITWISE_NOT", r"~"),
         ("STRUCT", r"\bstruct\b"),
         ("CBUFFER", r"\bcbuffer\b"),
         ("TYPE_SHADER", r'\[shader\("(vertex|fragment|compute)"\)\]'),
diff --git a/crosstl/backend/slang/SlangParser.py b/crosstl/backend/slang/SlangParser.py
index daa5200..02ea263 100644
--- a/crosstl/backend/slang/SlangParser.py
+++ b/crosstl/backend/slang/SlangParser.py
@@ -497,7 +497,7 @@ class SlangParser:
         return left
 
     def parse_unary(self):
-        if self.current_token[0] in ["PLUS", "MINUS"]:
+        if self.current_token[0] in ["PLUS", "MINUS", "BITWISE_NOT"]:
             op = self.current_token[1]
             self.eat(self.current_token[0])
             operand = self.parse_unary()
diff --git a/crosstl/translator/lexer.py b/crosstl/translator/lexer.py
index 2192941..84cad4f 100644
--- a/crosstl/translator/lexer.py
+++ b/crosstl/translator/lexer.py
@@ -7,6 +7,7 @@ TOKENS = OrderedDict(
         ("COMMENT_SINGLE", r"//.*"),
         ("COMMENT_MULTI", r"/\*[\s\S]*?\*/"),
         ("SHADER", r"\bshader\b"),
+        ("BITWISE_NOT", r"~"),
         ("VOID", r"\bvoid\b"),
         ("STRUCT", r"\bstruct\b"),
         ("CBUFFER", r"\bcbuffer\b"),
diff --git a/crosstl/translator/parser.py b/crosstl/translator/parser.py
index c17f72a..18c76d2 100644
--- a/crosstl/translator/parser.py
+++ b/crosstl/translator/parser.py
@@ -690,11 +690,9 @@ class Parser:
         This method parses a unary expression in the shader code.
 
         Returns:
-
             ASTNode: An ASTNode object representing the unary expression
-
         """
-        if self.current_token[0] in ["PLUS", "MINUS"]:
+        if self.current_token[0] in ["PLUS", "MINUS", "BITWISE_NOT"]:
             op = self.current_token[0]
             self.eat(op)
             expr = self.parse_unary()


Test patch:
diff --git a/tests/test_backend/test_directx/test_codegen.py b/tests/test_backend/test_directx/test_codegen.py
index 292a824..0ffca73 100644
--- a/tests/test_backend/test_directx/test_codegen.py
+++ b/tests/test_backend/test_directx/test_codegen.py
@@ -718,5 +718,21 @@ def test_half_dtype_codegen():
         pytest.fail("half dtype parsing or code generation not implemented.")
 
 
+def test_bitwise_not_codegen():
+    code = """
+    void main() {
+        int a = 5;
+        int b = ~a;  // Bitwise NOT
+    }
+    """
+    try:
+        tokens = tokenize_code(code)
+        ast = parse_code(tokens)
+        generated_code = generate_code(ast)
+        print(generated_code)
+    except SyntaxError:
+        pytest.fail("Bitwise NOT operator code generation not implemented")
+
+
 if __name__ == "__main__":
     pytest.main()
diff --git a/tests/test_backend/test_directx/test_lexer.py b/tests/test_backend/test_directx/test_lexer.py
index 02f78a2..cf1b7b0 100644
--- a/tests/test_backend/test_directx/test_lexer.py
+++ b/tests/test_backend/test_directx/test_lexer.py
@@ -299,5 +299,20 @@ def test_half_dtype_tokenization():
         pytest.fail("half dtype tokenization is not implemented.")
 
 
+def test_bitwise_not_tokenization():
+    code = """
+        int a = ~5;  // Bitwise NOT
+    """
+    tokens = tokenize_code(code)
+
+    has_not = False
+    for token in tokens:
+        if token == ("BITWISE_NOT", "~"):
+            has_not = True
+            break
+
+    assert has_not, "Bitwise NOT operator (~) not tokenized correctly"
+
+
 if __name__ == "__main__":
     pytest.main()
diff --git a/tests/test_backend/test_directx/test_parser.py b/tests/test_backend/test_directx/test_parser.py
index 7af3a86..fb1a46d 100644
--- a/tests/test_backend/test_directx/test_parser.py
+++ b/tests/test_backend/test_directx/test_parser.py
@@ -415,5 +415,19 @@ def test_double_dtype_parsing():
         pytest.fail("half dtype not implemented.")
 
 
+def test_bitwise_not_parsing():
+    code = """
+    void main() {
+        int a = 5;
+        int b = ~a;  // Bitwise NOT
+    }
+    """
+    try:
+        tokens = tokenize_code(code)
+        parse_code(tokens)
+    except SyntaxError:
+        pytest.fail("Bitwise NOT operator parsing not implemented")
+
+
 if __name__ == "__main__":
     pytest.main()
diff --git a/tests/test_backend/test_metal/test_codegen.py b/tests/test_backend/test_metal/test_codegen.py
index 12a0426..5e50729 100644
--- a/tests/test_backend/test_metal/test_codegen.py
+++ b/tests/test_backend/test_metal/test_codegen.py
@@ -331,5 +331,21 @@ def test_else_if():
     print(generated_code)
 
 
+def test_bitwise_not_codegen():
+    code = """
+    void main() {
+        int a = 5;
+        int b = ~a;  // Bitwise NOT
+    }
+    """
+    try:
+        tokens = tokenize_code(code)
+        ast = parse_code(tokens)
+        generated_code = generate_code(ast)
+        print(generated_code)
+    except SyntaxError:
+        pytest.fail("Bitwise NOT operator code generation not implemented")
+
+
 if __name__ == "__main__":
     pytest.main()
diff --git a/tests/test_backend/test_metal/test_lexer.py b/tests/test_backend/test_metal/test_lexer.py
index 8ea7b83..2569891 100644
--- a/tests/test_backend/test_metal/test_lexer.py
+++ b/tests/test_backend/test_metal/test_lexer.py
@@ -142,5 +142,20 @@ def test_mod_tokenization():
     assert has_mod, "Modulus operator (%) not tokenized correctly"
 
 
+def test_bitwise_not_tokenization():
+    code = """
+        int a = ~5;  // Bitwise NOT
+    """
+    tokens = tokenize_code(code)
+
+    has_not = False
+    for token in tokens:
+        if token == ("BITWISE_NOT", "~"):
+            has_not = True
+            break
+
+    assert has_not, "Bitwise NOT operator (~) not tokenized correctly"
+
+
 if __name__ == "__main__":
     pytest.main()
diff --git a/tests/test_backend/test_metal/test_parser.py b/tests/test_backend/test_metal/test_parser.py
index 15b2f6f..35e9764 100644
--- a/tests/test_backend/test_metal/test_parser.py
+++ b/tests/test_backend/test_metal/test_parser.py
@@ -167,5 +167,19 @@ def test_mod_parsing():
         pytest.fail("Modulus operator parsing not implemented")
 
 
+def test_bitwise_not_parsing():
+    code = """
+    void main() {
+        int a = 5;
+        int b = ~a;  // Bitwise NOT
+    }
+    """
+    try:
+        tokens = tokenize_code(code)
+        parse_code(tokens)
+    except SyntaxError:
+        pytest.fail("Bitwise NOT operator parsing not implemented")
+
+
 if __name__ == "__main__":
     pytest.main()
diff --git a/tests/test_backend/test_mojo/test_lexer.py b/tests/test_backend/test_mojo/test_lexer.py
index 95f7d18..3b98728 100644
--- a/tests/test_backend/test_mojo/test_lexer.py
+++ b/tests/test_backend/test_mojo/test_lexer.py
@@ -25,5 +25,18 @@ def test_mod_tokenization():
     assert has_mod, "Modulus operator (%) not tokenized correctly"
 
 
+def test_bitwise_not_tokenization():
+    code = """
+        int a = ~5;  // Bitwise NOT
+    """
+    tokens = tokenize_code(code)
+    has_not = False
+    for token in tokens:
+        if token == ("BITWISE_NOT", "~"):
+            has_not = True
+            break
+    assert has_not, "Bitwise NOT operator (~) not tokenized correctly"
+
+
 if __name__ == "__main__":
     pytest.main()
diff --git a/tests/test_backend/test_opengl/test_lexer.py b/tests/test_backend/test_opengl/test_lexer.py
index ab67738..61c490c 100644
--- a/tests/test_backend/test_opengl/test_lexer.py
+++ b/tests/test_backend/test_opengl/test_lexer.py
@@ -128,17 +128,27 @@ def test_mod_tokenization():
         int a = 10 % 3;  // Basic modulus
     """
     tokens = tokenize_code(code)
-
-    # Find the modulus operator in tokens
     has_mod = False
     for token in tokens:
         if token == ("MOD", "%"):
             has_mod = True
             break
-
     assert has_mod, "Modulus operator (%) not tokenized correctly"
 
 
+def test_bitwise_not_tokenization():
+    code = """
+        int a = ~5;  // Bitwise NOT
+    """
+    tokens = tokenize_code(code)
+    has_not = False
+    for token in tokens:
+        if token == ("BITWISE_NOT", "~"):
+            has_not = True
+            break
+    assert has_not, "Bitwise NOT operator (~) not tokenized correctly"
+
+
 def test_unsigned_int_dtype_tokenization():
     code = """
     double ComputeArea(double radius) {
diff --git a/tests/test_backend/test_slang/test_lexer.py b/tests/test_backend/test_slang/test_lexer.py
index 1ef8bd2..2f7245b 100644
--- a/tests/test_backend/test_slang/test_lexer.py
+++ b/tests/test_backend/test_slang/test_lexer.py
@@ -106,16 +106,26 @@ def test_mod_tokenization():
         int a = 10 % 3;  // Basic modulus
     """
     tokens = tokenize_code(code)
-
-    # Find the modulus operator in tokens
     has_mod = False
     for token in tokens:
         if token == ("MOD", "%"):
             has_mod = True
             break
-
     assert has_mod, "Modulus operator (%) not tokenized correctly"
 
 
+def test_bitwise_not_tokenization():
+    code = """
+        int a = ~5;  // Bitwise NOT
+    """
+    tokens = tokenize_code(code)
+    has_not = False
+    for token in tokens:
+        if token == ("BITWISE_NOT", "~"):
+            has_not = True
+            break
+    assert has_not, "Bitwise NOT operator (~) not tokenized correctly"
+
+
 if __name__ == "__main__":
     pytest.main()
diff --git a/tests/test_backend/test_vulkan/test_lexer.py b/tests/test_backend/test_vulkan/test_lexer.py
index e68db35..bd8938e 100644
--- a/tests/test_backend/test_vulkan/test_lexer.py
+++ b/tests/test_backend/test_vulkan/test_lexer.py
@@ -25,5 +25,18 @@ def test_mod_tokenization():
     assert has_mod, "Modulus operator (%) not tokenized correctly"
 
 
+def test_bitwise_not_tokenization():
+    code = """
+        int a = ~5;  // Bitwise NOT
+    """
+    tokens = tokenize_code(code)
+    has_not = False
+    for token in tokens:
+        if token == ("BITWISE_NOT", "~"):
+            has_not = True
+            break
+    assert has_not, "Bitwise NOT operator (~) not tokenized correctly"
+
+
 if __name__ == "__main__":
     pytest.main()
diff --git a/tests/test_backend/test_vulkan/test_parser.py b/tests/test_backend/test_vulkan/test_parser.py
index ec7789a..36f9976 100644
--- a/tests/test_backend/test_vulkan/test_parser.py
+++ b/tests/test_backend/test_vulkan/test_parser.py
@@ -36,5 +36,19 @@ def test_mod_parsing():
         pytest.fail("Modulus operator parsing not implemented")
 
 
+def test_bitwise_not_parsing():
+    code = """
+    void main() {
+        int a = 5;
+        int b = ~a;  // Bitwise NOT
+    }
+    """
+    try:
+        tokens = tokenize_code(code)
+        parse_code(tokens)
+    except SyntaxError:
+        pytest.fail("Bitwise NOT operator parsing not implemented")
+
+
 if __name__ == "__main__":
     pytest.main()
diff --git a/tests/test_translator/test_lexer.py b/tests/test_translator/test_lexer.py
index 35c778a..f4793b0 100644
--- a/tests/test_translator/test_lexer.py
+++ b/tests/test_translator/test_lexer.py
@@ -256,5 +256,19 @@ def test_illegal_character():
         tokenize_code(code)
 
 
+def test_bitwise_not_tokenization():
+    code = """
+        int a = 5;
+        int b = ~a;  // Bitwise NOT
+    """
+    tokens = tokenize_code(code)
+    has_not = False
+    for token in tokens:
+        if token == ("BITWISE_NOT", "~"):
+            has_not = True
+            break
+    assert has_not, "Bitwise NOT operator (~) not tokenized correctly"
+
+
 if __name__ == "__main__":
     pytest.main()
diff --git a/tests/test_translator/test_parser.py b/tests/test_translator/test_parser.py
index 8f3ca4a..e2e3463 100644
--- a/tests/test_translator/test_parser.py
+++ b/tests/test_translator/test_parser.py
@@ -2,6 +2,7 @@ from crosstl.translator.lexer import Lexer
 import pytest
 from typing import List
 from crosstl.translator.parser import Parser
+from crosstl.translator.ast import ShaderNode
 
 
 def tokenize_code(code: str) -> List:
@@ -621,5 +622,42 @@ def test_modulus_operations():
         pytest.fail("Modulus operations not working")
 
 
+def test_bitwise_not():
+    code = """
+    shader test {
+        void main() {
+            int a = 5;
+            int b = ~a;  // Bitwise NOT
+        }
+    }
+    """
+    try:
+        tokens = tokenize_code(code)
+        ast = parse_code(tokens)
+        assert isinstance(ast, ShaderNode)
+    except SyntaxError:
+        pytest.fail("Bitwise NOT operator parsing failed")
+
+
+def test_bitwise_expressions():
+    code = """
+    shader test {
+        void main() {
+            int a = 5;
+            int b = ~a;  // Bitwise NOT
+            int c = a & b;  // Bitwise AND
+            int d = a | b;  // Bitwise OR
+            int e = a ^ b;  // Bitwise XOR
+        }
+    }
+    """
+    try:
+        tokens = tokenize_code(code)
+        ast = parse_code(tokens)
+        assert isinstance(ast, ShaderNode)
+    except SyntaxError:
+        pytest.fail("Bitwise expressions parsing failed")
+
+
 if __name__ == "__main__":
     pytest.main()