mirror of
				https://github.com/python/cpython.git
				synced 2025-10-30 05:01:30 +00:00 
			
		
		
		
	 5bb6f0fcba
			
		
	
	
		5bb6f0fcba
		
			
		
	
	
	
	
		
			
			These are the most popular specializations of `LOAD_ATTR` and `STORE_ATTR` that weren't already viable uops: * Split LOAD_ATTR_METHOD_WITH_VALUES * Split LOAD_ATTR_METHOD_NO_DICT * Split LOAD_ATTR_SLOT * Split STORE_ATTR_SLOT * Split STORE_ATTR_INSTANCE_VALUE Also: * Add `-v` flag to code generator which prints a list of non-viable uops (easter-egg: it can print execution counts -- see source) * Double _Py_UOP_MAX_TRACE_LENGTH to 128 I had dropped one of the DEOPT_IF() calls! :-(
		
			
				
	
	
		
			893 lines
		
	
	
	
		
			33 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			893 lines
		
	
	
	
		
			33 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| """Generate the main interpreter switch.
 | |
| Reads the instruction definitions from bytecodes.c.
 | |
| Writes the cases to generated_cases.c.h, which is #included in ceval.c.
 | |
| """
 | |
| 
 | |
| import argparse
 | |
| import contextlib
 | |
| import itertools
 | |
| import os
 | |
| import posixpath
 | |
| import sys
 | |
| import textwrap
 | |
| import typing
 | |
| 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
 | |
| from instructions import (
 | |
|     AnyInstruction,
 | |
|     AbstractInstruction,
 | |
|     Component,
 | |
|     Instruction,
 | |
|     MacroInstruction,
 | |
|     MacroParts,
 | |
|     PseudoInstruction,
 | |
|     OverriddenInstructionPlaceHolder,
 | |
|     TIER_ONE,
 | |
|     TIER_TWO,
 | |
| )
 | |
| import parsing
 | |
| from parsing import StackEffect
 | |
| 
 | |
| 
 | |
| HERE = os.path.dirname(__file__)
 | |
| ROOT = os.path.join(HERE, "../..")
 | |
| THIS = os.path.relpath(__file__, ROOT).replace(os.path.sep, posixpath.sep)
 | |
| 
 | |
| DEFAULT_INPUT = os.path.relpath(os.path.join(ROOT, "Python/bytecodes.c"))
 | |
| DEFAULT_OUTPUT = os.path.relpath(os.path.join(ROOT, "Python/generated_cases.c.h"))
 | |
| DEFAULT_OPCODE_IDS_H_OUTPUT = os.path.relpath(
 | |
|     os.path.join(ROOT, "Include/opcode_ids.h")
 | |
| )
 | |
| DEFAULT_OPCODE_TARGETS_H_OUTPUT = os.path.relpath(
 | |
|     os.path.join(ROOT, "Python/opcode_targets.h")
 | |
| )
 | |
| DEFAULT_METADATA_OUTPUT = os.path.relpath(
 | |
|     os.path.join(ROOT, "Include/internal/pycore_opcode_metadata.h")
 | |
| )
 | |
| DEFAULT_PYMETADATA_OUTPUT = os.path.relpath(
 | |
|     os.path.join(ROOT, "Lib/_opcode_metadata.py")
 | |
| )
 | |
| DEFAULT_EXECUTOR_OUTPUT = os.path.relpath(
 | |
|     os.path.join(ROOT, "Python/executor_cases.c.h")
 | |
| )
 | |
| DEFAULT_ABSTRACT_INTERPRETER_OUTPUT = os.path.relpath(
 | |
|     os.path.join(ROOT, "Python/abstract_interp_cases.c.h")
 | |
| )
 | |
| 
 | |
| # Constants used instead of size for macro expansions.
 | |
| # Note: 1, 2, 4 must match actual cache entry sizes.
 | |
| OPARG_SIZES = {
 | |
|     "OPARG_FULL": 0,
 | |
|     "OPARG_CACHE_1": 1,
 | |
|     "OPARG_CACHE_2": 2,
 | |
|     "OPARG_CACHE_4": 4,
 | |
|     "OPARG_TOP": 5,
 | |
|     "OPARG_BOTTOM": 6,
 | |
|     "OPARG_SET_IP": 7,
 | |
| }
 | |
| 
 | |
| INSTR_FMT_PREFIX = "INSTR_FMT_"
 | |
| 
 | |
| # TODO: generate all these after updating the DSL
 | |
| SPECIALLY_HANDLED_ABSTRACT_INSTR = {
 | |
|     "LOAD_FAST",
 | |
|     "LOAD_FAST_CHECK",
 | |
|     "LOAD_FAST_AND_CLEAR",
 | |
|     "LOAD_CONST",
 | |
|     "STORE_FAST",
 | |
|     "STORE_FAST_MAYBE_NULL",
 | |
|     "COPY",
 | |
|     # Arithmetic
 | |
|     "_BINARY_OP_MULTIPLY_INT",
 | |
|     "_BINARY_OP_ADD_INT",
 | |
|     "_BINARY_OP_SUBTRACT_INT",
 | |
| }
 | |
| 
 | |
| arg_parser = argparse.ArgumentParser(
 | |
|     description="Generate the code for the interpreter switch.",
 | |
|     formatter_class=argparse.ArgumentDefaultsHelpFormatter,
 | |
| )
 | |
| 
 | |
