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

gh-104504: cases generator: Add --warn-unreachable to the mypy config #108112

Merged
merged 9 commits into from
Aug 20, 2023
15 changes: 15 additions & 0 deletions Tools/cases_generator/_typing_backports.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""Backports from newer versions of the typing module.

We backport these features here so that Python can still build
while using an older Python version for PYTHON_FOR_REGEN.
"""

from typing import NoReturn


def assert_never(obj: NoReturn) -> NoReturn:
"""Statically assert that a line of code is unreachable.

Backport of typing.assert_never (introduced in Python 3.11).
"""
raise AssertionError(f"Expected code to be unreachable, but got: {obj}")
7 changes: 4 additions & 3 deletions Tools/cases_generator/analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import sys
import typing

from _typing_backports import assert_never
from flags import InstructionFlags, variable_used
from formatting import prettify_filename, UNUSED
from instructions import (
Expand Down Expand Up @@ -172,7 +173,7 @@ def parse_file(self, filename: str, instrs_idx: dict[str, int]) -> None:
self.pseudos[name] = thing
self.everything.append(thing)
case _:
typing.assert_never(thing)
assert_never(thing)
if not psr.eof():
raise psr.make_syntax_error(f"Extra stuff at the end of {filename}")

Expand Down Expand Up @@ -368,7 +369,7 @@ def analyze_macro(self, macro: parsing.Macro) -> MacroInstruction:
# SAVE_IP in a macro is a no-op in Tier 1
flags.add(instr.instr_flags)
case _:
typing.assert_never(component)
assert_never(component)
format = "IB" if flags.HAS_ARG_FLAG else "IX"
if offset:
format += "C" + "0" * (offset - 1)
Expand Down Expand Up @@ -409,5 +410,5 @@ def check_macro_components(
case parsing.CacheEffect():
components.append(uop)
case _:
typing.assert_never(uop)
assert_never(uop)
return components
33 changes: 13 additions & 20 deletions Tools/cases_generator/generate_cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from collections.abc import Iterator

import stacking # Early import to avoid circular import
from _typing_backports import assert_never
from analysis import Analyzer
from formatting import Formatter, list_effect_size
from flags import InstructionFlags, variable_used
Expand Down Expand Up @@ -146,16 +147,14 @@
class Generator(Analyzer):
def get_stack_effect_info(
self, thing: parsing.InstDef | parsing.Macro | parsing.Pseudo
) -> tuple[AnyInstruction | None, str | None, str | None]:
) -> tuple[AnyInstruction | None, str, str]:
def effect_str(effects: list[StackEffect]) -> str:
n_effect, sym_effect = list_effect_size(effects)
if sym_effect:
return f"{sym_effect} + {n_effect}" if n_effect else sym_effect
return str(n_effect)

instr: AnyInstruction | None
popped: str | None
pushed: str | None
match thing:
case parsing.InstDef():
if thing.kind != "op" or self.instrs[thing.name].is_viable_uop():
Expand All @@ -171,25 +170,23 @@ def effect_str(effects: list[StackEffect]) -> str:
popped, pushed = stacking.get_stack_effect_info_for_macro(instr)
case parsing.Pseudo():
instr = self.pseudo_instrs[thing.name]
popped = pushed = None
AlexWaygood marked this conversation as resolved.
Show resolved Hide resolved
# Calculate stack effect, and check that it's the the same
# for all targets.
for target in self.pseudos[thing.name].targets:
for idx, target in enumerate(self.pseudos[thing.name].targets):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clever idea. I wrote a whole paragraph arguing against using enumerate() here, then realized it wasn't worth arguing over. :-)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI, the saga continues. Currently we get errors from pyright on this: "popped is possibly unbound" (on all occurrences of popped and pushed in this function below here).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI, the saga continues. Currently we get errors from pyright on this: "popped is possibly unbound" (on all occurrences of popped and pushed in this function below here).

Yeah, and mypy will also give you that error if you add --enable-error-code=possibly-undefined to the mypy config :/

There is a third way of fixing this. I'll make a PR and see how you like it.

target_instr = self.instrs.get(target)
# Currently target is always an instr. This could change
# in the future, e.g., if we have a pseudo targetting a
# macro instruction.
assert target_instr
target_popped = effect_str(target_instr.input_effects)
target_pushed = effect_str(target_instr.output_effects)
if pushed is None:
assert popped is None
if idx == 0:
popped, pushed = target_popped, target_pushed
else:
assert popped == target_popped
assert pushed == target_pushed
case _:
typing.assert_never(thing)
assert_never(thing)
return instr, popped, pushed

@contextlib.contextmanager
Expand All @@ -209,7 +206,6 @@ def write_stack_effect_functions(self) -> None:
continue
instr, popped, pushed = self.get_stack_effect_info(thing)
if instr is not None:
assert popped is not None and pushed is not None
popped_data.append((instr, popped))
pushed_data.append((instr, pushed))

Expand Down Expand Up @@ -379,7 +375,6 @@ def write_metadata(self, metadata_filename: str, pymetadata_filename: str) -> No
# Compute the set of all instruction formats.
all_formats: set[str] = set()
for thing in self.everything:
format: str | None
match thing:
case OverriddenInstructionPlaceHolder():
continue
Expand All @@ -388,17 +383,15 @@ def write_metadata(self, metadata_filename: str, pymetadata_filename: str) -> No
case parsing.Macro():
format = self.macro_instrs[thing.name].instr_fmt
case parsing.Pseudo():
format = None
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly, here mypy was eagerly narrowing the type of format to None, meaning that it was inferring the else branch on line 396 to be unreachable.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still feels like a mypy bug to me, but your fix is fine.

for target in self.pseudos[thing.name].targets:
for idx, target in enumerate(self.pseudos[thing.name].targets):
target_instr = self.instrs.get(target)
assert target_instr
if format is None:
if idx == 0:
format = target_instr.instr_fmt
else:
assert format == target_instr.instr_fmt
assert format is not None
case _:
typing.assert_never(thing)
assert_never(thing)
all_formats.add(format)
Comment on lines +389 to 395
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And ditto here for format.


# Turn it into a sorted list of enum values.
Expand Down Expand Up @@ -488,7 +481,7 @@ def write_metadata(self, metadata_filename: str, pymetadata_filename: str) -> No
self.pseudo_instrs[thing.name]
)
case _:
typing.assert_never(thing)
assert_never(thing)

with self.metadata_item(
"const struct opcode_macro_expansion "
Expand Down Expand Up @@ -525,7 +518,7 @@ def write_metadata(self, metadata_filename: str, pymetadata_filename: str) -> No
case parsing.Pseudo():
pass
case _:
typing.assert_never(thing)
assert_never(thing)

with self.metadata_item(
"const char * const _PyOpcode_uop_name[OPCODE_UOP_NAME_SIZE]", "=", ";"
Expand Down Expand Up @@ -774,7 +767,7 @@ def write_instructions(
case parsing.Pseudo():
pass
case _:
typing.assert_never(thing)
assert_never(thing)

print(
f"Wrote {n_instrs} instructions and {n_macros} macros "
Expand Down Expand Up @@ -818,7 +811,7 @@ def write_executor_instructions(
case parsing.Pseudo():
pass
case _:
typing.assert_never(thing)
assert_never(thing)
print(
f"Wrote {n_instrs} instructions and {n_uops} ops to {executor_filename}",
file=sys.stderr,
Expand Down Expand Up @@ -850,7 +843,7 @@ def write_abstract_interpreter_instructions(
case parsing.Pseudo():
pass
case _:
typing.assert_never(thing)
assert_never(thing)
print(
f"Wrote some stuff to {abstract_interpreter_filename}",
file=sys.stderr,
Expand Down
9 changes: 4 additions & 5 deletions Tools/cases_generator/mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@
files = Tools/cases_generator/
pretty = True

# Make sure Python can still be built
# using Python 3.10 for `PYTHON_FOR_REGEN`...
python_version = 3.10

# Be strict:
# ...And be strict:
strict = True
strict_concatenate = True
enable_error_code = ignore-without-code,redundant-expr,truthy-bool

# Don't enable this one yet;
# it has a lot of false positives on `cases_generator`
warn_unreachable = False
warn_unreachable = True