| """Generate the cases for the tier 2 optimizer. | |
| Reads the instruction definitions from bytecodes.c and optimizer_bytecodes.c | |
| Writes the cases to optimizer_cases.c.h, which is #included in Python/optimizer_analysis.c. | |
| """ | |
| import argparse | |
| from analyzer import ( | |
| Analysis, | |
| Instruction, | |
| Uop, | |
| analyze_files, | |
| StackItem, | |
| analysis_error, | |
| CodeSection, | |
| Label, | |
| ) | |
| from generators_common import ( | |
| DEFAULT_INPUT, | |
| ROOT, | |
| write_header, | |
| Emitter, | |
| TokenIterator, | |
| always_true, | |
| ) | |
| from cwriter import CWriter | |
| from typing import TextIO | |
| from lexer import Token | |
| from stack import Local, Stack, StackError, Storage | |
| DEFAULT_OUTPUT = ROOT / "Python/optimizer_cases.c.h" | |
| DEFAULT_ABSTRACT_INPUT = (ROOT / "Python/optimizer_bytecodes.c").absolute().as_posix() | |
| def validate_uop(override: Uop, uop: Uop) -> None: | |
| """ | |
| Check that the overridden uop (defined in 'optimizer_bytecodes.c') | |
| has the same stack effects as the original uop (defined in 'bytecodes.c'). | |
| Ensure that: | |
| - The number of inputs and outputs is the same. | |
| - The names of the inputs and outputs are the same | |
| (except for 'unused' which is ignored). | |
| - The sizes of the inputs and outputs are the same. | |
| """ | |
| for stack_effect in ('inputs', 'outputs'): | |
| orig_effects = getattr(uop.stack, stack_effect) | |
| new_effects = getattr(override.stack, stack_effect) | |
| if len(orig_effects) != len(new_effects): | |
| msg = ( | |
| f"{uop.name}: Must have the same number of {stack_effect} " | |
| "in bytecodes.c and optimizer_bytecodes.c " | |
| f"({len(orig_effects)} != {len(new_effects)})" | |
| ) | |
| raise analysis_error(msg, override.body.open) | |
| for orig, new in zip(orig_effects, new_effects, strict=True): | |
| if orig.name != new.name and orig.name != "unused" and new.name != "unused": | |
| msg = ( | |
| f"{uop.name}: {stack_effect.capitalize()} must have " | |
| "equal names in bytecodes.c and optimizer_bytecodes.c " | |
| f"({orig.name} != {new.name})" | |
| ) | |
| raise analysis_error(msg, override.body.open) | |
| if orig.size != new.size: | |
| msg = ( | |
| f"{uop.name}: {stack_effect.capitalize()} must have " | |
| "equal sizes in bytecodes.c and optimizer_bytecodes.c " | |
| f"({orig.size!r} != {new.size!r})" | |
| ) | |
| raise analysis_error(msg, override.body.open) | |
| def type_name(var: StackItem) -> str: | |
| if var.is_array(): | |
| return "JitOptRef *" | |
| return "JitOptRef " | |
| def stackref_type_name(var: StackItem) -> str: | |
| assert not var.is_array(), "Unsafe to convert a symbol to an array-like StackRef." | |
| return "_PyStackRef " | |
| def declare_variables(uop: Uop, out: CWriter, skip_inputs: bool) -> None: | |
| variables = {"unused"} | |
| if not skip_inputs: | |
| for var in reversed(uop.stack.inputs): | |
| if var.used and var.name not in variables: | |
| variables.add(var.name) | |
| out.emit(f"{type_name(var)}{var.name};\n") | |
| for var in uop.stack.outputs: | |
| if var.peek: | |
| continue | |
| if var.name not in variables: | |
| variables.add(var.name) | |
| out.emit(f"{type_name(var)}{var.name};\n") | |
| def decref_inputs( | |
| out: CWriter, | |
| tkn: Token, | |
| tkn_iter: TokenIterator, | |
| uop: Uop, | |
| stack: Stack, | |
| inst: Instruction | None, | |
| ) -> None: | |
| next(tkn_iter) | |
| next(tkn_iter) | |
| next(tkn_iter) | |
| out.emit_at("", tkn) | |
| def emit_default(out: CWriter, uop: Uop, stack: Stack) -> None: | |
| assert stack.physical_sp is not None | |
| null = CWriter.null() | |
| for var in reversed(uop.stack.inputs): | |
| stack.pop(var, null) | |
| offset = stack.base_offset - stack.physical_sp | |
| for var in uop.stack.outputs: | |
| if var.is_array() and not var.peek and not var.name == "unused": | |
| c_offset = offset.to_c() | |
| out.emit(f"{var.name} = &stack_pointer[{c_offset}];\n") | |
| offset = offset.push(var) | |
| for var in uop.stack.outputs: | |
| local = Local.undefined(var) | |
| stack.push(local) | |
| if var.name != "unused" and not var.peek: | |
| local.in_local = True | |
| if var.is_array(): | |
| if var.size == "1": | |
| out.emit(f"{var.name}[0] = sym_new_not_null(ctx);\n") | |
| else: | |
| out.emit(f"for (int _i = {var.size}; --_i >= 0;) {{\n") | |
| out.emit(f"{var.name}[_i] = sym_new_not_null(ctx);\n") | |
| out.emit("}\n") | |
| elif var.name == "null": | |
| out.emit(f"{var.name} = sym_new_null(ctx);\n") | |
| else: | |
| out.emit(f"{var.name} = sym_new_not_null(ctx);\n") | |
| class OptimizerEmitter(Emitter): | |
| def __init__(self, out: CWriter, labels: dict[str, Label], original_uop: Uop, stack: Stack): | |
| super().__init__(out, labels) | |
| self._replacers["REPLACE_OPCODE_IF_EVALUATES_PURE"] = self.replace_opcode_if_evaluates_pure | |
| self.original_uop = original_uop | |
| self.stack = stack | |
| def emit_save(self, storage: Storage) -> None: | |
| storage.flush(self.out) | |
| def emit_reload(self, storage: Storage) -> None: | |
| pass | |
| def reload_stack( | |
| self, | |
| tkn: Token, | |
| tkn_iter: TokenIterator, | |
| uop: CodeSection, | |
| storage: Storage, | |
| inst: Instruction | None, | |
| ) -> bool: | |
| next(tkn_iter) | |
| next(tkn_iter) | |
| next(tkn_iter) | |
| return True | |
| def goto_label(self, goto: Token, label: Token, storage: Storage) -> None: | |
| self.out.emit(goto) | |
| self.out.emit(label) | |
| def replace_opcode_if_evaluates_pure( | |
| self, | |
| tkn: Token, | |
| tkn_iter: TokenIterator, | |
| uop: CodeSection, | |
| storage: Storage, | |
| inst: Instruction | None, | |
| ) -> bool: | |
| assert isinstance(uop, Uop) | |
| input_identifiers = [] | |
| for token in tkn_iter: | |
| if token.kind == "IDENTIFIER": | |
| input_identifiers.append(token) | |
| if token.kind == "SEMI": | |
| break | |
| output_identifier = input_identifiers.pop() | |
| if len(input_identifiers) == 0: | |
| raise analysis_error( | |
| "To evaluate an operation as pure, it must have at least 1 input", | |
| tkn | |
| ) | |
| # Check that the input identifiers belong to the uop's | |
| # input stack effect | |
| uop_stack_effect_input_identifers = {inp.name for inp in uop.stack.inputs} | |
| for input_tkn in input_identifiers: | |
| if input_tkn.text not in uop_stack_effect_input_identifers: | |
| raise analysis_error(f"{input_tkn.text} referenced in " | |
| f"REPLACE_OPCODE_IF_EVALUATES_PURE but does not " | |
| f"exist in the base uop's input stack effects", | |
| input_tkn) | |
| input_identifiers_as_str = {tkn.text for tkn in input_identifiers} | |
| used_stack_inputs = [inp for inp in uop.stack.inputs if inp.name in input_identifiers_as_str] | |
| assert len(used_stack_inputs) > 0 | |
| self.out.start_line() | |
| emitter = OptimizerConstantEmitter(self.out, {}, self.original_uop, self.stack.copy()) | |
| emitter.emit("if (\n") | |
| for inp in used_stack_inputs[:-1]: | |
| emitter.emit(f"sym_is_safe_const(ctx, {inp.name}) &&\n") | |
| emitter.emit(f"sym_is_safe_const(ctx, {used_stack_inputs[-1].name})\n") | |
| emitter.emit(') {\n') | |
| # Declare variables, before they are shadowed. | |
| for inp in used_stack_inputs: | |
| if inp.used: | |
| emitter.emit(f"{type_name(inp)}{inp.name}_sym = {inp.name};\n") | |
| # Shadow the symbolic variables with stackrefs. | |
| for inp in used_stack_inputs: | |
| if inp.is_array(): | |
| raise analysis_error("Pure evaluation cannot take array-like inputs.", tkn) | |
| if inp.used: | |
| emitter.emit(f"{stackref_type_name(inp)}{inp.name} = sym_get_const_as_stackref(ctx, {inp.name}_sym);\n") | |
| # Rename all output variables to stackref variant. | |
| for outp in self.original_uop.stack.outputs: | |
| if outp.is_array(): | |
| raise analysis_error( | |
| "Array output StackRefs not supported for evaluating pure ops.", | |
| self.original_uop.body.open | |
| ) | |
| emitter.emit(f"_PyStackRef {outp.name}_stackref;\n") | |
| storage = Storage.for_uop(self.stack, self.original_uop, CWriter.null(), check_liveness=False) | |
| # No reference management of outputs needed. | |
| for var in storage.outputs: | |
| var.in_local = True | |
| emitter.emit("/* Start of uop copied from bytecodes for constant evaluation */\n") | |
| emitter.emit_tokens(self.original_uop, storage, inst=None, emit_braces=False) | |
| self.out.start_line() | |
| emitter.emit("/* End of uop copied from bytecodes for constant evaluation */\n") | |
| for outp in self.original_uop.stack.outputs: | |
| if not outp.name == output_identifier.text: | |
| emitter.emit(f"(void){outp.name}_stackref;\n") | |
| # Output stackref is created from new reference. | |
| emitter.emit(f"{output_identifier.text} = sym_new_const_steal(ctx, PyStackRef_AsPyObjectSteal({output_identifier.text}_stackref));\n") | |
| if self.original_uop.name.startswith('_'): | |
| # Map input count to output index (from TOS) and the appropriate constant-loading uop | |
| input_count_to_uop = { | |
| 1: { | |
| # (a -- res), usually for unary ops | |
| 0: [("_POP_TOP", "0, 0"), | |
| ("_LOAD_CONST_INLINE_BORROW", | |
| "0, (uintptr_t)result")], | |
| # (left -- res, left) | |
| # usually for unary ops with passthrough references | |
| 1: [("_LOAD_CONST_INLINE_BORROW", | |
| "0, (uintptr_t)result"), | |
| ("_SWAP", "2, 0")], | |
| }, | |
| 2: { | |
| # (a, b -- res), usually for binary ops | |
| 0: [("_POP_TOP", "0, 0"), | |
| ("_POP_TOP", "0, 0"), | |
| ("_LOAD_CONST_INLINE_BORROW", | |
| "0, (uintptr_t)result")], | |
| # (left, right -- res, left, right) | |
| # usually for binary ops with passthrough references | |
| 2: [("_LOAD_CONST_INLINE_BORROW", | |
| "0, (uintptr_t)result"), | |
| ("_RROT_3", "0, 0")], | |
| }, | |
| } | |
| output_index = -1 | |
| for idx, outp in enumerate(reversed(uop.stack.outputs)): | |
| if outp.name == output_identifier.text: | |
| output_index = idx | |
| break | |
| else: | |
| raise analysis_error(f"Could not find output {output_identifier.text} in uop.", output_identifier) | |
| assert output_index >= 0 | |
| input_count = len(used_stack_inputs) | |
| if input_count in input_count_to_uop and output_index in input_count_to_uop[input_count]: | |
| ops = input_count_to_uop[input_count][output_index] | |
| input_desc = "one input" if input_count == 1 else "two inputs" | |
| ops_desc = " + ".join(op for op, _ in ops) | |
| emitter.emit(f"if (sym_is_const(ctx, {output_identifier.text})) {{\n") | |
| emitter.emit(f"PyObject *result = sym_get_const(ctx, {output_identifier.text});\n") | |
| emitter.emit(f"if (_Py_IsImmortal(result)) {{\n") | |
| emitter.emit(f"// Replace with {ops_desc} since we have {input_desc} and an immortal result\n") | |
| for op, args in ops: | |
| emitter.emit(f"ADD_OP({op}, {args});\n") | |
| emitter.emit("}\n") | |
| emitter.emit("}\n") | |
| storage.flush(self.out) | |
| emitter.emit("break;\n") | |
| emitter.emit("}\n") | |
| return True | |
| class OptimizerConstantEmitter(OptimizerEmitter): | |
| def __init__(self, out: CWriter, labels: dict[str, Label], original_uop: Uop, stack: Stack): | |
| super().__init__(out, labels, original_uop, stack) | |
| # Replace all outputs to point to their stackref versions. | |
| overrides = { | |
| outp.name: self.emit_stackref_override for outp in self.original_uop.stack.outputs | |
| } | |
| self._replacers = {**self._replacers, **overrides} | |
| self.cannot_escape = True | |
| def emit_to_with_replacement( | |
| self, | |
| out: CWriter, | |
| tkn_iter: TokenIterator, | |
| end: str, | |
| uop: CodeSection, | |
| storage: Storage, | |
| inst: Instruction | None | |
| ) -> Token: | |
| parens = 0 | |
| for tkn in tkn_iter: | |
| if tkn.kind == end and parens == 0: | |
| return tkn | |
| if tkn.kind == "LPAREN": | |
| parens += 1 | |
| if tkn.kind == "RPAREN": | |
| parens -= 1 | |
| if tkn.text in self._replacers: | |
| self._replacers[tkn.text](tkn, tkn_iter, uop, storage, inst) | |
| else: | |
| out.emit(tkn) | |
| raise analysis_error(f"Expecting {end}. Reached end of file", tkn) | |
| def emit_stackref_override( | |
| self, | |
| tkn: Token, | |
| tkn_iter: TokenIterator, | |
| uop: CodeSection, | |
| storage: Storage, | |
| inst: Instruction | None, | |
| ) -> bool: | |
| self.out.emit(tkn) | |
| self.out.emit("_stackref ") | |
| return True | |
| def deopt_if( | |
| self, | |
| tkn: Token, | |
| tkn_iter: TokenIterator, | |
| uop: CodeSection, | |
| storage: Storage, | |
| inst: Instruction | None, | |
| ) -> bool: | |
| self.out.start_line() | |
| self.out.emit("if (") | |
| lparen = next(tkn_iter) | |
| assert lparen.kind == "LPAREN" | |
| first_tkn = tkn_iter.peek() | |
| self.emit_to_with_replacement(self.out, tkn_iter, "RPAREN", uop, storage, inst) | |
| self.emit(") {\n") | |
| next(tkn_iter) # Semi colon | |
| # We guarantee this will deopt in real-world code | |
| # via constants analysis. So just bail. | |
| self.emit("ctx->done = true;\n") | |
| self.emit("break;\n") | |
| self.emit("}\n") | |
| return not always_true(first_tkn) | |
| exit_if = deopt_if | |
| def error_if( | |
| self, | |
| tkn: Token, | |
| tkn_iter: TokenIterator, | |
| uop: CodeSection, | |
| storage: Storage, | |
| inst: Instruction | None, | |
| ) -> bool: | |
| lparen = next(tkn_iter) | |
| assert lparen.kind == "LPAREN" | |
| first_tkn = tkn_iter.peek() | |
| unconditional = always_true(first_tkn) | |
| if unconditional: | |
| next(tkn_iter) | |
| next(tkn_iter) # RPAREN | |
| self.out.start_line() | |
| else: | |
| self.out.emit_at("if ", tkn) | |
| self.emit(lparen) | |
| self.emit_to_with_replacement(self.out, tkn_iter, "RPAREN", uop, storage, inst) | |
| self.out.emit(") {\n") | |
| next(tkn_iter) # Semi colon | |
| storage.clear_inputs("at ERROR_IF") | |
| self.out.emit("goto error;\n") | |
| if not unconditional: | |
| self.out.emit("}\n") | |
| return not unconditional | |
| def write_uop( | |
| override: Uop | None, | |
| uop: Uop, | |
| out: CWriter, | |
| stack: Stack, | |
| debug: bool, | |
| skip_inputs: bool, | |
| ) -> None: | |
| locals: dict[str, Local] = {} | |
| prototype = override if override else uop | |
| try: | |
| out.start_line() | |
| if override: | |
| storage = Storage.for_uop(stack, prototype, out, check_liveness=False) | |
| if debug: | |
| args = [] | |
| for input in prototype.stack.inputs: | |
| if not input.peek or override: | |
| args.append(input.name) | |
| out.emit(f'DEBUG_PRINTF({", ".join(args)});\n') | |
| if override: | |
| idx = 0 | |
| for cache in uop.caches: | |
| if cache.name != "unused": | |
| if cache.size == 4: | |
| type = cast = "PyObject *" | |
| else: | |
| type = f"uint{cache.size*16}_t " | |
| cast = f"uint{cache.size*16}_t" | |
| out.emit(f"{type}{cache.name} = ({cast})this_instr->operand{idx};\n") | |
| idx += 1 | |
| if override: | |
| emitter = OptimizerEmitter(out, {}, uop, stack.copy()) | |
| # No reference management of inputs needed. | |
| for var in storage.inputs: # type: ignore[possibly-undefined] | |
| var.in_local = False | |
| _, storage = emitter.emit_tokens(override, storage, None, False) | |
| out.start_line() | |
| storage.flush(out) | |
| out.start_line() | |
| else: | |
| emit_default(out, uop, stack) | |
| out.start_line() | |
| stack.flush(out) | |
| except StackError as ex: | |
| raise analysis_error(ex.args[0], prototype.body.open) # from None | |
| SKIPS = ("_EXTENDED_ARG",) | |
| def generate_abstract_interpreter( | |
| filenames: list[str], | |
| abstract: Analysis, | |
| base: Analysis, | |
| outfile: TextIO, | |
| debug: bool, | |
| ) -> None: | |
| write_header(__file__, filenames, outfile) | |
| out = CWriter(outfile, 2, False) | |
| out.emit("\n") | |
| base_uop_names = set([uop.name for uop in base.uops.values()]) | |
| for abstract_uop_name in abstract.uops: | |
| if abstract_uop_name not in base_uop_names: | |
| raise ValueError(f"All abstract uops should override base uops, " | |
| f"but {abstract_uop_name} is not.") | |
| for uop in base.uops.values(): | |
| override: Uop | None = None | |
| if uop.name in abstract.uops: | |
| override = abstract.uops[uop.name] | |
| validate_uop(override, uop) | |
| if uop.properties.tier == 1: | |
| continue | |
| if uop.replicates: | |
| continue | |
| if uop.is_super(): | |
| continue | |
| if not uop.is_viable(): | |
| out.emit(f"/* {uop.name} is not a viable micro-op for tier 2 */\n\n") | |
| continue | |
| out.emit(f"case {uop.name}: {{\n") | |
| if override: | |
| declare_variables(override, out, skip_inputs=False) | |
| else: | |
| declare_variables(uop, out, skip_inputs=True) | |
| stack = Stack(check_stack_bounds=True) | |
| write_uop(override, uop, out, stack, debug, skip_inputs=(override is None)) | |
| out.start_line() | |
| out.emit("break;\n") | |
| out.emit("}") | |
| out.emit("\n\n") | |
| def generate_tier2_abstract_from_files( | |
| filenames: list[str], outfilename: str, debug: bool = False | |
| ) -> None: | |
| assert len(filenames) == 2, "Need a base file and an abstract cases file." | |
| base = analyze_files([filenames[0]]) | |
| abstract = analyze_files([filenames[1]]) | |
| with open(outfilename, "w") as outfile: | |
| generate_abstract_interpreter(filenames, abstract, base, outfile, debug) | |
| arg_parser = argparse.ArgumentParser( | |
| description="Generate the code for the tier 2 interpreter.", | |
| formatter_class=argparse.ArgumentDefaultsHelpFormatter, | |
| ) | |
| arg_parser.add_argument( | |
| "-o", "--output", type=str, help="Generated code", default=DEFAULT_OUTPUT | |
| ) | |
| arg_parser.add_argument("input", nargs="*", help="Abstract interpreter definition file") | |
| arg_parser.add_argument( | |
| "base", nargs="*", help="The base instruction definition file(s)" | |
| ) | |
| arg_parser.add_argument("-d", "--debug", help="Insert debug calls", action="store_true") | |
| if __name__ == "__main__": | |
| args = arg_parser.parse_args() | |
| if not args.input: | |
| args.base.append(DEFAULT_INPUT) | |
| args.input.append(DEFAULT_ABSTRACT_INPUT) | |
| else: | |
| args.base.append(args.input[-1]) | |
| args.input.pop() | |
| abstract = analyze_files(args.input) | |
| base = analyze_files(args.base) | |
| with open(args.output, "w") as outfile: | |
| generate_abstract_interpreter(args.input, abstract, base, outfile, args.debug) |