| arg_parser.add_argument(
 | |
|     "-v",
 | |
|     "--verbose",
 | |
|     help="Print list of non-viable uops and exit",
 | |
|     action="store_true",
 | |
| )
 | |
| arg_parser.add_argument(
 | |
|     "-o", "--output", type=str, help="Generated code", default=DEFAULT_OUTPUT
 | |
| )
 | |
| arg_parser.add_argument(
 | |
|     "-n",
 | |
|     "--opcode_ids_h",
 | |
|     type=str,
 | |
|     help="Header file with opcode number definitions",
 | |
|     default=DEFAULT_OPCODE_IDS_H_OUTPUT,
 | |
| )
 | |
| arg_parser.add_argument(
 | |
|     "-t",
 | |
|     "--opcode_targets_h",
 | |
|     type=str,
 | |
|     help="File with opcode targets for computed gotos",
 | |
|     default=DEFAULT_OPCODE_TARGETS_H_OUTPUT,
 | |
| )
 | |
| arg_parser.add_argument(
 | |
|     "-m",
 | |
|     "--metadata",
 | |
|     type=str,
 | |
|     help="Generated C metadata",
 | |
|     default=DEFAULT_METADATA_OUTPUT,
 | |
| )
 | |
| arg_parser.add_argument(
 | |
|     "-p",
 | |
|     "--pymetadata",
 | |
|     type=str,
 | |
|     help="Generated Python metadata",
 | |
|     default=DEFAULT_PYMETADATA_OUTPUT,
 | |
| )
 | |
| arg_parser.add_argument(
 | |
|     "-l", "--emit-line-directives", help="Emit #line directives", action="store_true"
 | |
| )
 | |
| arg_parser.add_argument(
 | |
|     "input", nargs=argparse.REMAINDER, help="Instruction definition file(s)"
 | |
| )
 | |
| arg_parser.add_argument(
 | |
|     "-e",
 | |
|     "--executor-cases",
 | |
|     type=str,
 | |
|     help="Write executor cases to this file",
 | |
|     default=DEFAULT_EXECUTOR_OUTPUT,
 | |
| )
 | |
| arg_parser.add_argument(
 | |
|     "-a",
 | |
|     "--abstract-interpreter-cases",
 | |
|     type=str,
 | |
|     help="Write abstract interpreter cases to this file",
 | |
|     default=DEFAULT_ABSTRACT_INTERPRETER_OUTPUT,
 | |
| )
 | |
| 
 | |
| 
 | |
| class Generator(Analyzer):
 | |
|     def get_stack_effect_info(
 | |
|         self, thing: parsing.InstDef | parsing.Macro | parsing.Pseudo
 | |
|     ) -> 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 = None
 | |
|         pushed: str | None = None
 | |
|         match thing:
 | |
|             case parsing.InstDef():
 | |
|                 instr = self.instrs[thing.name]
 | |
|                 popped = effect_str(instr.input_effects)
 | |
|                 pushed = effect_str(instr.output_effects)
 | |
|             case parsing.Macro():
 | |
|                 instr = self.macro_instrs[thing.name]
 | |
|                 popped, pushed = stacking.get_stack_effect_info_for_macro(instr)
 | |
|             case parsing.Pseudo():
 | |
|                 instr = self.pseudo_instrs[thing.name]
 | |
|                 # Calculate stack effect, and check that it's the same
 | |
|                 # for all targets.
 | |
|                 for target in self.pseudos[thing.name].targets:
 | |
|                     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 popped is None:
 | |
|                         popped, pushed = target_popped, target_pushed
 | |
|                     else:
 | |
|                         assert popped == target_popped
 | |
|                         assert pushed == target_pushed
 | |
|             case _:
 | |
|                 assert_never(thing)
 | |
|         assert popped is not None and pushed is not None
 | |
|         return instr, popped, pushed
 | |
| 
 | |
|     @contextlib.contextmanager
 | |
|     def metadata_item(self, signature: str, open: str, close: str) -> Iterator[None]:
 | |
|         self.out.emit("")
 | |
|         self.out.emit(f"extern {signature};")
 | |
|         self.out.emit("#ifdef NEED_OPCODE_METADATA")
 | |
|         with self.out.block(f"{signature} {open}", close):
 | |
|             yield
 | |
|         self.out.emit("#endif // NEED_OPCODE_METADATA")
 | |
| 
 | |
|     def write_stack_effect_functions(self) -> None:
 | |
|         popped_data: list[tuple[AnyInstruction, str]] = []
 | |
|         pushed_data: list[tuple[AnyInstruction, str]] = []
 | |
|         for thing in self.everything:
 | |
|             if isinstance(thing, OverriddenInstructionPlaceHolder):
 | |
|                 continue
 | |
|             if isinstance(thing, parsing.Macro) and thing.name in self.instrs:
 | |
|                 continue
 | |
|             instr, popped, pushed = self.get_stack_effect_info(thing)
 | |
|             if instr is not None:
 | |
|                 popped_data.append((instr, popped))
 | |
|                 pushed_data.append((instr, pushed))
 | |
| 
 | |
|         def write_function(
 | |
|             direction: str, data: list[tuple[AnyInstruction, str]]
 | |
|         ) -> None:
 | |
|             with self.metadata_item(
 | |
|                 f"int _PyOpcode_num_{direction}(int opcode, int oparg, bool jump)",
 | |
|                 "",
 | |
|                 "",
 | |
|             ):
 | |
|                 with self.out.block("switch(opcode)"):
 | |
