Repository: CrossGL/crosstl (python)

Issue:
Add `else if` Conditional Statements to the DirectX backend
To add `else if` support to CrossGL, modify the following files in the `crosstl/src/backend/DirectX` directory:

- [ ] **DirectxLexer.py** : Add token recognition for `else if`.
- [ ] **DirectxParser.py** : Update grammar and AST to handle `else if`.
- [ ] **DirectxCrossGLCodeGen.py** : Implement `else if` for Crossgl output.


Your contribution is appreciated! Feel free to reach out on our [Discord Server ](https://discord.com/invite/uyRQKXhcyW) if you have any query.

Pull request description:
Added else if support to Directx backend
Grammar and AST update to support else if commands.

Provides else if support using a recursive based approach, might break due to recursion limit, but highly unlikely.

I had to place the else if token before else in the Regex parser, otherwise it would be considered as 2 separate else and if tokens. 

I cross tested my code with the following function :

float fn1(){
    float noise = 1;
    float height = noise * 10.0;
    if(noise > height){
        height = 2;
    }
    else if(noise < height){
        height = 1;
    }
    else if(noise == height){
        height = 3;
    }
    else {
        height = 0;
    }
    return height;
}

I wasn't entirely sure if I should add this to the pre existing example .hlsl files. Let me know if I should do that as well.
Closes #33 

Patch:
diff --git a/crosstl/src/backend/DirectX/DirectxCrossGLCodeGen.py b/crosstl/src/backend/DirectX/DirectxCrossGLCodeGen.py
index 910eec2..e936097 100644
--- a/crosstl/src/backend/DirectX/DirectxCrossGLCodeGen.py
+++ b/crosstl/src/backend/DirectX/DirectxCrossGLCodeGen.py
@@ -157,9 +157,13 @@ class HLSLToCrossGLConverter:
         code += "    " * indent + "}"
 
         if node.else_body:
-            code += " else {\n"
-            code += self.generate_function_body(node.else_body, indent + 1, is_main)
-            code += "    " * indent + "}"
+            if isinstance(node.else_body, IfNode):
+                code += " else "
+                code += self.generate_if_statement(node.else_body, indent, is_main)
+            else:
+                code += " else {\n"
+                code += self.generate_function_body(node.else_body, indent + 1, is_main)
+                code += "    " * indent + "}"
 
         code += "\n"
         return code
diff --git a/crosstl/src/backend/DirectX/DirectxLexer.py b/crosstl/src/backend/DirectX/DirectxLexer.py
index 70e72f9..6583503 100644
--- a/crosstl/src/backend/DirectX/DirectxLexer.py
+++ b/crosstl/src/backend/DirectX/DirectxLexer.py
@@ -16,6 +16,7 @@ TOKENS = [
     ("VOID", r"\bvoid\b"),
     ("RETURN", r"\breturn\b"),
     ("IF", r"\bif\b"),
+    ("ELSE_IF", r"\belse\sif\b"),
     ("ELSE", r"\belse\b"),
     ("FOR", r"\bfor\b"),
     ("REGISTER", r"\bregister\b"),
diff --git a/crosstl/src/backend/DirectX/DirectxParser.py b/crosstl/src/backend/DirectX/DirectxParser.py
index 1ebe60a..ea4f434 100644
--- a/crosstl/src/backend/DirectX/DirectxParser.py
+++ b/crosstl/src/backend/DirectX/DirectxParser.py
@@ -215,6 +215,22 @@ class HLSLParser:
         if self.current_token[0] == "ELSE":
             self.eat("ELSE")
             else_body = self.parse_block()
+        elif self.current_token[0] == "ELSE_IF":
+            else_body = self.parse_else_if_statement()
+        return IfNode(condition, if_body, else_body)
+
+    def parse_else_if_statement(self):
+        self.eat("ELSE_IF")
+        self.eat("LPAREN")
+        condition = self.parse_expression()
+        self.eat("RPAREN")
+        if_body = self.parse_block()
+        else_body = None
+        if self.current_token[0] == "ELSE":
+            self.eat("ELSE")
+            else_body = self.parse_block()
+        elif self.current_token[0] == "ELSE_IF":
+            else_body = self.parse_else_if_statement()
         return IfNode(condition, if_body, else_body)
 
     def parse_for_statement(self):
diff --git a/crosstl/src/translator/lexer.py b/crosstl/src/translator/lexer.py
index 0a0ca33..48ae39e 100644
--- a/crosstl/src/translator/lexer.py
+++ b/crosstl/src/translator/lexer.py
@@ -43,9 +43,15 @@ TOKENS = [
     ("DECREMENT", r"--"),
     ("EQUAL", r"=="),
     ("NOT_EQUAL", r"!="),
+    ("ASSIGN_AND", r"&="),
+    ("ASSIGN_OR", r"\|="),
+    ("ASSIGN_XOR", r"\^="),
     ("AND", r"&&"),
     ("OR", r"\|\|"),
+    ("XOR", r"\^"),
     ("NOT", r"!"),
+    ("ASSIGN_MOD", r"%="),
+    ("MOD", r"%"),
     ("INCREMENT", r"\+\+"),
     ("DECREMENT", r"\-\-"),
     ("PLUS", r"\+"),


Test patch:
diff --git a/tests/test_backend/test_directx/test_codegen.py b/tests/test_backend/test_directx/test_codegen.py
index c72432c..8e7e3a8 100644
--- a/tests/test_backend/test_directx/test_codegen.py
+++ b/tests/test_backend/test_directx/test_codegen.py
@@ -262,6 +262,59 @@ def test_function_call_codegen():
         pytest.fail("Function call parsing or code generation not implemented.")
 
 
+def test_else_if_codegen():
+    code = """
+    struct VSInput {
+    float4 position : POSITION;
+    float4 color : TEXCOORD0;
+    };
+
+    struct VSOutput {
+        float4 out_position : TEXCOORD0;
+    };
+
+    VSOutput VSMain(VSInput input) {
+        VSOutput output;
+        output.out_position =  input.position;
+        if (input.color.r > 0.5) {
+            output.out_position = input.color;
+        }
+        else {
+            output.out_position = float4(0.0, 0.0, 0.0, 1.0);
+        }
+        return output;
+    }
+
+    struct PSInput {
+        float4 in_position : TEXCOORD0;
+    };
+
+    struct PSOutput {
+        float4 out_color : SV_TARGET0;
+    };
+
+    PSOutput PSMain(PSInput input) {
+        PSOutput output;
+        if (input.in_position.r > 0.5) {
+            output.out_color = input.in_position;
+        } else if (input.in_position.r == 0.5){
+            output.out_color = float4(1.0, 1.0, 1.0, 1.0);
+        } else {
+            output.out_color = float4(0.0, 0.0, 0.0, 1.0);
+        }
+        return output;
+    }
+    """
+    try:
+        tokens = tokenize_code(code)
+        ast = parse_code(tokens)
+        generated_code = generate_code(ast)
+        print("############## else if code ##############")
+        print(generated_code)
+    except SyntaxError:
+        pytest.fail("Else_if statement parsing or code generation not implemented.")
+
+
 # Run all tests
 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 3565f97..18f7464 100644
--- a/tests/test_backend/test_directx/test_lexer.py
+++ b/tests/test_backend/test_directx/test_lexer.py
@@ -20,8 +20,10 @@ def test_struct_tokenization():
         float4 out_position : TEXCOORD0;
     };
     """
-    tokens = tokenize_code(code)
-    print(tokens)
+    try:
+        tokenize_code(code)
+    except SyntaxError:
+        pytest.fail("struct tokenization not implemented.")
 
 
 def test_if_tokenization():
@@ -38,7 +40,7 @@ def test_if_tokenization():
     try:
         tokenize_code(code)
     except SyntaxError:
-        pytest.fail("Function call tokenization not implemented.")
+        pytest.fail("if tokenization not implemented.")
 
 
 def test_for_tokenization():
@@ -54,7 +56,7 @@ def test_for_tokenization():
     try:
         tokenize_code(code)
     except SyntaxError:
-        pytest.fail("Function call tokenization not implemented.")
+        pytest.fail("for tokenization not implemented.")
 
 
 def test_else_tokenization():
@@ -72,7 +74,7 @@ def test_else_tokenization():
     try:
         tokenize_code(code)
     except SyntaxError:
-        pytest.fail("Function call tokenization not implemented.")
+        pytest.fail("else tokenization not implemented.")
 
 
 def test_function_call_tokenization():
@@ -89,6 +91,26 @@ def test_function_call_tokenization():
         pytest.fail("Function call tokenization not implemented.")
 
 
+def test_else_if_tokenization():
+    code = """
+    PSOutput PSMain(PSInput input) {
+        PSOutput output;
+        if (input.in_position.r > 0.5) {
+            output.out_color = input.in_position;
+        } else if (input.in_position.r == 0.5){
+            output.out_color = float4(1.0, 1.0, 1.0, 1.0);
+        } else {
+            output.out_color = float4(0.0, 0.0, 0.0, 1.0);
+        }
+        return output;
+    }
+    """
+    try:
+        tokenize_code(code)
+    except SyntaxError:
+        pytest.fail("else_if tokenization not implemented.")
+
+
 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 5e6f875..564351f 100644
--- a/tests/test_backend/test_directx/test_parser.py
+++ b/tests/test_backend/test_directx/test_parser.py
@@ -55,7 +55,7 @@ def test_if_parsing():
         tokens = tokenize_code(code)
         parse_code(tokens)
     except SyntaxError:
-        pytest.fail("Struct parsing not implemented.")
+        pytest.fail("if parsing not implemented.")
 
 
 def test_for_parsing():
@@ -72,7 +72,7 @@ def test_for_parsing():
         tokens = tokenize_code(code)
         parse_code(tokens)
     except SyntaxError:
-        pytest.fail("Struct parsing not implemented.")
+        pytest.fail("for parsing not implemented.")
 
 
 def test_else_parsing():
@@ -91,7 +91,7 @@ def test_else_parsing():
         tokens = tokenize_code(code)
         parse_code(tokens)
     except SyntaxError:
-        pytest.fail("Struct parsing not implemented.")
+        pytest.fail("else parsing not implemented.")
 
 
 def test_function_call_parsing():
@@ -106,7 +106,28 @@ def test_function_call_parsing():
         tokens = tokenize_code(code)
         parse_code(tokens)
     except SyntaxError:
-        pytest.fail("Struct parsing not implemented.")
+        pytest.fail("function call parsing not implemented.")
+
+
+def test_else_if_parsing():
+    code = """
+    PSOutput PSMain(PSInput input) {
+        PSOutput output;
+        if (input.in_position.r > 0.5) {
+            output.out_color = input.in_position;
+        } else if (input.in_position.r == 0.5){
+            output.out_color = float4(1.0, 1.0, 1.0, 1.0);
+        } else {
+            output.out_color = float4(0.0, 0.0, 0.0, 1.0);
+        }
+        return output;
+    }
+    """
+    try:
+        tokens = tokenize_code(code)
+        parse_code(tokens)
+    except SyntaxError:
+        pytest.fail("else_if parsing not implemented.")
 
 
 # Run all tests