Repository: hedyorg/hedy

Issue:
[BUG] Level 11 (new) - Turtle opens randomly
**Describe the bug**
The turtle opens in the new level 11. Also happens with the second example code.

**Paste the Hedy code & level**
a  = 1
b = 2
c = a + b
print c ' is 3'

**Add a screenshot (optional)**
![image](https://user-images.githubusercontent.com/80678586/140352114-8648e731-1c08-4881-930e-c96a6775f86f.png)


**Expected behavior**
It shouldn't open.

**What machine and browser you were using (optional)**
If the issue concerns things in the website, let us know:
- What computer you are using (Windows, Mac, Linux?)
- What browser you were using (Chrome, Edge, Safari)



Discussion:

Resolving patch:
diff --git a/hedy.py b/hedy.py
--- a/hedy.py
+++ b/hedy.py
@@ -13,7 +13,7 @@
 from dataclasses import dataclass, field
 
 # Some useful constants
-HEDY_MAX_LEVEL = 10
+HEDY_MAX_LEVEL = 11
 MAX_LINES = 100
 
 #dictionary to store transpilers
@@ -216,6 +216,10 @@ class UnquotedTextException(HedyException):
     def __init__(self, **arguments):
         super().__init__('Unquoted Text', **arguments)
 
+class UnquotedAssignTextException(HedyException):
+    def __init__(self, **arguments):
+        super().__init__('Unquoted Assignment', **arguments)
+
 class EmptyProgramException(HedyException):
     def __init__(self):
         super().__init__('Empty Program')
@@ -317,7 +321,7 @@ def ask(self, args):
             return None
 
     def assign(self, args):
-        # todo now all assigns are strings, later (form 5) this should distinguish int and str
+        # todo now all assigns are strings, later (from 11) this should distinguish int and str
         return Assignment(args[0], 'string')
 
     def assign_list(self, args):
@@ -514,14 +518,15 @@ def forward(self, args):
     def turn(self, args):
         return True
 
-    # somehow a token (or only this token?) is not picked up by the default rule so it needs
-    # its own rule
-    def NUMBER(self, args):
+    # somehow tokens are not picked up by the default rule so they need their own rule
+    def INT(self, args):
         return False
 
     def NAME(self, args):
         return False
 
+    def NUMBER(self, args):
+        return False
 
 
 
@@ -565,7 +570,6 @@ def valid_echo(ast):
     #otherwise, both have to be in the list and echo shold come after
     return no_echo or ('echo' in command_names and 'ask' in command_names) and command_names.index('echo') > command_names.index('ask')
 
-
 class IsComplete(Filter):
     def __init__(self, level):
         self.level = level
@@ -706,7 +710,6 @@ def find_in_lookup(self, variable):
         # this now grabs the last occurrence, once we have slicing we want to be more precise!
         return lookup_vars[-1] if lookup_vars else None
 
-
 # todo: could be moved into the transpiler class
 def is_variable(name, lookup):
     all_names = [a.name for a in lookup]
@@ -731,7 +734,9 @@ def check_var_usage(self, args):
         # this function checks whether arguments are valid
         # we can proceed if all arguments are either quoted OR all variables
 
-        unquoted_args = [a for a in args if not is_quoted(a)]
+        args_to_process = [a for a in args if not type(a) is Tree] #we do not check trees (calcs) they are always ok
+
+        unquoted_args = [a for a in args_to_process if not is_quoted(a)]
         unquoted_in_lookup = [is_variable(a, self.lookup) for a in unquoted_args]
 
         if unquoted_in_lookup == [] or all(unquoted_in_lookup):
@@ -922,7 +927,7 @@ class ConvertToPython_5(ConvertToPython_4):
 
     def print(self, args):
         # we only check non-Tree (= non calculation) arguments
-        self.check_var_usage([a for a in args if not type(a) is Tree])
+        self.check_var_usage(args)
         self.check_arg_types(args, 'print', self.level)
 
         #force all to be printed as strings (since there can not be int arguments)
@@ -937,7 +942,6 @@ def print(self, args):
         arguments = ''.join(args_new)
         return "print(f'" + arguments + "')"
 
-    #we can now have ints as types so chck must force str
     def equality_check(self, args):
         arg0 = process_variable(args[0], self.lookup)
         arg1 = process_variable(args[1], self.lookup)
@@ -954,6 +958,8 @@ def assign(self, args):
             if type(value) is Tree:
                 return parameter + " = " + value.children
             else:
+                #assigns may contain string (accidentally) i.e. name = 'Hedy'
+                value = process_characters_needing_escape(value)
                 return parameter + " = '" + value + "'"
         else:
             parameter = args[0]
@@ -991,7 +997,6 @@ def division(self, args):
 class ConvertToPython_6(ConvertToPython_5):
     def number(self, args):
         return ''.join(args)
-
     def repeat(self, args):
         var_name = self.get_fresh_var('i')
         times = process_variable(args[0], self.lookup)
@@ -1027,22 +1032,6 @@ def elses(self, args):
 
         return "\nelse:\n" + "\n".join(all_lines)
 
-    def assign(self, args):  # TODO: needs to be merged with 6, when 6 is improved to with printing expressions directly
-        if len(args) == 2:
-            parameter = args[0]
-            value = args[1]
-            if type(value) is Tree:
-                return parameter + " = " + value.children
-            else:
-                if "'" in value or 'random.choice' in value:  # TODO: should be a call to wrap nonvarargument is quotes!
-                    return parameter + " = " + value
-                else:
-                    return parameter + " = '" + value + "'"
-        else:
-            parameter = args[0]
-            values = args[1:]
-            return parameter + " = [" + ", ".join(values) + "]"
-
     def var_access(self, args):
         if len(args) == 1: #accessing a var
             return args[0]
@@ -1069,6 +1058,113 @@ def for_loop(self, args):
 for {args[0]} in range(int({args[1]}), int({args[2]}) + {stepvar_name}, {stepvar_name}):
 {body}"""
 
+@hedy_transpiler(level=11)
+class ConvertToPython_11(ConvertToPython_10):
+
+    def process_token_or_tree(self, argument):
+        if type(argument) is Tree:
+            return f'{str(argument.children)}'
+        else:
+            return f'{argument}'
+
+    def is_int(self, n):
+        try:
+            to_int = int(n)
+            return to_int == n
+        except ValueError:
+            return False
+    def is_float(self, n):
+        try:
+            float(n)
+            return True
+        except ValueError:
+            return False
+
+    def ask(self, args):
+        var = args[0]
+        remaining_args = args[1:]
+        self.check_arg_types(remaining_args, 'ask', self.level)
+
+        assign = f'{var} = input(' + '+'.join(remaining_args) + ")"
+
+        tryblock = textwrap.dedent(f"""
+        try:
+          prijs = int({var})
+        except ValueError:
+          try:
+            prijs = float({var})
+          except ValueError:
+            pass""") #no number? leave as string
+        return assign + tryblock
+
+    def process_calculation(self, args, operator):
+        # arguments of a sum are either a token or a
+        # tree resulting from earlier processing
+        # for trees we need to grap the inner string
+        # for tokens we simply return the argument (no more casting to str needed)
+
+        args = [self.process_token_or_tree(a) for a in args]
+
+        # convert types of the arguments
+        converted_args = []
+        for arg in args:
+            if self.is_float(arg):
+                converted_args.append(f'float({arg})')
+            elif self.is_int(arg):
+                converted_args.append(f'int({arg})')
+            else:
+                # variable? default to float for now (todo: use typesystem here)
+                converted_args.append(f'float({arg})')
+
+        return Tree('sum', f'{args[0]} {operator} {args[1]}')
+
+    def print(self, args):
+        # TODO: new type checking is needed for string assignment
+        self.check_var_usage(args)
+        self.check_arg_types(args, 'print', self.level)
+
+        args_new = []
+        for a in args:
+            if type(a) is Tree:
+                args_new.append("{" + a.children + "}")
+            else:
+                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 + "')"
+
+    def text_in_quotes(self, args):
+        text = args[0]
+        return "'" + text + "'" # keep quotes in the Python code (producing name = 'Henk')
+
+    def assign(self, args):
+        right_hand_side = args[1]
+
+        # we now need to check if the right hand side of te assign is
+        # either a var or quoted, if it is not (and undefined var is raised)
+        # the real issue is probably that the kid forgot quotes
+        try:
+            correct_rhs = self.check_var_usage([right_hand_side]) #check_var_usage expects a list of arguments so place this one in a list.
+        except UndefinedVarException as E:
+            # is the text a number? then no quotes are fine. if not, raise maar!
+
+            if not (self.is_int(right_hand_side) or self.is_float(right_hand_side)):
+                raise UnquotedAssignTextException(text = args[1])
+
+        if len(args) == 2:
+            parameter = args[0]
+            value = args[1]
+            if type(value) is Tree:
+                return parameter + " = " + value.children
+            else:
+                # we no longer escape quotes here because they are now needed
+                return parameter + " = " + value + ""
+        else:
+            parameter = args[0]
+            values = args[1:]
+            return parameter + " = [" + ", ".join(values) + "]"
+
 # @hedy_transpiler(level=10)
 # @hedy_transpiler(level=11)
 # class ConvertToPython_10_11(ConvertToPython_9):
@@ -1098,7 +1194,7 @@ def for_loop(self, args):
 # class ConvertToPython_13(ConvertToPython_12):
 #     def print(self, args):
 #         # we only check non-Tree (= non calculation) arguments
-#         self.check_var_usage([a for a in args if not type(a) is Tree])
+#         self.check_var_usage(args)
 #         self.check_arg_types(args, 'print', self.level)
 #
 #         #force all to be printed as strings (since there can not be int arguments)


Test patch:
diff --git a/tests/tests_level_01.py b/tests/tests_level_01.py
--- a/tests/tests_level_01.py
+++ b/tests/tests_level_01.py
@@ -285,7 +285,7 @@ def test_print_without_argument_gives_incomplete(self):
     )
   def test_non_keyword_with_argument_gives_invalid(self):
     self.multi_level_tester(
-      max_level=22,
+      max_level=10,
       code="abc felienne 123",
       exception=hedy.InvalidCommandException,
       test_name=self.name()
diff --git a/tests/tests_level_02.py b/tests/tests_level_02.py
--- a/tests/tests_level_02.py
+++ b/tests/tests_level_02.py
@@ -259,6 +259,7 @@ def test_turn_number_var(self):
   # issue #792
   def test_turn_right_number(self):
     self.multi_level_tester(
+      max_level=10,
       code="turn right 90",
       exception=hedy.InvalidArgumentTypeException,
       test_name=self.name()
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
@@ -39,7 +39,7 @@ def test_print_comma(self):
     print(f'ik heet ,')""")
     self.multi_level_tester(
       code=code,
-      max_level=11,
+      max_level=10,
       expected=expected,
       test_name=self.name(),
       extra_check_function=self.is_not_turtle()
@@ -72,7 +72,6 @@ def test_print_with_slashes(self):
 
   # ask
   def test_assign_print(self):
-    # todo make version for over level 11
     code = textwrap.dedent("""\
     naam is Hedy
     print 'ik heet' naam""")
@@ -82,7 +81,7 @@ def test_assign_print(self):
     print(f'ik heet{naam}')""")
 
     self.multi_level_tester(
-      max_level=11,
+      max_level=10,
       code=code,
       expected=expected,
       extra_check_function=self.is_not_turtle(),
@@ -108,7 +107,7 @@ def test_ask_Spanish(self):
     expected = textwrap.dedent("""\
     color = input('Cuál es tu color favorito?')""")
     self.multi_level_tester(
-      max_level=11,
+      max_level=10,
       code=code,
       expected=expected,
       extra_check_function=self.is_not_turtle(),
@@ -306,7 +305,7 @@ def test_var_undefined_error_message(self):
     self.multi_level_tester(
       code=code,
       exception=hedy.UndefinedVarException,
-      max_level=11,
+      max_level=10,
       test_name=self.name()
     )
 
diff --git a/tests/tests_level_04.py b/tests/tests_level_04.py
--- a/tests/tests_level_04.py
+++ b/tests/tests_level_04.py
@@ -279,7 +279,7 @@ def test_no_space_after_keyword_gives_invalid(self):
     code = textwrap.dedent("print'test'")
 
     self.multi_level_tester(
-      max_level=22,
+      max_level=10,
       code=code,
       exception=hedy.InvalidCommandException,
       test_name=self.name()
diff --git a/tests/tests_level_05.py b/tests/tests_level_05.py
--- a/tests/tests_level_05.py
+++ b/tests/tests_level_05.py
@@ -8,17 +8,31 @@ class TestsLevel5(HedyTester):
   # test/command order: 6: ['print', 'ask', 'is', 'if', 'repeat', 'turn', 'forward', calculations]
 
   # print tests
+  def test_print_quoted_var(self):
+    code = textwrap.dedent("""\
+    naam is 'Hedy'
+    print 'ik heet ' naam""")
+    expected = textwrap.dedent("""\
+    naam = '\\'Hedy\\''
+    print(f'ik heet {naam}')""")
+
+    self.multi_level_tester(
+      max_level=10,
+      code=code,
+      expected=expected,
+      test_name=self.name()
+    )
+
   def test_unsupported_float_with_dot(self):
     self.multi_level_tester(
-      max_level=11,
+      max_level=10,
       code="print 1.5 + 1",
       exception=hedy.UnsupportedFloatException,
       test_name=self.name()
     )
-
   def test_unsupported_float_with_comma(self):
     self.multi_level_tester(
-      max_level=11,
+      max_level=10,
       code="print 1,5 + 1",
       exception=hedy.UnsupportedFloatException,
       test_name=self.name()
@@ -179,7 +193,7 @@ def test_assign_parses_periods(self):
     expected = "period = '.'"
 
     self.multi_level_tester(
-      max_level=20,
+      max_level=10,
       code=code,
       expected=expected,
       extra_check_function=self.is_not_turtle(),
diff --git a/tests/tests_level_11.py b/tests/tests_level_11.py
new file mode 100644
--- /dev/null
+++ b/tests/tests_level_11.py
@@ -0,0 +1,170 @@
+import hedy
+import textwrap
+from tests_level_01 import HedyTester
+
+class TestsLevel11(HedyTester):
+  level = 11
+
+  # ask tests
+  def test_ask_number_answer(self):
+    code = textwrap.dedent("""\
+    prijs is ask 'hoeveel?'
+    gespaard is 7
+    sparen is prijs - gespaard
+    print 'hallo' sparen""")
+    expected = textwrap.dedent("""\
+    prijs = input('hoeveel?')
+    try:
+      prijs = int(prijs)
+    except ValueError:
+      try:
+        prijs = float(prijs)
+      except ValueError:
+        pass
+    gespaard = 7
+    sparen = prijs - gespaard
+    print(f'hallo{sparen}')""")
+    result = hedy.transpile(code, self.level)
+
+    self.assertEqual(expected, result.code)
+    self.assertEqual(False, result.has_turtle)
+
+  #new calculations
+  def test_int_addition_directly(self):
+    code = textwrap.dedent("""\
+    print 2 + 2""")
+    expected = textwrap.dedent("""\
+    print(f'{2 + 2}')""")
+    result = hedy.transpile(code, self.level)
+
+    self.assertEqual(expected, result.code)
+    self.assertEqual(False, result.has_turtle)
+  def test_float_addition_directly(self):
+    code = textwrap.dedent("""\
+    print 2.5 + 2.5""")
+    expected = textwrap.dedent("""\
+    print(f'{2.5 + 2.5}')""")
+    result = hedy.transpile(code, self.level)
+
+    self.assertEqual(expected, result.code)
+    self.assertEqual(False, result.has_turtle)
+  def test_float_addition_with_string(self):
+    code = textwrap.dedent("""\
+    print 'het antwoord is ' 2.5 + 2.5""")
+    expected = textwrap.dedent("""\
+    print(f'het antwoord is {2.5 + 2.5}')""")
+    result = hedy.transpile(code, self.level)
+
+    self.assertEqual(expected, result.code)
+    self.assertEqual(False, result.has_turtle)
+  def test_float_addition_in_var(self):
+    code = textwrap.dedent("""\
+    antwoord is 2.5 + 2.5
+    print antwoord
+    """)
+    expected = textwrap.dedent("""\
+    antwoord = 2.5 + 2.5
+    print(f'{antwoord}')""")
+    result = hedy.transpile(code, self.level)
+
+    self.assertEqual(expected, result.code)
+    self.assertEqual(False, result.has_turtle)
+  def test_float_addition_with_var(self):
+    code = textwrap.dedent("""\
+    getal1 is 5
+    getal2 is 4.3
+    print 'dat is dan: ' getal1 + getal2
+    """)
+    expected = textwrap.dedent("""\
+    getal1 = 5
+    getal2 = 4.3
+    print(f'dat is dan: {getal1 + getal2}')""")
+    result = hedy.transpile(code, self.level)
+
+    self.assertEqual(expected, result.code)
+    self.assertEqual(False, result.has_turtle)
+
+
+  def test_store_calc_in_var(self):
+    code = textwrap.dedent("""\
+    a is 1
+    b is 2
+    c is a + b
+    print c ' is 3'""")
+    expected = textwrap.dedent("""\
+    a = 1
+    b = 2
+    c = a + b
+    print(f'{c} is 3')""")
+    result = hedy.transpile(code, self.level)
+
+    self.assertEqual(expected, result.code)
+    self.assertEqual(False, result.has_turtle)
+
+  def test_add_string_vars(self):
+    code = textwrap.dedent("""\
+    getal1 is '5'
+    getal2 is '6'
+    print 'dat is dan: ' getal1 + getal2""")
+    expected = textwrap.dedent("""\
+    getal1 = '5'
+    getal2 = '6'
+    print(f'dat is dan: {getal1 + getal2}')""")
+    result = hedy.transpile(code, self.level)
+
+    self.assertEqual(expected, result.code)
+    self.assertEqual(False, result.has_turtle)
+
+    output = self.run_code(result)
+    expected_output = 'dat is dan: 56'
+
+    self.assertEqual(expected_output, output)
+  def test_add_int_vars(self):
+    code = textwrap.dedent("""\
+    getal1 is 5
+    getal2 is 6
+    print 'dat is dan: ' getal1 + getal2""")
+    expected = textwrap.dedent("""\
+    getal1 = 5
+    getal2 = 6
+    print(f'dat is dan: {getal1 + getal2}')""")
+    result = hedy.transpile(code, self.level)
+
+    self.assertEqual(expected, result.code)
+
+    output = self.run_code(result)
+    expected_output = 'dat is dan: 11'
+
+    self.assertEqual(expected_output, output)
+
+  def test_assign_string_with_quotes(self):
+    code = textwrap.dedent("""\
+    name is 'felienne'
+    print name""")
+    expected = textwrap.dedent("""\
+    name = 'felienne'
+    print(f'{name}')""")
+    result = hedy.transpile(code, self.level)
+
+    self.assertEqual(expected, result.code)
+    self.assertEqual(False, result.has_turtle)
+  def test_assign_string_with_quotes_and_string_value(self):
+    code = textwrap.dedent("""\
+    name is 'felienne'
+    print 'hallo ' name""")
+    expected = textwrap.dedent("""\
+    name = 'felienne'
+    print(f'hallo {name}')""")
+    result = hedy.transpile(code, self.level)
+
+    self.assertEqual(expected, result.code)
+    self.assertEqual(False, result.has_turtle)
+
+  # negative tests
+  def test_assign_string_without_quotes(self):
+    code = textwrap.dedent("""\
+    name is felienne
+    print name""")
+
+    with self.assertRaises(hedy.UnquotedAssignTextException) as context:
+      result = hedy.transpile(code, self.level)