|                     for instr, effect in data:
 | |
|                         self.out.emit(f"case {instr.name}:")
 | |
|                         self.out.emit(f"    return {effect};")
 | |
|                     self.out.emit("default:")
 | |
|                     self.out.emit("    return -1;")
 | |
| 
 | |
|         write_function("popped", popped_data)
 | |
|         write_function("pushed", pushed_data)
 | |
|         self.out.emit("")
 | |
| 
 | |
|     def from_source_files(self) -> str:
 | |
|         filenames = []
 | |
|         for filename in self.input_filenames:
 | |
|             try:
 | |
|                 filename = os.path.relpath(filename, ROOT)
 | |
|             except ValueError:
 | |
|                 # May happen on Windows if root and temp on different volumes
 | |
|                 pass
 | |
|             filenames.append(filename.replace(os.path.sep, posixpath.sep))
 | |
|         paths = f"\n{self.out.comment}   ".join(filenames)
 | |
|         return f"{self.out.comment} from:\n{self.out.comment}   {paths}\n"
 | |
| 
 | |
|     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(self.from_source_files())
 | |
|         self.out.write_raw(f"{self.out.comment} Do not edit!\n")
 | |
| 
 | |
|     def assign_opcode_ids(self) -> None:
 | |
|         """Assign IDs to opcodes"""
 | |
| 
 | |
|         ops: list[tuple[bool, str]] = []  # (has_arg, name) for each opcode
 | |
|         instrumented_ops: list[str] = []
 | |
| 
 | |
|         specialized_ops: set[str] = set()
 | |
|         for name, family in self.families.items():
 | |
|             specialized_ops.update(family.members)
 | |
| 
 | |
|         for instr in self.macro_instrs.values():
 | |
|             name = instr.name
 | |
|             if name in specialized_ops:
 | |
|                 continue
 | |
|             if name.startswith("INSTRUMENTED_"):
 | |
|                 instrumented_ops.append(name)
 | |
|             else:
 | |
|                 ops.append((instr.instr_flags.HAS_ARG_FLAG, name))
 | |
| 
 | |
|         # Special case: this instruction is implemented in ceval.c
 | |
|         # rather than bytecodes.c, so we need to add it explicitly
 | |
|         # here (at least until we add something to bytecodes.c to
 | |
|         # declare external instructions).
 | |
|         instrumented_ops.append("INSTRUMENTED_LINE")
 | |
| 
 | |
|         # assert lists are unique
 | |
|         assert len(set(ops)) == len(ops)
 | |
|         assert len(set(instrumented_ops)) == len(instrumented_ops)
 | |
| 
 | |
|         opname: list[str | None] = [None] * 512
 | |
|         opmap: dict[str, int] = {}
 | |
|         markers: dict[str, int] = {}
 | |
| 
 | |
|         def map_op(op: int, name: str) -> None:
 | |
|             assert op < len(opname)
 | |
|             assert opname[op] is None, (op, name)
 | |
|             assert name not in opmap
 | |
|             opname[op] = name
 | |
|             opmap[name] = op
 | |
| 
 | |
|         # 0 is reserved for cache entries. This helps debugging.
 | |
|         map_op(0, "CACHE")
 | |
| 
 | |
|         # 17 is reserved as it is the initial value for the specializing counter.
 | |
|         # This helps catch cases where we attempt to execute a cache.
 | |
|         map_op(17, "RESERVED")
 | |
| 
 | |
|         # 149 is RESUME - it is hard coded as such in Tools/build/deepfreeze.py
 | |
|         map_op(149, "RESUME")
 | |
| 
 | |
|         # Specialized ops appear in their own section
 | |
|         # Instrumented opcodes are at the end of the valid range
 | |
|         min_internal = 150
 | |
|         min_instrumented = 254 - (len(instrumented_ops) - 1)
 | |
|         assert min_internal + len(specialized_ops) < min_instrumented
 | |
| 
 | |
|         next_opcode = 1
 | |
|         for has_arg, name in sorted(ops):
 | |
|             if name in opmap:
 | |
|                 continue  # an anchored name, like CACHE
 | |
|             map_op(next_opcode, name)
 | |
|             if has_arg and "HAVE_ARGUMENT" not in markers:
 | |
|                 markers["HAVE_ARGUMENT"] = next_opcode
 | |
| 
 | |
|             while opname[next_opcode] is not None:
 | |
|                 next_opcode += 1
 | |
| 
 | |
|         assert next_opcode < min_internal, next_opcode
 | |
| 
 | |
|         for i, op in enumerate(sorted(specialized_ops)):
 | |
|             map_op(min_internal + i, op)
 | |
| 
 | |
|         markers["MIN_INSTRUMENTED_OPCODE"] = min_instrumented
 | |
|         for i, op in enumerate(instrumented_ops):
 | |
|             map_op(min_instrumented + i, op)
 | |
| 
 | |
|         # Pseudo opcodes are after the valid range
 | |
|         for i, op in enumerate(sorted(self.pseudos)):
 | |
|             map_op(256 + i, op)
 | |
| 
 | |
|         assert 255 not in opmap.values()  # 255 is reserved
 | |
|         self.opmap = opmap
 | |
|         self.markers = markers
 | |
| 
 | |
|     def write_opcode_ids(
 | |
|         self, opcode_ids_h_filename: str, opcode_targets_filename: str
 | |
|     ) -> None:
 | |
|         """Write header file that defined the opcode IDs"""
 | |
