Repository: hedyorg/hedy

Issue:
VarUndefined should be raised in levels 5 and higher
We have the error but it is no longer in use since the new grammar system (but it could be!)


Discussion:
#699 Reinstates this and has updated the Dutch error message, this needs to happen for the other langs too
https://github.com/Felienne/hedy/pull/715 updates the text for all langs, but levels over 5 are still open.

Resolving patch:
diff --git a/hedy.py b/hedy.py
--- a/hedy.py
+++ b/hedy.py
@@ -232,7 +232,7 @@ def filter_ask_assign(self, args):
         return ask_assign
 
     def for_loop(self, args):
-        # for loop iterator is a var so should be added to the list of vars
+      # for loop iterator is a var so should be added to the list of vars
       iterator = str(args[0])
       commands = args[1:]
       return [iterator] + self.filter_ask_assign(args)
@@ -577,6 +577,12 @@ def process_variable(name, lookup):
     else:
         return f"'{name}'"
 
+def process_variable_for_fstring(name, lookup):
+    #processes a variable by hashing and escaping when needed
+    if name in lookup:
+        return "{" + hash_var(name) + "}"
+    else:
+        return name
 
 @hedy_transpiler(level=2)
 class ConvertToPython_2(ConvertToPython_1):
@@ -680,16 +686,27 @@ def var_access(self, args):
     def text(self, args):
         return ''.join([str(c) for c in args])
 
-    def print(self, args):
+    def check_print_arguments(self, args):
+        # this function checks whether arguments of a print are valid
+        #we can print if all arguments are either quoted OR they are all variables
+
         unquoted_args = [a for a in args if not is_quoted(a)]
         unquoted_in_lookup = [a in self.lookup for a in unquoted_args]
-        #we can print if all arguments are quoted OR they are all variables
+
         if unquoted_in_lookup == [] or all(unquoted_in_lookup):
-            return make_f_string(args, self.lookup)
+            # all good? return for further processing
+            return args
         else:
+            # return first name with issue
+            # note this is where issue #832 can be addressed by checking whether
+            # first_unquoted_var ius similar to something in the lookup list
             first_unquoted_var = unquoted_args[0]
             raise UndefinedVarException(name=first_unquoted_var)
 
+    def print(self, args):
+        args = self.check_print_arguments(args)
+        return make_f_string(args, self.lookup)
+
     def print_nq(self, args):
         return ConvertToPython_2.print(self, args)
 
@@ -737,15 +754,18 @@ def in_list_check(self, args):
 class ConvertToPython_5(ConvertToPython_4):
 
     def print(self, args):
+        # we only check non-Tree (= non calculation) arguments
+        self.check_print_arguments([a for a in args if not type(a) is Tree])
+
+
         #force all to be printed as strings (since there can not be int arguments)
         args_new = []
         for a in args:
             if type(a) is Tree:
                 args_new.append("{" + a.children + "}")
-            elif "'" not in a:
-                args_new.append("{" + a + "}")
             else:
-                args_new.append(a.replace("'", ""))
+                a = a.replace("'", "") #no quotes needed in fstring
+                args_new.append(process_variable_for_fstring(a, self.lookup))
 
         arguments = ''.join(args_new)
         return "print(f'" + arguments + "')"
@@ -857,7 +877,7 @@ def assign(self, args):  # TODO: needs to be merged with 6, when 6 is improved t
 
     def var_access(self, args):
         if len(args) == 1: #accessing a var
-            return process_variable(args[0], self.lookup)
+            return args[0]
         else:
         # this is list_access
             return args[0] + "[" + str(args[1]) + "]" if type(args[1]) is not Tree else "random.choice(" + str(args[0]) + ")"
@@ -924,7 +944,10 @@ def list_access(self, args):
         if args[1] == 'random':
             return 'random.choice(' + args[0] + ')'
         else:
-            return args[0] + '[' + args[1] + '-1]'
+            list_access_shifted = args[0] + '[' + args[1] + '-1]'
+            # when printing later, we need to know this is a var
+            self.lookup.append(list_access_shifted)
+            return list_access_shifted
 
     def change_list_item(self, args):
         return args[0] + '[' + args[1] + '-1] = ' + args[2]
