Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion mypyc/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,32 @@
debug_level="{}",
strict_dunder_typing={},
log_trace={},
target_python={!r},
),
)
"""


def extract_target_python(args: list[str]) -> tuple[list[str], str | None]:
"""Remove --target-python from args (it's not a mypy option).

Return (remaining args, target python or None).
"""
remaining = []
target_python = None
it = iter(args)
for arg in it:
if arg == "--target-python":
target_python = next(it, None)
if target_python is None:
sys.exit("error: --target-python requires an argument (e.g. 3.13 or 3.14t)")
elif arg.startswith("--target-python="):
target_python = arg.split("=", 1)[1]
else:
remaining.append(arg)
return remaining, target_python


def main() -> None:
build_dir = "build" # can this be overridden??
try:
Expand All @@ -50,11 +71,13 @@ def main() -> None:
# mypyc_trace.txt.
log_trace = bool(int(os.getenv("MYPYC_LOG_TRACE", "0")))

args, target_python = extract_target_python(sys.argv[1:])

setup_file = os.path.join(build_dir, "setup.py")
with open(setup_file, "w") as f:
f.write(
setup_format.format(
sys.argv[1:], opt_level, debug_level, strict_dunder_typing, log_trace
args, opt_level, debug_level, strict_dunder_typing, log_trace, target_python
)
)

Expand Down
38 changes: 31 additions & 7 deletions mypyc/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,12 @@
from mypy.util import write_junit_xml
from mypyc.annotate import generate_annotated_html
from mypyc.codegen import emitmodule
from mypyc.common import IS_FREE_THREADED, RUNTIME_C_FILES, shared_lib_name
from mypyc.common import RUNTIME_C_FILES, shared_lib_name
from mypyc.errors import Errors
from mypyc.ir.deps import SourceDep
from mypyc.ir.pprint import format_modules
from mypyc.namegen import exported_name
from mypyc.options import CompilerOptions
from mypyc.options import CompilerOptions, TargetPython


class ModDesc(NamedTuple):
Expand Down Expand Up @@ -219,6 +219,9 @@ def get_mypy_config(
fscache: FileSystemCache | None,
) -> tuple[list[BuildSource], list[BuildSource], Options]:
"""Construct mypy BuildSources and Options from file and options lists"""
for arg in mypy_options:
if arg == "--python-version" or arg.startswith("--python-version="):
fail("error: mypyc does not accept --python-version")
all_sources, options = process_options(mypy_options, fscache=fscache, mypyc=True)
if only_compile_paths is not None:
paths_set = set(only_compile_paths)
Expand All @@ -233,8 +236,8 @@ def get_mypy_config(
return mypyc_sources, all_sources, options

# Override whatever python_version is inferred from the .ini file,
# and set the python_version to be the currently used version.
options.python_version = sys.version_info[:2]
# and set the python_version to be the target version.
options.python_version = compiler_options.target_python.version

if options.python_version[0] == 2:
fail("Python 2 not supported")
Expand All @@ -258,7 +261,11 @@ def is_package_source(source: BuildSource) -> bool:


def generate_c_extension_shim(
full_module_name: str, module_name: str, dir_name: str, group_name: str
full_module_name: str,
module_name: str,
dir_name: str,
group_name: str,
target_python: TargetPython | None = None,
) -> str:
"""Create a C extension shim with a passthrough PyInit function.

Expand All @@ -267,11 +274,13 @@ def generate_c_extension_shim(
module_name: the final component of the module name
dir_name: the directory to place source code
group_name: the name of the group
target_python: the Python build to generate code for (default: running Python)
"""
target_python = target_python or TargetPython.host()
cname = "%s.c" % full_module_name.replace(".", os.sep)
cpath = os.path.join(dir_name, cname)

if IS_FREE_THREADED:
if target_python.free_threaded:
# We use multi-phase init in free-threaded builds to enable free threading.
shim_name = "module_shim_no_gil_multiphase.tmpl"
else:
Expand Down Expand Up @@ -374,6 +383,7 @@ def build_using_shared_lib(
build_dir: str,
extra_compile_args: list[str],
extra_include_dirs: list[str],
target_python: TargetPython | None = None,
) -> list[Extension]:
"""Produce the list of extension modules when a shared library is needed.

Expand All @@ -398,7 +408,9 @@ def build_using_shared_lib(

for source in sources:
module_name = source.module.split(".")[-1]
shim_file = generate_c_extension_shim(source.module, module_name, build_dir, group_name)
shim_file = generate_c_extension_shim(
source.module, module_name, build_dir, group_name, target_python
)

# We include the __init__ in the "module name" we stick in the Extension,
# since this seems to be needed for it to end up in the right place.
Expand Down Expand Up @@ -893,6 +905,7 @@ def mypycify(
depends_on_librt_internal: bool = False,
install_librt: bool = False,
experimental_features: bool = False,
target_python: str | TargetPython | None = None,
) -> list[Extension]:
"""Main entry point to building using mypyc.

Expand Down Expand Up @@ -951,7 +964,16 @@ def mypycify(
experimental_features: Enable experimental features (install_librt=True is
also needed if using experimental librt features). These
have no backward compatibility guarantees!
target_python: The Python build to generate code for, such as "3.13" or "3.14t"
(a "t" suffix means free-threaded). Defaults to the running
Python. The generated C must be compiled against the headers of
the target Python.
"""
if isinstance(target_python, str):
try:
target_python = TargetPython.parse(target_python)
except ValueError as e:
fail(f"error: {e}")

# Skip redundant inplace .so copies on every build_ext invocation.
_patch_setuptools_copy_extensions_to_source()
Expand All @@ -969,6 +991,7 @@ def mypycify(
log_trace=log_trace,
depends_on_librt_internal=depends_on_librt_internal,
experimental_features=experimental_features,
target_python=target_python,
)

# Generate all the actual important C code
Expand Down Expand Up @@ -1039,6 +1062,7 @@ def mypycify(
build_dir,
cflags,
extra_include_dirs,
compiler_options.target_python,
)
)
else:
Expand Down
17 changes: 11 additions & 6 deletions mypyc/codegen/emit.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

from __future__ import annotations

import sys
from collections.abc import Callable
from typing import Final

Expand All @@ -12,7 +11,6 @@
ATTR_PREFIX,
BITMAP_BITS,
FAST_ISINSTANCE_MAX_SUBCLASSES,
HAVE_IMMORTAL,
MODULE_PREFIX,
NATIVE_PREFIX,
PREFIX,
Expand Down Expand Up @@ -73,6 +71,7 @@
vec_item_type_tags,
)
from mypyc.namegen import NameGenerator, exported_name
from mypyc.options import TargetPython
from mypyc.primitives.registry import builtin_names
from mypyc.sametype import is_same_type

Expand Down Expand Up @@ -141,13 +140,15 @@ def __init__(
strict_traceback_checks: bool,
group_name: str | None = None,
group_map: dict[str, str | None] | None = None,
target_python: TargetPython | None = None,
) -> None:
"""Setup shared emitter state.

Args:
names: The name generator to use
group_map: Map from module names to group name
group_name: Current group name
target_python: The Python build to generate code for (default: running Python)
"""
self.temp_counter = 0
self.names = names
Expand All @@ -168,6 +169,7 @@ def __init__(
self.literals = Literals()
# See mypyc/options.py for context.
self.strict_traceback_checks = strict_traceback_checks
self.target_python = target_python or TargetPython.host()


class ErrorHandler:
Expand Down Expand Up @@ -211,17 +213,20 @@ def __init__(
self,
context: EmitterContext,
value_names: dict[Value, str] | None = None,
capi_version: tuple[int, int] | None = None,
filepath: str | None = None,
) -> None:
self.context = context
self.capi_version = capi_version or sys.version_info[:2]
self.target_python = context.target_python
self.names = context.names
self.value_names = value_names or {}
self.fragments: list[str] = []
self._indent = 0
self.filepath = filepath

@property
def capi_version(self) -> tuple[int, int]:
return self.target_python.version

# Low-level operations

def indent(self) -> None:
Expand Down Expand Up @@ -598,7 +603,7 @@ def emit_inc_ref(self, dest: str, rtype: RType, *, rare: bool = False) -> None:
self.emit_line(f"{prefix}_INCREF({dest});")
elif not rtype.is_unboxed:
# Always inline, since this is a simple but very hot op
if rtype.may_be_immortal or not HAVE_IMMORTAL:
if rtype.may_be_immortal or not self.target_python.have_immortal:
self.emit_line("CPy_INCREF(%s);" % dest)
else:
self.emit_line("CPy_INCREF_NO_IMM(%s);" % dest)
Expand Down Expand Up @@ -632,7 +637,7 @@ def emit_dec_ref(
self.emit_line(f"CPy_{x}DecRef({dest});")
else:
# Inlined
if rtype.may_be_immortal or not HAVE_IMMORTAL:
if rtype.may_be_immortal or not self.target_python.have_immortal:
self.emit_line(f"CPy_{x}DECREF({dest});")
else:
self.emit_line(f"CPy_{x}DECREF_NO_IMM({dest});")
Expand Down
7 changes: 3 additions & 4 deletions mypyc/codegen/emitclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
BITMAP_TYPE,
CPYFUNCTION_NAME,
GENERATOR_HELPER_NAME,
IS_FREE_THREADED,
MYPYC_DEFAULTS_SETUP,
NATIVE_PREFIX,
PREFIX,
Expand Down Expand Up @@ -1255,7 +1254,7 @@ def generate_getter(cl: ClassIR, attr: str, rtype: RType, emitter: Emitter) -> N
emitter.emit_line("{")
attr_expr = f"self->{attr_field}"

if IS_FREE_THREADED and is_simple_refcounted_pointer(rtype):
if emitter.target_python.free_threaded and is_simple_refcounted_pointer(rtype):
# In free-threaded builds, load the attribute and take a new reference with
# an optimistic validated incref to avoid racing with a concurrent setter.
# CPy_GetAttrRef returns NULL if the attribute is undefined (NULL field),
Expand Down Expand Up @@ -1319,7 +1318,7 @@ def generate_setter(cl: ClassIR, attr: str, rtype: RType, emitter: Emitter) -> N
emitter.emit_line("return -1;")
emitter.emit_line("}")

if IS_FREE_THREADED and is_simple_refcounted_pointer(rtype):
if emitter.target_python.free_threaded and is_simple_refcounted_pointer(rtype):
# In free-threaded builds, publish the new value via CPy_SetAttrRef, which
# takes the owner's critical section so a concurrent reader (see
# CPy_GetAttrRef) can always secure a reference to the value it observes,
Expand Down Expand Up @@ -1498,7 +1497,7 @@ def success() -> None:
wrapper_name = emit_instance(cl.methods["__call__"], cl.coroutine_name)
struct_name = cl.struct_name(emitter.names)
attr = emitter.attr(CPYFUNCTION_NAME)
if IS_FREE_THREADED:
if emitter.target_python.free_threaded:
emitter.emit_line(
f"CPy_InitAttrRefIfNull(type, (PyObject **)&(({struct_name} *)type)->{attr}, "
f"{wrapper_name});"
Expand Down
10 changes: 4 additions & 6 deletions mypyc/codegen/emitfunc.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@
c_array_initializer,
)
from mypyc.common import (
HAVE_IMMORTAL,
IS_FREE_THREADED,
NATIVE_PREFIX,
REG_PREFIX,
RUNNING_FIELD,
Expand Down Expand Up @@ -466,7 +464,7 @@ def emit_load_attr_take_ref(
ClassIR.attrs_are_thread_confined.
"""
use_get_attr_ref = (
IS_FREE_THREADED
self.emitter.target_python.free_threaded
and is_simple_refcounted_pointer(attr_rtype)
and not op.is_borrowed
and not cl.attrs_are_thread_confined()
Expand Down Expand Up @@ -629,7 +627,7 @@ def visit_set_attr(self, op: SetAttr) -> None:
self.emit_line(f"{dest} = 1;")
self.emitter.emit_error_check(tmp, ret_type, f"{dest} = 0;")
elif (
IS_FREE_THREADED
self.emitter.target_python.free_threaded
and is_simple_refcounted_pointer(attr_rtype)
and not cl.attrs_are_thread_confined()
):
Expand Down Expand Up @@ -763,12 +761,12 @@ def visit_inc_ref(self, op: IncRef) -> None:
if (
isinstance(op.src, Box)
and (is_none_rprimitive(op.src.src.type) or is_bool_or_bit_rprimitive(op.src.src.type))
and HAVE_IMMORTAL
and self.emitter.target_python.have_immortal
):
# On Python 3.12+, None/True/False are immortal, and we can skip inc ref
return

if isinstance(op.src, LoadLiteral) and HAVE_IMMORTAL:
if isinstance(op.src, LoadLiteral) and self.emitter.target_python.have_immortal:
value = op.src.value
# We can skip inc ref for immortal literals on Python 3.12+
if type(value) is int and -5 <= value <= 256:
Expand Down
Loading
Loading