| 
 | |
|         with open(opcode_ids_h_filename, "w") as f:
 | |
|             # Create formatter
 | |
|             self.out = Formatter(f, 0)
 | |
| 
 | |
|             self.write_provenance_header()
 | |
| 
 | |
|             self.out.emit("")
 | |
|             self.out.emit("#ifndef Py_OPCODE_IDS_H")
 | |
|             self.out.emit("#define Py_OPCODE_IDS_H")
 | |
|             self.out.emit("#ifdef __cplusplus")
 | |
|             self.out.emit('extern "C" {')
 | |
|             self.out.emit("#endif")
 | |
|             self.out.emit("")
 | |
|             self.out.emit("/* Instruction opcodes for compiled code */")
 | |
| 
 | |
|             def define(name: str, opcode: int) -> None:
 | |
|                 self.out.emit(f"#define {name:<38} {opcode:>3}")
 | |
| 
 | |
|             all_pairs: list[tuple[int, int, str]] = []
 | |
|             # 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, 2, name) for (name, i) in self.opmap.items())
 | |
|             for i, _, name in sorted(all_pairs):
 | |
|                 assert name is not None
 | |
|                 define(name, i)
 | |
| 
 | |
|             self.out.emit("")
 | |
|             self.out.emit("#ifdef __cplusplus")
 | |
|             self.out.emit("}")
 | |
|             self.out.emit("#endif")
 | |
|             self.out.emit("#endif /* !Py_OPCODE_IDS_H */")
 | |
| 
 | |
|         with open(opcode_targets_filename, "w") as f:
 | |
|             # Create formatter
 | |
|             self.out = Formatter(f, 0)
 | |
| 
 | |
|             with self.out.block("static void *opcode_targets[256] =", ";"):
 | |
|                 targets = ["_unknown_opcode"] * 256
 | |
|                 for name, op in self.opmap.items():
 | |
|                     if op < 256:
 | |
|                         targets[op] = f"TARGET_{name}"
 | |
|                 f.write(",\n".join([f"    &&{s}" for s in targets]))
 | |
| 
 | |
|     def write_metadata(self, metadata_filename: str, pymetadata_filename: str) -> None:
 | |
|         """Write instruction metadata to output file."""
 | |
| 
 | |
|         # Compute the set of all instruction formats.
 | |
|         all_formats: set[str] = set()
 | |
|         for thing in self.everything:
 | |
|             format: str | None = None
 | |
|             match thing:
 | |
|                 case OverriddenInstructionPlaceHolder():
 | |
|                     continue
 | |
|                 case parsing.InstDef():
 | |
|                     format = self.instrs[thing.name].instr_fmt
 | |
|                 case parsing.Macro():
 | |
|                     format = self.macro_instrs[thing.name].instr_fmt
 | |
|                 case parsing.Pseudo():
 | |
|                     for target in self.pseudos[thing.name].targets:
 | |
|                         target_instr = self.instrs.get(target)
 | |
|                         assert target_instr
 | |
|                         if format is None:
 | |
|                             format = target_instr.instr_fmt
 | |
|                         else:
 | |
|                             assert format == target_instr.instr_fmt
 | |
|                 case _:
 | |
|                     assert_never(thing)
 | |
|             assert format is not None
 | |
|             all_formats.add(format)
 | |
| 
 | |
|         # Turn it into a sorted list of enum values.
 | |
|         format_enums = [INSTR_FMT_PREFIX + format for format in sorted(all_formats)]
 | |
| 
 | |
|         with open(metadata_filename, "w") as f:
 | |
|             # Create formatter
 | |
|             self.out = Formatter(f, 0)
 | |
| 
 | |
|             self.write_provenance_header()
 | |
| 
 | |
|             self.out.emit("")
 | |
|             self.out.emit("#ifndef Py_BUILD_CORE")
 | |
|             self.out.emit('#  error "this header requires Py_BUILD_CORE define"')
 | |
|             self.out.emit("#endif")
 | |
|             self.out.emit("")
 | |
|             self.out.emit("#include <stdbool.h>              // bool")
 | |
| 
 | |
|             self.write_pseudo_instrs()
 | |
| 
 | |
|             self.out.emit("")
 | |
|             self.write_uop_items(lambda name, counter: f"#define {name} {counter}")
 | |
| 
 | |
|             self.write_stack_effect_functions()
 | |
| 
 | |
|             # Write the enum definition for instruction formats.
 | |
|             with self.out.block("enum InstructionFormat", ";"):
 | |
|                 for enum in format_enums:
 | |
|                     self.out.emit(enum + ",")
 | |
| 
 | |
|             self.out.emit("")
 | |
|             self.out.emit(
 | |
|                 "#define IS_VALID_OPCODE(OP) \\\n"
 | |
|                 "    (((OP) >= 0) && ((OP) < OPCODE_METADATA_SIZE) && \\\n"
 | |
|                 "     (_PyOpcode_opcode_metadata[(OP)].valid_entry))"
 | |
|             )
 | |
| 
 | |
|             self.out.emit("")
 | |
|             InstructionFlags.emit_macros(self.out)
 | |
| 
 | |
|             self.out.emit("")
 | |
|             with self.out.block("struct opcode_metadata", ";"):
 | |
|                 self.out.emit("bool valid_entry;")
 | |
|                 self.out.emit("enum InstructionFormat instr_format;")
 | |
|                 self.out.emit("int flags;")
 | |
