Skip to content

Commit

Permalink
Add user-friendly errors for calculations with incorrect types #3465
Browse files Browse the repository at this point in the history
  • Loading branch information
boryanagoncharenko committed Mar 8, 2024
1 parent 66c2d35 commit a41d344
Show file tree
Hide file tree
Showing 22 changed files with 598 additions and 368 deletions.
174 changes: 114 additions & 60 deletions hedy.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,37 +179,47 @@ def needs_colon(rule):
return f'{rule[0:pos]} _COLON {rule[pos:]}'


def _translate_index_error(code, list_name):
exception_text = gettext('catch_index_exception').replace('{list_name}', style_command(list_name))
return textwrap.dedent(f"""\
try:
{code}
except IndexError:
raise Exception({repr(exception_text)})
""")
PREPROCESS_RULES = {
'needs_colon': needs_colon
}


def translate_value_error(command, value, suggested_type):
return translate_error(gettext('catch_value_exception'), [
('{command}', command, 1),
('{value}', value, 1),
('{suggestion}', translate_suggestion(suggested_type), 0)
])


def translate_values_error(command, suggested_type):
return translate_error(gettext("catch_multiple_values_exception"), [
('{command}', command, 1),
('{value}', '{}', 1),
('{suggestion}', translate_suggestion(suggested_type), 0)
])


def translate_error(exception_text, variables):
for template, value, is_highlighted in variables:
result = style_command(value) if is_highlighted else value
exception_text = exception_text.replace(template, result)
# The error is transpiled in f-strings with ", ' and ''' quotes. The only option is to use """.
return '"""' + exception_text + '"""'

def translate_value_error(command, value, suggestion_type):
exception_text = gettext('catch_value_exception')

def translate_suggestion(suggestion_type):
# Right now we only have three types of suggestion
# In the future we might change this if the number increases
if suggestion_type == 'number':
suggestion_text = gettext('suggestion_number')
return gettext('suggestion_number')
elif suggestion_type == 'color':
suggestion_text = gettext('suggestion_color')
return gettext('suggestion_color')
elif suggestion_type == 'note':
suggestion_text = gettext('suggestion_note')

exception_text = exception_text.replace('{command}', style_command(command))
exception_text = exception_text.replace('{value}', style_command(value))
exception_text = exception_text.replace('{suggestion}', suggestion_text)

return repr(exception_text)


PREPROCESS_RULES = {
'needs_colon': needs_colon
}
return gettext('suggestion_note')
elif suggestion_type == 'numbers_or_strings':
return gettext('suggestion_numbers_or_strings')
return ''


class Command:
Expand Down Expand Up @@ -1709,7 +1719,7 @@ def make_color(self, parameter, language):
def make_turtle_command(self, parameter, command, command_text, add_sleep, type):
exception = ''
if isinstance(parameter, str):
exception = self.make_catch_exception([parameter])
exception = self.make_index_error_check_if_list([parameter])
variable = self.get_fresh_var('__trtl')
exception_text = translate_value_error(command, variable, 'number')
transpiled = exception + textwrap.dedent(f"""\
Expand Down Expand Up @@ -1742,27 +1752,35 @@ def make_turtle_color_command(self, parameter, command, command_text, language):
{variable} = color_dict[{variable}]
t.{command_text}({variable}){self.add_debug_breakpoint()}""")

