Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

💻 Implemented "is" and sleep command for Micro:bit for level 2 #5587

Merged
merged 28 commits into from
Jun 18, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
a74ab64
Implemented "is" in Micro:bit
rmagedon97 Jun 3, 2024
6a2fa46
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 3, 2024
f32d60a
Adding the sleep command in Micro:bit
rmagedon97 Jun 3, 2024
ff08dec
Merge branch 'Microbit-Level_1' of https://github.com/hedyorg/hedy in…
rmagedon97 Jun 3, 2024
9db42bf
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 3, 2024
23b0c95
Adding a test for "is"
rmagedon97 Jun 3, 2024
4cab818
Merge branch 'Microbit-Level_1' of https://github.com/hedyorg/hedy in…
rmagedon97 Jun 3, 2024
4c2a803
fixed "is" test and added sleep test for Micro:bit
rmagedon97 Jun 3, 2024
e050f1a
Merge branch 'main' into Microbit-Level_1
rmagedon97 Jun 4, 2024
a7abf51
Merge branch 'main' into Microbit-Level_1
rmagedon97 Jun 4, 2024
40472ec
Add microbit transpiler classes with a separate lookup
boryanagoncharenko Jun 5, 2024
bad8416
Refactoring
rmagedon97 Jun 7, 2024
66fe55b
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 7, 2024
a53594d
Merge branch 'main' into Microbit-Level_1
rmagedon97 Jun 7, 2024
7818514
Fixed the sleep command for level 2 and added tests
rmagedon97 Jun 9, 2024
1e82607
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 9, 2024
8c34c0f
Fixed assign function for MBit
rmagedon97 Jun 11, 2024
c77adae
Merge branch 'Microbit-Level_1' of https://github.com/hedyorg/hedy in…
rmagedon97 Jun 11, 2024
92c9445
Merge branch 'main' into Microbit-Level_1
rmagedon97 Jun 11, 2024
b576232
Merge branch 'main' into Microbit-Level_1
rmagedon97 Jun 14, 2024
34d6f71
Fixed the tests for sleep
rmagedon97 Jun 14, 2024
b81a2d8
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 14, 2024
c178cc2
Fixed the 2 failing tests
rmagedon97 Jun 16, 2024
956cf14
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 16, 2024
a021e34
Merge branch 'main' into Microbit-Level_1
rmagedon97 Jun 16, 2024
9b10444
fix
rmagedon97 Jun 16, 2024
e5062e0
clear implemented
rmagedon97 Jun 16, 2024
2864866
Merge branch 'main' into Microbit-Level_1
mergify[bot] Jun 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
246 changes: 196 additions & 50 deletions hedy.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@

# dictionary to store transpilers
TRANSPILER_LOOKUP = {}
MICROBIT_TRANSPILER_LOOKUP = {}

# define source-map
source_map = SourceMap()
Expand Down Expand Up @@ -290,7 +291,7 @@ def add_level(commands, level, add=None, remove=None):
add = []
if not remove:
remove = []
commands[level] = [c for c in commands[level-1] if c not in remove] + add
commands[level] = [c for c in commands[level - 1] if c not in remove] + add


# Commands per Hedy level which are used to suggest the closest command when kids make a mistake
Expand All @@ -313,7 +314,6 @@ def add_level(commands, level, add=None, remove=None):
add_level(commands_per_level, level=17, add=['elif'])
add_level(commands_per_level, level=18, add=['input'], remove=['ask'])


command_turn_literals = ['right', 'left']
english_colors = ['black', 'blue', 'brown', 'gray', 'green', 'orange', 'pink', 'purple', 'red', 'white', 'yellow']

Expand Down Expand Up @@ -1119,7 +1119,8 @@ def __default__(self, args, children, meta):
# for the achievements we want to be able to also detect which operators were used by a kid
operators = ['addition', 'subtraction', 'multiplication', 'division']

if production_rule_name in commands_per_level[self.level] or production_rule_name in operators or production_rule_name == 'if_pressed_else':
if production_rule_name in commands_per_level[
self.level] or production_rule_name in operators or production_rule_name == 'if_pressed_else':
# if_pressed_else is not in the yamls, upsetting lookup code to get an alternative later
# lookup should be fixed instead, making a special case for now
if production_rule_name == 'else': # use of else also has an if
Expand Down Expand Up @@ -1318,22 +1319,22 @@ def if_pressed_no_colon(self, meta, args):

def if_pressed_elifs_no_colon(self, meta, args):
# if_pressed_elifs starts with _EOL, so we need to add +1 to its line
raise exceptions.MissingColonException(command=Command.elif_, line_number=meta.line+1)
raise exceptions.MissingColonException(command=Command.elif_, line_number=meta.line + 1)