|             self.out.emit("")
 | |
| 
 | |
|             with self.out.block("struct opcode_macro_expansion", ";"):
 | |
|                 self.out.emit("int nuops;")
 | |
|                 self.out.emit(
 | |
|                     "struct { int16_t uop; int8_t size; int8_t offset; } uops[12];"
 | |
|                 )
 | |
|             self.out.emit("")
 | |
| 
 | |
|             for key, value in OPARG_SIZES.items():
 | |
|                 self.out.emit(f"#define {key} {value}")
 | |
|             self.out.emit("")
 | |
| 
 | |
|             self.out.emit(
 | |
|                 "#define OPCODE_METADATA_FMT(OP) "
 | |
|                 "(_PyOpcode_opcode_metadata[(OP)].instr_format)"
 | |
|             )
 | |
|             self.out.emit("#define SAME_OPCODE_METADATA(OP1, OP2) \\")
 | |
|             self.out.emit(
 | |
|                 "        (OPCODE_METADATA_FMT(OP1) == OPCODE_METADATA_FMT(OP2))"
 | |
|             )
 | |
|             self.out.emit("")
 | |
| 
 | |
|             # Write metadata array declaration
 | |
|             self.out.emit("#define OPCODE_METADATA_SIZE 512")
 | |
|             self.out.emit("#define OPCODE_UOP_NAME_SIZE 512")
 | |
|             self.out.emit("#define OPCODE_MACRO_EXPANSION_SIZE 256")
 | |
| 
 | |
|             with self.metadata_item(
 | |
|                 "const struct opcode_metadata "
 | |
|                 "_PyOpcode_opcode_metadata[OPCODE_METADATA_SIZE]",
 | |
|                 "=",
 | |
|                 ";",
 | |
|             ):
 | |
|                 # Write metadata for each instruction
 | |
|                 for thing in self.everything:
 | |
|                     match thing:
 | |
|                         case OverriddenInstructionPlaceHolder():
 | |
|                             continue
 | |
|                         case parsing.InstDef():
 | |
|                             self.write_metadata_for_inst(self.instrs[thing.name])
 | |
|                         case parsing.Macro():
 | |
|                             if thing.name not in self.instrs:
 | |
|                                 self.write_metadata_for_macro(
 | |
|                                     self.macro_instrs[thing.name]
 | |
|                                 )
 | |
|                         case parsing.Pseudo():
 | |
|                             self.write_metadata_for_pseudo(
 | |
|                                 self.pseudo_instrs[thing.name]
 | |
|                             )
 | |
|                         case _:
 | |
|                             assert_never(thing)
 | |
| 
 | |
|             with self.metadata_item(
 | |
|                 "const struct opcode_macro_expansion "
 | |
|                 "_PyOpcode_macro_expansion[OPCODE_MACRO_EXPANSION_SIZE]",
 | |
|                 "=",
 | |
|                 ";",
 | |
|             ):
 | |
|                 # Write macro expansion for each non-pseudo instruction
 | |
|                 for mac in self.macro_instrs.values():
 | |
|                     if is_super_instruction(mac):
 | |
|                         # Special-case the heck out of super-instructions
 | |
|                         self.write_super_expansions(mac.name)
 | |
|                     else:
 | |
|                         self.write_macro_expansions(
 | |
|                             mac.name, mac.parts, mac.cache_offset
 | |
|                         )
 | |
| 
 | |
|             with self.metadata_item(
 | |
|                 "const char * const _PyOpcode_uop_name[OPCODE_UOP_NAME_SIZE]", "=", ";"
 | |
|             ):
 | |
|                 self.write_uop_items(lambda name, counter: f'[{name}] = "{name}",')
 | |
| 
 | |
|             with self.metadata_item(
 | |
|                 f"const char *const _PyOpcode_OpName[{1 + max(self.opmap.values())}]",
 | |
|                 "=",
 | |
|                 ";",
 | |
|             ):
 | |
|                 for name in self.opmap:
 | |
|                     self.out.emit(f'[{name}] = "{name}",')
 | |
| 
 | |
|             with self.metadata_item(
 | |
|                 f"const uint8_t _PyOpcode_Caches[256]",
 | |
|                 "=",
 | |
|                 ";",
 | |
|             ):
 | |
|                 family_member_names: set[str] = set()
 | |
|                 for family in self.families.values():
 | |
|                     family_member_names.update(family.members)
 | |
|                 for mac in self.macro_instrs.values():
 | |
|                     if (
 | |
|                         mac.cache_offset > 0
 | |
|                         and mac.name not in family_member_names
 | |
|                         and not mac.name.startswith("INSTRUMENTED_")
 | |
|                     ):
 | |
|                         self.out.emit(f"[{mac.name}] = {mac.cache_offset},")
 | |
|                 # Irregular case:
 | |
|                 self.out.emit("[JUMP_BACKWARD] = 1,")
 | |
| 
 | |
|             deoptcodes = {}
 | |
|             for name, op in self.opmap.items():
 | |
|                 if op < 256:
 | |
|                     deoptcodes[name] = name
 | |
|             for name, family in self.families.items():
 | |
|                 for m in family.members:
 | |
|                     deoptcodes[m] = name
 | |
|             # special case:
 | |
|             deoptcodes["BINARY_OP_INPLACE_ADD_UNICODE"] = "BINARY_OP"
 | |
| 
 | |
|             with self.metadata_item(f"const uint8_t _PyOpcode_Deopt[256]", "=", ";"):
 | |