def make_catch_exception(self, args):
def make_index_error_check_if_list(self, args):
lists_names = []
list_args = []
var_regex = r"[\p{Lu}\p{Ll}\p{Lt}\p{Lm}\p{Lo}\p{Nl}_]+|[\p{Mn}\p{Mc}\p{Nd}\p{Pc}·]+"
# List usage comes in indexation and random choice
list_regex = fr"(({var_regex})+\[int\(({var_regex})\)-1\])|(random\.choice\(({var_regex})\))"
var_regex = r"[\p{Lu}\p{Ll}\p{Lt}\p{Lm}\p{Lo}\p{Nl}_]+|[\p{Mn}\p{Mc}\p{Nd}\p{Pc}·]+"
list_access_with_int_cast = fr"(({var_regex})+\[int\(({var_regex})\)-1\])"
list_access_without_cast = fr"(({var_regex})+\[({var_regex})-1\])"
list_access_random = fr"(random\.choice\(({var_regex})\))"
list_regex = f"{list_access_with_int_cast}|{list_access_without_cast}|{list_access_random}"
for arg in args:
# Expressions come inside a Tree object, so unpack them
if isinstance(arg, Tree):
arg = arg.children[0]
for group in regex.findall(list_regex, arg):
if group[0] != '':
list_args.append(group[0])
lists_names.append(group[1])
else:
list_args.append(group[3])
lists_names.append(group[4])
code = ""
for i, list_name in enumerate(lists_names):
code += _translate_index_error(list_args[i], list_name)
return code
match = [e for e in group if e][:2]
list_args.append(match[0])
lists_names.append(match[1])

errors = [self.make_index_error(list_args[i], list_name) for i, list_name in enumerate(lists_names)]
return ''.join(errors)

def make_index_error(self, code, list_name):
exception_text = translate_error(gettext('catch_index_exception'), [('{list_name}', list_name, 1)])
return textwrap.dedent(f"""\
try:
{code}
except IndexError:
raise Exception({exception_text})
""")


@v_args(meta=True)
Expand Down Expand Up @@ -1830,7 +1848,7 @@ def print(self, meta, args):
r"[·\p{Lu}\p{Ll}\p{Lt}\p{Lm}\p{Lo}\p{Nl}\p{Mn}\p{Mc}\p{Nd}\p{Pc}]+|[^·\p{Lu}\p{Ll}\p{Lt}\p{Lm}\p{Lo}\p{Nl}]+",
a)
args_new.append(''.join([self.process_variable_for_fstring(x, meta.line) for x in res]))
exception = self.make_catch_exception(args)
exception = self.make_index_error_check_if_list(args)
argument_string = ' '.join(args_new)
if not self.microbit:
return exception + f"print(f'{argument_string}'){self.add_debug_breakpoint()}"
Expand Down Expand Up @@ -1883,7 +1901,7 @@ def assign(self, meta, args):
value = args[1]

