| """Generate CPython API wrapper functions for native functions. | |
| The wrapper functions are used by the CPython runtime when calling | |
| native functions from interpreted code, and when the called function | |
| can't be determined statically in compiled code. They validate, match, | |
| unbox and type check function arguments, and box return values as | |
| needed. All wrappers accept and return 'PyObject *' (boxed) values. | |
| The wrappers aren't used for most calls between two native functions | |
| or methods in a single compilation unit. | |
| """ | |
| from __future__ import annotations | |
| from collections.abc import Sequence | |
| from mypy.nodes import ARG_NAMED, ARG_NAMED_OPT, ARG_OPT, ARG_POS, ARG_STAR, ARG_STAR2, ArgKind | |
| from mypy.operators import op_methods_to_symbols, reverse_op_method_names, reverse_op_methods | |
| from mypyc.codegen.emit import AssignHandler, Emitter, ErrorHandler, GotoHandler, ReturnHandler | |
| from mypyc.common import ( | |
| BITMAP_BITS, | |
| BITMAP_TYPE, | |
| DUNDER_PREFIX, | |
| NATIVE_PREFIX, | |
| PREFIX, | |
| bitmap_name, | |
| ) | |
| from mypyc.ir.class_ir import ClassIR | |
| from mypyc.ir.func_ir import FUNC_STATICMETHOD, FuncIR, RuntimeArg | |
| from mypyc.ir.rtypes import ( | |
| RInstance, | |
| RType, | |
| is_bool_rprimitive, | |
| is_int_rprimitive, | |
| is_object_rprimitive, | |
| object_rprimitive, | |
| ) | |
| from mypyc.namegen import NameGenerator | |
| # Generic vectorcall wrapper functions (Python 3.7+) | |
| # | |
| # A wrapper function has a signature like this: | |
| # | |
| # PyObject *fn(PyObject *self, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) | |
| # | |
| # The function takes a self object, pointer to an array of arguments, | |
| # the number of positional arguments, and a tuple of keyword argument | |
| # names (that are stored starting in args[nargs]). | |
| # | |
| # It returns the returned object, or NULL on an exception. | |
| # | |
| # These are more efficient than legacy wrapper functions, since | |
| # usually no tuple or dict objects need to be created for the | |
| # arguments. Vectorcalls also use pre-constructed str objects for | |
| # keyword argument names and other pre-computed information, instead | |
| # of processing the argument format string on each call. | |
| def wrapper_function_header(fn: FuncIR, names: NameGenerator) -> str: | |
| """Return header of a vectorcall wrapper function. | |
| See comment above for a summary of the arguments. | |
| """ | |
| assert not fn.internal | |
| return ( | |
| "PyObject *{prefix}{name}(" | |
| "PyObject *self, PyObject *const *args, size_t nargs, PyObject *kwnames)" | |
| ).format(prefix=PREFIX, name=fn.cname(names)) | |
| def generate_traceback_code( | |
| fn: FuncIR, emitter: Emitter, source_path: str, module_name: str | |
| ) -> str: | |
| # If we hit an error while processing arguments, then we emit a | |
| # traceback frame to make it possible to debug where it happened. | |
| # Unlike traceback frames added for exceptions seen in IR, we do this | |
| # even if there is no `traceback_name`. This is because the error will | |
| # have originated here and so we need it in the traceback. | |
| globals_static = emitter.static_name("globals", module_name) | |
| traceback_code = 'CPy_AddTraceback("%s", "%s", %d, %s);' % ( | |
| source_path.replace("\\", "\\\\"), | |
| fn.traceback_name or fn.name, | |
| fn.line, | |
| globals_static, | |
| ) | |
| return traceback_code | |
| def make_arg_groups(args: list[RuntimeArg]) -> dict[ArgKind, list[RuntimeArg]]: | |
| """Group arguments by kind.""" | |
| return {k: [arg for arg in args if arg.kind == k] for k in ArgKind} | |
| def reorder_arg_groups(groups: dict[ArgKind, list[RuntimeArg]]) -> list[RuntimeArg]: | |
| """Reorder argument groups to match their order in a format string.""" | |
| return groups[ARG_POS] + groups[ARG_OPT] + groups[ARG_NAMED_OPT] + groups[ARG_NAMED] | |
| def make_static_kwlist(args: list[RuntimeArg]) -> str: | |
| arg_names = "".join(f'"{arg.name}", ' for arg in args) | |
| return f"static const char * const kwlist[] = {{{arg_names}0}};" | |
| def make_format_string(func_name: str | None, groups: dict[ArgKind, list[RuntimeArg]]) -> str: | |
| """Return a format string that specifies the accepted arguments. | |
| The format string is an extended subset of what is supported by | |
| PyArg_ParseTupleAndKeywords(). Only the type 'O' is used, and we | |
| also support some extensions: | |
| - Required keyword-only arguments are introduced after '@' | |
| - If the function receives *args or **kwargs, we add a '%' prefix | |
| Each group requires the previous groups' delimiters to be present | |
| first. | |
| These are used by both vectorcall and legacy wrapper functions. | |
| """ | |
| format = "" | |
| if groups[ARG_STAR] or groups[ARG_STAR2]: | |
| format += "%" | |
| format += "O" * len(groups[ARG_POS]) | |
| if groups[ARG_OPT] or groups[ARG_NAMED_OPT] or groups[ARG_NAMED]: | |
| format += "|" + "O" * len(groups[ARG_OPT]) | |
| if groups[ARG_NAMED_OPT] or groups[ARG_NAMED]: | |
| format += "$" + "O" * len(groups[ARG_NAMED_OPT]) | |
| if groups[ARG_NAMED]: | |
| format += "@" + "O" * len(groups[ARG_NAMED]) | |
| if func_name is not None: | |
| format += f":{func_name}" | |
| return format | |
| def generate_wrapper_function( | |
| fn: FuncIR, emitter: Emitter, source_path: str, module_name: str | |
| ) -> None: | |
| """Generate a CPython-compatible vectorcall wrapper for a native function. | |
| In particular, this handles unboxing the arguments, calling the native function, and | |
| then boxing the return value. | |
| """ | |
| emitter.emit_line(f"{wrapper_function_header(fn, emitter.names)} {{") | |
| # If fn is a method, then the first argument is a self param | |
| real_args = list(fn.args) | |
| if fn.sig.num_bitmap_args: | |
| real_args = real_args[: -fn.sig.num_bitmap_args] | |
| if fn.class_name and fn.decl.kind != FUNC_STATICMETHOD: | |
| arg = real_args.pop(0) | |
| emitter.emit_line(f"PyObject *obj_{arg.name} = self;") | |
| # Need to order args as: required, optional, kwonly optional, kwonly required | |
| # This is because CPyArg_ParseStackAndKeywords format string requires | |
| # them grouped in that way. | |
| groups = make_arg_groups(real_args) | |
| reordered_args = reorder_arg_groups(groups) | |
| emitter.emit_line(make_static_kwlist(reordered_args)) | |
| fmt = make_format_string(fn.name, groups) | |
| # Define the arguments the function accepts (but no types yet) | |
| emitter.emit_line(f'static CPyArg_Parser parser = {{"{fmt}", kwlist, 0}};') | |
| for arg in real_args: | |
| emitter.emit_line( | |
| "PyObject *obj_{}{};".format(arg.name, " = NULL" if arg.optional else "") | |
| ) | |
| cleanups = [f"CPy_DECREF(obj_{arg.name});" for arg in groups[ARG_STAR] + groups[ARG_STAR2]] | |
| arg_ptrs: list[str] = [] | |
| if groups[ARG_STAR] or groups[ARG_STAR2]: | |
| arg_ptrs += [f"&obj_{groups[ARG_STAR][0].name}" if groups[ARG_STAR] else "NULL"] | |
| arg_ptrs += [f"&obj_{groups[ARG_STAR2][0].name}" if groups[ARG_STAR2] else "NULL"] | |
| arg_ptrs += [f"&obj_{arg.name}" for arg in reordered_args] | |
| if fn.name == "__call__": | |
| nargs = "PyVectorcall_NARGS(nargs)" | |
| else: | |
| nargs = "nargs" | |
| parse_fn = "CPyArg_ParseStackAndKeywords" | |
| # Special case some common signatures | |
| if not real_args: | |
| # No args | |
| parse_fn = "CPyArg_ParseStackAndKeywordsNoArgs" | |
| elif len(real_args) == 1 and len(groups[ARG_POS]) == 1: | |
| # Single positional arg | |
| parse_fn = "CPyArg_ParseStackAndKeywordsOneArg" | |
| elif len(real_args) == len(groups[ARG_POS]) + len(groups[ARG_OPT]): | |
| # No keyword-only args, *args or **kwargs | |
| parse_fn = "CPyArg_ParseStackAndKeywordsSimple" | |
| emitter.emit_lines( | |
| "if (!{}(args, {}, kwnames, &parser{})) {{".format( | |
| parse_fn, nargs, "".join(", " + n for n in arg_ptrs) | |
| ), | |
| "return NULL;", | |
| "}", | |
| ) | |
| for i in range(fn.sig.num_bitmap_args): | |
| name = bitmap_name(i) | |
| emitter.emit_line(f"{BITMAP_TYPE} {name} = 0;") | |
| traceback_code = generate_traceback_code(fn, emitter, source_path, module_name) | |
| generate_wrapper_core( | |
| fn, | |
| emitter, | |
| groups[ARG_OPT] + groups[ARG_NAMED_OPT], | |
| cleanups=cleanups, | |
| traceback_code=traceback_code, | |
| ) | |
| emitter.emit_line("}") | |
| # Legacy generic wrapper functions | |
| # | |
| # These take a self object, a Python tuple of positional arguments, | |
| # and a dict of keyword arguments. These are a lot slower than | |
| # vectorcall wrappers, especially in calls involving keyword | |
| # arguments. | |
| def legacy_wrapper_function_header(fn: FuncIR, names: NameGenerator) -> str: | |
| return "PyObject *{prefix}{name}(PyObject *self, PyObject *args, PyObject *kw)".format( | |
| prefix=PREFIX, name=fn.cname(names) | |
| ) | |
| def generate_legacy_wrapper_function( | |
| fn: FuncIR, emitter: Emitter, source_path: str, module_name: str | |
| ) -> None: | |
| """Generates a CPython-compatible legacy wrapper for a native function. | |
| In particular, this handles unboxing the arguments, calling the native function, and | |
| then boxing the return value. | |
| """ | |
| emitter.emit_line(f"{legacy_wrapper_function_header(fn, emitter.names)} {{") | |
| # If fn is a method, then the first argument is a self param | |
| real_args = list(fn.args) | |
| if fn.sig.num_bitmap_args: | |
| real_args = real_args[: -fn.sig.num_bitmap_args] | |
| if fn.class_name and (fn.decl.name == "__new__" or fn.decl.kind != FUNC_STATICMETHOD): | |
| arg = real_args.pop(0) | |
| emitter.emit_line(f"PyObject *obj_{arg.name} = self;") | |
| # Need to order args as: required, optional, kwonly optional, kwonly required | |
| # This is because CPyArg_ParseTupleAndKeywords format string requires | |
| # them grouped in that way. | |
| groups = make_arg_groups(real_args) | |
| reordered_args = reorder_arg_groups(groups) | |
| emitter.emit_line(make_static_kwlist(reordered_args)) | |
| for arg in real_args: | |
| emitter.emit_line( | |
| "PyObject *obj_{}{};".format(arg.name, " = NULL" if arg.optional else "") | |
| ) | |
| cleanups = [f"CPy_DECREF(obj_{arg.name});" for arg in groups[ARG_STAR] + groups[ARG_STAR2]] | |
| arg_ptrs: list[str] = [] | |
| if groups[ARG_STAR] or groups[ARG_STAR2]: | |
| arg_ptrs += [f"&obj_{groups[ARG_STAR][0].name}" if groups[ARG_STAR] else "NULL"] | |
| arg_ptrs += [f"&obj_{groups[ARG_STAR2][0].name}" if groups[ARG_STAR2] else "NULL"] | |
| arg_ptrs += [f"&obj_{arg.name}" for arg in reordered_args] | |
| emitter.emit_lines( | |
| 'if (!CPyArg_ParseTupleAndKeywords(args, kw, "{}", "{}", kwlist{})) {{'.format( | |
| make_format_string(None, groups), fn.name, "".join(", " + n for n in arg_ptrs) | |
| ), | |
| "return NULL;", | |
| "}", | |
| ) | |
| for i in range(fn.sig.num_bitmap_args): | |
| name = bitmap_name(i) | |
| emitter.emit_line(f"{BITMAP_TYPE} {name} = 0;") | |
| traceback_code = generate_traceback_code(fn, emitter, source_path, module_name) | |
| generate_wrapper_core( | |
| fn, | |
| emitter, | |
| groups[ARG_OPT] + groups[ARG_NAMED_OPT], | |
| cleanups=cleanups, | |
| traceback_code=traceback_code, | |
| ) | |
| emitter.emit_line("}") | |
| # Specialized wrapper functions | |
| def generate_dunder_wrapper(cl: ClassIR, fn: FuncIR, emitter: Emitter) -> str: | |
| """Generates a wrapper for native __dunder__ methods to be able to fit into the mapping | |
| protocol slot. This specifically means that the arguments are taken as *PyObjects and returned | |
| as *PyObjects. | |
| """ | |
| gen = WrapperGenerator(cl, emitter) | |
| gen.set_target(fn) | |
| gen.emit_header() | |
| gen.emit_arg_processing() | |
| gen.emit_call() | |
| gen.finish() | |
| return gen.wrapper_name() | |
| def generate_ipow_wrapper(cl: ClassIR, fn: FuncIR, emitter: Emitter) -> str: | |
| """Generate a wrapper for native __ipow__. | |
| Since __ipow__ fills a ternary slot, but almost no one defines __ipow__ to take three | |
| arguments, the wrapper needs to tweaked to force it to accept three arguments. | |
| """ | |
| gen = WrapperGenerator(cl, emitter) | |
| gen.set_target(fn) | |
| assert len(fn.args) in (2, 3), "__ipow__ should only take 2 or 3 arguments" | |
| gen.arg_names = ["self", "exp", "mod"] | |
| gen.emit_header() | |
| gen.emit_arg_processing() | |
| handle_third_pow_argument( | |
| fn, | |
| emitter, | |
| gen, | |
| if_unsupported=[ | |
| 'PyErr_SetString(PyExc_TypeError, "__ipow__ takes 2 positional arguments but 3 were given");', | |
| "return NULL;", | |
| ], | |
| ) | |
| gen.emit_call() | |
| gen.finish() | |
| return gen.wrapper_name() | |
| def generate_bin_op_wrapper(cl: ClassIR, fn: FuncIR, emitter: Emitter) -> str: | |
| """Generates a wrapper for a native binary dunder method. | |
| The same wrapper that handles the forward method (e.g. __add__) also handles | |
| the corresponding reverse method (e.g. __radd__), if defined. | |
| Both arguments and the return value are PyObject *. | |
| """ | |
| gen = WrapperGenerator(cl, emitter) | |
| gen.set_target(fn) | |
| if fn.name in ("__pow__", "__rpow__"): | |
| gen.arg_names = ["left", "right", "mod"] | |
| else: | |
| gen.arg_names = ["left", "right"] | |
| wrapper_name = gen.wrapper_name() | |
| gen.emit_header() | |
| if fn.name not in reverse_op_methods and fn.name in reverse_op_method_names: | |
| # There's only a reverse operator method. | |
| generate_bin_op_reverse_only_wrapper(fn, emitter, gen) | |
| else: | |
| rmethod = reverse_op_methods[fn.name] | |
| fn_rev = cl.get_method(rmethod) | |
| if fn_rev is None: | |
| # There's only a forward operator method. | |
| generate_bin_op_forward_only_wrapper(fn, emitter, gen) | |
| else: | |
| # There's both a forward and a reverse operator method. | |
| generate_bin_op_both_wrappers(cl, fn, fn_rev, emitter, gen) | |
| return wrapper_name | |
| def generate_bin_op_forward_only_wrapper( | |
| fn: FuncIR, emitter: Emitter, gen: WrapperGenerator | |
| ) -> None: | |
| gen.emit_arg_processing(error=GotoHandler("typefail"), raise_exception=False) | |
| handle_third_pow_argument(fn, emitter, gen, if_unsupported=["goto typefail;"]) | |
| gen.emit_call(not_implemented_handler="goto typefail;") | |
| gen.emit_error_handling() | |
| emitter.emit_label("typefail") | |
| # If some argument has an incompatible type, treat this the same as | |
| # returning NotImplemented, and try to call the reverse operator method. | |
| # | |
| # Note that in normal Python you'd instead of an explicit | |
| # return of NotImplemented, but it doesn't generally work here | |
| # the body won't be executed at all if there is an argument | |
| # type check failure. | |
| # | |
| # The recommended way is to still use a type check in the | |
| # body. This will only be used in interpreted mode: | |
| # | |
| # def __add__(self, other: int) -> Foo: | |
| # if not isinstance(other, int): | |
| # return NotImplemented | |
| # ... | |
| generate_bin_op_reverse_dunder_call(fn, emitter, reverse_op_methods[fn.name]) | |
| gen.finish() | |
| def generate_bin_op_reverse_only_wrapper( | |
| fn: FuncIR, emitter: Emitter, gen: WrapperGenerator | |
| ) -> None: | |
| gen.arg_names = ["right", "left"] | |
| gen.emit_arg_processing(error=GotoHandler("typefail"), raise_exception=False) | |
| handle_third_pow_argument(fn, emitter, gen, if_unsupported=["goto typefail;"]) | |
| gen.emit_call() | |
| gen.emit_error_handling() | |
| emitter.emit_label("typefail") | |
| emitter.emit_line("Py_INCREF(Py_NotImplemented);") | |
| emitter.emit_line("return Py_NotImplemented;") | |
| gen.finish() | |
| def generate_bin_op_both_wrappers( | |
| cl: ClassIR, fn: FuncIR, fn_rev: FuncIR, emitter: Emitter, gen: WrapperGenerator | |
| ) -> None: | |
| # There's both a forward and a reverse operator method. First | |
| # check if we should try calling the forward one. If the | |
| # argument type check fails, fall back to the reverse method. | |
| # | |
| # Similar to above, we can't perfectly match Python semantics. | |
| # In regular Python code you'd return NotImplemented if the | |
| # operand has the wrong type, but in compiled code we'll never | |
| # get to execute the type check. | |
| emitter.emit_line( | |
| "if (PyObject_IsInstance(obj_left, (PyObject *){})) {{".format( | |
| emitter.type_struct_name(cl) | |
| ) | |
| ) | |
| gen.emit_arg_processing(error=GotoHandler("typefail"), raise_exception=False) | |
| handle_third_pow_argument(fn, emitter, gen, if_unsupported=["goto typefail2;"]) | |
| # Ternary __rpow__ calls aren't a thing so immediately bail | |
| # if ternary __pow__ returns NotImplemented. | |
| if fn.name == "__pow__" and len(fn.args) == 3: | |
| fwd_not_implemented_handler = "goto typefail2;" | |
| else: | |
| fwd_not_implemented_handler = "goto typefail;" | |
| gen.emit_call(not_implemented_handler=fwd_not_implemented_handler) | |
| gen.emit_error_handling() | |
| emitter.emit_line("}") | |
| emitter.emit_label("typefail") | |
| emitter.emit_line( | |
| "if (PyObject_IsInstance(obj_right, (PyObject *){})) {{".format( | |
| emitter.type_struct_name(cl) | |
| ) | |
| ) | |
| gen.set_target(fn_rev) | |
| gen.arg_names = ["right", "left"] | |
| gen.emit_arg_processing(error=GotoHandler("typefail2"), raise_exception=False) | |
| handle_third_pow_argument(fn_rev, emitter, gen, if_unsupported=["goto typefail2;"]) | |
| gen.emit_call() | |
| gen.emit_error_handling() | |
| emitter.emit_line("} else {") | |
| generate_bin_op_reverse_dunder_call(fn, emitter, fn_rev.name) | |
| emitter.emit_line("}") | |
| emitter.emit_label("typefail2") | |
| emitter.emit_line("Py_INCREF(Py_NotImplemented);") | |
| emitter.emit_line("return Py_NotImplemented;") | |
| gen.finish() | |
| def generate_bin_op_reverse_dunder_call(fn: FuncIR, emitter: Emitter, rmethod: str) -> None: | |
| if fn.name in ("__pow__", "__rpow__"): | |
| # Ternary pow() will never call the reverse dunder. | |
| emitter.emit_line("if (obj_mod == Py_None) {") | |
| emitter.emit_line( | |
| 'return CPy_CallReverseOpMethod(obj_left, obj_right, "{}", mypyc_interned_str.{});'.format( | |
| op_methods_to_symbols[fn.name], rmethod | |
| ) | |
| ) | |
| if fn.name in ("__pow__", "__rpow__"): | |
| emitter.emit_line("} else {") | |
| emitter.emit_line("Py_INCREF(Py_NotImplemented);") | |
| emitter.emit_line("return Py_NotImplemented;") | |
| emitter.emit_line("}") | |
| def handle_third_pow_argument( | |
| fn: FuncIR, emitter: Emitter, gen: WrapperGenerator, *, if_unsupported: list[str] | |
| ) -> None: | |
| if fn.name not in ("__pow__", "__rpow__", "__ipow__"): | |
| return | |
| if (fn.name in ("__pow__", "__ipow__") and len(fn.args) == 2) or fn.name == "__rpow__": | |
| # If the power dunder only supports two arguments and the third | |
| # argument (AKA mod) is set to a non-default value, simply bail. | |
| # | |
| # Importantly, this prevents any ternary __rpow__ calls from | |
| # happening (as per the language specification). | |
| emitter.emit_line("if (obj_mod != Py_None) {") | |
| for line in if_unsupported: | |
| emitter.emit_line(line) | |
| emitter.emit_line("}") | |
| # The slot wrapper will receive three arguments, but the call only | |
| # supports two so make sure that the third argument isn't passed | |
| # along. This is needed as two-argument __(i)pow__ is allowed and | |
| # rather common. | |
| if len(gen.arg_names) == 3: | |
| gen.arg_names.pop() | |
| RICHCOMPARE_OPS = { | |
| "__lt__": "Py_LT", | |
| "__gt__": "Py_GT", | |
| "__le__": "Py_LE", | |
| "__ge__": "Py_GE", | |
| "__eq__": "Py_EQ", | |
| "__ne__": "Py_NE", | |
| } | |
| def generate_richcompare_wrapper(cl: ClassIR, emitter: Emitter) -> str | None: | |
| """Generates a wrapper for richcompare dunder methods.""" | |
| # Sort for determinism on Python 3.5 | |
| matches = sorted(name for name in RICHCOMPARE_OPS if cl.has_method(name)) | |
| if not matches: | |
| return None | |
| name = f"{DUNDER_PREFIX}_RichCompare_{cl.name_prefix(emitter.names)}" | |
| emitter.emit_line( | |
| "static PyObject *{name}(PyObject *obj_lhs, PyObject *obj_rhs, int op) {{".format( | |
| name=name | |
| ) | |
| ) | |
| emitter.emit_line("switch (op) {") | |
| for func in matches: | |
| emitter.emit_line(f"case {RICHCOMPARE_OPS[func]}: {{") | |
| method = cl.get_method(func) | |
| assert method is not None | |
| generate_wrapper_core(method, emitter, arg_names=["lhs", "rhs"]) | |
| emitter.emit_line("}") | |
| emitter.emit_line("}") | |
| emitter.emit_line("Py_INCREF(Py_NotImplemented);") | |
| emitter.emit_line("return Py_NotImplemented;") | |
| emitter.emit_line("}") | |
| return name | |
| def generate_get_wrapper(cl: ClassIR, fn: FuncIR, emitter: Emitter) -> str: | |
| """Generates a wrapper for native __get__ methods.""" | |
| name = f"{DUNDER_PREFIX}{fn.name}{cl.name_prefix(emitter.names)}" | |
| emitter.emit_line( | |
| "static PyObject *{name}(PyObject *self, PyObject *instance, PyObject *owner) {{".format( | |
| name=name | |
| ) | |
| ) | |
| emitter.emit_line("instance = instance ? instance : Py_None;") | |
| emitter.emit_line(f"return {emitter.native_function_call(fn.decl)}(self, instance, owner);") | |
| emitter.emit_line("}") | |
| return name | |
| def generate_hash_wrapper(cl: ClassIR, fn: FuncIR, emitter: Emitter) -> str: | |
| """Generates a wrapper for native __hash__ methods.""" | |
| name = f"{DUNDER_PREFIX}{fn.name}{cl.name_prefix(emitter.names)}" | |
| emitter.emit_line(f"static Py_ssize_t {name}(PyObject *self) {{") | |
| emitter.emit_line( | |
| "{}retval = {}{}{}(self);".format( | |
| emitter.ctype_spaced(fn.ret_type), | |
| emitter.get_group_prefix(fn.decl), | |
| NATIVE_PREFIX, | |
| fn.cname(emitter.names), | |
| ) | |
| ) | |
| emitter.emit_error_check("retval", fn.ret_type, "return -1;") | |
| if is_int_rprimitive(fn.ret_type): | |
| emitter.emit_line("Py_ssize_t val = CPyTagged_AsSsize_t(retval);") | |
| else: | |
| emitter.emit_line("Py_ssize_t val = PyLong_AsSsize_t(retval);") | |
| emitter.emit_dec_ref("retval", fn.ret_type) | |
| emitter.emit_line("if (PyErr_Occurred()) return -1;") | |
| # We can't return -1 from a hash function.. | |
| emitter.emit_line("if (val == -1) return -2;") | |
| emitter.emit_line("return val;") | |
| emitter.emit_line("}") | |
| return name | |
| def generate_len_wrapper(cl: ClassIR, fn: FuncIR, emitter: Emitter) -> str: | |
| """Generates a wrapper for native __len__ methods.""" | |
| name = f"{DUNDER_PREFIX}{fn.name}{cl.name_prefix(emitter.names)}" | |
| emitter.emit_line(f"static Py_ssize_t {name}(PyObject *self) {{") | |
| emitter.emit_line( | |
| "{}retval = {}{}{}(self);".format( | |
| emitter.ctype_spaced(fn.ret_type), | |
| emitter.get_group_prefix(fn.decl), | |
| NATIVE_PREFIX, | |
| fn.cname(emitter.names), | |
| ) | |
| ) | |
| emitter.emit_error_check("retval", fn.ret_type, "return -1;") | |
| if is_int_rprimitive(fn.ret_type): | |
| emitter.emit_line("Py_ssize_t val = CPyTagged_AsSsize_t(retval);") | |
| else: | |
| emitter.emit_line("Py_ssize_t val = PyLong_AsSsize_t(retval);") | |
| emitter.emit_dec_ref("retval", fn.ret_type) | |
| emitter.emit_line("if (PyErr_Occurred()) return -1;") | |
| emitter.emit_line("return val;") | |
| emitter.emit_line("}") | |
| return name | |
| def generate_am_send_wrapper(cl: ClassIR, fn: FuncIR, emitter: Emitter) -> str: | |
| """Generate an am_send slot for a generator or coroutine class. | |
| This implements the C-level send protocol (used by PyIter_Send), which reports | |
| normal completion without raising StopIteration. | |
| """ | |
| name = f"{DUNDER_PREFIX}am_send{cl.name_prefix(emitter.names)}" | |
| emitter.emit_line( | |
| f"static PySendResult {name}(PyObject *self, PyObject *arg, PyObject **result) {{" | |
| ) | |
| emitter.emit_line("PyObject *stop_iter_value = NULL;") | |
| emitter.emit_line( | |
| "PyObject *retval = {}{}{}(self, Py_None, Py_None, Py_None, arg, &stop_iter_value);".format( | |
| emitter.get_group_prefix(fn.decl), NATIVE_PREFIX, fn.cname(emitter.names) | |
| ) | |
| ) | |
| emitter.emit_line("if (retval != NULL) {") | |
| emitter.emit_line("*result = retval;") | |
| emitter.emit_line("return PYGEN_NEXT;") | |
| emitter.emit_line("}") | |
| emitter.emit_line("if (stop_iter_value == NULL) {") | |
| # The helper raised an exception instead of using the out parameter. StopIteration | |
| # signals normal completion when the generator or coroutine is already exhausted. | |
| # TODO: Convert an explicit StopIteration raised in the body to RuntimeError instead | |
| # of treating it as normal completion. | |
| emitter.emit_line("if (PyErr_ExceptionMatches(PyExc_StopIteration)) {") | |
| emitter.emit_line("stop_iter_value = CPy_FetchStopIterationValue();") | |
| emitter.emit_line("}") | |
| emitter.emit_line("if (stop_iter_value == NULL) {") | |
| emitter.emit_line("*result = NULL;") | |
| emitter.emit_line("return PYGEN_ERROR;") | |
| emitter.emit_line("}") | |
| emitter.emit_line("}") | |
| emitter.emit_line("*result = stop_iter_value;") | |
| emitter.emit_line("return PYGEN_RETURN;") | |
| emitter.emit_line("}") | |
| return name | |
| def generate_bool_wrapper(cl: ClassIR, fn: FuncIR, emitter: Emitter) -> str: | |
| """Generates a wrapper for native __bool__ methods.""" | |
| name = f"{DUNDER_PREFIX}{fn.name}{cl.name_prefix(emitter.names)}" | |
| emitter.emit_line(f"static int {name}(PyObject *self) {{") | |
| emitter.emit_line( | |
| "{}val = {}(self);".format( | |
| emitter.ctype_spaced(fn.ret_type), emitter.native_function_call(fn.decl) | |
| ) | |
| ) | |
| emitter.emit_error_check("val", fn.ret_type, "return -1;") | |
| # This wouldn't be that hard to fix but it seems unimportant and | |
| # getting error handling and unboxing right would be fiddly. (And | |
| # way easier to do in IR!) | |
| assert is_bool_rprimitive(fn.ret_type), "Only bool return supported for __bool__" | |
| emitter.emit_line("return val;") | |
| emitter.emit_line("}") | |
| return name | |
| def generate_del_item_wrapper(cl: ClassIR, fn: FuncIR, emitter: Emitter) -> str: | |
| """Generates a wrapper for native __delitem__. | |
| This is only called from a combined __delitem__/__setitem__ wrapper. | |
| """ | |
| name = "{}{}{}".format(DUNDER_PREFIX, "__delitem__", cl.name_prefix(emitter.names)) | |
| input_args = ", ".join(f"PyObject *obj_{arg.name}" for arg in fn.args) | |
| emitter.emit_line(f"static int {name}({input_args}) {{") | |
| generate_set_del_item_wrapper_inner(fn, emitter, fn.args) | |
| return name | |
| def generate_set_del_item_wrapper(cl: ClassIR, fn: FuncIR, emitter: Emitter) -> str: | |
| """Generates a wrapper for native __setitem__ method (also works for __delitem__). | |
| This is used with the mapping protocol slot. Arguments are taken as *PyObjects and we | |
| return a negative C int on error. | |
| Create a separate wrapper function for __delitem__ as needed and have the | |
| __setitem__ wrapper call it if the value is NULL. Return the name | |
| of the outer (__setitem__) wrapper. | |
| """ | |
| method_cls = cl.get_method_and_class("__delitem__") | |
| del_name = None | |
| if method_cls and method_cls[1] == cl: | |
| # Generate a separate wrapper for __delitem__ | |
| del_name = generate_del_item_wrapper(cl, method_cls[0], emitter) | |
| args = fn.args | |
| if fn.name == "__delitem__": | |
| # Add an extra argument for value that we expect to be NULL. | |
| args = list(args) + [RuntimeArg("___value", object_rprimitive, ARG_POS)] | |
| name = "{}{}{}".format(DUNDER_PREFIX, "__setitem__", cl.name_prefix(emitter.names)) | |
| input_args = ", ".join(f"PyObject *obj_{arg.name}" for arg in args) | |
| emitter.emit_line(f"static int {name}({input_args}) {{") | |
| # First check if this is __delitem__ | |
| emitter.emit_line(f"if (obj_{args[2].name} == NULL) {{") | |
| if del_name is not None: | |
| # We have a native implementation, so call it | |
| emitter.emit_line(f"return {del_name}(obj_{args[0].name}, obj_{args[1].name});") | |
| else: | |
| # Try to call superclass method instead | |
| emitter.emit_line( | |
| "PyObject *super = " | |
| f"CPy_Super(CPyImport_GetModuleCache(&CPyModule_builtins), obj_{args[0].name});" | |
| ) | |
| emitter.emit_line("if (super == NULL) return -1;") | |
| emitter.emit_line( | |
| 'PyObject *result = PyObject_CallMethod(super, "__delitem__", "O", obj_{});'.format( | |
| args[1].name | |
| ) | |
| ) | |
| emitter.emit_line("Py_DECREF(super);") | |
| emitter.emit_line("Py_XDECREF(result);") | |
| emitter.emit_line("return result == NULL ? -1 : 0;") | |
| emitter.emit_line("}") | |
| method_cls = cl.get_method_and_class("__setitem__") | |
| if method_cls and method_cls[1] == cl: | |
| generate_set_del_item_wrapper_inner(fn, emitter, args) | |
| else: | |
| emitter.emit_line( | |
| "PyObject *super = " | |
| f"CPy_Super(CPyImport_GetModuleCache(&CPyModule_builtins), obj_{args[0].name});" | |
| ) | |
| emitter.emit_line("if (super == NULL) return -1;") | |
| emitter.emit_line("PyObject *result;") | |
| if method_cls is None and cl.builtin_base is None: | |
| msg = f"'{cl.name}' object does not support item assignment" | |
| emitter.emit_line(f'PyErr_SetString(PyExc_TypeError, "{msg}");') | |
| emitter.emit_line("result = NULL;") | |
| else: | |
| # A base class may have __setitem__ | |
| emitter.emit_line( | |
| 'result = PyObject_CallMethod(super, "__setitem__", "OO", obj_{}, obj_{});'.format( | |
| args[1].name, args[2].name | |
| ) | |
| ) | |
| emitter.emit_line("Py_DECREF(super);") | |
| emitter.emit_line("Py_XDECREF(result);") | |
| emitter.emit_line("return result == NULL ? -1 : 0;") | |
| emitter.emit_line("}") | |
| return name | |
| def generate_set_del_item_wrapper_inner( | |
| fn: FuncIR, emitter: Emitter, args: Sequence[RuntimeArg] | |
| ) -> None: | |
| for arg in args: | |
| generate_arg_check(arg.name, arg.type, emitter, GotoHandler("fail")) | |
| native_args = ", ".join(f"arg_{arg.name}" for arg in args) | |
| emitter.emit_line( | |
| "{}val = {}({});".format( | |
| emitter.ctype_spaced(fn.ret_type), emitter.native_function_call(fn.decl), native_args | |
| ) | |
| ) | |
| emitter.emit_error_check("val", fn.ret_type, "goto fail;") | |
| emitter.emit_dec_ref("val", fn.ret_type) | |
| emitter.emit_line("return 0;") | |
| emitter.emit_label("fail") | |
| emitter.emit_line("return -1;") | |
| emitter.emit_line("}") | |
| def generate_contains_wrapper(cl: ClassIR, fn: FuncIR, emitter: Emitter) -> str: | |
| """Generates a wrapper for a native __contains__ method.""" | |
| name = f"{DUNDER_PREFIX}{fn.name}{cl.name_prefix(emitter.names)}" | |
| emitter.emit_line(f"static int {name}(PyObject *self, PyObject *obj_item) {{") | |
| generate_arg_check("item", fn.args[1].type, emitter, ReturnHandler("-1")) | |
| emitter.emit_line( | |
| "{}val = {}(self, arg_item);".format( | |
| emitter.ctype_spaced(fn.ret_type), emitter.native_function_call(fn.decl) | |
| ) | |
| ) | |
| emitter.emit_error_check("val", fn.ret_type, "return -1;") | |
| if is_bool_rprimitive(fn.ret_type): | |
| emitter.emit_line("return val;") | |
| else: | |
| emitter.emit_line("int boolval = PyObject_IsTrue(val);") | |
| emitter.emit_dec_ref("val", fn.ret_type) | |
| emitter.emit_line("return boolval;") | |
| emitter.emit_line("}") | |
| return name | |
| # Helpers | |
| def generate_wrapper_core( | |
| fn: FuncIR, | |
| emitter: Emitter, | |
| optional_args: list[RuntimeArg] | None = None, | |
| arg_names: list[str] | None = None, | |
| cleanups: list[str] | None = None, | |
| traceback_code: str | None = None, | |
| ) -> None: | |
| """Generates the core part of a wrapper function for a native function. | |
| This expects each argument as a PyObject * named obj_{arg} as a precondition. | |
| It converts the PyObject *s to the necessary types, checking and unboxing if necessary, | |
| makes the call, then boxes the result if necessary and returns it. | |
| """ | |
| gen = WrapperGenerator(None, emitter) | |
| gen.set_target(fn) | |
| if arg_names: | |
| gen.arg_names = arg_names | |
| gen.cleanups = cleanups or [] | |
| gen.optional_args = optional_args or [] | |
| gen.traceback_code = traceback_code or "" | |
| error = ReturnHandler("NULL") if not gen.use_goto() else GotoHandler("fail") | |
| gen.emit_arg_processing(error=error) | |
| gen.emit_call() | |
| gen.emit_error_handling() | |
| def generate_arg_check( | |
| name: str, | |
| typ: RType, | |
| emitter: Emitter, | |
| error: ErrorHandler | None = None, | |
| *, | |
| optional: bool = False, | |
| raise_exception: bool = True, | |
| bitmap_arg_index: int = 0, | |
| ) -> None: | |
| """Insert a runtime check for argument and unbox if necessary. | |
| The object is named PyObject *obj_{}. This is expected to generate | |
| a value of name arg_{} (unboxed if necessary). For each primitive a runtime | |
| check ensures the correct type. | |
| """ | |
| error = error or AssignHandler() | |
| if typ.is_unboxed: | |
| if typ.error_overlap and optional: | |
| # Update bitmap is value is provided. | |
| init = emitter.c_undefined_value(typ) | |
| emitter.emit_line(f"{emitter.ctype(typ)} arg_{name} = {init};") | |
| emitter.emit_line(f"if (obj_{name} != NULL) {{") | |
| bitmap = bitmap_name(bitmap_arg_index // BITMAP_BITS) | |
| emitter.emit_line(f"{bitmap} |= 1 << {bitmap_arg_index & (BITMAP_BITS - 1)};") | |
| emitter.emit_unbox( | |
| f"obj_{name}", | |
| f"arg_{name}", | |
| typ, | |
| declare_dest=False, | |
| raise_exception=raise_exception, | |
| error=error, | |
| borrow=True, | |
| ) | |
| emitter.emit_line("}") | |
| else: | |
| # Borrow when unboxing to avoid reference count manipulation. | |
| emitter.emit_unbox( | |
| f"obj_{name}", | |
| f"arg_{name}", | |
| typ, | |
| declare_dest=True, | |
| raise_exception=raise_exception, | |
| error=error, | |
| borrow=True, | |
| optional=optional, | |
| ) | |
| elif is_object_rprimitive(typ): | |
| # Object is trivial since any object is valid | |
| if optional: | |
| emitter.emit_line(f"PyObject *arg_{name};") | |
| emitter.emit_line(f"if (obj_{name} == NULL) {{") | |
| emitter.emit_line(f"arg_{name} = {emitter.c_error_value(typ)};") | |
| emitter.emit_lines("} else {", f"arg_{name} = obj_{name}; ", "}") | |
| else: | |
| emitter.emit_line(f"PyObject *arg_{name} = obj_{name};") | |
| else: | |
| emitter.emit_cast( | |
| f"obj_{name}", | |
| f"arg_{name}", | |
| typ, | |
| declare_dest=True, | |
| raise_exception=raise_exception, | |
| error=error, | |
| optional=optional, | |
| ) | |
| class WrapperGenerator: | |
| """Helper that simplifies the generation of wrapper functions.""" | |
| # TODO: Use this for more wrappers | |
| def __init__(self, cl: ClassIR | None, emitter: Emitter) -> None: | |
| self.cl = cl | |
| self.emitter = emitter | |
| self.cleanups: list[str] = [] | |
| self.optional_args: list[RuntimeArg] = [] | |
| self.traceback_code = "" | |
| def set_target(self, fn: FuncIR) -> None: | |
| """Set the wrapped function. | |
| It's fine to modify the attributes initialized here later to customize | |
| the wrapper function. | |
| """ | |
| self.target_name = fn.name | |
| self.target_cname = fn.cname(self.emitter.names) | |
| # Cached native-call expression so cross-group targets go through the | |
| # exports table; same as `NATIVE_PREFIX + cname` for in-group calls. | |
| self.target_native_call = self.emitter.native_function_call(fn.decl) | |
| self.num_bitmap_args = fn.sig.num_bitmap_args | |
| if self.num_bitmap_args: | |
| self.args = fn.args[: -self.num_bitmap_args] | |
| else: | |
| self.args = fn.args | |
| self.arg_names = [arg.name for arg in self.args] | |
| self.ret_type = fn.ret_type | |
| def wrapper_name(self) -> str: | |
| """Return the name of the wrapper function.""" | |
| return "{}{}{}".format( | |
| DUNDER_PREFIX, | |
| self.target_name, | |
| self.cl.name_prefix(self.emitter.names) if self.cl else "", | |
| ) | |
| def use_goto(self) -> bool: | |
| """Do we use a goto for error handling (instead of straight return)?""" | |
| return bool(self.cleanups or self.traceback_code) | |
| def emit_header(self) -> None: | |
| """Emit the function header of the wrapper implementation.""" | |
| input_args = ", ".join(f"PyObject *obj_{arg}" for arg in self.arg_names) | |
| self.emitter.emit_line( | |
| "static PyObject *{name}({input_args}) {{".format( | |
| name=self.wrapper_name(), input_args=input_args | |
| ) | |
| ) | |
| def emit_arg_processing( | |
| self, error: ErrorHandler | None = None, raise_exception: bool = True | |
| ) -> None: | |
| """Emit validation and unboxing of arguments.""" | |
| error = error or self.error() | |
| bitmap_arg_index = 0 | |
| for arg_name, arg in zip(self.arg_names, self.args): | |
| # Suppress the argument check for *args/**kwargs, since we know it must be right. | |
| typ = arg.type if arg.kind not in (ARG_STAR, ARG_STAR2) else object_rprimitive | |
| optional = arg in self.optional_args | |
| generate_arg_check( | |
| arg_name, | |
| typ, | |
| self.emitter, | |
| error, | |
| raise_exception=raise_exception, | |
| optional=optional, | |
| bitmap_arg_index=bitmap_arg_index, | |
| ) | |
| if optional and typ.error_overlap: | |
| bitmap_arg_index += 1 | |
| def emit_call(self, not_implemented_handler: str = "") -> None: | |
| """Emit call to the wrapper function. | |
| If not_implemented_handler is non-empty, use this C code to handle | |
| a NotImplemented return value (if it's possible based on the return type). | |
| """ | |
| native_args = ", ".join(f"arg_{arg}" for arg in self.arg_names) | |
| if self.num_bitmap_args: | |
| bitmap_args = ", ".join( | |
| [bitmap_name(i) for i in reversed(range(self.num_bitmap_args))] | |
| ) | |
| native_args = f"{native_args}, {bitmap_args}" | |
| ret_type = self.ret_type | |
| emitter = self.emitter | |
| if ret_type.is_unboxed or self.use_goto(): | |
| # TODO: The Py_RETURN macros return the correct PyObject * with reference count | |
| # handling. Are they relevant? | |
| emitter.emit_line( | |
| "{}retval = {}({});".format( | |
| emitter.ctype_spaced(ret_type), self.target_native_call, native_args | |
| ) | |
| ) | |
| emitter.emit_lines(*self.cleanups) | |
| if ret_type.is_unboxed: | |
| emitter.emit_error_check("retval", ret_type, "return NULL;") | |
| emitter.emit_box("retval", "retbox", ret_type, declare_dest=True) | |
| emitter.emit_line("return {};".format("retbox" if ret_type.is_unboxed else "retval")) | |
| else: | |
| if not_implemented_handler and not isinstance(ret_type, RInstance): | |
| # The return value type may overlap with NotImplemented. | |
| emitter.emit_line(f"PyObject *retbox = {self.target_native_call}({native_args});") | |
| emitter.emit_lines( | |
| "if (retbox == Py_NotImplemented) {", | |
| not_implemented_handler, | |
| "}", | |
| "return retbox;", | |
| ) | |
| else: | |
| emitter.emit_line(f"return {self.target_native_call}({native_args});") | |
| # TODO: Tracebacks? | |
| def error(self) -> ErrorHandler: | |
| """Figure out how to deal with errors in the wrapper.""" | |
| if self.cleanups or self.traceback_code: | |
| # We'll have a label at the end with error handling code. | |
| return GotoHandler("fail") | |
| else: | |
| # Nothing special needs to done to handle errors, so just return. | |
| return ReturnHandler("NULL") | |
| def emit_error_handling(self) -> None: | |
| """Emit error handling block at the end of the wrapper, if needed.""" | |
| emitter = self.emitter | |
| if self.use_goto(): | |
| emitter.emit_label("fail") | |
| emitter.emit_lines(*self.cleanups) | |
| if self.traceback_code: | |
| emitter.emit_line(self.traceback_code) | |
| emitter.emit_line("return NULL;") | |
| def finish(self) -> None: | |
| self.emitter.emit_line("}") |