|                 for opt, deopt in sorted(deoptcodes.items()):
 | |
|                     self.out.emit(f"[{opt}] = {deopt},")
 | |
| 
 | |
|             self.out.emit("")
 | |
|             self.out.emit("#define EXTRA_CASES \\")
 | |
|             valid_opcodes = set(self.opmap.values())
 | |
|             with self.out.indent():
 | |
|                 for op in range(256):
 | |
|                     if op not in valid_opcodes:
 | |
|                         self.out.emit(f"case {op}: \\")
 | |
|                 self.out.emit("    ;\n")
 | |
| 
 | |
|         with open(pymetadata_filename, "w") as f:
 | |
|             # Create formatter
 | |
|             self.out = Formatter(f, 0, comment="#")
 | |
| 
 | |
|             self.write_provenance_header()
 | |
| 
 | |
|             # emit specializations
 | |
|             specialized_ops = set()
 | |
| 
 | |
|             self.out.emit("")
 | |
|             self.out.emit("_specializations = {")
 | |
|             for name, family in self.families.items():
 | |
|                 with self.out.indent():
 | |
|                     self.out.emit(f'"{family.name}": [')
 | |
|                     with self.out.indent():
 | |
|                         for m in family.members:
 | |
|                             self.out.emit(f'"{m}",')
 | |
|                         specialized_ops.update(family.members)
 | |
|                     self.out.emit(f"],")
 | |
|             self.out.emit("}")
 | |
| 
 | |
|             # Handle special case
 | |
|             self.out.emit("")
 | |
|             self.out.emit("# An irregular case:")
 | |
|             self.out.emit(
 | |
|                 '_specializations["BINARY_OP"].append('
 | |
|                 '"BINARY_OP_INPLACE_ADD_UNICODE")'
 | |
|             )
 | |
|             specialized_ops.add("BINARY_OP_INPLACE_ADD_UNICODE")
 | |
| 
 | |
|             ops = sorted((id, name) for (name, id) in self.opmap.items())
 | |
|             # emit specialized opmap
 | |
|             self.out.emit("")
 | |
|             with self.out.block("_specialized_opmap ="):
 | |
|                 for op, name in ops:
 | |
|                     if name in specialized_ops:
 | |
|                         self.out.emit(f"'{name}': {op},")
 | |
| 
 | |
|             # emit opmap
 | |
|             self.out.emit("")
 | |
|             with self.out.block("opmap ="):
 | |
|                 for op, name in ops:
 | |
|                     if name not in specialized_ops:
 | |
|                         self.out.emit(f"'{name}': {op},")
 | |
| 
 | |
|             for name in ["MIN_INSTRUMENTED_OPCODE", "HAVE_ARGUMENT"]:
 | |
|                 self.out.emit(f"{name} = {self.markers[name]}")
 | |
| 
 | |
|     def write_pseudo_instrs(self) -> None:
 | |
|         """Write the IS_PSEUDO_INSTR macro"""
 | |
|         self.out.emit("\n\n#define IS_PSEUDO_INSTR(OP)  ( \\")
 | |
|         for op in self.pseudos:
 | |
|             self.out.emit(f"    ((OP) == {op}) || \\")
 | |
|         self.out.emit(f"    0)")
 | |
| 
 | |
|     def write_uop_items(self, make_text: typing.Callable[[str, int], str]) -> None:
 | |
|         """Write '#define XXX NNN' for each uop"""
 | |
|         counter = 300  # TODO: Avoid collision with pseudo instructions
 | |
|         seen = set()
 | |
| 
 | |
|         def add(name: str) -> None:
 | |
|             if name in seen:
 | |
|                 return
 | |
|             nonlocal counter
 | |
|             self.out.emit(make_text(name, counter))
 | |
|             counter += 1
 | |
|             seen.add(name)
 | |
| 
 | |
|         # These two are first by convention
 | |
|         add("_EXIT_TRACE")
 | |
|         add("_SET_IP")
 | |
| 
 | |
|         for instr in self.instrs.values():
 | |
|             # Skip ops that are also macros -- those are desugared inst()s
 | |
|             if instr.name not in self.macros:
 | |
|                 add(instr.name)
 | |
| 
 | |
|     def write_macro_expansions(
 | |
|         self, name: str, parts: MacroParts, cache_offset: int
 | |
|     ) -> None:
 | |
|         """Write the macro expansions for a macro-instruction."""
 | |
|         # TODO: Refactor to share code with write_cody(), is_viaible_uop(), etc.
 | |
|         offset = 0  # Cache effect offset
 | |
|         expansions: list[tuple[str, int, int]] = []  # [(name, size, offset), ...]
 | |
|         for part in parts:
 | |
|             if isinstance(part, Component):
 | |
|                 # All component instructions must be viable uops
 | |
|                 if not part.instr.is_viable_uop():
 | |
|                     # This note just reminds us about macros that cannot
 | |
|                     # be expanded to Tier 2 uops. It is not an error.
 | |
|                     # It is sometimes emitted for macros that have a
 | |
|                     # manual translation in translate_bytecode_to_trace()
 | |
|                     # in Python/optimizer.c.
 | |
|                     if len(parts) > 1 or part.instr.name != name:
 | |
|                         self.note(
 | |
|                             f"Part {part.instr.name} of {name} is not a viable uop",
 | |
|                             part.instr.inst,
 | |
|                         )
 | |
|                     return
 | |