def if_pressed_elses_no_colon(self, meta, args):
# if_pressed_elses starts with _EOL, so we need to add +1 to its line
raise exceptions.MissingColonException(command=Command.else_, line_number=meta.line+1)
raise exceptions.MissingColonException(command=Command.else_, line_number=meta.line + 1)

def ifs_no_colon(self, meta, args):
raise exceptions.MissingColonException(command=Command.if_, line_number=meta.line)

def elifs_no_colon(self, meta, args):
# elifs starts with _EOL, so we need to add +1 to its line
raise exceptions.MissingColonException(command=Command.elif_, line_number=meta.line+1)
raise exceptions.MissingColonException(command=Command.elif_, line_number=meta.line + 1)

def elses_no_colon(self, meta, args):
# elses starts with _EOL, so we need to add +1 to its line
raise exceptions.MissingColonException(command=Command.else_, line_number=meta.line+1)
raise exceptions.MissingColonException(command=Command.else_, line_number=meta.line + 1)

def for_list_no_colon(self, meta, args):
raise exceptions.MissingColonException(command=Command.for_list, line_number=meta.line)
Expand Down Expand Up @@ -1420,9 +1421,12 @@ def get_allowed_types(command, level):


# decorator used to store each class in the lookup table
def hedy_transpiler(level):
def hedy_transpiler(level, microbit=False):
def decorator(c):
TRANSPILER_LOOKUP[level] = c
if not microbit:
TRANSPILER_LOOKUP[level] = c
else:
MICROBIT_TRANSPILER_LOOKUP[level] = c
c.level = level
return c

Expand All @@ -1431,12 +1435,11 @@ def decorator(c):

@v_args(meta=True)
class ConvertToPython(Transformer):
def __init__(self, lookup, language="en", numerals_language="Latin", is_debug=False, microbit=False):
def __init__(self, lookup, language="en", numerals_language="Latin", is_debug=False):
self.lookup = lookup
self.language = language
self.numerals_language = numerals_language
self.is_debug = is_debug
self.microbit = microbit

def add_debug_breakpoint(self):
if self.is_debug:
Expand Down Expand Up @@ -1637,12 +1640,11 @@ def indent(s, spaces_amount=2):
@source_map_transformer(source_map)
class ConvertToPython_1(ConvertToPython):

def __init__(self, lookup, language, numerals_language, is_debug, microbit=False):
def __init__(self, lookup, language, numerals_language, is_debug):
self.numerals_language = numerals_language
self.language = language
self.lookup = lookup
self.is_debug = is_debug
self.microbit = microbit
__class__.level = 1

def program(self, meta, args):
Expand All @@ -1667,11 +1669,7 @@ def NEGATIVE_NUMBER(self, meta, args):
def print(self, meta, args):
# escape needed characters
argument = process_characters_needing_escape(args[0])
if not self.microbit:
return f"print('" + argument + "')" + self.add_debug_breakpoint()
else:
return textwrap.dedent(f"""\
display.scroll('{argument}')""")
return f"print('" + argument + "')" + self.add_debug_breakpoint()

def ask(self, meta, args):
argument = process_characters_needing_escape(args[0])
Expand Down Expand Up @@ -1869,11 +1867,8 @@ def var_access_print(self, meta, args):
def print(self, meta, args):
args_new = [self.make_print_ask_arg(a, meta) for a in args]
argument_string = ' '.join(args_new)
if not self.microbit:
exception = self.make_index_error_check_if_list(args)
return exception + f"print(f'{argument_string}'){self.add_debug_breakpoint()}"
else:
return f"""display.scroll('{argument_string}')"""
exception = self.make_index_error_check_if_list(args)
return exception + f"print(f'{argument_string}'){self.add_debug_breakpoint()}"

def ask(self, meta, args):
var = args[0]
Expand All @@ -1889,9 +1884,9 @@ def make_print_ask_arg(self, arg, meta, var_to_escape=''):
# therefore we should not process it anymore and thread it as a variable:
# we set the line number to 100 so there is never an issue with variable access before
# assignment (regular code will not work since random.choice(dieren) is never defined as var as such)

if "random.choice" in arg or "[" in arg:
return self.process_variable_for_fstring(arg, meta.line, var_to_escape)

# this regex splits words from non-letter characters, such that name! becomes [name, !]
p = 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}]+"
res = regex.findall(p, arg)
Expand Down Expand Up @@ -1933,17 +1928,14 @@ def assign(self, meta, args):
variable_name = args[0]
value = args[1]

