From 5a4136a7f6c56aee95e3ca7c7403d6debd19446f Mon Sep 17 00:00:00 2001 From: rocky Date: Wed, 6 Jul 2022 13:00:52 -0400 Subject: [PATCH] Some limited support for 3.8 "=" specifier --- test/bytecode_3.8_run/02_fstring_debug.pyc | Bin 0 -> 432 bytes test/simple_source/bug38/02_fstring_debug.py | 32 +++++++++++ uncompyle6/parsers/parse38.py | 24 ++++++++ uncompyle6/parsers/reducecheck/iflaststmt.py | 20 ++++++- .../parsers/reducecheck/joined_str_check.py | 47 ++++++++++++++++ uncompyle6/parsers/reducecheck/whilestmt.py | 31 +++++++++++ uncompyle6/parsers/reducecheck/whilestmt38.py | 41 ++++++++++++++ uncompyle6/semantics/customize37.py | 6 +- uncompyle6/semantics/customize38.py | 52 +++++++++++++++++- 9 files changed, 248 insertions(+), 5 deletions(-) create mode 100644 test/bytecode_3.8_run/02_fstring_debug.pyc create mode 100644 test/simple_source/bug38/02_fstring_debug.py create mode 100644 uncompyle6/parsers/reducecheck/joined_str_check.py create mode 100644 uncompyle6/parsers/reducecheck/whilestmt.py create mode 100644 uncompyle6/parsers/reducecheck/whilestmt38.py diff --git a/test/bytecode_3.8_run/02_fstring_debug.pyc b/test/bytecode_3.8_run/02_fstring_debug.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a62b99f36d755fb6d190ea914cac4379e1939ae9 GIT binary patch literal 432 zcmYjNJx>Bb5S`uIg9E`Ba?w^Ra}<1PVj%`&XF+3Zu8;_`-~|w}$3?hMnBc#-_yhbA zCN?%U_E(u$@fWC^JvHGa@6FE6+q}tM=kr-03|`+mUmW0DH=7XDG>QHt2?8?Yk$C24 zL;}TtGeFY?Okw&7T!xm*F+@9hYgKv_X!(3x*;HDNsq}V. -def iflaststmt(self, lhs, n, rule, ast, tokens, first, last): - testexpr = ast[0] +def iflaststmt( + self, lhs: str, n: int, rule, tree, tokens: list, first: int, last: int +) -> bool: + testexpr = tree[0] if testexpr[0] in ("testtrue", "testfalse"): diff --git a/uncompyle6/parsers/reducecheck/joined_str_check.py b/uncompyle6/parsers/reducecheck/joined_str_check.py new file mode 100644 index 00000000..96ddc7b9 --- /dev/null +++ b/uncompyle6/parsers/reducecheck/joined_str_check.py @@ -0,0 +1,47 @@ +# Copyright (c) 2022 Rocky Bernstein +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +def joined_str_ok( + self, lhs: str, n: int, rule, tree, tokens: list, first: int, last: int +) -> bool: + # In Python 3.8, there is a new "=" specifier. + # See https://docs.python.org/3/whatsnew/3.8.html#f-strings-support-for-self-documenting-expressions-and-debugging + # We detect this here inside joined_str by looking for an + # expr->LOAD_STR which has an "=" added at the end + # and is equal without the "=" to expr->formated_value2->LOAD_CONST + # converted to a string. + expr1 = tree[0] + if expr1 != "expr": + return False + load_str = expr1[0] + if load_str != "LOAD_STR": + return False + format_value_equal = load_str.attr + if format_value_equal[-1] != "=": + return False + expr2 = tree[1] + if expr2 != "expr": + return False + formatted_value = expr2[0] + if not formatted_value.kind.startswith("formatted_value"): + return False + expr2a = formatted_value[0] + if expr2a != "expr": + return False + load_const = expr2a[0] + if load_const == "LOAD_CONST": + format_value2 = load_const.attr + return str(format_value2) == format_value_equal[:-1] + return True diff --git a/uncompyle6/parsers/reducecheck/whilestmt.py b/uncompyle6/parsers/reducecheck/whilestmt.py new file mode 100644 index 00000000..6d8ade00 --- /dev/null +++ b/uncompyle6/parsers/reducecheck/whilestmt.py @@ -0,0 +1,31 @@ +# Copyright (c) 2020 Rocky Bernstein +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +def whilestmt( + self, lhs: str, n: int, rule, tree, tokens: list, first: int, last: int +) -> bool: + # When we are missing a COME_FROM_LOOP, the + # "while" statement is nested inside an if/else + # so after the POP_BLOCK we have a JUMP_FORWARD which forms the "else" portion of the "if" + # Check this. + # print("XXX", first, last, rule) + # for t in range(first, last): print(tokens[t]) + # print("="*40) + + return tokens[last - 1] == "POP_BLOCK" and tokens[last] not in ( + "JUMP_FORWARD", + "COME_FROM_LOOP", + "COME_FROM", + ) diff --git a/uncompyle6/parsers/reducecheck/whilestmt38.py b/uncompyle6/parsers/reducecheck/whilestmt38.py new file mode 100644 index 00000000..d53d82a8 --- /dev/null +++ b/uncompyle6/parsers/reducecheck/whilestmt38.py @@ -0,0 +1,41 @@ +# Copyright (c) 2022 Rocky Bernstein +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +def whilestmt38_check( + self, lhs: str, n: int, rule, ast, tokens: list, first: int, last: int +) -> bool: + # When we are missing a COME_FROM_LOOP, the + # "while" statement is nested inside an if/else + # so after the POP_BLOCK we have a JUMP_FORWARD which forms the "else" portion of the "if" + # Check this. + # print("XXX", first, last, rule) + # for t in range(first, last): + # print(tokens[t]) + # print("=" * 40) + + if tokens[last] != "COME_FROM" and tokens[last - 1] == "COME_FROM": + last -= 1 + if tokens[last - 1].kind.startswith("RAISE_VARARGS"): + return True + while tokens[last] == "COME_FROM": + last -= 1 + # In a "while" loop, (in contrast to "for" loop), the loop jump is + # always to the first offset + first_offset = tokens[first].off2int() + if tokens[last] == "JUMP_LOOP" and ( + tokens[last].attr == first_offset or tokens[last - 1].attr == first_offset + ): + return False + return True diff --git a/uncompyle6/semantics/customize37.py b/uncompyle6/semantics/customize37.py index 1dfc5fe7..fe4c0c4b 100644 --- a/uncompyle6/semantics/customize37.py +++ b/uncompyle6/semantics/customize37.py @@ -24,6 +24,8 @@ from uncompyle6.semantics.consts import ( from uncompyle6.semantics.helper import flatten_list +FSTRING_CONVERSION_MAP = {1: "!s", 2: "!r", 3: "!a", "X": ":X"} + ####################### def customize_for_version37(self, version): ######################## @@ -39,7 +41,9 @@ def customize_for_version37(self, version): PRECEDENCE["call_ex_kw4"] = 1 PRECEDENCE["call_kw"] = 0 PRECEDENCE["call_kw36"] = 1 - PRECEDENCE["formatted_value1"] = 100 + PRECEDENCE["formatted_value1"] = 38 # f"...". This has to be below "named_expr" to make + # f'{(x := 10)}' preserve parenthesis + PRECEDENCE["formatted_value2"] = 38 # See above PRECEDENCE["if_exp_37a"] = 28 PRECEDENCE["if_exp_37b"] = 28 PRECEDENCE["dict_unpack"] = 0 # **{...} diff --git a/uncompyle6/semantics/customize38.py b/uncompyle6/semantics/customize38.py index 0e6e5e70..02ae5089 100644 --- a/uncompyle6/semantics/customize38.py +++ b/uncompyle6/semantics/customize38.py @@ -20,6 +20,8 @@ ####################### from uncompyle6.semantics.consts import PRECEDENCE, TABLE_DIRECT +from uncompyle6.semantics.customize37 import FSTRING_CONVERSION_MAP +from uncompyle6.semantics.helper import escape_string, strip_quotes def customize_for_version38(self, version): @@ -125,7 +127,7 @@ def customize_for_version38(self, version): "set_for": (" for %c in %c", (2, "store"), (0, "expr_or_arg"),), "whilestmt38": ( "%|while %c:\n%+%c%-\n\n", - (1, "testexpr"), + (1, ("bool_op", "testexpr", "testexprc")), (2, ("l_stmts", "pass")), ), "whileTruestmt38": ( @@ -282,6 +284,54 @@ def customize_for_version38(self, version): self.n_set_afor = n_set_afor + def n_formatted_value_debug(node): + p = self.prec + self.prec = 100 + + formatted_value = node[1] + value_equal = node[0].attr + assert formatted_value.kind.startswith("formatted_value") + old_in_format_string = self.in_format_string + self.in_format_string = formatted_value.kind + format_value_attr = node[-1] + + post_str = "" + if node[-1] == "BUILD_STRING_3": + post_load_str = node[-2] + post_str = self.traverse(post_load_str, indent="") + post_str = strip_quotes(post_str) + + if format_value_attr == "FORMAT_VALUE_ATTR": + attr = format_value_attr.attr + if attr & 4: + fmt = strip_quotes(self.traverse(node[3], indent="")) + attr_flags = attr & 3 + if attr_flags: + conversion = "%s:%s" % ( + FSTRING_CONVERSION_MAP.get(attr_flags, ""), + fmt, + ) + else: + conversion = ":%s" % fmt + else: + conversion = FSTRING_CONVERSION_MAP.get(attr, "") + f_str = "f%s" % escape_string( + "{%s%s}%s" % (value_equal, conversion, post_str) + ) + else: + f_conversion = self.traverse(formatted_value, indent="") + # Remove leaving "f" and quotes + conversion = strip_quotes(f_conversion[1:]) + f_str = "f%s" % escape_string(f"{value_equal}{conversion}" + post_str) + + self.write(f_str) + self.in_format_string = old_in_format_string + + self.prec = p + self.prune() + + self.n_formatted_value_debug = n_formatted_value_debug + def n_suite_stmts_return(node): if len(node) > 1: assert len(node) == 2