|                 if not part.active_caches:
 | |
|                     if part.instr.name == "_SET_IP":
 | |
|                         size, offset = OPARG_SIZES["OPARG_SET_IP"], cache_offset
 | |
|                     else:
 | |
|                         size, offset = OPARG_SIZES["OPARG_FULL"], 0
 | |
|                 else:
 | |
|                     # If this assert triggers, is_viable_uops() lied
 | |
|                     assert len(part.active_caches) == 1, (name, part.instr.name)
 | |
|                     cache = part.active_caches[0]
 | |
|                     size, offset = cache.effect.size, cache.offset
 | |
|                 expansions.append((part.instr.name, size, offset))
 | |
|         assert len(expansions) > 0, f"Macro {name} has empty expansion?!"
 | |
|         self.write_expansions(name, expansions)
 | |
| 
 | |
|     def write_super_expansions(self, name: str) -> None:
 | |
|         """Write special macro expansions for super-instructions.
 | |
| 
 | |
|         If you get an assertion failure here, you probably have accidentally
 | |
|         violated one of the assumptions here.
 | |
| 
 | |
|         - A super-instruction's name is of the form FIRST_SECOND where
 | |
|           FIRST and SECOND are regular instructions whose name has the
 | |
|           form FOO_BAR. Thus, there must be exactly 3 underscores.
 | |
|           Example: LOAD_CONST_STORE_FAST.
 | |
| 
 | |
|         - A super-instruction's body uses `oparg1 and `oparg2`, and no
 | |
|           other instruction's body uses those variable names.
 | |
| 
 | |
|         - A super-instruction has no active (used) cache entries.
 | |
| 
 | |
|         In the expansion, the first instruction's operand is all but the
 | |
|         bottom 4 bits of the super-instruction's oparg, and the second
 | |
|         instruction's operand is the bottom 4 bits. We use the special
 | |
|         size codes OPARG_TOP and OPARG_BOTTOM for these.
 | |
|         """
 | |
|         pieces = name.split("_")
 | |
|         assert len(pieces) == 4, f"{name} doesn't look like a super-instr"
 | |
|         name1 = "_".join(pieces[:2])
 | |
|         name2 = "_".join(pieces[2:])
 | |
|         assert name1 in self.instrs, f"{name1} doesn't match any instr"
 | |
|         assert name2 in self.instrs, f"{name2} doesn't match any instr"
 | |
|         instr1 = self.instrs[name1]
 | |
|         instr2 = self.instrs[name2]
 | |
|         assert not instr1.active_caches, f"{name1} has active caches"
 | |
|         assert not instr2.active_caches, f"{name2} has active caches"
 | |
|         expansions: list[tuple[str, int, int]] = [
 | |
|             (name1, OPARG_SIZES["OPARG_TOP"], 0),
 | |
|             (name2, OPARG_SIZES["OPARG_BOTTOM"], 0),
 | |
|         ]
 | |
|         self.write_expansions(name, expansions)
 | |
| 
 | |
|     def write_expansions(
 | |
|         self, name: str, expansions: list[tuple[str, int, int]]
 | |
|     ) -> None:
 | |
|         pieces = [
 | |
|             f"{{ {name}, {size}, {offset} }}" for name, size, offset in expansions
 | |
|         ]
 | |
|         self.out.emit(
 | |
|             f"[{name}] = "
 | |
|             f"{{ .nuops = {len(pieces)}, .uops = {{ {', '.join(pieces)} }} }},"
 | |
|         )
 | |
| 
 | |
|     def emit_metadata_entry(self, name: str, fmt: str, flags: InstructionFlags) -> None:
 | |
|         flag_names = flags.names(value=True)
 | |
|         if not flag_names:
 | |
|             flag_names.append("0")
 | |
|         self.out.emit(
 | |
|             f"[{name}] = {{ true, {INSTR_FMT_PREFIX}{fmt},"
 | |
|             f" {' | '.join(flag_names)} }},"
 | |
|         )
 | |
| 
 | |
|     def write_metadata_for_inst(self, instr: Instruction) -> None:
 | |
|         """Write metadata for a single instruction."""
 | |
|         self.emit_metadata_entry(instr.name, instr.instr_fmt, instr.instr_flags)
 | |
| 
 | |
|     def write_metadata_for_macro(self, mac: MacroInstruction) -> None:
 | |
|         """Write metadata for a macro-instruction."""
 | |
|         self.emit_metadata_entry(mac.name, mac.instr_fmt, mac.instr_flags)
 | |
| 
 | |
|     def write_metadata_for_pseudo(self, ps: PseudoInstruction) -> None:
 | |
|         """Write metadata for a macro-instruction."""
 | |
|         self.emit_metadata_entry(ps.name, ps.instr_fmt, ps.instr_flags)
 | |
| 
 | |
|     def write_instructions(
 | |
|         self, output_filename: str, emit_line_directives: bool
 | |
|     ) -> None:
 | |
|         """Write instructions to output file."""
 | |
|         with open(output_filename, "w") as f:
 | |
|             # Create formatter
 | |
|             self.out = Formatter(f, 8, emit_line_directives)
 | |
| 
 | |
|             self.write_provenance_header()
 | |
| 
 | |
|             # Write and count instructions of all kinds
 | |
|             n_macros = 0
 | |
|             for thing in self.everything:
 | |
|                 match thing:
 | |
|                     case OverriddenInstructionPlaceHolder():
 | |
