gh-104504: Run mypy on cases_generator in CI (and blacken the code) (gh-108090)

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
This commit is contained in:
Dong-hee Na 2023-08-18 22:42:45 +09:00 committed by GitHub
parent fd19509220
commit 28cab71f95
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 313 additions and 194 deletions

View File

@ -13,7 +13,7 @@ updates:
- "version-update:semver-minor" - "version-update:semver-minor"
- "version-update:semver-patch" - "version-update:semver-patch"
- package-ecosystem: "pip" - package-ecosystem: "pip"
directory: "/Tools/clinic/" directory: "/Tools/"
schedule: schedule:
interval: "monthly" interval: "monthly"
labels: labels:

View File

@ -8,6 +8,7 @@ on:
pull_request: pull_request:
paths: paths:
- "Tools/clinic/**" - "Tools/clinic/**"
- "Tools/cases_generator/**"
- ".github/workflows/mypy.yml" - ".github/workflows/mypy.yml"
workflow_dispatch: workflow_dispatch:
@ -25,15 +26,18 @@ concurrency:
jobs: jobs:
mypy: mypy:
name: Run mypy on Tools/clinic/ strategy:
matrix:
target: ["Tools/cases_generator", "Tools/clinic"]
name: Run mypy on ${{ matrix.target }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 10 timeout-minutes: 10
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-python@v4 - uses: actions/setup-python@v4
with: with:
python-version: "3.x" python-version: "3.11"
cache: pip cache: pip
cache-dependency-path: Tools/clinic/requirements-dev.txt cache-dependency-path: Tools/requirements-dev.txt
- run: pip install -r Tools/clinic/requirements-dev.txt - run: pip install -r Tools/requirements-dev.txt
- run: mypy --config-file Tools/clinic/mypy.ini - run: mypy --config-file ${{ matrix.target }}/mypy.ini

View File

@ -16,12 +16,11 @@ class InstructionFlags:
HAS_FREE_FLAG: bool HAS_FREE_FLAG: bool
HAS_LOCAL_FLAG: bool HAS_LOCAL_FLAG: bool
def __post_init__(self): def __post_init__(self) -> None:
self.bitmask = {name: (1 << i) for i, name in enumerate(self.names())} self.bitmask = {name: (1 << i) for i, name in enumerate(self.names())}
@staticmethod @staticmethod
def fromInstruction(instr: parsing.Node): def fromInstruction(instr: parsing.Node) -> "InstructionFlags":
has_free = ( has_free = (
variable_used(instr, "PyCell_New") variable_used(instr, "PyCell_New")
or variable_used(instr, "PyCell_GET") or variable_used(instr, "PyCell_GET")
@ -41,7 +40,7 @@ class InstructionFlags:
) )
@staticmethod @staticmethod
def newEmpty(): def newEmpty() -> "InstructionFlags":
return InstructionFlags(False, False, False, False, False, False) return InstructionFlags(False, False, False, False, False, False)
def add(self, other: "InstructionFlags") -> None: def add(self, other: "InstructionFlags") -> None:
@ -49,7 +48,7 @@ class InstructionFlags:
if value: if value:
setattr(self, name, value) setattr(self, name, value)
def names(self, value=None) -> list[str]: def names(self, value: bool | None = None) -> list[str]:
if value is None: if value is None:
return list(dataclasses.asdict(self).keys()) return list(dataclasses.asdict(self).keys())
return [n for n, v in dataclasses.asdict(self).items() if v == value] return [n for n, v in dataclasses.asdict(self).items() if v == value]
@ -62,7 +61,7 @@ class InstructionFlags:
return flags return flags
@classmethod @classmethod
def emit_macros(cls, out: Formatter): def emit_macros(cls, out: Formatter) -> None:
flags = cls.newEmpty() flags = cls.newEmpty()
for name, value in flags.bitmask.items(): for name, value in flags.bitmask.items():
out.emit(f"#define {name} ({value})") out.emit(f"#define {name} ({value})")
@ -90,9 +89,9 @@ def variable_used_unspecialized(node: parsing.Node, name: str) -> bool:
text = "".join(token.text.split()) text = "".join(token.text.split())
# TODO: Handle nested #if # TODO: Handle nested #if
if text == "#if": if text == "#if":
if ( if i + 1 < len(node.tokens) and node.tokens[i + 1].text in (
i + 1 < len(node.tokens) "ENABLE_SPECIALIZATION",
and node.tokens[i + 1].text in ("ENABLE_SPECIALIZATION", "TIER_ONE") "TIER_ONE",
): ):
skipping = True skipping = True
elif text in ("#else", "#endif"): elif text in ("#else", "#endif"):

View File

@ -1,6 +1,7 @@
import contextlib import contextlib
import re import re
import typing import typing
from collections.abc import Iterator
from parsing import StackEffect, Family from parsing import StackEffect, Family
@ -58,13 +59,13 @@ class Formatter:
self.set_lineno(self.lineno + 1, self.filename) self.set_lineno(self.lineno + 1, self.filename)
@contextlib.contextmanager @contextlib.contextmanager
def indent(self): def indent(self) -> Iterator[None]:
self.prefix += " " self.prefix += " "
yield yield
self.prefix = self.prefix[:-4] self.prefix = self.prefix[:-4]
@contextlib.contextmanager @contextlib.contextmanager
def block(self, head: str, tail: str = ""): def block(self, head: str, tail: str = "") -> Iterator[None]:
if head: if head:
self.emit(head + " {") self.emit(head + " {")
else: else:
@ -77,7 +78,7 @@ class Formatter:
self, self,
input_effects: list[StackEffect], input_effects: list[StackEffect],
output_effects: list[StackEffect], output_effects: list[StackEffect],
): ) -> None:
shrink, isym = list_effect_size(input_effects) shrink, isym = list_effect_size(input_effects)
grow, osym = list_effect_size(output_effects) grow, osym = list_effect_size(output_effects)
diff = grow - shrink diff = grow - shrink
@ -90,7 +91,7 @@ class Formatter:
if osym and osym != isym: if osym and osym != isym:
self.emit(f"STACK_GROW({osym});") self.emit(f"STACK_GROW({osym});")
def declare(self, dst: StackEffect, src: StackEffect | None): def declare(self, dst: StackEffect, src: StackEffect | None) -> None:
if dst.name == UNUSED or dst.cond == "0": if dst.name == UNUSED or dst.cond == "0":
return return
typ = f"{dst.type}" if dst.type else "PyObject *" typ = f"{dst.type}" if dst.type else "PyObject *"
@ -107,7 +108,7 @@ class Formatter:
sepa = "" if typ.endswith("*") else " " sepa = "" if typ.endswith("*") else " "
self.emit(f"{typ}{sepa}{dst.name}{init};") self.emit(f"{typ}{sepa}{dst.name}{init};")
def assign(self, dst: StackEffect, src: StackEffect): def assign(self, dst: StackEffect, src: StackEffect) -> None:
if src.name == UNUSED or dst.name == UNUSED: if src.name == UNUSED or dst.name == UNUSED:
return return
cast = self.cast(dst, src) cast = self.cast(dst, src)

