Repository: hedyorg/hedy

Issue:
[BUG] Translation code does not preserve is/equal symbol
The translation code works well but has one small caveat since we allow `is` and `=` for assignment from level 6 on (since https://github.com/Felienne/hedy/pull/1504), code that uses `is` (which is allowed) get translated to `=` because in the grammar (and thus in the AST) we do not distinguish between the two.

There is a test for this in the code since https://github.com/Felienne/hedy/pull/1578, describing the current (undesirable) state.

https://github.com/Felienne/hedy/blob/c0effe1c66c2115848dda65b3e4c8b0373e6aeda/tests/test_translation_level_06.py#L41

What we want is code remains as is when translating, so code with lines with a `=` keep that and lines using a `is` keep the `is`.

This is not a big deal of course because the code will still work in the new form but I think it is not pretty. And in conditions (allowed since #1561) the example code uses `is` but then when you would translate it, it would become `=`.

And there will a similar issue with `is` versus `=` versus `==` when we address 3. of #1475 too!

The solution is not hard perse, but is a bit intricate and would involve:
1. making separate rules for an assignment with `is` and one for assignment with `=`
2. rewriting the parser so both rules map to the existing code and output the same Python
3. updating the translation code for both rules differently.



Discussion:
Maybe this would be a fun issue for you @racertop? Involves a bit of parser work that you now are quite comfortable with I guess and helps Spanish speakers!
Yes, of course! Thank you for considering me!

Resolving patch:
diff --git a/hedy.py b/hedy.py
--- a/hedy.py
+++ b/hedy.py
@@ -344,6 +344,12 @@ def ask(self, tree):
         # in level 1 there is no variable name on the left side of the ask command
         if self.level > 1:
             self.add_to_lookup(tree.children[0].children[0], tree)
+    
+    def ask_is(self, tree):
+        self.ask(tree)
+    
+    def ask_equals(self, tree):
+        self.ask(tree)
 
     def input(self, tree):
         var_name = tree.children[0].children[0]
@@ -352,10 +358,22 @@ def input(self, tree):
     def assign(self, tree):
         var_name = tree.children[0].children[0]
         self.add_to_lookup(var_name, tree.children[1])
+    
+    def assign_is(self, tree):
+        return self.assign(tree)
+    
+    def assign_equals(self, tree):
+        return self.assign(tree)
 
     def assign_list(self, tree):
         var_name = tree.children[0].children[0]
         self.add_to_lookup(var_name, tree)
+    
+    def assign_list_is(self, tree):
+        return self.assign_list(tree)
+
+    def assign_list_equals(self, tree):
+        return self.assign_list(tree)
 
     # list access is added to the lookup table not because it must be escaped
     # for example we print(dieren[1]) not print('dieren[1]')
@@ -371,6 +389,12 @@ def list_access(self, tree):
     def list_access_var(self, tree):
         self.add_to_lookup(tree.children[0].children[0], tree)
 
+    def list_access_var_is(self, tree):
+        return self.list_access_var(tree)
+
+    def list_access_var_equals(self, tree):
+        return self.list_access_var(tree)
+
     def change_list_item(self, tree):
         self.add_to_lookup(tree.children[0].children[0], tree, True)
 
@@ -413,6 +437,12 @@ def ask(self, tree):
             self.save_type_to_lookup(tree.children[0].children[0], HedyType.any)
         self.validate_args_type_allowed(tree.children[1:], Command.ask)
         return self.to_typed_tree(tree, HedyType.any)
+    
+    def ask_is(self, tree):
+        self.ask(tree)
+    
+    def ask_equals(self, tree):
+        self.ask(tree)
 
     def input(self, tree):
         self.validate_args_type_allowed(tree.children[1:], Command.ask)
@@ -435,10 +465,22 @@ def assign(self, tree):
         self.save_type_to_lookup(tree.children[0].children[0], type_)
         return self.to_typed_tree(tree, HedyType.none)
 
+    def assign_is(self, tree):
+        return self.assign(tree)
+    
+    def assign_equals(self, tree):
+        return self.assign(tree)
+    
     def assign_list(self, tree):
         self.save_type_to_lookup(tree.children[0].children[0], HedyType.list)
         return self.to_typed_tree(tree, HedyType.list)
 
+    def assign_list_is(self, tree):
+        return self.assign_list(tree)
+
+    def assign_list_equals(self, tree):
+        return self.assign_list(tree)
+        
     # TODO: list_access, list_access_var and repeat_list types can be inferred but for now use 'any'
     def list_access(self, tree):
         self.validate_args_type_allowed(tree.children[0], Command.list_access)
@@ -457,6 +499,12 @@ def list_access_var(self, tree):
         self.save_type_to_lookup(tree.children[0].children[0], HedyType.any)
         return self.to_typed_tree(tree)
 
+    def list_access_var_is(self, tree):
+        return self.list_access_var(tree)
+
+    def list_access_var_equals(self, tree):
+        return self.list_access_var(tree)
+
     def add(self, tree):
         self.validate_args_type_allowed(tree.children[1], Command.add_to_list)
         return self.to_typed_tree(tree)
@@ -731,8 +779,7 @@ def translate_keyword(self, keyword):
         # some keywords have names that are not a valid name for a command
         # that's why we call them differently in the grammar
         # we have to translate them to the regular names here for further communciation
-
-        if keyword == 'assign' or keyword == 'assign_list':
+        if keyword in  ['assign', 'assign_is', 'assign_list', 'assign_list_is']:
             return 'is'
         if keyword == 'ifelse':
             return 'else'
@@ -748,6 +795,8 @@ def translate_keyword(self, keyword):
             return 'and'
         if keyword == 'while_loop':
             return 'while'
+        if keyword == 'ask_is' or keyword == 'ask_equals':
+            return 'ask'
         return keyword
 
     def __default__(self, args, children, meta):
@@ -846,6 +895,10 @@ def ask(self, args, meta):
         # in level 2 and up, ask without arguments is a list of 1, namely the var name
         incomplete = (args == [] and self.level == 1) or (len(args) == 1 and self.level >= 2)
         return not incomplete, ('ask', meta.line)
+    def ask_is(self, args, meta):
+        return self.ask(args, meta)
+    def ask_equals(self, args, meta):
+        return self.ask(args, meta)
     def print(self, args, meta):
         return args != [], ('print', meta.line)
     def input(self, args, meta):
@@ -1234,6 +1287,30 @@ def assign(self, args):
             value = process_characters_needing_escape(value)
             return parameter + " = '" + value + "'"
 
+    def assign_is(self, args):
+        return self.assign(args)
+    
+    def assign_equals(self, args):
+        return self.assign(args)
+
+    def list_access_var_is(self, args):
+        return super().list_access_var(args)
+    
+    def list_access_var_equals(self, args):
+        return super().list_access_var(args)
+    
+    def ask_is(self, args):
+        return super().ask(args)
+    
+    def ask_equals(self, args):
+        return super().ask(args)
+    
+    def assign_list_is(self, args):
+        return super().assign_list(args)
+    
+    def assign_list_equals(self, args):
+        return super().assign_list(args)
+    
     def process_token_or_tree(self, argument):
         if type(argument) is Tree:
             return f'{str(argument.children[0])}'
@@ -1385,6 +1462,12 @@ def ask(self, args):
           except ValueError:
             pass""")  # no number? leave as string
 
+    def ask_is(self, args):
+        return self.ask(args)
+    
+    def ask_equals(self, args):
+        return self.ask(args)
+
     def process_calculation(self, args, operator):
         # arguments of a sum are either a token or a
         # tree resulting from earlier processing
@@ -1404,6 +1487,12 @@ def assign_list(self, args):
         values = args[1:]
         return parameter + " = [" + ", ".join(values) + "]"
 
+    def assign_list_is(self, args):
+        return self.assign_list(args)
+
+    def assign_list_equals(self, args):
+        return self.assign_list(args)
+
     def assign(self, args):
         right_hand_side = args[1]
 
@@ -1426,7 +1515,12 @@ def assign(self, args):
         else:
             # we no longer escape quotes here because they are now needed
             return parameter + " = " + value + ""
-
+    
+    def assign_is(self, args):
+        return self.assign(args)
+    
+    def assign_equals(self, args):
+        return self.assign(args)
 @hedy_transpiler(level=13)
 class ConvertToPython_13(ConvertToPython_12):
     def andcondition(self, args):
diff --git a/hedy_translation.py b/hedy_translation.py
--- a/hedy_translation.py
+++ b/hedy_translation.py
@@ -217,16 +217,37 @@ def multiplication(self, args):
     def division(self, args):
         return args[0] + " / " + args[1]
 
-    def assign(self, args):
+    def assign_equals(self, args):
         return args[0] + " = " + ''.join([str(c) for c in args[1:]])
 
-    def ask(self, args):
+    def assign_is(self, args):
+        return args[0] + " "+ self.keywords["is"] + " " + ''.join([str(c) for c in args[1:]])
+
+    def ask_equals(self, args):
         var = args[0]
         remaining_args = args[1:]
         return var + " = " + self.keywords["ask"] + " " + ''.join(remaining_args)
 
-    def assign_list(self, args):
+    def ask_is(self, args):
+        var = args[0]
+        remaining_args = args[1:]
+        return var + " " + self.keywords["is"] + " " + self.keywords["ask"] + " " + ''.join(remaining_args)
+
+    def assign_list_is(self, args):
         return args[0] + " " + self.keywords["is"] + " " + ', '.join([str(c) for c in args[1:]])
+    
+    def assign_list_equals(self, args):
+        return args[0] + " " + " = " + " " + ', '.join([str(c) for c in args[1:]])
+
+    def list_access_var_equals(self, args):
+        var = args[0]
+        var_list = args[1]
+        return var + " = " + var_list + " " + self.keywords["at"] + " " + args[2]
+    
+    def list_access_var_is(self, args):
+        var = args[0]
+        var_list = args[1]
+        return var + " " + self.keywords["is"] + " " + var_list + " " + self.keywords["at"]  + " " + args[2]
 
 @hedy_translator(level=7)
 class ConvertToLang7(ConvertToLang6):
@@ -324,8 +345,11 @@ def while_loop(self, args):
 
 @hedy_translator(level=16)
 class ConvertToLang16(ConvertToLang15):
-
-    def assign_list(self, args):
+    
+    def assign_list_is(self, args):
+        return args[0] + " " + self.keywords["is"] + " " + "[" + ', '.join([str(c) for c in args[1:]]) + "]"
+    
+    def assign_list_equals(self, args):
         return args[0] + " = [" + ', '.join([str(c) for c in args[1:]]) + "]"
 
     def list_access(self, args):


Test patch:
diff --git a/tests/Tester.py b/tests/Tester.py
--- a/tests/Tester.py
+++ b/tests/Tester.py
@@ -98,7 +98,7 @@ def single_level_tester(self, code, level=None, exception=None, expected=None, e
       all_commands = hedy.all_commands(code, level, lang)
       if expected_commands is not None:
         self.assertEqual(expected_commands, all_commands)
-      if (not 'ask' in all_commands) and (not 'input' in all_commands): #<- use this to run tests locally with unittest
+      if True: #(not 'ask' in all_commands) and (not 'input' in all_commands): #<- use this to run tests locally with unittest
         self.assertTrue(self.validate_Python_code(result))
       if output is not None:
         self.assertEqual(output, HedyTester.run_code(result))
diff --git a/tests/test_translation_level_06.py b/tests/test_translation_level_06.py
--- a/tests/test_translation_level_06.py
+++ b/tests/test_translation_level_06.py
@@ -1,6 +1,7 @@
 import hedy
 from test_level_01 import HedyTester
 import hedy_translation
+import textwrap
 
 # tests should be ordered as follows:
 # * Translation from English to Dutch
@@ -18,7 +19,7 @@ def test_multiplication(self):
         code = "vermenigvuldiging is 3 * 8"
 
         result = hedy_translation.translate_keywords(code, from_lang="en", to_lang="nl", level=self.level)
-        expected = "vermenigvuldiging = 3 * 8"
+        expected = "vermenigvuldiging is 3 * 8"
 
         self.assertEqual(expected, result)
 
@@ -34,22 +35,69 @@ def test_division_dutch_english(self):
         code = "angle is 360 / angles"
 
         result = hedy_translation.translate_keywords(code, from_lang="nl", to_lang="en", level=self.level)
-        expected = "angle = 360 / angles"
+        expected = "angle is 360 / angles"
 
         self.assertEqual(expected, result)
 
     def test_division_with_equals_dutch_english(self):
-        code = "angle is 360 / angles"
+        code = "angle = 360 / angles"
 
         result = hedy_translation.translate_keywords(code, from_lang="nl", to_lang="en", level=self.level)
         expected = "angle = 360 / angles"
-
         self.assertEqual(expected, result)
+    
+    def test_translate_back_is(self):
+        code ="breuk is 13 / 4"
+
+        result = hedy_translation.translate_keywords(code, from_lang="en", to_lang="nl", level=self.level)
+        result = hedy_translation.translate_keywords(result, from_lang="nl", to_lang="en", level=self.level)
 
-    def test_translate_back(self):
-        code ="breuk = 13 / 4"
+        self.assertEqual(code, result)
+
+    def test_translate_back_equals(self):
+        code = "breuk = 13 / 4"
 
         result = hedy_translation.translate_keywords(code, from_lang="en", to_lang="nl", level=self.level)
         result = hedy_translation.translate_keywords(result, from_lang="nl", to_lang="en", level=self.level)
 
-        self.assertEqual(code, result)
\ No newline at end of file
+        self.assertEqual(code, result)
+
+    def test_ask_with_equals_spanish_english(self):
+        code = "nombre = preguntar '¿Cual es tu nombre?'"
+
+        result = hedy_translation.translate_keywords(code, from_lang="es", to_lang="en", level=self.level)
+
+        expected = "nombre = ask '¿Cual es tu nombre?'"
+
+        self.assertEqual(expected, result)
+
+    def test_ask_with_is_spanish_english(self):
+        code = "nombre es preguntar '¿Cual es tu nombre?'"
+
+        result = hedy_translation.translate_keywords(code, from_lang="es", to_lang="en", level=self.level)
+
+        expected = "nombre is ask '¿Cual es tu nombre?'"
+
+        self.assertEqual(expected, result)
+    
+    def test_assign_list_is_spanish_english(self):
+        code = "lenguajes es Hedy, Python, C"
+
+        result = hedy_translation.translate_keywords(code, from_lang="es", to_lang="en", level=self.level)
+
+        expected = "lenguajes is Hedy, Python, C"
+
+        self.assertEqual(result, expected)
+
+    def test_assign_list_var_spanish_english(self):
+        code = textwrap.dedent("""\
+        lenguajes es Hedy, Python, C
+        a = lenguajes en 0""")
+
+        result = hedy_translation.translate_keywords(code, from_lang="es", to_lang="en", level=self.level)
+
+        expected = textwrap.dedent("""\
+        lenguajes is Hedy, Python, C
+        a = lenguajes at 0""")
+
+        self.assertEqual(result, expected)
\ No newline at end of file