|                         self.write_overridden_instr_place_holder(thing)
 | |
|                     case parsing.InstDef():
 | |
|                         pass
 | |
|                     case parsing.Macro():
 | |
|                         n_macros += 1
 | |
|                         mac = self.macro_instrs[thing.name]
 | |
|                         stacking.write_macro_instr(
 | |
|                             mac, self.out, self.families.get(mac.name)
 | |
|                         )
 | |
|                     case parsing.Pseudo():
 | |
|                         pass
 | |
|                     case _:
 | |
|                         assert_never(thing)
 | |
| 
 | |
|         print(
 | |
|             f"Wrote {n_macros} cases to {output_filename}",
 | |
|             file=sys.stderr,
 | |
|         )
 | |
| 
 | |
|     def write_executor_instructions(
 | |
|         self, executor_filename: str, emit_line_directives: bool
 | |
|     ) -> None:
 | |
|         """Generate cases for the Tier 2 interpreter."""
 | |
|         n_uops = 0
 | |
|         with open(executor_filename, "w") as f:
 | |
|             self.out = Formatter(f, 8, emit_line_directives)
 | |
|             self.write_provenance_header()
 | |
|             for instr in self.instrs.values():
 | |
|                 if instr.is_viable_uop():
 | |
|                     n_uops += 1
 | |
|                     self.out.emit("")
 | |
|                     with self.out.block(f"case {instr.name}:"):
 | |
|                         stacking.write_single_instr(instr, self.out, tier=TIER_TWO)
 | |
|                         if instr.check_eval_breaker:
 | |
|                             self.out.emit("CHECK_EVAL_BREAKER();")
 | |
|                         self.out.emit("break;")
 | |
|         print(
 | |
|             f"Wrote {n_uops} cases to {executor_filename}",
 | |
|             file=sys.stderr,
 | |
|         )
 | |
| 
 | |
|     def write_abstract_interpreter_instructions(
 | |
|         self, abstract_interpreter_filename: str, emit_line_directives: bool
 | |
|     ) -> None:
 | |
|         """Generate cases for the Tier 2 abstract interpreter/analzyer."""
 | |
|         with open(abstract_interpreter_filename, "w") as f:
 | |
|             self.out = Formatter(f, 8, emit_line_directives)
 | |
|             self.write_provenance_header()
 | |
|             for instr in self.instrs.values():
 | |
|                 instr = AbstractInstruction(instr.inst)
 | |
|                 if (
 | |
|                     instr.is_viable_uop()
 | |
|                     and instr.name not in SPECIALLY_HANDLED_ABSTRACT_INSTR
 | |
|                 ):
 | |
|                     self.out.emit("")
 | |
|                     with self.out.block(f"case {instr.name}:"):
 | |
|                         instr.write(self.out, tier=TIER_TWO)
 | |
|                         self.out.emit("break;")
 | |
|         print(
 | |
|             f"Wrote some stuff to {abstract_interpreter_filename}",
 | |
|             file=sys.stderr,
 | |
|         )
 | |
| 
 | |
|     def write_overridden_instr_place_holder(
 | |
|         self, place_holder: OverriddenInstructionPlaceHolder
 | |
|     ) -> None:
 | |
|         self.out.emit("")
 | |
|         self.out.emit(
 | |
|             f"{self.out.comment} TARGET({place_holder.name}) overridden by later definition"
 | |
|         )
 | |
| 
 | |
| 
 | |
| def is_super_instruction(mac: MacroInstruction) -> bool:
 | |
|     if (
 | |
|         len(mac.parts) == 1
 | |
|         and isinstance(mac.parts[0], Component)
 | |
|         and variable_used(mac.parts[0].instr.inst, "oparg1")
 | |
|     ):
 | |
|         assert variable_used(mac.parts[0].instr.inst, "oparg2")
 | |
|         return True
 | |
|     else:
 | |
|         return False
 | |
| 
 | |
| 
 | |
| def main() -> None:
 | |
|     """Parse command line, parse input, analyze, write output."""
 | |
|     args = arg_parser.parse_args()  # Prints message and sys.exit(2) on error
 | |
|     if len(args.input) == 0:
 | |
|         args.input.append(DEFAULT_INPUT)
 | |
| 
 | |
|     # Raises OSError if input unreadable
 | |
|     a = Generator(args.input)
 | |
| 
 | |
|     a.parse()  # Raises SyntaxError on failure
 | |
|     a.analyze()  # Prints messages and sets a.errors on failure
 | |
|     if a.errors:
 | |
|         sys.exit(f"Found {a.errors} errors")
 | |
|     if args.verbose:
 | |
|         # Load execution counts from bmraw.json, if it exists
 | |
|         a.report_non_viable_uops("bmraw.json")
 | |
|         return
 | |
| 
 | |
|     # These raise OSError if output can't be written
 | |
|     a.write_instructions(args.output, args.emit_line_directives)
 | |
| 
 | |
|     a.assign_opcode_ids()
 | |
|     a.write_opcode_ids(args.opcode_ids_h, args.opcode_targets_h)
 | |
|     a.write_metadata(args.metadata, args.pymetadata)
 | |
|     a.write_executor_instructions(args.executor_cases, args.emit_line_directives)
 | |
|     a.write_abstract_interpreter_instructions(
 | |
|         args.abstract_interpreter_cases, args.emit_line_directives
 | |
|     )
 | |
| 
 | |
| 
 | |
| if __name__ == "__main__":
 | |
|     main()
 |