View File

@ -10,6 +10,7 @@ import os
import posixpath import posixpath
import sys import sys
import typing import typing
from collections.abc import Iterator
import stacking # Early import to avoid circular import import stacking # Early import to avoid circular import
from analysis import Analyzer from analysis import Analyzer
@ -23,7 +24,6 @@ from instructions import (
MacroInstruction, MacroInstruction,
MacroParts, MacroParts,
PseudoInstruction, PseudoInstruction,
StackEffect,
OverriddenInstructionPlaceHolder, OverriddenInstructionPlaceHolder,
TIER_ONE, TIER_ONE,
TIER_TWO, TIER_TWO,
@ -80,12 +80,10 @@ SPECIALLY_HANDLED_ABSTRACT_INSTR = {
"STORE_FAST", "STORE_FAST",
"STORE_FAST_MAYBE_NULL", "STORE_FAST_MAYBE_NULL",
"COPY", "COPY",
# Arithmetic # Arithmetic
"_BINARY_OP_MULTIPLY_INT", "_BINARY_OP_MULTIPLY_INT",
"_BINARY_OP_ADD_INT", "_BINARY_OP_ADD_INT",
"_BINARY_OP_SUBTRACT_INT", "_BINARY_OP_SUBTRACT_INT",
} }
arg_parser = argparse.ArgumentParser( arg_parser = argparse.ArgumentParser(
@ -144,6 +142,7 @@ arg_parser.add_argument(
default=DEFAULT_ABSTRACT_INTERPRETER_OUTPUT, default=DEFAULT_ABSTRACT_INTERPRETER_OUTPUT,
) )
class Generator(Analyzer): class Generator(Analyzer):
def get_stack_effect_info( def get_stack_effect_info(
self, thing: parsing.InstDef | parsing.Macro | parsing.Pseudo self, thing: parsing.InstDef | parsing.Macro | parsing.Pseudo
@ -183,7 +182,8 @@ class Generator(Analyzer):
assert target_instr assert target_instr
target_popped = effect_str(target_instr.input_effects) target_popped = effect_str(target_instr.input_effects)
target_pushed = effect_str(target_instr.output_effects) target_pushed = effect_str(target_instr.output_effects)
if popped is None and pushed is None: if pushed is None:
assert popped is None
popped, pushed = target_popped, target_pushed popped, pushed = target_popped, target_pushed
else: else:
assert popped == target_popped assert popped == target_popped
@ -193,7 +193,7 @@ class Generator(Analyzer):
return instr, popped, pushed return instr, popped, pushed
@contextlib.contextmanager @contextlib.contextmanager
def metadata_item(self, signature, open, close): def metadata_item(self, signature: str, open: str, close: str) -> Iterator[None]:
self.out.emit("") self.out.emit("")
self.out.emit(f"extern {signature};") self.out.emit(f"extern {signature};")
self.out.emit("#ifdef NEED_OPCODE_METADATA") self.out.emit("#ifdef NEED_OPCODE_METADATA")
@ -216,9 +216,10 @@ class Generator(Analyzer):
def write_function( def write_function(
direction: str, data: list[tuple[AnyInstruction, str]] direction: str, data: list[tuple[AnyInstruction, str]]
) -> None: ) -> None:
with self.metadata_item( with self.metadata_item(
f"int _PyOpcode_num_{direction}(int opcode, int oparg, bool jump)", "", "" f"int _PyOpcode_num_{direction}(int opcode, int oparg, bool jump)",
"",
"",
): ):
with self.out.block("switch(opcode)"): with self.out.block("switch(opcode)"):
for instr, effect in data: for instr, effect in data:
@ -243,23 +244,24 @@ class Generator(Analyzer):
paths = f"\n{self.out.comment} ".join(filenames) paths = f"\n{self.out.comment} ".join(filenames)
return f"{self.out.comment} from:\n{self.out.comment} {paths}\n" return f"{self.out.comment} from:\n{self.out.comment} {paths}\n"
def write_provenance_header(self): def write_provenance_header(self) -> None:
self.out.write_raw(f"{self.out.comment} This file is generated by {THIS}\n") self.out.write_raw(f"{self.out.comment} This file is generated by {THIS}\n")
self.out.write_raw(self.from_source_files()) self.out.write_raw(self.from_source_files())
self.out.write_raw(f"{self.out.comment} Do not edit!\n") self.out.write_raw(f"{self.out.comment} Do not edit!\n")
def assign_opcode_ids(self): def assign_opcode_ids(self) -> None:
"""Assign IDs to opcodes""" """Assign IDs to opcodes"""
ops: list[(bool, str)] = [] # (has_arg, name) for each opcode ops: list[tuple[bool, str]] = [] # (has_arg, name) for each opcode
instrumented_ops: list[str] = [] instrumented_ops: list[str] = []
for instr in itertools.chain( for instr in itertools.chain(
[instr for instr in self.instrs.values() if instr.kind != "op"], [instr for instr in self.instrs.values() if instr.kind != "op"],
self.macro_instrs.values()): self.macro_instrs.values(),
):
assert isinstance(instr, (Instruction, MacroInstruction, PseudoInstruction))
name = instr.name name = instr.name
if name.startswith('INSTRUMENTED_'): if name.startswith("INSTRUMENTED_"):
instrumented_ops.append(name) instrumented_ops.append(name)
else: else:
ops.append((instr.instr_flags.HAS_ARG_FLAG, name)) ops.append((instr.instr_flags.HAS_ARG_FLAG, name))
@ -268,33 +270,32 @@ class Generator(Analyzer):
# rather than bytecodes.c, so we need to add it explicitly # rather than bytecodes.c, so we need to add it explicitly
# here (at least until we add something to bytecodes.c to # here (at least until we add something to bytecodes.c to
# declare external instructions). # declare external instructions).
instrumented_ops.append('INSTRUMENTED_LINE') instrumented_ops.append("INSTRUMENTED_LINE")
# assert lists are unique # assert lists are unique
assert len(set(ops)) == len(ops) assert len(set(ops)) == len(ops)
assert len(set(instrumented_ops)) == len(instrumented_ops) assert len(set(instrumented_ops)) == len(instrumented_ops)
opname: list[str or None] = [None] * 512 opname: list[str | None] = [None] * 512
opmap: dict = {} opmap: dict[str, int] = {}
markers: dict = {} markers: dict[str, int] = {}
def map_op(op, name): def map_op(op: int, name: str) -> None:
assert op < len(opname) assert op < len(opname)
assert opname[op] is None assert opname[op] is None
assert name not in opmap assert name not in opmap
opname[op] = name opname[op] = name
opmap[name] = op opmap[name] = op
# 0 is reserved for cache entries. This helps debugging. # 0 is reserved for cache entries. This helps debugging.
map_op(0, 'CACHE') map_op(0, "CACHE")
# 17 is reserved as it is the initial value for the specializing counter. # 17 is reserved as it is the initial value for the specializing counter.
# This helps catch cases where we attempt to execute a cache. # This helps catch cases where we attempt to execute a cache.
map_op(17, 'RESERVED') map_op(17, "RESERVED")
# 166 is RESUME - it is hard coded as such in Tools/build/deepfreeze.py # 166 is RESUME - it is hard coded as such in Tools/build/deepfreeze.py
map_op(166, 'RESUME') map_op(166, "RESUME")
next_opcode = 1 next_opcode = 1
@ -306,13 +307,13 @@ class Generator(Analyzer):
assert next_opcode < 255 assert next_opcode < 255
map_op(next_opcode, name) map_op(next_opcode, name)
if has_arg and 'HAVE_ARGUMENT' not in markers: if has_arg and "HAVE_ARGUMENT" not in markers:
markers['HAVE_ARGUMENT'] = next_opcode markers["HAVE_ARGUMENT"] = next_opcode
# Instrumented opcodes are at the end of the valid range # Instrumented opcodes are at the end of the valid range
min_instrumented = 254 - (len(instrumented_ops) - 1) min_instrumented = 254 - (len(instrumented_ops) - 1)
assert next_opcode <= min_instrumented assert next_opcode <= min_instrumented
markers['MIN_INSTRUMENTED_OPCODE'] = min_instrumented markers["MIN_INSTRUMENTED_OPCODE"] = min_instrumented
for i, op in enumerate(instrumented_ops): for i, op in enumerate(instrumented_ops):
map_op(min_instrumented + i, op) map_op(min_instrumented + i, op)
@ -320,11 +321,13 @@ class Generator(Analyzer):
for i, op in enumerate(sorted(self.pseudos)): for i, op in enumerate(sorted(self.pseudos)):
map_op(256 + i, op) map_op(256 + i, op)
assert 255 not in opmap # 255 is reserved assert 255 not in opmap.values() # 255 is reserved
self.opmap = opmap self.opmap = opmap
self.markers = markers self.markers = markers
def write_opcode_ids(self, opcode_ids_h_filename, opcode_targets_filename): def write_opcode_ids(
self, opcode_ids_h_filename: str, opcode_targets_filename: str
) -> None:
"""Write header file that defined the opcode IDs""" """Write header file that defined the opcode IDs"""
with open(opcode_ids_h_filename, "w") as f: with open(opcode_ids_h_filename, "w") as f:
@ -337,15 +340,15 @@ class Generator(Analyzer):
self.out.emit("#ifndef Py_OPCODE_IDS_H") self.out.emit("#ifndef Py_OPCODE_IDS_H")
self.out.emit("#define Py_OPCODE_IDS_H") self.out.emit("#define Py_OPCODE_IDS_H")
self.out.emit("#ifdef __cplusplus") self.out.emit("#ifdef __cplusplus")
self.out.emit("extern \"C\" {") self.out.emit('extern "C" {')
self.out.emit("#endif") self.out.emit("#endif")
self.out.emit("") self.out.emit("")
self.out.emit("/* Instruction opcodes for compiled code */") self.out.emit("/* Instruction opcodes for compiled code */")
def define(name, opcode): def define(name: str, opcode: int) -> None:
self.out.emit(f"#define {name:<38} {opcode:>3}") self.out.emit(f"#define {name:<38} {opcode:>3}")
all_pairs = [] all_pairs: list[tuple[int, int, str]] = []
# the second item in the tuple sorts the markers before the ops # the second item in the tuple sorts the markers before the ops
all_pairs.extend((i, 1, name) for (name, i) in self.markers.items()) all_pairs.extend((i, 1, name) for (name, i) in self.markers.items())
all_pairs.extend((i, 2, name) for (name, i) in self.opmap.items()) all_pairs.extend((i, 2, name) for (name, i) in self.opmap.items())
@ -370,7 +373,6 @@ class Generator(Analyzer):
targets[op] = f"TARGET_{name}" targets[op] = f"TARGET_{name}"
f.write(",\n".join([f" &&{s}" for s in targets])) f.write(",\n".join([f" &&{s}" for s in targets]))
def write_metadata(self, metadata_filename: str, pymetadata_filename: str) -> None: def write_metadata(self, metadata_filename: str, pymetadata_filename: str) -> None:
"""Write instruction metadata to output file.""" """Write instruction metadata to output file."""
@ -469,7 +471,7 @@ class Generator(Analyzer):
"const struct opcode_metadata " "const struct opcode_metadata "
"_PyOpcode_opcode_metadata[OPCODE_METADATA_SIZE]", "_PyOpcode_opcode_metadata[OPCODE_METADATA_SIZE]",
"=", "=",
";" ";",
): ):
# Write metadata for each instruction # Write metadata for each instruction
for thing in self.everything: for thing in self.everything:
@ -482,7 +484,9 @@ class Generator(Analyzer):
case parsing.Macro(): case parsing.Macro():
self.write_metadata_for_macro(self.macro_instrs[thing.name]) self.write_metadata_for_macro(self.macro_instrs[thing.name])
case parsing.Pseudo(): case parsing.Pseudo():
self.write_metadata_for_pseudo(self.pseudo_instrs[thing.name]) self.write_metadata_for_pseudo(
self.pseudo_instrs[thing.name]
)
case _: case _:
typing.assert_never(thing) typing.assert_never(thing)
@ -490,7 +494,7 @@ class Generator(Analyzer):
"const struct opcode_macro_expansion " "const struct opcode_macro_expansion "
"_PyOpcode_macro_expansion[OPCODE_MACRO_EXPANSION_SIZE]", "_PyOpcode_macro_expansion[OPCODE_MACRO_EXPANSION_SIZE]",
"=", "=",
";" ";",
): ):
# Write macro expansion for each non-pseudo instruction # Write macro expansion for each non-pseudo instruction
for thing in self.everything: for thing in self.everything:
@ -529,7 +533,9 @@ class Generator(Analyzer):
self.write_uop_items(lambda name, counter: f'[{name}] = "{name}",') self.write_uop_items(lambda name, counter: f'[{name}] = "{name}",')
with self.metadata_item( with self.metadata_item(
f"const char *const _PyOpcode_OpName[{1 + max(self.opmap.values())}]", "=", ";" f"const char *const _PyOpcode_OpName[{1 + max(self.opmap.values())}]",
"=",
";",
): ):
for name in self.opmap: for name in self.opmap:
self.out.emit(f'[{name}] = "{name}",') self.out.emit(f'[{name}] = "{name}",')
@ -542,11 +548,9 @@ class Generator(Analyzer):
for m in family.members: for m in family.members:
deoptcodes[m] = name deoptcodes[m] = name
# special case: # special case:
deoptcodes['BINARY_OP_INPLACE_ADD_UNICODE'] = 'BINARY_OP' deoptcodes["BINARY_OP_INPLACE_ADD_UNICODE"] = "BINARY_OP"
with self.metadata_item( with self.metadata_item(f"const uint8_t _PyOpcode_Deopt[256]", "=", ";"):
f"const uint8_t _PyOpcode_Deopt[256]", "=", ";"
):
for opt, deopt in sorted(deoptcodes.items()): for opt, deopt in sorted(deoptcodes.items()):
self.out.emit(f"[{opt}] = {deopt},") self.out.emit(f"[{opt}] = {deopt},")
@ -604,10 +608,9 @@ class Generator(Analyzer):
if name not in specialized_ops: if name not in specialized_ops:
self.out.emit(f"'{name}': {op},") self.out.emit(f"'{name}': {op},")
for name in ['MIN_INSTRUMENTED_OPCODE', 'HAVE_ARGUMENT']: for name in ["MIN_INSTRUMENTED_OPCODE", "HAVE_ARGUMENT"]:
self.out.emit(f"{name} = {self.markers[name]}") self.out.emit(f"{name} = {self.markers[name]}")
def write_pseudo_instrs(self) -> None: def write_pseudo_instrs(self) -> None:
"""Write the IS_PSEUDO_INSTR macro""" """Write the IS_PSEUDO_INSTR macro"""
self.out.emit("\n\n#define IS_PSEUDO_INSTR(OP) ( \\") self.out.emit("\n\n#define IS_PSEUDO_INSTR(OP) ( \\")
@ -834,7 +837,10 @@ class Generator(Analyzer):
pass pass
case parsing.InstDef(): case parsing.InstDef():
instr = AbstractInstruction(self.instrs[thing.name].inst) instr = AbstractInstruction(self.instrs[thing.name].inst)
if instr.is_viable_uop() and instr.name not in SPECIALLY_HANDLED_ABSTRACT_INSTR: if (
instr.is_viable_uop()
and instr.name not in SPECIALLY_HANDLED_ABSTRACT_INSTR
):
self.out.emit("") self.out.emit("")
with self.out.block(f"case {thing.name}:"): with self.out.block(f"case {thing.name}:"):
instr.write(self.out, tier=TIER_TWO) instr.write(self.out, tier=TIER_TWO)
@ -878,7 +884,7 @@ class Generator(Analyzer):
self.out.emit(f"DISPATCH();") self.out.emit(f"DISPATCH();")
def main(): def main() -> None:
"""Parse command line, parse input, analyze, write output.""" """Parse command line, parse input, analyze, write output."""
args = arg_parser.parse_args() # Prints message and sys.exit(2) on error args = arg_parser.parse_args() # Prints message and sys.exit(2) on error
if len(args.input) == 0: if len(args.input) == 0:
@ -899,8 +905,9 @@ def main():
a.write_opcode_ids(args.opcode_ids_h, args.opcode_targets_h) a.write_opcode_ids(args.opcode_ids_h, args.opcode_targets_h)
a.write_metadata(args.metadata, args.pymetadata) a.write_metadata(args.metadata, args.pymetadata)
a.write_executor_instructions(args.executor_cases, args.emit_line_directives) a.write_executor_instructions(args.executor_cases, args.emit_line_directives)
a.write_abstract_interpreter_instructions(args.abstract_interpreter_cases, a.write_abstract_interpreter_instructions(
args.emit_line_directives) args.abstract_interpreter_cases, args.emit_line_directives
)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -4,132 +4,221 @@
import re import re
from dataclasses import dataclass from dataclasses import dataclass
from collections.abc import Iterator
def choice(*opts):
def choice(*opts: str) -> str:
return "|".join("(%s)" % opt for opt in opts) return "|".join("(%s)" % opt for opt in opts)
# Regexes # Regexes
# Longer operators must go before shorter ones. # Longer operators must go before shorter ones.
PLUSPLUS = r'\+\+' PLUSPLUS = r"\+\+"
MINUSMINUS = r'--' MINUSMINUS = r"--"
# -> # ->
ARROW = r'->' ARROW = r"->"
ELLIPSIS = r'\.\.\.' ELLIPSIS = r"\.\.\."
# Assignment operators # Assignment operators
TIMESEQUAL = r'\*=' TIMESEQUAL = r"\*="
DIVEQUAL = r'/=' DIVEQUAL = r"/="
MODEQUAL = r'%=' MODEQUAL = r"%="
PLUSEQUAL = r'\+=' PLUSEQUAL = r"\+="
MINUSEQUAL = r'-=' MINUSEQUAL = r"-="
LSHIFTEQUAL = r'<<=' LSHIFTEQUAL = r"<<="
RSHIFTEQUAL = r'>>=' RSHIFTEQUAL = r">>="
ANDEQUAL = r'&=' ANDEQUAL = r"&="
OREQUAL = r'\|=' OREQUAL = r"\|="
XOREQUAL = r'\^=' XOREQUAL = r"\^="
# Operators # Operators
PLUS = r'\+' PLUS = r"\+"
MINUS = r'-' MINUS = r"-"
TIMES = r'\*' TIMES = r"\*"
DIVIDE = r'/' DIVIDE = r"/"
MOD = r'%' MOD = r"%"
NOT = r'~' NOT = r"~"
XOR = r'\^' XOR = r"\^"
LOR = r'\|\|' LOR = r"\|\|"
LAND = r'&&' LAND = r"&&"
LSHIFT = r'<<' LSHIFT = r"<<"
RSHIFT = r'>>' RSHIFT = r">>"
LE = r'<=' LE = r"<="
GE = r'>=' GE = r">="
EQ = r'==' EQ = r"=="
NE = r'!=' NE = r"!="
LT = r'<' LT = r"<"
GT = r'>' GT = r">"
LNOT = r'!' LNOT = r"!"
OR = r'\|' OR = r"\|"
AND = r'&' AND = r"&"
EQUALS = r'=' EQUALS = r"="
# ? # ?
CONDOP = r'\?' CONDOP = r"\?"
# Delimiters # Delimiters
LPAREN = r'\(' LPAREN = r"\("
RPAREN = r'\)' RPAREN = r"\)"
LBRACKET = r'\[' LBRACKET = r"\["
RBRACKET = r'\]' RBRACKET = r"\]"
LBRACE = r'\{' LBRACE = r"\{"
RBRACE = r'\}' RBRACE = r"\}"
COMMA = r',' COMMA = r","
PERIOD = r'\.' PERIOD = r"\."
SEMI = r';' SEMI = r";"
COLON = r':' COLON = r":"
BACKSLASH = r'\\' BACKSLASH = r"\\"
operators = {op: pattern for op, pattern in globals().items() if op == op.upper()} operators = {op: pattern for op, pattern in globals().items() if op == op.upper()}
for op in operators: for op in operators:
globals()[op] = op globals()[op] = op
opmap = { pattern.replace("\\", "") or '\\' : op for op, pattern in operators.items() } opmap = {pattern.replace("\\", "") or "\\": op for op, pattern in operators.items()}
# Macros # Macros
macro = r'# *(ifdef|ifndef|undef|define|error|endif|if|else|include|#)' macro = r"# *(ifdef|ifndef|undef|define|error|endif|if|else|include|#)"
MACRO = 'MACRO' MACRO = "MACRO"
id_re = r'[a-zA-Z_][0-9a-zA-Z_]*' id_re = r"[a-zA-Z_][0-9a-zA-Z_]*"
IDENTIFIER = 'IDENTIFIER' IDENTIFIER = "IDENTIFIER"
suffix = r'([uU]?[lL]?[lL]?)' suffix = r"([uU]?[lL]?[lL]?)"
octal = r'0[0-7]+' + suffix octal = r"0[0-7]+" + suffix
hex = r'0[xX][0-9a-fA-F]+' hex = r"0[xX][0-9a-fA-F]+"
decimal_digits = r'(0|[1-9][0-9]*)' decimal_digits = r"(0|[1-9][0-9]*)"
decimal = decimal_digits + suffix decimal = decimal_digits + suffix
exponent = r"""([eE][-+]?[0-9]+)""" exponent = r"""([eE][-+]?[0-9]+)"""
fraction = r"""([0-9]*\.[0-9]+)|([0-9]+\.)""" fraction = r"""([0-9]*\.[0-9]+)|([0-9]+\.)"""
float = '(((('+fraction+')'+exponent+'?)|([0-9]+'+exponent+'))[FfLl]?)' float = "((((" + fraction + ")" + exponent + "?)|([0-9]+" + exponent + "))[FfLl]?)"
number_re = choice(octal, hex, float, decimal) number_re = choice(octal, hex, float, decimal)
NUMBER = 'NUMBER' NUMBER = "NUMBER"
simple_escape = r"""([a-zA-Z._~!=&\^\-\\?'"])""" simple_escape = r"""([a-zA-Z._~!=&\^\-\\?'"])"""
decimal_escape = r"""(\d+)""" decimal_escape = r"""(\d+)"""
hex_escape = r"""(x[0-9a-fA-F]+)""" hex_escape = r"""(x[0-9a-fA-F]+)"""
escape_sequence = r"""(\\("""+simple_escape+'|'+decimal_escape+'|'+hex_escape+'))' escape_sequence = (
string_char = r"""([^"\\\n]|"""+escape_sequence+')' r"""(\\(""" + simple_escape + "|" + decimal_escape + "|" + hex_escape + "))"
)
string_char = r"""([^"\\\n]|""" + escape_sequence + ")"
str_re = '"' + string_char + '*"' str_re = '"' + string_char + '*"'
STRING = 'STRING' STRING = "STRING"
char = r'\'.\'' # TODO: escape sequence char = r"\'.\'" # TODO: escape sequence
CHARACTER = 'CHARACTER' CHARACTER = "CHARACTER"
comment_re = r'//.*|/\*([^*]|\*[^/])*\*/' comment_re = r"//.*|/\*([^*]|\*[^/])*\*/"
COMMENT = 'COMMENT' COMMENT = "COMMENT"
newline = r"\n" newline = r"\n"
invalid = r"\S" # A single non-space character that's not caught by any of the other patterns invalid = (
matcher = re.compile(choice(id_re, number_re, str_re, char, newline, macro, comment_re, *operators.values(), invalid)) r"\S" # A single non-space character that's not caught by any of the other patterns
letter = re.compile(r'[a-zA-Z_]')
kwds = (
'AUTO', 'BREAK', 'CASE', 'CHAR', 'CONST',
'CONTINUE', 'DEFAULT', 'DO', 'DOUBLE', 'ELSE', 'ENUM', 'EXTERN',
'FLOAT', 'FOR', 'GOTO', 'IF', 'INLINE', 'INT', 'LONG', 'OVERRIDE',
'REGISTER', 'OFFSETOF',
'RESTRICT', 'RETURN', 'SHORT', 'SIGNED', 'SIZEOF', 'STATIC', 'STRUCT',
'SWITCH', 'TYPEDEF', 'UNION', 'UNSIGNED', 'VOID',
'VOLATILE', 'WHILE'
) )
for name in kwds: matcher = re.compile(
globals()[name] = name choice(
id_re,
number_re,
str_re,
char,
newline,
macro,
comment_re,
*operators.values(),
invalid,
)
)
letter = re.compile(r"[a-zA-Z_]")
kwds = []
AUTO = "AUTO"
kwds.append(AUTO)
BREAK = "BREAK"
kwds.append(BREAK)
CASE = "CASE"
kwds.append(CASE)
CHAR = "CHAR"
kwds.append(CHAR)
CONST = "CONST"
kwds.append(CONST)
CONTINUE = "CONTINUE"
kwds.append(CONTINUE)
DEFAULT = "DEFAULT"
kwds.append(DEFAULT)
DO = "DO"
kwds.append(DO)
DOUBLE = "DOUBLE"
kwds.append(DOUBLE)
ELSE = "ELSE"
kwds.append(ELSE)
ENUM = "ENUM"
kwds.append(ENUM)
EXTERN = "EXTERN"
kwds.append(EXTERN)
FLOAT = "FLOAT"
kwds.append(FLOAT)
FOR = "FOR"
kwds.append(FOR)
GOTO = "GOTO"
kwds.append(GOTO)
IF = "IF"
kwds.append(IF)
INLINE = "INLINE"
kwds.append(INLINE)
INT = "INT"
kwds.append(INT)
LONG = "LONG"
kwds.append(LONG)
OVERRIDE = "OVERRIDE"
kwds.append(OVERRIDE)
REGISTER = "REGISTER"
kwds.append(REGISTER)
OFFSETOF = "OFFSETOF"
kwds.append(OFFSETOF)
RESTRICT = "RESTRICT"
kwds.append(RESTRICT)
RETURN = "RETURN"
kwds.append(RETURN)
SHORT = "SHORT"
kwds.append(SHORT)
SIGNED = "SIGNED"
kwds.append(SIGNED)
SIZEOF = "SIZEOF"
kwds.append(SIZEOF)
STATIC = "STATIC"
kwds.append(STATIC)
STRUCT = "STRUCT"
kwds.append(STRUCT)
SWITCH = "SWITCH"
kwds.append(SWITCH)
TYPEDEF = "TYPEDEF"
kwds.append(TYPEDEF)
UNION = "UNION"
kwds.append(UNION)
UNSIGNED = "UNSIGNED"
kwds.append(UNSIGNED)
VOID = "VOID"
kwds.append(VOID)
VOLATILE = "VOLATILE"
kwds.append(VOLATILE)
WHILE = "WHILE"
kwds.append(WHILE)
keywords = {name.lower(): name for name in kwds} keywords = {name.lower(): name for name in kwds}
__all__ = []
__all__.extend(kwds)
def make_syntax_error( def make_syntax_error(
message: str, filename: str, line: int, column: int, line_text: str, message: str,
filename: str | None,
line: int,
column: int,
line_text: str,
) -> SyntaxError: ) -> SyntaxError:
return SyntaxError(message, (filename, line, column, line_text)) return SyntaxError(message, (filename, line, column, line_text))
@ -142,30 +231,30 @@ class Token:
end: tuple[int, int] end: tuple[int, int]
@property @property
def line(self): def line(self) -> int:
return self.begin[0] return self.begin[0]
@property @property
def column(self): def column(self) -> int:
return self.begin[1] return self.begin[1]
@property @property
def end_line(self): def end_line(self) -> int:
return self.end[0] return self.end[0]
@property @property
def end_column(self): def end_column(self) -> int:
return self.end[1] return self.end[1]
@property @property
def width(self): def width(self) -> int:
return self.end[1] - self.begin[1] return self.end[1] - self.begin[1]
def replaceText(self, txt): def replaceText(self, txt: str) -> "Token":
assert isinstance(txt, str) assert isinstance(txt, str)
return Token(self.kind, txt, self.begin, self.end) return Token(self.kind, txt, self.begin, self.end)
def __repr__(self): def __repr__(self) -> str:
b0, b1 = self.begin b0, b1 = self.begin
e0, e1 = self.end e0, e1 = self.end
if b0 == e0: if b0 == e0:
@ -174,7 +263,7 @@ class Token:
return f"{self.kind}({self.text!r}, {b0}:{b1}, {e0}:{e1})" return f"{self.kind}({self.text!r}, {b0}:{b1}, {e0}:{e1})"
def tokenize(src, line=1, filename=None): def tokenize(src: str, line: int = 1, filename: str | None = None) -> Iterator[Token]:
linestart = -1 linestart = -1
for m in matcher.finditer(src): for m in matcher.finditer(src):
start, end = m.span() start, end = m.span()
@ -183,37 +272,42 @@ def tokenize(src, line=1, filename=None):
kind = keywords[text] kind = keywords[text]
elif letter.match(text): elif letter.match(text):
kind = IDENTIFIER kind = IDENTIFIER
elif text == '...': elif text == "...":
kind = ELLIPSIS kind = ELLIPSIS
elif text == '.': elif text == ".":
kind = PERIOD kind = PERIOD
elif text[0] in '0123456789.': elif text[0] in "0123456789.":
kind = NUMBER kind = NUMBER
elif text[0] == '"': elif text[0] == '"':
kind = STRING kind = STRING
elif text in opmap: elif text in opmap:
kind = opmap[text] kind = opmap[text]
elif text == '\n': elif text == "\n":
linestart = start linestart = start
line += 1 line += 1
kind = '\n' kind = "\n"
elif text[0] == "'": elif text[0] == "'":
kind = CHARACTER kind = CHARACTER
elif text[0] == '#': elif text[0] == "#":
kind = MACRO kind = MACRO
elif text[0] == '/' and text[1] in '/*': elif text[0] == "/" and text[1] in "/*":
kind = COMMENT kind = COMMENT
else: else:
lineend = src.find("\n", start) lineend = src.find("\n", start)
if lineend == -1: if lineend == -1:
lineend = len(src) lineend = len(src)
raise make_syntax_error(f"Bad token: {text}", raise make_syntax_error(
filename, line, start-linestart+1, src[linestart:lineend]) f"Bad token: {text}",
filename,
line,
start - linestart + 1,
src[linestart:lineend],
)
if kind == COMMENT: if kind == COMMENT:
begin = line, start - linestart begin = line, start - linestart
newlines = text.count('\n') newlines = text.count("\n")
if newlines: if newlines:
linestart = start + text.rfind('\n') linestart = start + text.rfind("\n")
line += newlines line += newlines
else: else:
begin = line, start - linestart begin = line, start - linestart
@ -221,10 +315,6 @@ def tokenize(src, line=1, filename=None):
yield Token(kind, text, begin, (line, start - linestart + len(text))) yield Token(kind, text, begin, (line, start - linestart + len(text)))
__all__ = []
__all__.extend([kind for kind in globals() if kind.upper() == kind])
def to_text(tkns: list[Token], dedent: int = 0) -> str: def to_text(tkns: list[Token], dedent: int = 0) -> str:
res: list[str] = [] res: list[str] = []
line, col = -1, 1 + dedent line, col = -1, 1 + dedent
@ -235,21 +325,22 @@ def to_text(tkns: list[Token], dedent: int = 0) -> str:
# assert(l >= line), (line, txt, start, end) # assert(l >= line), (line, txt, start, end)
while l > line: while l > line:
line += 1 line += 1
res.append('\n') res.append("\n")
col = 1 + dedent col = 1 + dedent
res.append(' '*(c-col)) res.append(" " * (c - col))
text = tkn.text text = tkn.text
if dedent != 0 and tkn.kind == 'COMMENT' and '\n' in text: if dedent != 0 and tkn.kind == "COMMENT" and "\n" in text:
if dedent < 0: if dedent < 0:
text = text.replace('\n', '\n' + ' '*-dedent) text = text.replace("\n", "\n" + " " * -dedent)
# TODO: dedent > 0 # TODO: dedent > 0
res.append(text) res.append(text)
line, col = tkn.end line, col = tkn.end
return ''.join(res) return "".join(res)
if __name__ == "__main__": if __name__ == "__main__":
import sys import sys
filename = sys.argv[1] filename = sys.argv[1]
if filename == "-c": if filename == "-c":
src = sys.argv[2] src = sys.argv[2]

