Skip to content

Commit cbdb079

Browse files
committed
[mypyc] Make separate=True compilation work for real-world projects (sqlglot)
Backport of the sqlglot-essential separate=True fixes from release-1.20's separate_flag branch. See SEPARATE_FLAG_NOTES.md for full details. 1. Non-extension classes never have vtables -- short-circuit is_method_final to True for them. 2. emit_method_call uses method_decl(name) instead of get_method(name).decl so cross-group methods (decl-only in caller's group) work. 3. Route cross-group native/wrapper calls through the exports table via new Emitter.native_function_call / wrapper_function_call helpers; mark CPyPy_* wrapper declarations needs_export=True. 4. Defer cross-group imports to shim-load time: split exec_<group> into a self-only capsule setup and a deferred ensure_deps_<group>(). Shim uses PyImport_ImportModuleLevel with fromlist (no dotted getattr walk) and PyObject_GetAttrString for capsule fetch (no PyCapsule_Import walk). 5. Fix broken CPyImport_ImportFrom submodule fallback (was calling PyObject_GetItem on a module); Py_XDECREF potentially-NULL pointers. 6. Incremental-mode plumbing: compile_modules_to_ir syncs freshly built ClassIR/FuncIR into deser_ctx; load_type_map tolerates mypy-synthetic TypeInfos with no mypyc ClassIR.
1 parent 4d7c28c commit cbdb079

9 files changed

Lines changed: 196 additions & 64 deletions

File tree

mypyc/codegen/emit.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
FAST_ISINSTANCE_MAX_SUBCLASSES,
1515
HAVE_IMMORTAL,
1616
NATIVE_PREFIX,
17+
PREFIX,
1718
REG_PREFIX,
1819
STATIC_PREFIX,
1920
TYPE_PREFIX,
@@ -331,6 +332,23 @@ def c_error_value(self, rtype: RType) -> str:
331332
def native_function_name(self, fn: FuncDecl) -> str:
332333
return f"{NATIVE_PREFIX}{fn.cname(self.names)}"
333334