if self.is_random(value) or self.is_list(value):
exception = self.make_catch_exception([value])
exception = self.make_index_error_check_if_list([value])
return exception + variable_name + " = " + value + self.add_debug_breakpoint()
else:
if self.is_variable(value, meta.line): # if the value is a variable, this is a reassign
Expand All @@ -1902,7 +1920,7 @@ def sleep(self, meta, args):
value = f'"{args[0]}"' if self.is_int(args[0]) else args[0]
if not self.is_int(args[0]):
self.add_variable_access_location(value, meta.line)
exceptions = self.make_catch_exception(args)
exceptions = self.make_index_error_check_if_list(args)
try_prefix = "try:\n" + textwrap.indent(exceptions, " ")
exception_text = translate_value_error(Command.sleep, value, 'number')
code = try_prefix + textwrap.dedent(f"""\
Expand Down Expand Up @@ -2010,7 +2028,7 @@ def print_ask_args(self, meta, args):

def print(self, meta, args):
argument_string = self.print_ask_args(meta, args)
exceptions = self.make_catch_exception(args)
exceptions = self.make_index_error_check_if_list(args)
if not self.microbit:
return exceptions + f"print(f'{argument_string}'){self.add_debug_breakpoint()}"
else:
Expand Down Expand Up @@ -2167,7 +2185,7 @@ def sleep(self, meta, args):
if not self.is_int(args[0]):
self.add_variable_access_location(value, meta.line)

exceptions = self.make_catch_exception(args)
exceptions = self.make_index_error_check_if_list(args)
try_prefix = "try:\n" + textwrap.indent(exceptions, " ")
exception_text = translate_value_error(Command.sleep, value, 'number')
code = try_prefix + textwrap.dedent(f"""\
Expand Down Expand Up @@ -2208,7 +2226,7 @@ def assign(self, meta, args):
if self.is_variable(value, meta.line):
value = self.process_variable(value, meta.line)
if self.is_list(value) or self.is_random(value):
exception = self.make_catch_exception([value])
exception = self.make_index_error_check_if_list([value])
return exception + parameter + " = " + value + self.add_debug_breakpoint()
else:
return parameter + " = " + value
Expand All @@ -2227,13 +2245,23 @@ def process_token_or_tree(self, argument, meta):
self.add_variable_access_location(argument, meta.line)
return f'int({argument})'

def process_token_or_tree_for_calculation(self, argument, command, meta):
if type(argument) is Tree:
return f'{str(argument.children[0])}'
if argument.isnumeric():
latin_numeral = int(argument)
return f'int({latin_numeral})'
self.add_variable_access_location(argument, meta.line)
exception_text = translate_value_error(command, argument, 'number')
return f'int_with_error({argument}, {exception_text})'

def process_calculation(self, args, operator, meta):
# 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 add int around them

args = [self.process_token_or_tree(a, meta) for a in args]
args = [self.process_token_or_tree_for_calculation(a, operator, meta) for a in args]
return Tree('sum', [f'{args[0]} {operator} {args[1]}'])

def addition(self, meta, args):
Expand Down Expand Up @@ -2478,7 +2506,7 @@ def call(self, meta, args):

def returns(self, meta, args):
argument_string = self.print_ask_args(meta, args)
exception = self.make_catch_exception(args)
exception = self.make_index_error_check_if_list(args)
return exception + f"return f'''{argument_string}'''"

def number(self, meta, args):
Expand Down Expand Up @@ -2507,14 +2535,6 @@ def text_in_quotes(self, meta, args):
return f'"{text}"'
return f"'{text}'"

def process_token_or_tree(self, argument, meta):
if isinstance(argument, Tree):
return f'{str(argument.children[0])}'
else:
# this is a variable, add to the table
self.add_variable_access_location(argument, meta.line)
return argument

def print_ask_args(self, meta, args):
result = super().print_ask_args(meta, args)
if "'''" in result:
Expand All @@ -2523,7 +2543,7 @@ def print_ask_args(self, meta, args):

def print(self, meta, args):
argument_string = self.print_ask_args(meta, args)
exception = self.make_catch_exception(args)
exception = self.make_index_error_check_if_list(args)
if not self.microbit:
return exception + f"print(f'''{argument_string}''')" + self.add_debug_breakpoint()
else:
Expand Down Expand Up @@ -2573,11 +2593,11 @@ def assign(self, meta, args):
self.check_var_usage_when_quotes_are_required(right_hand_side, meta)

if isinstance(right_hand_side, Tree):
exception = self.make_catch_exception([right_hand_side.children[0]])
exception = self.make_index_error_check_if_list([right_hand_side.children[0]])
return exception + left_hand_side + " = " + right_hand_side.children[0] + self.add_debug_breakpoint()
else:
# we no longer escape quotes here because they are now needed
exception = self.make_catch_exception([right_hand_side])
exception = self.make_index_error_check_if_list([right_hand_side])
return exception + left_hand_side + " = " + right_hand_side + "" + self.add_debug_breakpoint()

def var(self, meta, args):
Expand Down Expand Up @@ -2612,6 +2632,40 @@ def make_turn(self, parameter):
def make_forward(self, parameter):
return self.make_turtle_command(parameter, Command.forward, 'forward', True, 'float')

def process_token_or_tree(self, argument, meta):
if isinstance(argument, Tree):
return f'{str(argument.children[0])}'
else:
# this is a variable, add to the table
self.add_variable_access_location(argument, meta.line)
return argument

def process_token_or_tree_for_calculation(self, argument, command, meta):
if type(argument) is Tree:
return f'{str(argument.children[0])}'
else:
# this is a variable, add to the table
self.add_variable_access_location(argument, meta.line)
exception_text = translate_value_error(command, argument, 'number')
return f'number_with_error({argument}, {exception_text})'

def process_token_or_tree_for_addition(self, argument, meta):
if type(argument) is Tree:
return f'{str(argument.children[0])}'
else:
# this is a variable, add to the table
self.add_variable_access_location(argument, meta.line)
return argument

# From level 12 concatenation should also work, so the args could be either numbers or strings
def addition(self, meta, args):
args = [self.process_token_or_tree_for_addition(a, meta) for a in args]
if all([self.is_int(a) or self.is_float(a) for a in args]):
return Tree('sum', [f'{args[0]} + {args[1]}'])
else:
exception_text = translate_values_error(Command.addition, 'numbers_or_strings')
return Tree('sum', [f'sum_with_error({args[0]}, {args[1]}, {exception_text})'])

def division(self, meta, args):
return self.process_calculation(args, '/', meta)

Expand Down Expand Up @@ -2671,7 +2725,7 @@ def while_loop(self, meta, args):
all_lines = [ConvertToPython.indent(x) for x in args[1:]]
body = "\n".join(all_lines)
body = add_sleep_to_command(body, True, self.is_debug, location="after")
exceptions = self.make_catch_exception([args[0]])
exceptions = self.make_index_error_check_if_list([args[0]])
return exceptions + "while " + args[0] + ":" + self.add_debug_breakpoint() + "\n" + body

def ifpressed(self, meta, args):
Expand Down Expand Up @@ -2715,12 +2769,12 @@ def change_list_item(self, meta, args):
self.add_variable_access_location(args[1], meta.line)
self.add_variable_access_location(args[2], meta.line)

exception = _translate_index_error(left_side, args[0])
exception = self.make_index_error_check_if_list([left_side])
return exception + left_side + ' = ' + right_side + self.add_debug_breakpoint()

def ifs(self, meta, args):
all_lines = [ConvertToPython.indent(x) for x in args[1:]]
exceptions = self.make_catch_exception([args[0]])
exceptions = self.make_index_error_check_if_list([args[0]])
return exceptions + "if " + args[0] + ":" + self.add_debug_breakpoint() + "\n" + "\n".join(all_lines)


Expand Down
23 changes: 10 additions & 13 deletions messages.pot
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2024-02-28 13:55+0100\n"
"POT-Creation-Date: 2024-03-07 22:34+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <[email protected]>\n"
Expand Down Expand Up @@ -278,6 +278,9 @@ msgstr ""
msgid "catch_index_exception"
msgstr ""

msgid "catch_multiple_values_exception"
msgstr ""

msgid "catch_value_exception"
msgstr ""

Expand Down Expand Up @@ -1427,6 +1430,9 @@ msgstr ""
msgid "select_lang"
msgstr ""

msgid "select_levels"
msgstr ""

msgid "select_tag"
msgstr ""

Expand All @@ -1451,12 +1457,6 @@ msgstr ""
msgid "share_by_giving_link"
msgstr ""

msgid "share_confirm"
msgstr ""

msgid "share_success_detail"
msgstr ""

msgid "share_your_program"
msgstr ""

Expand Down Expand Up @@ -1571,6 +1571,9 @@ msgstr ""
msgid "suggestion_number"
msgstr ""

msgid "suggestion_numbers_or_strings"
msgstr ""

msgid "surname"
msgstr ""

Expand Down Expand Up @@ -1742,12 +1745,6 @@ msgstr ""
msgid "unsaved_class_changes"
msgstr ""

msgid "unshare_confirm"
msgstr ""

msgid "unshare_success_detail"
msgstr ""

msgid "update_adventure_prompt"
msgstr ""

Expand Down
Loading

0 comments on commit a41d344

Please sign in to comment.