View File

@ -0,0 +1,14 @@
[mypy]
files = Tools/cases_generator/
pretty = True
python_version = 3.10
# 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

View File

@ -32,7 +32,7 @@ class Context(NamedTuple):
end: int end: int
owner: PLexer owner: PLexer
def __repr__(self): def __repr__(self) -> str:
return f"<{self.owner.filename}: {self.begin}-{self.end}>" return f"<{self.owner.filename}: {self.begin}-{self.end}>"
@ -75,7 +75,7 @@ class StackEffect(Node):
size: str = "" # Optional `[size]` size: str = "" # Optional `[size]`
# Note: size cannot be combined with type or cond # Note: size cannot be combined with type or cond
def __repr__(self): def __repr__(self) -> str:
items = [self.name, self.type, self.cond, self.size] items = [self.name, self.type, self.cond, self.size]
while items and items[-1] == "": while items and items[-1] == "":
del items[-1] del items[-1]

View File

@ -413,22 +413,22 @@ def write_components(
return next_instr_is_set return next_instr_is_set
def write_single_instr_for_abstract_interp( def write_single_instr_for_abstract_interp(instr: Instruction, out: Formatter) -> None:
instr: Instruction, out: Formatter
):
try: try:
_write_components_for_abstract_interp( _write_components_for_abstract_interp(
[Component(instr, instr.active_caches)], [Component(instr, instr.active_caches)],
out, out,
) )
except AssertionError as err: except AssertionError as err:
raise AssertionError(f"Error writing abstract instruction {instr.name}") from err raise AssertionError(
f"Error writing abstract instruction {instr.name}"
) from err
def _write_components_for_abstract_interp( def _write_components_for_abstract_interp(
parts: list[Component], parts: list[Component],
out: Formatter, out: Formatter,
): ) -> None:
managers = get_managers(parts) managers = get_managers(parts)
for mgr in managers: for mgr in managers:
if mgr is managers[-1]: if mgr is managers[-1]:
@ -438,5 +438,7 @@ def _write_components_for_abstract_interp(
# NULL out the output stack effects # NULL out the output stack effects
for poke in mgr.pokes: for poke in mgr.pokes:
if not poke.effect.size and poke.effect.name not in mgr.instr.unmoved_names: if not poke.effect.size and poke.effect.name not in mgr.instr.unmoved_names:
out.emit(f"PARTITIONNODE_OVERWRITE((_Py_PARTITIONNODE_t *)" out.emit(
f"PARTITIONNODE_NULLROOT, PEEK(-({poke.offset.as_index()})), true);") f"PARTITIONNODE_OVERWRITE((_Py_PARTITIONNODE_t *)"
f"PARTITIONNODE_NULLROOT, PEEK(-({poke.offset.as_index()})), true);"
)

View File

@ -1,2 +0,0 @@
# Requirements file for external linters and checks we run on Tools/clinic/ in CI
mypy==1.4.1

View File

@ -0,0 +1,3 @@
# Requirements file for external linters and checks we run on
# Tools/clinic and Tools/cases_generator/ in CI
mypy==1.5.1