Repository: hedyorg/hedy

Issue:
Confusing: level-1 reported when compiling on level fails
See the new "run locally" functionality that was/is being added in #120.

Run the following program:

```
#level 5
print Hello world!
```

This is an incorrect program for level 5 (it's actually a level 1 program), so we are expecting to see a parse error. However, running it and printing the full error message is a little confusing:

```
$ python3 hedy.py test.hedy
An error occurred: Parse
level=4           <--- 🚨THIS IS CONFUSING
parse_error=No terminal defined for '!' at line 1 col 18

print hello world!
                 ^

Expecting: {'SPACE', '__ANON_8', '_EOL'}
```

What's happening is that on catching a parsing mistake, the parsing is retried on a level lower:

https://github.com/Felienne/hedy/blob/b4359bf202c952fd1d946e5a91a57abcbe2991b7/hedy.py#L633-L638

However, if parsing on level 4 **also** fails, THAT exception is not caught again but left to bubble up to the user, which is now very confusing because the final exception the user sees is about the failing parse at level 4, instead of the failing parse at level 5.


Discussion:
yeah, I think for now we should disable the "fall back" compilation until it works better!

Resolving patch:
diff --git a/hedy.py b/hedy.py
--- a/hedy.py
+++ b/hedy.py
@@ -499,7 +499,7 @@ def _generate_indentation(self):
 
     def start(self, children):
         return "".join(self._call_children(children))
-    
+
     def statement(self, children):
         args = self._call_children(children)
         return "".join([self._generate_indentation() + x for x in args])
@@ -581,7 +581,7 @@ def modulo(self, children):
     def unary_plus(self, children):
         args = self._call_children(children)
         return args[0]
-    
+
     def unary_minus(self, children):
         args = self._call_children(children)
         return "-" + args[0]
@@ -630,20 +630,19 @@ def transpile(input_string, level):
     try:
         return transpile_inner(input_string, level)
     except Exception as E:
-        #we retry HedyExceptions of the type Parse (and Lark Errors) but we raise Invalids
+        # we retry HedyExceptions of the type Parse (and Lark Errors) but we raise Invalids
         if E.args[0] == 'Parse':
             #try 1 level lower
             if level > 1:
                 try:
-                    new_level = level-1
-                    result = transpile_inner(input_string, level-1)
-                    raise HedyException('Wrong Level', correct_code = result, original_level=level, working_level=new_level)
-                except LarkError as e:
-                    raise HedyException('Parse', level=level, parse_error=e.args[0])
-            else:
-                raise E
-        else:
-            raise E
+                    new_level = level - 1
+                    result = transpile_inner(input_string, new_level)
+                except (LarkError, HedyException) as innerE:
+                    # Parse at `level - 1` failed as well, just re-raise original error
+                    raise E
+                # If the parse at `level - 1` succeeded, then a better error is "wrong level"
+                raise HedyException('Wrong Level', correct_code=result, original_level=level, working_level=new_level) from E
+        raise E
 
 def repair(input_string):
     #the only repair we can do now is remove leading spaces, more can be added!
@@ -664,7 +663,7 @@ def transpile_inner(input_string, level):
 
         except Exception as e:
             # TODO: if all else fails, here we could translate Lark error messages into more sensible texts!
-            raise HedyException('Parse', level=level, parse_error=e.args[0])
+            raise HedyException('Parse', level=level, parse_error=e.args[0]) from e
 
         is_valid = IsValid().transform(program_root)
         if not is_valid[0]:
@@ -728,9 +727,9 @@ def transpile_inner(input_string, level):
         except VisitError as E:
             raise E.orig_exc
     elif level >= 8 and level <= 13:
-        parser = Lark(create_grammar(level), parser='lalr', postlex=BasicIndenter2(), debug=True) 
+        parser = Lark(create_grammar(level), parser='lalr', postlex=BasicIndenter2(), debug=True)
         python = 'import random\n'
-        python += ConvertToPython().transform(parser.parse(input_string + '\n'))  # TODO: temporary fix, statements have to end with _EOL 
+        python += ConvertToPython().transform(parser.parse(input_string + '\n'))  # TODO: temporary fix, statements have to end with _EOL
         return python
     else:
         raise Exception('Levels over 7 are not implemented yet')


Test patch:
diff --git a/tests.py b/tests.py
--- a/tests.py
+++ b/tests.py
@@ -42,6 +42,18 @@ def test_print_with_list_var_random(self):
             self.assertIn(run_code(result), ['Hond', 'Kat', 'Kangoeroe'])
             print('Passed at level ', i)
 
+
+    def test_parse_error_shows_right_level(self):
+        """Check that a parse error that can't be fixed by downgrading the level is propagated properly."""
+
+        # This isn't correct Hedy at level 5 nor level 4
+        try:
+            hedy.transpile("print Hello world!", 5)
+            self.fail('Should have thrown')
+        except hedy.HedyException as e:
+            self.assertEqual(e.error_code, 'Parse')
+            self.assertEqual(e.arguments.get('level'), 5)
+
     # def test_print_undefined_var(self):
     #     min_level = 7
     #
@@ -153,10 +165,6 @@ def test_transpile_other(self):
             result = hedy.transpile("abc felienne 123", 2)
         self.assertEqual(str(context.exception), 'Invalid')
 
-
-
-
-
     def test_transpile_ask_Spanish(self):
         result = hedy.transpile("ask ask Cuál es tu color favorito?", 2)
         self.assertEqual(result, "answer = input('ask Cuál es tu color favorito?')")
@@ -170,9 +178,6 @@ def test_spaces_in_arguments(self):
         result = hedy.transpile("print hallo      wereld", 2)
         self.assertEqual(result, "import random\nprint('hallo'+' '+'wereld')")
 
-
-
-
     def test_transpile_print(self):
         result = hedy.transpile("print Hallo welkom bij Hedy!", 2)
         self.assertEqual(result, "import random\nprint('Hallo'+' '+'welkom'+' '+'bij'+' '+'Hedy'+'!')")
@@ -925,7 +930,7 @@ def test_nesting(self):
                 x = 22
         else:
             x = 222
-""") 
+""")
 
 class TestsLevel10(unittest.TestCase):
     def test_list(self):