@@ -1008,7 +1031,11 @@ def while_loop(self, args):
 class ConvertToPython_19_20(ConvertToPython_18):
     def length(self, args):
         arg0 = args[0]
-        return f"len({arg0})"
+        length_string = f"len({arg0})"
+
+        #when accessing len we need to know it is a var
+        self.lookup.append(length_string)
+        return length_string
 
     def assign(self, args):  # TODO: needs to be merged with 6, when 6 is improved to with printing expressions directly
         if len(args) == 2:


Test patch:
diff --git a/tests/tests_level_03.py b/tests/tests_level_03.py
--- a/tests/tests_level_03.py
+++ b/tests/tests_level_03.py
@@ -272,16 +272,21 @@ def test_forward_ask(self):
 
   #negative tests
   def test_var_undefined_error_message(self):
-
     code = textwrap.dedent("""\
       naam is Hedy
       print 'ik heet ' name""")
 
-    with self.assertRaises(hedy.UndefinedVarException) as context:
-      result = hedy.transpile(code, self.level)
+    self.multi_level_tester(
+      code=code,
+      exception=hedy.UndefinedVarException,
+      max_level=11,
+      test_name=self.name()
+    )
+
+    # deze extra check functie kan nu niet mee omdat die altijd op result werkt
+    # evt toch splitsen in 2 (pos en neg?)
+    # self.assertEqual('name', context.exception.arguments['name'])
 
-    self.assertEqual('Var Undefined', context.exception.error_code)
-    self.assertEqual('name', context.exception.arguments['name'])
   def test_issue_375(self):
     code = textwrap.dedent("""
       is Foobar
diff --git a/tests/tests_level_07.py b/tests/tests_level_07.py
--- a/tests/tests_level_07.py
+++ b/tests/tests_level_07.py
@@ -140,14 +140,16 @@ def test_repeat_basic_print(self):
   def test_allow_space_after_else_line(self):
     #todo should work up to 11??
     code = textwrap.dedent("""\
+    a is 1
     if a is 1
       print a
     else   
       print 'nee'""")
 
     expected = textwrap.dedent("""\
-    if str('a') == str('1'):
-      print(f'a')
+    a = '1'
+    if str(a) == str('1'):
+      print(f'{a}')
     else:
       print(f'nee')""")
 
diff --git a/tests/tests_level_10.py b/tests/tests_level_10.py
--- a/tests/tests_level_10.py
+++ b/tests/tests_level_10.py
@@ -98,14 +98,16 @@ def test_forloop(self):
   def test_allow_space_after_else_line(self):
 
     code = textwrap.dedent("""\
+    a is 1
     if a is 1:
       print a
     else:   
       print 'nee'""")
 
     expected = textwrap.dedent("""\
-    if str('a') == str('1'):
-      print(f'a')
+    a = '1'
+    if str(a) == str('1'):
+      print(f'{a}')
     else:
       print(f'nee')""")
 
@@ -121,14 +123,16 @@ def test_allow_space_before_colon(self):
     max_level=10
 
     code = textwrap.dedent("""\
+    a is 1
     if a is 1  :
       print a
     else:   
       print 'nee'""")
 
     expected = textwrap.dedent("""\
-    if str('a') == str('1'):
-      print(f'a')
+    a = '1'
+    if str(a) == str('1'):
+      print(f'{a}')
     else:
       print(f'nee')""")
 
diff --git a/tests/tests_level_12.py b/tests/tests_level_12.py
--- a/tests/tests_level_12.py
+++ b/tests/tests_level_12.py
@@ -6,7 +6,7 @@ class TestsLevel12(HedyTester):
   level = 12
   #level 12 adds round brackets
 
-
+  # print tests
   def test_print_brackets(self):
     code = textwrap.dedent("""\
     leeftijd is input('Hoe oud ben jij?')
@@ -90,12 +90,14 @@ def test_for_loop(self):
 
   def test_allow_space_after_else_line(self):
     code = textwrap.dedent("""\
+    a is 1
     if a is 1:
       print(a)
     else:   
       print('nee')""")
 
     expected = textwrap.dedent("""\
+    a = '1'
     if str(a) == str('1'):
       print(f'{a}')
     else:
@@ -110,12 +112,14 @@ def test_allow_space_after_else_line(self):
 
   def test_allow_space_before_colon(self):
     code = textwrap.dedent("""\
+    a is 1
     if a is 1  :
       print(a)
     else:   
       print('nee')""")
 
     expected = textwrap.dedent("""\
+    a = '1'
     if str(a) == str('1'):
       print(f'{a}')
     else:
@@ -218,23 +222,7 @@ def test_if_under_else_in_for(self):
       test_name=self.name()
     )
 
-#programs with issues to see if we catch them properly
-# (so this should fail, for now)
-# at one point we want a real "Indent" error and a better error message
-# for this!
-
-  # def test_level_7_no_indentation(self):
-  #   #test that we get a parse error here
-  #   code = textwrap.dedent("""\
-  #   antwoord is ask Hoeveel is 10 keer tien?
-  #   if antwoord is 100
-  #   print 'goed zo'
-  #   else
-  #   print 'bah slecht'""")
-  #
-  #   with self.assertRaises(Exception) as context:
-  #     result = hedy.transpile(code, 10)
-  #   self.assertEqual(str(context.exception), 'Parse')
+
 
   def test_multiple_spaces_after_print(self):
     code = "print    ('hallo!')"
@@ -276,4 +264,21 @@ def test_multiple_spaces_before_and_after_bracket(self):
       expected=expected,
       test_name=self.name(),
       extra_check_function=self.is_not_turtle()
-    )
\ No newline at end of file
+    )
+
+  # negative tests
+  def test_var_undefined_error_message(self):
+    code = textwrap.dedent("""\
+      naam is Hedy
+      print('ik heet ' name)""")
+
+    self.multi_level_tester(
+      code=code,
+      exception=hedy.UndefinedVarException,
+      max_level=20,
+      test_name=self.name()
+    )
+
+    # deze extra check functie kan nu niet mee omdat die altijd op result werkt
+    # evt toch splitsen in 2 (pos en neg?)
+    # self.assertEqual('name', context.exception.arguments['name'])
\ No newline at end of file
diff --git a/tests/tests_level_14.py b/tests/tests_level_14.py
--- a/tests/tests_level_14.py
+++ b/tests/tests_level_14.py
@@ -13,7 +13,7 @@ def test_bool_true(self):
         expected = textwrap.dedent("""\
         ja = True
         if ja == True:
-          print(f'ja')""")
+          print(f'{ja}')""")
 
         self.multi_level_tester(
           max_level=20,
@@ -31,7 +31,7 @@ def test_bool_false(self):
         expected = textwrap.dedent("""\
         ja = False
         if ja == False:
-          print(f'ja')""")
+          print(f'{ja}')""")
         self.multi_level_tester(
           max_level=20,
           code=code,
@@ -48,7 +48,7 @@ def test_bool_true2(self):
         expected = textwrap.dedent("""\
         ja = True
         if ja == True:
-          print(f'ja')""")
+          print(f'{ja}')""")
 
         self.multi_level_tester(
           max_level=20,
@@ -66,7 +66,7 @@ def test_bool_false2(self):
         expected = textwrap.dedent("""\
         ja = False
         if ja == False:
-          print(f'ja')""")
+          print(f'{ja}')""")
 
         self.multi_level_tester(
           max_level=20,
diff --git a/tests/tests_level_21.py b/tests/tests_level_21.py
--- a/tests/tests_level_21.py
+++ b/tests/tests_level_21.py
@@ -133,3 +133,20 @@ def test_if_nesting(self):
           extra_check_function=self.is_not_turtle(),
           test_name=self.name()
         )
+
+    # negative tests
+    def test_var_undefined_error_message(self):
+      code = textwrap.dedent("""\
+        naam = Hedy
+        print('ik heet ' name)""")
+
+      self.multi_level_tester(
+        code=code,
+        exception=hedy.UndefinedVarException,
+        max_level=self.max_Hedy_level,
+        test_name=self.name()
+      )
+
+      # deze extra check functie kan nu niet mee omdat die altijd op result werkt
+      # evt toch splitsen in 2 (pos en neg?)
+      # self.assertEqual('name', context.exception.arguments['name'])
\ No newline at end of file