if self.is_random(value) or self.is_list(value):
exception = self.make_index_error_check_if_list([value])
return exception + variable_name + " = " + value + self.add_debug_breakpoint()
exception = self.make_index_error_check_if_list([value])
if self.is_variable(value, meta.line):
# if the assigned value is a variable, this is a reassign
value = self.process_variable(value, meta.line)
else:
if self.is_variable(value, meta.line): # if the value is a variable, this is a reassign
value = self.process_variable(value, meta.line)
return variable_name + " = " + value + self.add_debug_breakpoint()
else:
# if the assigned value is not a variable and contains single quotes, escape them
value = process_characters_needing_escape(value)
return variable_name + " = '" + value + "'" + self.add_debug_breakpoint()
# if it is not a variable, put it in single quotes and escape them
value = f"'{process_characters_needing_escape(value)}'"
return exception + variable_name + " = " + value + self.add_debug_breakpoint()

def sleep(self, meta, args):
if not args:
Expand Down Expand Up @@ -2061,11 +2053,7 @@ def print_ask_args(self, meta, args):
def print(self, meta, args):
argument_string = self.print_ask_args(meta, 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:
return textwrap.dedent(f"""\
display.scroll('{argument_string}')""")
return exceptions + f"print(f'{argument_string}'){self.add_debug_breakpoint()}"

def ask(self, meta, args):
var = args[0]
Expand Down Expand Up @@ -2099,8 +2087,8 @@ def clear(self, meta, args):
@source_map_transformer(source_map)
class ConvertToPython_5(ConvertToPython_4):

def __init__(self, lookup, language, numerals_language, is_debug, microbit):
super().__init__(lookup, language, numerals_language, is_debug, microbit)
def __init__(self, lookup, language, numerals_language, is_debug):
super().__init__(lookup, language, numerals_language, is_debug)

def ifs(self, meta, args): # might be worth asking if we want a debug breakpoint here
return f"""if {args[0]}:{self.add_debug_breakpoint()}
Expand Down Expand Up @@ -2561,11 +2549,7 @@ def print_ask_args(self, meta, args):
def print(self, meta, args):
argument_string = self.print_ask_args(meta, 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:
return textwrap.dedent(f"""\
display.scroll('{argument_string}')""")
return exception + f"print(f'''{argument_string}''')" + self.add_debug_breakpoint()

def ask(self, meta, args):
var = args[0]
Expand Down Expand Up @@ -2830,6 +2814,168 @@ def print_empty_brackets(self, meta, args):
return self.print(meta, args)


@v_args(meta=True)
rmagedon97 marked this conversation as resolved.
Show resolved Hide resolved
@hedy_transpiler(level=1, microbit=True)
@source_map_transformer(source_map)
class MicrobitConvertToPython_1(ConvertToPython_1):
def print(self, meta, args):
# escape needed characters
argument = process_characters_needing_escape(args[0])
return f"display.scroll('{argument}')"


@v_args(meta=True)
@hedy_transpiler(level=2, microbit=True)
@source_map_transformer(source_map)
class MicrobitConvertToPython_2(MicrobitConvertToPython_1, ConvertToPython_2):
def print(self, meta, args):
args_new = [self.make_print_ask_arg(a, meta) for a in args]
argument_string = ' '.join(args_new)
return f"display.scroll({argument_string})"

def make_print_ask_arg(self, arg, meta, var_to_escape=''):
if self.is_variable(arg, meta.line):
return arg
else:
# If the argument is not a variable, return it as a string literal with quotes
return f"'{arg}'"

def sleep(self, meta, args):
if not args:
return f"sleep(1000){self.add_debug_breakpoint()}" # Default 1 second sleep in milliseconds
else:
value = args[0]
if self.is_int(value):
# Direct conversion of seconds to milliseconds
milliseconds = int(value) * 1000
else:
# If the value is a variable, ensure it's used correctly
milliseconds = f"{value} * 1000"
return f" sleep({milliseconds}){self.add_debug_breakpoint()}"
rmagedon97 marked this conversation as resolved.
Show resolved Hide resolved

def assign(self, meta, args):
variable_name = args[0]
value = args[1]

if self.is_variable(value, meta.line):
# if the assigned value is a variable, this is a reassign
value = self.process_variable(value, meta.line)
else:
# if the assigned value is not a variable, add quotes around it and escape inner quotes
value = f"'{process_characters_needing_escape(value)}'"
return " " + str(variable_name) + " = " + value + self.add_debug_breakpoint()


@v_args(meta=True)
@hedy_transpiler(level=3, microbit=True)
@source_map_transformer(source_map)
class MicrobitConvertToPython_3(MicrobitConvertToPython_2, ConvertToPython_3):
pass


@v_args(meta=True)
@hedy_transpiler(level=4, microbit=True)
@source_map_transformer(source_map)
class MicrobitConvertToPython_4(MicrobitConvertToPython_3, ConvertToPython_4):
def print(self, meta, args):
argument_string = self.print_ask_args(meta, args)
return f"display.scroll('{argument_string}')"


@v_args(meta=True)
@hedy_transpiler(level=5, microbit=True)
@source_map_transformer(source_map)
class MicrobitConvertToPython_5(MicrobitConvertToPython_4, ConvertToPython_5):
pass


@v_args(meta=True)
@hedy_transpiler(level=6, microbit=True)
@source_map_transformer(source_map)
class MicrobitConvertToPython_6(MicrobitConvertToPython_5, ConvertToPython_6):
pass


@v_args(meta=True)
@hedy_transpiler(level=7, microbit=True)
@source_map_transformer(source_map)
class MicrobitConvertToPython_7(MicrobitConvertToPython_6, ConvertToPython_7):
pass


@v_args(meta=True)
@hedy_transpiler(level=8, microbit=True)
@hedy_transpiler(level=9, microbit=True)
@source_map_transformer(source_map)
class MicrobitConvertToPython_8_9(MicrobitConvertToPython_7, ConvertToPython_8_9):
pass


@v_args(meta=True)
@hedy_transpiler(level=10, microbit=True)
@source_map_transformer(source_map)
class MicrobitConvertToPython_10(MicrobitConvertToPython_8_9, ConvertToPython_10):
pass


@v_args(meta=True)
@hedy_transpiler(level=11, microbit=True)
@source_map_transformer(source_map)
class MicrobitConvertToPython_11(MicrobitConvertToPython_10, ConvertToPython_11):
pass


@v_args(meta=True)
@hedy_transpiler(level=12, microbit=True)
@source_map_transformer(source_map)
class MicrobitConvertToPython_12(MicrobitConvertToPython_11, ConvertToPython_12):
def print(self, meta, args):
argument_string = self.print_ask_args(meta, args)
return f"display.scroll('{argument_string}')"


@v_args(meta=True)
@hedy_transpiler(level=13, microbit=True)
@source_map_transformer(source_map)
class MicrobitConvertToPython_13(MicrobitConvertToPython_12, ConvertToPython_13):
pass


@v_args(meta=True)
@hedy_transpiler(level=14, microbit=True)
@source_map_transformer(source_map)
class MicrobitConvertToPython_14(MicrobitConvertToPython_13, ConvertToPython_14):
pass


@v_args(meta=True)
@hedy_transpiler(level=15, microbit=True)
@source_map_transformer(source_map)
class MicrobitConvertToPython_15(MicrobitConvertToPython_14, ConvertToPython_15):
pass


@v_args(meta=True)
@hedy_transpiler(level=16, microbit=True)
@source_map_transformer(source_map)
class MicrobitConvertToPython_16(MicrobitConvertToPython_15, ConvertToPython_16):
pass


@v_args(meta=True)
@hedy_transpiler(level=17, microbit=True)
@source_map_transformer(source_map)
class MicrobitConvertToPython_17(MicrobitConvertToPython_16, ConvertToPython_17):
pass


@v_args(meta=True)
@hedy_transpiler(level=18, microbit=True)
@source_map_transformer(source_map)
class MicrobitConvertToPython_18(MicrobitConvertToPython_17, ConvertToPython_18):
pass


# this is only a couple of MB in total, safe to cache
@cache
def create_grammar(level, lang, skip_faulty):
Expand Down Expand Up @@ -3073,7 +3219,8 @@ def get_parser(level, lang="en", keep_all_tokens=False, skip_faulty=False):


ParseResult = namedtuple('ParseResult', ['code', 'source_map', 'has_turtle',
'has_pressed', 'has_clear', 'has_music', 'has_sleep', 'commands', 'roles_of_variables'])
'has_pressed', 'has_clear', 'has_music', 'has_sleep', 'commands',
'roles_of_variables'])


def transpile_inner_with_skipping_faulty(input_string, level, lang="en", unused_allowed=True):
Expand Down Expand Up @@ -3612,9 +3759,8 @@ def transpile_inner(input_string, level, lang="en", populate_source_map=False, i
abstract_syntax_tree, lookup_table, commands = create_AST(input_string, level, lang)

# grab the right transpiler from the lookup
convertToPython = TRANSPILER_LOOKUP[level]
python = convertToPython(lookup_table, lang, numerals_language, is_debug,
microbit).transform(abstract_syntax_tree)
convertToPython = MICROBIT_TRANSPILER_LOOKUP[level] if microbit else TRANSPILER_LOOKUP[level]
python = convertToPython(lookup_table, lang, numerals_language, is_debug).transform(abstract_syntax_tree)

has_clear = "clear" in commands
has_turtle = "forward" in commands or "turn" in commands or "color" in commands
Expand Down
Loading
Loading