335+
def native_function_call(self, fn: FuncDecl) -> str:
336+
"""Return the C expression for a call to `fn`'s native (CPyDef_) entry.
337+
338+
For cross-group references under `separate=True`, this prepends the
339+
exports-table indirection (e.g. `exports_other.CPyDef_foo`). Same as
340+
`native_function_name()` for in-group calls.
341+
"""
342+
return f"{self.get_group_prefix(fn)}{NATIVE_PREFIX}{fn.cname(self.names)}"
343+
344+
def wrapper_function_call(self, fn: FuncDecl) -> str:
345+
"""Return the C expression for a call to `fn`'s Python-wrapper (CPyPy_) entry.
346+
347+
Like `native_function_call`, but for the PyObject-level wrapper that
348+
boxes/unboxes arguments. Used from slot generators (tp_init, etc.).
349+
"""
350+
return f"{self.get_group_prefix(fn)}{PREFIX}{fn.cname(self.names)}"
351+
334352
def tuple_c_declaration(self, rtuple: RTuple) -> list[str]:
335353
result = [
336354
f"#ifndef MYPYC_DECLARED_{rtuple.struct_name}",

mypyc/codegen/emitclass.py

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -683,11 +683,15 @@ def emit_null_check() -> None:
683683
emitter.emit_line(f"PyObject *self = {setup_name}({type_arg});")
684684
emit_null_check()
685685
return
686-
prefix = emitter.get_group_prefix(new_fn.decl) + NATIVE_PREFIX if native_prefix else PREFIX
686+
call = (
687+
emitter.native_function_call(new_fn.decl)
688+
if native_prefix
689+
else emitter.wrapper_function_call(new_fn.decl)
690+
)
687691
all_args = type_arg
688692
if new_args != "":
689693
all_args += ", " + new_args
690-
emitter.emit_line(f"PyObject *self = {prefix}{new_fn.cname(emitter.names)}({all_args});")
694+
emitter.emit_line(f"PyObject *self = {call}({all_args});")
691695
emit_null_check()
692696

693697
# skip __init__ if __new__ returns some other type
@@ -721,17 +725,13 @@ def generate_constructor_for_class(
721725

722726
args = ", ".join(["self"] + fn_args)
723727
if init_fn is not None:
724-
prefix = PREFIX if use_wrapper else NATIVE_PREFIX
725-
cast = "!= NULL ? 0 : -1" if use_wrapper else ""
726-
emitter.emit_line(
727-
"char res = {}{}{}({}){};".format(
728-
emitter.get_group_prefix(init_fn.decl),
729-
prefix,
730-
init_fn.cname(emitter.names),
731-
args,
732-
cast,
733-
)
728+
call = (
729+
emitter.wrapper_function_call(init_fn.decl)
730+
if use_wrapper
731+
else emitter.native_function_call(init_fn.decl)
734732
)
733+
cast = "!= NULL ? 0 : -1" if use_wrapper else ""
734+
emitter.emit_line(f"char res = {call}({args}){cast};")
735735
emitter.emit_line("if (res == 2) {")
736736
emitter.emit_line("Py_DECREF(self);")
737737
emitter.emit_line("return NULL;")
@@ -764,9 +764,8 @@ def generate_init_for_class(cl: ClassIR, init_fn: FuncIR, emitter: Emitter) -> s
764764
emitter.emit_line("{")
765765
if cl.allow_interpreted_subclasses or cl.builtin_base or cl.has_method("__new__"):
766766
emitter.emit_line(
767-
"return {}{}(self, args, kwds) != NULL ? 0 : -1;".format(
768-
PREFIX, init_fn.cname(emitter.names)
769-
)
767+
f"return {emitter.wrapper_function_call(init_fn.decl)}"
768+
"(self, args, kwds) != NULL ? 0 : -1;"
770769
)
771770
else:
772771
emitter.emit_line("return 0;")
@@ -812,7 +811,7 @@ def generate_new_for_class(
812811
# can enforce that instances are always properly initialized. This
813812
# is needed to support always defined attributes.
814813
emitter.emit_line(
815-
f"PyObject *ret = {PREFIX}{init_fn.cname(emitter.names)}(self, args, kwds);"
814+
f"PyObject *ret = {emitter.wrapper_function_call(init_fn.decl)}(self, args, kwds);"
816815
)
817816
emitter.emit_lines("if (ret == NULL)", " return NULL;")
818817
emitter.emit_line("return self;")

mypyc/codegen/emitfunc.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,12 @@
100100

101101

102102
def native_function_type(fn: FuncIR, emitter: Emitter) -> str:
103-
args = ", ".join(emitter.ctype(arg.type) for arg in fn.args) or "void"
104-
ret = emitter.ctype(fn.ret_type)
103+
return native_function_type_from_decl(fn.decl, emitter)
104+
105+
106+
def native_function_type_from_decl(decl: FuncDecl, emitter: Emitter) -> str:
107+
args = ", ".join(emitter.ctype(arg.type) for arg in decl.sig.args) or "void"
108+
ret = emitter.ctype(decl.sig.ret_type)
105109
return f"{ret} (*)({args})"
106110

107111

@@ -626,8 +630,11 @@ def emit_method_call(self, dest: str, op_obj: Value, name: str, op_args: list[Va
626630
rtype = op_obj.type
627631
assert isinstance(rtype, RInstance), rtype
628632
class_ir = rtype.class_ir
629-
method = rtype.class_ir.get_method(name)
630-
assert method is not None
633+
# Use method_decl (not get_method) because under separate compilation the
634+
# FuncIR body may live in a different group — only its declaration is
635+
# visible here, and a decl is all we need to emit a direct C call
636+
# (the symbol resolves through that group's exports table).
637+
method_decl = rtype.class_ir.method_decl(name)
631638

632639
# Can we call the method directly, bypassing vtable?
633640
is_direct = class_ir.is_method_final(name)
@@ -636,18 +643,17 @@ def emit_method_call(self, dest: str, op_obj: Value, name: str, op_args: list[Va
636643
# turned into the class for class methods
637644
obj_args = (
638645
[]
639-
if method.decl.kind == FUNC_STATICMETHOD
646+
if method_decl.kind == FUNC_STATICMETHOD
640647
else [f"(PyObject *)Py_TYPE({obj})"]
641-
if method.decl.kind == FUNC_CLASSMETHOD
648+
if method_decl.kind == FUNC_CLASSMETHOD
642649
else [obj]
643650
)
644651
args = ", ".join(obj_args + [self.reg(arg) for arg in op_args])
645-
mtype = native_function_type(method, self.emitter)
652+
mtype = native_function_type_from_decl(method_decl, self.emitter)
646653
version = "_TRAIT" if rtype.class_ir.is_trait else ""
647654
if is_direct:
648655
# Directly call method, without going through the vtable.
649-
lib = self.emitter.get_group_prefix(method.decl)
650-
self.emit_line(f"{dest}{lib}{NATIVE_PREFIX}{method.cname(self.names)}({args});")
656+
self.emit_line(f"{dest}{self.emitter.native_function_call(method_decl)}({args});")
651657
else:
652658
# For classes with allow_interpreted_subclasses where the method is
653659
# not overridden by any compiled subclass, use a direct call guarded

mypyc/codegen/emitmodule.py

Lines changed: 78 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,14 @@ def compile_modules_to_ir(
297297
else:
298298
scc_ir = compile_scc_to_ir(trees, result, mapper, compiler_options, errors)
299299
modules.update(scc_ir)
300+
# A later SCC loaded from cache may reference classes/functions
301+
# defined in this freshly-built SCC; populate deser_ctx so the
302+
# cached IR deserializer can resolve those cross-SCC references.
303+
for module_ir in scc_ir.values():
304+
for cl in module_ir.classes:
305+
deser_ctx.classes.setdefault(cl.fullname, cl)
306+
for fn in module_ir.functions:
307+
deser_ctx.functions.setdefault(fn.decl.id, fn)
300308

301309
return modules
302310

@@ -470,13 +478,16 @@ def generate_function_declaration(fn: FuncIR, emitter: Emitter) -> None:
470478
f"{native_function_header(fn.decl, emitter)};", needs_export=True
471479
)
472480
if fn.name != TOP_LEVEL_NAME and not fn.internal:
481+
# needs_export=True so Python-wrapper (CPyPy_) symbols are reachable from
482+
# other groups via the export table — needed for cross-group inherited
483+
# __init__ / __new__ slot dispatch under `separate=True`.
473484
if is_fastcall_supported(fn, emitter.capi_version):
474485
emitter.context.declarations[PREFIX + fn.cname(emitter.names)] = HeaderDeclaration(
475-
f"{wrapper_function_header(fn, emitter.names)};"
486+
f"{wrapper_function_header(fn, emitter.names)};", needs_export=True
476487
)
477488
else:
478489
emitter.context.declarations[PREFIX + fn.cname(emitter.names)] = HeaderDeclaration(
479-
f"{legacy_wrapper_function_header(fn, emitter.names)};"
490+
f"{legacy_wrapper_function_header(fn, emitter.names)};", needs_export=True
480491
)
481492

482493

@@ -820,6 +831,21 @@ def generate_shared_lib_init(self, emitter: Emitter) -> None:
820831
"goto fail;",
821832
"}",
822833
"",
834+
# Expose ensure_deps_<short> as a capsule so the shim can call
835+
# it before invoking the per-module init.
836+
f"extern int ensure_deps_{short_name}(void);",
837+
'capsule = PyCapsule_New((void *)ensure_deps_{sh}, "{lib}.ensure_deps", NULL);'.format(
838+
sh=short_name, lib=shared_lib_name(self.group_name)
839+
),
840+
"if (!capsule) {",
841+
"goto fail;",
842+
"}",
843+
'res = PyObject_SetAttrString(module, "ensure_deps", capsule);',
844+
"Py_DECREF(capsule);",
845+
"if (res < 0) {",
846+
"goto fail;",
847+
"}",
848+
"",
823849
)
824850

825851
for mod in self.modules:
@@ -851,25 +877,58 @@ def generate_shared_lib_init(self, emitter: Emitter) -> None:
851877
"",
852878
)
853879

854-
for group in sorted(self.context.group_deps):
855-
egroup = exported_name(group)
880+
# End of exec_<short_name>: only sets up capsules/module attributes.
881+
# Cross-group imports (populating `exports_<dep>` tables) are split
882+
# out into ensure_deps_<short_name>() below and run later, from the
883+
# shim's PyInit. See generate_shared_lib_init for details.
884+
emitter.emit_lines("return 0;", "fail:", "return -1;", "}")
885+
886+
if self.compiler_options.separate:
887+
# ensure_deps_<short>(): populates cross-group exports tables. Run
888+
# once, lazily, from the shim's PyInit just before invoking the
889+
# per-module init capsule. This defers cross-group imports out of
890+
# the shared-lib PyInit so they can't transitively trigger a
891+
# sibling package's __init__.py while another package __init__.py
892+
# is still mid-flight.
856893
emitter.emit_lines(
857-
'tmp = PyImport_ImportModule("{}"); if (!tmp) goto fail; Py_DECREF(tmp);'.format(
858-
shared_lib_name(group)
859-
),
860-
'struct export_table_{} *pexports_{} = PyCapsule_Import("{}.exports", 0);'.format(
861-
egroup, egroup, shared_lib_name(group)
862-
),
863-
f"if (!pexports_{egroup}) {{",
864-
"goto fail;",
865-
"}",
866-
"memcpy(&exports_{group}, pexports_{group}, sizeof(exports_{group}));".format(
867-
group=egroup
868-
),
869894
"",
895+
f"int ensure_deps_{short_name}(void)",
896+
"{",
897+
"static int done = 0;",
898+
"if (done) return 0;",
870899
)
871-
872-
emitter.emit_lines("return 0;", "fail:", "return -1;", "}")
900+
if self.context.group_deps:
901+
emitter.emit_line(
902+
'static PyObject *_mypyc_fromlist = NULL; '
903+
'if (!_mypyc_fromlist) { '
904+
'_mypyc_fromlist = Py_BuildValue("(s)", "*"); '
905+
'if (!_mypyc_fromlist) return -1; }'
906+
)
907+
emitter.emit_line("PyObject *tmp;")
908+
emitter.emit_line("PyObject *caps;")
909+
for group in sorted(self.context.group_deps):
910+
egroup = exported_name(group)
911+
# ImportModuleLevel with fromlist returns the leaf via
912+
# sys.modules (no dotted getattr walk), and fetching the
913+
# `exports` capsule directly off that module bypasses
914+
# PyCapsule_Import (which would redo the attribute walk).
915+
emitter.emit_lines(
916+
'tmp = PyImport_ImportModuleLevel("{}", NULL, NULL, _mypyc_fromlist, 0);'.format(
917+
shared_lib_name(group)
918+
),
919+
"if (!tmp) return -1;",
920+
'caps = PyObject_GetAttrString(tmp, "exports");',
921+
"Py_DECREF(tmp);",
922+
"if (!caps) return -1;",
923+
'struct export_table_{g} *pexports_{g} = '
924+
'(struct export_table_{g} *)PyCapsule_GetPointer(caps, "{lib}.exports");'.format(
925+
g=egroup, lib=shared_lib_name(group)
926+
),
927+
"Py_DECREF(caps);",
928+
f"if (!pexports_{egroup}) return -1;",
929+
"memcpy(&exports_{g}, pexports_{g}, sizeof(exports_{g}));".format(g=egroup),
930+
)
931+
emitter.emit_lines("done = 1;", "return 0;", "}")
873932

874933
if self.multi_phase_init:
875934
emitter.emit_lines(
@@ -914,6 +973,7 @@ def generate_shared_lib_init(self, emitter: Emitter) -> None:
914973
"}",
915974
f"if (exec_{short_name}(module) < 0) {{",
916975
"Py_DECREF(module);",
976+
"module = NULL;",
917977
"return NULL;",
918978
"}",
919979
"return module;",

mypyc/codegen/emitwrapper.py

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -538,7 +538,7 @@ def generate_get_wrapper(cl: ClassIR, fn: FuncIR, emitter: Emitter) -> str:
538538
)
539539
)
540540
emitter.emit_line("instance = instance ? instance : Py_None;")
541-
emitter.emit_line(f"return {NATIVE_PREFIX}{fn.cname(emitter.names)}(self, instance, owner);")
541+
emitter.emit_line(f"return {emitter.native_function_call(fn.decl)}(self, instance, owner);")
542542
emitter.emit_line("}")
543543

544544
return name
@@ -601,8 +601,8 @@ def generate_bool_wrapper(cl: ClassIR, fn: FuncIR, emitter: Emitter) -> str:
601601
name = f"{DUNDER_PREFIX}{fn.name}{cl.name_prefix(emitter.names)}"
602602
emitter.emit_line(f"static int {name}(PyObject *self) {{")
603603
emitter.emit_line(
604-
"{}val = {}{}(self);".format(
605-
emitter.ctype_spaced(fn.ret_type), NATIVE_PREFIX, fn.cname(emitter.names)
604+
"{}val = {}(self);".format(
605+
emitter.ctype_spaced(fn.ret_type), emitter.native_function_call(fn.decl)
606606
)
607607
)
608608
emitter.emit_error_check("val", fn.ret_type, "return -1;")
@@ -705,8 +705,10 @@ def generate_set_del_item_wrapper_inner(
705705
generate_arg_check(arg.name, arg.type, emitter, GotoHandler("fail"))
706706
native_args = ", ".join(f"arg_{arg.name}" for arg in args)
707707
emitter.emit_line(
708-
"{}val = {}{}({});".format(
709-
emitter.ctype_spaced(fn.ret_type), NATIVE_PREFIX, fn.cname(emitter.names), native_args
708+
"{}val = {}({});".format(
709+
emitter.ctype_spaced(fn.ret_type),
710+
emitter.native_function_call(fn.decl),
711+
native_args,
710712
)
711713
)
712714
emitter.emit_error_check("val", fn.ret_type, "goto fail;")
@@ -723,8 +725,8 @@ def generate_contains_wrapper(cl: ClassIR, fn: FuncIR, emitter: Emitter) -> str:
723725
emitter.emit_line(f"static int {name}(PyObject *self, PyObject *obj_item) {{")
724726
generate_arg_check("item", fn.args[1].type, emitter, ReturnHandler("-1"))
725727
emitter.emit_line(
726-
"{}val = {}{}(self, arg_item);".format(
727-
emitter.ctype_spaced(fn.ret_type), NATIVE_PREFIX, fn.cname(emitter.names)
728+
"{}val = {}(self, arg_item);".format(
729+
emitter.ctype_spaced(fn.ret_type), emitter.native_function_call(fn.decl)
728730
)
729731
)
730732
emitter.emit_error_check("val", fn.ret_type, "return -1;")
@@ -858,6 +860,9 @@ def set_target(self, fn: FuncIR) -> None:
858860
"""
859861
self.target_name = fn.name
860862
self.target_cname = fn.cname(self.emitter.names)
863+
# Cached native-call expression so cross-group targets go through the
864+
# exports table; same as `NATIVE_PREFIX + cname` for in-group calls.
865+
self.target_native_call = self.emitter.native_function_call(fn.decl)
861866
self.num_bitmap_args = fn.sig.num_bitmap_args
862867
if self.num_bitmap_args:
863868
self.args = fn.args[: -self.num_bitmap_args]
@@ -928,8 +933,8 @@ def emit_call(self, not_implemented_handler: str = "") -> None:
928933
# TODO: The Py_RETURN macros return the correct PyObject * with reference count
929934
# handling. Are they relevant?
930935
emitter.emit_line(
931-
"{}retval = {}{}({});".format(
932-
emitter.ctype_spaced(ret_type), NATIVE_PREFIX, self.target_cname, native_args
936+
"{}retval = {}({});".format(
937+
emitter.ctype_spaced(ret_type), self.target_native_call, native_args
933938
)
934939
)
935940
emitter.emit_lines(*self.cleanups)
@@ -942,9 +947,7 @@ def emit_call(self, not_implemented_handler: str = "") -> None:
942947
if not_implemented_handler and not isinstance(ret_type, RInstance):
943948
# The return value type may overlap with NotImplemented.
944949
emitter.emit_line(
945-
"PyObject *retbox = {}{}({});".format(
946-
NATIVE_PREFIX, self.target_cname, native_args
947-
)
950+
f"PyObject *retbox = {self.target_native_call}({native_args});"
948951
)
949952
emitter.emit_lines(
950953
"if (retbox == Py_NotImplemented) {",
@@ -953,7 +956,7 @@ def emit_call(self, not_implemented_handler: str = "") -> None:
953956
"return retbox;",
954957
)
955958
else:
956-
emitter.emit_line(f"return {NATIVE_PREFIX}{self.target_cname}({native_args});")
959+
emitter.emit_line(f"return {self.target_native_call}({native_args});")
957960
# TODO: Tracebacks?
958961

959962
def error(self) -> ErrorHandler:

mypyc/ir/class_ir.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,12 @@ def has_method(self, name: str) -> bool:
276276
return True
277277

278278
def is_method_final(self, name: str) -> bool:
279+
if not self.is_ext_class:
280+
# Non-extension classes don't use vtable dispatch; their mypyc-compiled
281+
# "fast" methods are always called directly by C name. Treating them as
282+
# final here keeps codegen from trying to index into a vtable that was
283+
# never computed (non-ext classes skip compute_vtable).
284+
return True
279285
subs = self.subclasses()
280286
if subs is None:
281287
return self.is_final_class

mypyc/irbuild/prepare.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,13 @@ def load_type_map(mapper: Mapper, modules: list[MypyFile], deser_ctx: DeserMaps)
166166
and not node.node.is_named_tuple
167167
and node.node.typeddict_type is None
168168
):
169-
ir = deser_ctx.classes[node.node.fullname]
169+
# Some TypeInfo entries are mypy-synthetic (e.g. anonymous
170+
# intersection classes like "<subclass of X and Y>") and have
171+
# no corresponding mypyc ClassIR. Skip those rather than
172+
# aborting the whole cache load.
173+
ir = deser_ctx.classes.get(node.node.fullname)
174+
if ir is None:
175+
continue
170176
mapper.type_to_ir[node.node] = ir
171177
mapper.symbol_fullnames.add(node.node.fullname)
172178
mapper.func_to_decl[node.node] = ir.ctor

0 commit comments

Comments
 (0)