Repository: python/mypy
Pull request: [mypyc] Switch to table-driven imports for smaller IR
Add CPyImport_ImportFromMany() which imports a module and a tuple of
names, placing them in the globals dict in one go.

Previously, each name would imported and placed in globals manually
in IR, leading to some pretty verbose code.

The other option to collect all from imports and perform them all at
once in the helper would remove even more ops, however, it has some
major downsides:

- It wouldn't be able to be done in IRBuild directly, instead being
  handled in the prebuild visitor and codegen... which all sounds
  really involved.

- It would cause from imports to be performed eagerly, potentially
  causing circular imports (especially in functions whose imports are
  probably there to avoid a circular import!).

The latter is the nail in the coffin for this idea.

---

Change how imports (not from imports!) are processed so they can be
table-driven (tuple-driven, really) and compact. Here's how it works:

Import nodes are divided in groups (in the prebuild visitor). Each group
consists of consecutive Import nodes:

    import mod         <| group 1
    import mod2         |

    def foo() -> None:
        import mod3    <- group 2 (*)
  
    import mod4        <- group 3

Every time we encounter the first import of a group, build IR to call
CPyImport_ImportMany() that will perform all of the group's imports in
one go.

(*) Imports in functions or classes are still transformed into the original,
verbose IR as speed is more important than codesize.

Previously, each module would imported and placed in globals manually
in IR, leading to some pretty verbose code.

The other option to collect all imports and perform them all at once in
the helper would remove even more ops, however, it's problematic for
the same reasons from the previous commit (spoiler: it's not safe).

Implementation notes:

  - I had to add support for loading the address of a static directly,
    so I shoehorned in LoadStatic support for LoadAddress.

  - Unfortunately by replacing multiple import nodes with a single
    function call at the IR level, if any import within a group fails,
    the traceback line number is static and will be probably wrong
    (pointing to the first import node in my original impl.).

    To fix this, I had to make CPyImport_ImportMany() add the traceback
    entry itself on failure (instead of letting codegen handle it
    automatically). This is admittedly ugly.

Overall, this doesn't speed up initialization. The only real speed
impact is that back to back imports in a tight loop seems to be 10-20%
slower. I believe that's acceptable given the code size reduction.

---

**Other changes:**

- Don't declare internal static for non-compiled modules

  It won't be read anywhere as the internal statics are only used to
  avoid runaway recursion with import cycles in our module init
  functions.

- Wrap long RArray initializers and long annotations in codegen

  Table-driven imports can load some rather large RArrays and tuple
  literals so this was needed to keep the generated C readable.

- Add LLBuilder helper for setting up a RArray

Resolves mypyc/mypyc#591.

Problem statement:
Experiment with table-driven import code
We generate some pretty verbose code for all of our module imports and it conceivably be made table driven, where we emit a table describing what modules to import and what names to import from them.

Hints from issue discussion:
This might also improve startup performance since we'd get fewer icache misses (but then it might not have a measurable impact).
I have a basically ready [implementation for `from  ... import ...`](https://github.com/ichard26/mypy/commit/fab3e6b9909d2d95c1cf298b1c5298730b605563). It uses a helper function which imports a module and an array of names, placing them in the globals dict in one go.

For example, here's the new C code for this function:

```python
def mod_from() -> None:
    from dataclasses import dataclass, replace
    from enum import Enum as MyEnum
```

```c
    cpy_r_r0 = CPyStatics[7]; /* 'dataclass' */
    cpy_r_r1 = CPyStatics[8]; /* 'replace' */
    PyObject *cpy_r_r2[2] = {cpy_r_r0, cpy_r_r1};
    cpy_r_r3 = (PyObject **)&cpy_r_r2;
    cpy_r_r4 = CPyStatic_globals;
    cpy_r_r5 = CPyStatics[9]; /* 'dataclasses' */
    cpy_r_r6 = CPyImport_ImportFromMany(cpy_r_r5, cpy_r_r4, 2, cpy_r_r3, cpy_r_r3);
    if (unlikely(cpy_r_r6 == NULL)) {
        CPy_AddTraceback("temp/simple_imports.py", "mod_from", 9, CPyStatic_globals);
        goto CPyL3;
    }
CPyL1: ;
    CPyModule_dataclasses = cpy_r_r6;
    CPy_INCREF(CPyModule_dataclasses);
    CPy_DECREF(cpy_r_r6);
    cpy_r_r7 = CPyStatics[10]; /* 'Enum' */
    PyObject *cpy_r_r8[1] = {cpy_r_r7};
    cpy_r_r9 = (PyObject **)&cpy_r_r8;
    cpy_r_r10 = CPyStatics[11]; /* 'MyEnum' */
    PyObject *cpy_r_r11[1] = {cpy_r_r10};
    cpy_r_r12 = (PyObject **)&cpy_r_r11;
    cpy_r_r13 = CPyStatic_globals;
    cpy_r_r14 = CPyStatics[12]; /* 'enum' */
    cpy_r_r15 = CPyImport_ImportFromMany(cpy_r_r14, cpy_r_r13, 1, cpy_r_r9, cpy_r_r12);
    if (unlikely(cpy_r_r15 == NULL)) {
        CPy_AddTraceback("temp/simple_imports.py", "mod_from", 10, CPyStatic_globals);
        goto CPyL3;
    }
CPyL2: ;
    CPyModule_enum = cpy_r_r15;
    CPy_INCREF(CPyModule_enum);
    CPy_DECREF(cpy_r_r15);
```

I also have a functional prototype for `import ...`. The approach I took is to collect all "import groups" (a series of Import nodes back to back) and call a helper function which performs the imports for each group. The array given to the helper is really a 2D array with each subarray representing the data needed to import one module (module static address, module ID, as name, etc.).

```python
def mod() -> None:
    import sys
    import pprint as pretty
    import sys
```

```c
    cpy_r_r0 = (PyObject **)&CPyModule_sys;
    cpy_r_r1 = CPyStatics[3]; /* 'sys' */
    cpy_r_r2 = (PyObject **)&CPyModule_pprint;
    cpy_r_r3 = CPyStatics[4]; /* 'pprint' */
    cpy_r_r4 = CPyStatics[5]; /* 'pretty' */
    cpy_r_r5 = (PyObject **)&CPyModule_sys;
    cpy_r_r6 = CPyStatics[3]; /* 'sys' */
    void *cpy_r_r7[3][3] = {
        {cpy_r_r0, cpy_r_r1, cpy_r_r1}, {cpy_r_r2, cpy_r_r3, cpy_r_r4},
        {cpy_r_r5, cpy_r_r6, cpy_r_r6}
    };
    cpy_r_r8 = (void *)&cpy_r_r7;
    cpy_r_r9 = CPyStatic_globals;
    cpy_r_r10 = CPyImport_ImportMany(cpy_r_r8, cpy_r_r9, 3);
    if (!cpy_r_r10) goto CPyL4;
```

I chose to go with _import groups_ instead of eagerly importing everything at startup to keep things simple (it "just works" for functions evidently) and avoid changing import semantics (which may lead to circular imports).

However, it still needs a lot of work:

- The wrong name and object is placed in the globals dict for submodules, i.e. [my branch doesn't implement this case yet](https://github.com/python/mypy/blob/4365dad81a248f336ee1126acbe7f8692791ca3b/mypyc/irbuild/statement.py#L226)

- On failure, the line number of the first import in the group was being printed regardless of which import actually failed. 
  - This isn't easily fixable as CallC ops cannot tweak the line number used in the traceback entry.
  - For the time being, I've just decided to omit the traceback entry. I'll probably take another look at this before submitting the PR.
 
- Polishing the new support for 2D RArrays and loading the memory address of a static (needed for this impl.)
- More testing!

For posterity, [here's a gist with the old and new C code for the file I've been testing my implementation on](https://gist.github.com/ichard26/1e02dd114536a2711f8f1d8de29a89f7). (Scroll down for the Python source.)

Commit message: Cargo cult the RArray helper added earlier

Patch to review:
diff --git a/mypyc/codegen/emit.py b/mypyc/codegen/emit.py
index 0f1f5ad071ad..1bc9bd4b0a08 100644
--- a/mypyc/codegen/emit.py
+++ b/mypyc/codegen/emit.py
@@ -2,7 +2,9 @@
 
 from __future__ import annotations
 
+import pprint
 import sys
+import textwrap
 from typing import Callable
 from typing_extensions import Final
 
@@ -191,10 +193,25 @@ def reg(self, reg: Value) -> str:
     def attr(self, name: str) -> str:
         return ATTR_PREFIX + name
 
-    def emit_line(self, line: str = "") -> None:
+    def emit_line(self, line: str = "", *, ann: object = None) -> None:
         if line.startswith("}"):
             self.dedent()
-        self.fragments.append(self._indent * " " + line + "\n")
+
+        comment = comment_continued = ""
+        if ann is not None:
+            line_width = self._indent + len(line)
+            formatted = pprint.pformat(ann, compact=True, width=max(90 - line_width, 20))
+            if not any(x in formatted for x in ("/*", "*/", "\0")):
+                if "\n" in formatted:
+                    first_line, rest = formatted.split("\n", maxsplit=1)
+                    comment = f" /* {first_line}"
+                    comment_continued = textwrap.indent(rest, (line_width + 3) * " ") + " */\n"
+                else:
+                    comment = f" /* {formatted} */"
+        self.fragments.append(self._indent * " " + line + comment + "\n")
+        if comment_continued:
+            self.fragments.extend(comment_continued.splitlines(keepends=True))
+
         if line.endswith("{"):
             self.indent()
 
@@ -1119,3 +1136,35 @@ def _emit_traceback(
         self.emit_line(line)
         if DEBUG_ERRORS:
             self.emit_line('assert(PyErr_Occurred() != NULL && "failure w/o err!");')
+
+
+def c_array_initializer(components: list[str], *, indented: bool = False) -> str:
+    """Construct an initializer for a C array variable.
+
+    Components are C expressions valid in an initializer.
+
+    For example, if components are ["1", "2"], the result
+    would be "{1, 2}", which can be used like this:
+
+        int a[] = {1, 2};
+
+    If the result is long, split it into multiple lines.
+    """
+    indent = " " * 4 if indented else ""
+    res = []
+    current: list[str] = []
+    cur_len = 0
+    for c in components:
+        if not current or cur_len + 2 + len(indent) + len(c) < 70:
+            current.append(c)
+            cur_len += len(c) + 2
+        else:
+            res.append(indent + ", ".join(current))
+            current = [c]
+            cur_len = len(c)
+    if not res:
+        # Result fits on a single line
+        return "{%s}" % ", ".join(current)
+    # Multi-line result
+    res.append(indent + ", ".join(current))
+    return "{\n    " + ",\n    ".join(res) + "\n" + indent + "}"
diff --git a/mypyc/codegen/emitfunc.py b/mypyc/codegen/emitfunc.py
index c6af1309550b..f2406ff1a257 100644
--- a/mypyc/codegen/emitfunc.py
+++ b/mypyc/codegen/emitfunc.py
@@ -5,7 +5,7 @@
 from typing_extensions import Final
 
 from mypyc.analysis.blockfreq import frequently_executed_blocks
-from mypyc.codegen.emit import DEBUG_ERRORS, Emitter, TracebackAndGotoHandler
+from mypyc.codegen.emit import DEBUG_ERRORS, Emitter, TracebackAndGotoHandler, c_array_initializer
 from mypyc.common import MODULE_PREFIX, NATIVE_PREFIX, REG_PREFIX, STATIC_PREFIX, TYPE_PREFIX
 from mypyc.ir.class_ir import ClassIR
 from mypyc.ir.func_ir import FUNC_CLASSMETHOD, FUNC_STATICMETHOD, FuncDecl, FuncIR, all_values
@@ -262,12 +262,12 @@ def visit_assign_multi(self, op: AssignMulti) -> None:
         # RArray values can only be assigned to once, so we can always
         # declare them on initialization.
         self.emit_line(
-            "%s%s[%d] = {%s};"
+            "%s%s[%d] = %s;"
             % (
                 self.emitter.ctype_spaced(typ.item_type),
                 dest,
                 len(op.src),
-                ", ".join(self.reg(s) for s in op.src),
+                c_array_initializer([self.reg(s) for s in op.src], indented=True),
             )
         )
 
@@ -282,15 +282,12 @@ def visit_load_error_value(self, op: LoadErrorValue) -> None:
 
     def visit_load_literal(self, op: LoadLiteral) -> None:
         index = self.literals.literal_index(op.value)
-        s = repr(op.value)
-        if not any(x in s for x in ("/*", "*/", "\0")):
-            ann = " /* %s */" % s
-        else:
-            ann = ""
         if not is_int_rprimitive(op.type):
-            self.emit_line("%s = CPyStatics[%d];%s" % (self.reg(op), index, ann))
+            self.emit_line("%s = CPyStatics[%d];" % (self.reg(op), index), ann=op.value)
         else:
-            self.emit_line("%s = (CPyTagged)CPyStatics[%d] | 1;%s" % (self.reg(op), index, ann))
+            self.emit_line(
+                "%s = (CPyTagged)CPyStatics[%d] | 1;" % (self.reg(op), index), ann=op.value
+            )
 
     def get_attr_expr(self, obj: str, op: GetAttr | SetAttr, decl_cl: ClassIR) -> str:
         """Generate attribute accessor for normal (non-property) access.
@@ -468,12 +465,7 @@ def visit_load_static(self, op: LoadStatic) -> None:
         name = self.emitter.static_name(op.identifier, op.module_name, prefix)
         if op.namespace == NAMESPACE_TYPE:
             name = "(PyObject *)%s" % name
-        ann = ""
-        if op.ann:
-            s = repr(op.ann)
-            if not any(x in s for x in ("/*", "*/", "\0")):
-                ann = " /* %s */" % s
-        self.emit_line(f"{dest} = {name};{ann}")
+        self.emit_line(f"{dest} = {name};", ann=op.ann)
 
     def visit_init_static(self, op: InitStatic) -> None:
         value = self.reg(op.value)
@@ -636,12 +628,7 @@ def visit_extend(self, op: Extend) -> None:
 
     def visit_load_global(self, op: LoadGlobal) -> None:
         dest = self.reg(op)
-        ann = ""
-        if op.ann:
-            s = repr(op.ann)
-            if not any(x in s for x in ("/*", "*/", "\0")):
-                ann = " /* %s */" % s
-        self.emit_line(f"{dest} = {op.identifier};{ann}")
+        self.emit_line(f"{dest} = {op.identifier};", ann=op.ann)
 
     def visit_int_op(self, op: IntOp) -> None:
         dest = self.reg(op)
@@ -727,7 +714,13 @@ def visit_get_element_ptr(self, op: GetElementPtr) -> None:
     def visit_load_address(self, op: LoadAddress) -> None:
         typ = op.type
         dest = self.reg(op)
-        src = self.reg(op.src) if isinstance(op.src, Register) else op.src
+        if isinstance(op.src, Register):
+            src = self.reg(op.src)
+        elif isinstance(op.src, LoadStatic):
+            prefix = self.PREFIX_MAP[op.src.namespace]
+            src = self.emitter.static_name(op.src.identifier, op.src.module_name, prefix)
+        else:
+            src = op.src
         self.emit_line(f"{dest} = ({typ._ctype})&{src};")
 
     def visit_keep_alive(self, op: KeepAlive) -> None:
@@ -776,8 +769,8 @@ def c_error_value(self, rtype: RType) -> str:
     def c_undefined_value(self, rtype: RType) -> str:
         return self.emitter.c_undefined_value(rtype)
 
-    def emit_line(self, line: str) -> None:
-        self.emitter.emit_line(line)
+    def emit_line(self, line: str, *, ann: object = None) -> None:
+        self.emitter.emit_line(line, ann=ann)
 
     def emit_lines(self, *lines: str) -> None:
         self.emitter.emit_lines(*lines)
diff --git a/mypyc/codegen/emitmodule.py b/mypyc/codegen/emitmodule.py
index a8226314039d..0e80ff6da1f2 100644
--- a/mypyc/codegen/emitmodule.py
+++ b/mypyc/codegen/emitmodule.py
@@ -26,7 +26,7 @@
 from mypy.plugin import Plugin, ReportConfigContext
 from mypy.util import hash_digest
 from mypyc.codegen.cstring import c_string_initializer
-from mypyc.codegen.emit import Emitter, EmitterContext, HeaderDeclaration
+from mypyc.codegen.emit import Emitter, EmitterContext, HeaderDeclaration, c_array_initializer
 from mypyc.codegen.emitclass import generate_class, generate_class_type_decl
 from mypyc.codegen.emitfunc import generate_native_function, native_function_header
 from mypyc.codegen.emitwrapper import (
@@ -296,11 +296,11 @@ def compile_ir_to_c(
     # compiled into a separate extension module.
     ctext: dict[str | None, list[tuple[str, str]]] = {}
     for group_sources, group_name in groups:
-        group_modules = [
-            (source.module, modules[source.module])
+        group_modules = {
+            source.module: modules[source.module]
             for source in group_sources
             if source.module in modules
-        ]
+        }
         if not group_modules:
             ctext[group_name] = []
             continue
@@ -465,7 +465,7 @@ def group_dir(group_name: str) -> str:
 class GroupGenerator:
     def __init__(
         self,
-        modules: list[tuple[str, ModuleIR]],
+        modules: dict[str, ModuleIR],
         source_paths: dict[str, str],
         group_name: str | None,
         group_map: dict[str, str | None],
@@ -512,7 +512,7 @@ def generate_c_for_modules(self) -> list[tuple[str, str]]:
         multi_file = self.use_shared_lib and self.multi_file
 
         # Collect all literal refs in IR.
-        for _, module in self.modules:
+        for module in self.modules.values():
             for fn in module.functions:
                 collect_literals(fn, self.context.literals)
 
@@ -528,7 +528,7 @@ def generate_c_for_modules(self) -> list[tuple[str, str]]:
 
         self.generate_literal_tables()
 
-        for module_name, module in self.modules:
+        for module_name, module in self.modules.items():
             if multi_file:
                 emitter = Emitter(self.context)
                 emitter.emit_line(f'#include "__native{self.short_group_suffix}.h"')
@@ -582,7 +582,7 @@ def generate_c_for_modules(self) -> list[tuple[str, str]]:
         declarations.emit_line("int CPyGlobalsInit(void);")
         declarations.emit_line()
 
-        for module_name, module in self.modules:
+        for module_name, module in self.modules.items():
             self.declare_finals(module_name, module.final_names, declarations)
             for cl in module.classes:
                 generate_class_type_decl(cl, emitter, ext_declarations, declarations)
@@ -790,7 +790,7 @@ def generate_shared_lib_init(self, emitter: Emitter) -> None:
             "",
         )
 
-        for mod, _ in self.modules:
+        for mod in self.modules:
             name = exported_name(mod)
             emitter.emit_lines(
                 f"extern PyObject *CPyInit_{name}(void);",
@@ -1023,12 +1023,13 @@ def module_internal_static_name(self, module_name: str, emitter: Emitter) -> str
         return emitter.static_name(module_name + "_internal", None, prefix=MODULE_PREFIX)
 
     def declare_module(self, module_name: str, emitter: Emitter) -> None:
-        # We declare two globals for each module:
+        # We declare two globals for each compiled module:
         # one used internally in the implementation of module init to cache results
         # and prevent infinite recursion in import cycles, and one used
         # by other modules to refer to it.
-        internal_static_name = self.module_internal_static_name(module_name, emitter)
-        self.declare_global("CPyModule *", internal_static_name, initializer="NULL")
+        if module_name in self.modules:
+            internal_static_name = self.module_internal_static_name(module_name, emitter)
+            self.declare_global("CPyModule *", internal_static_name, initializer="NULL")
         static_name = emitter.static_name(module_name, None, prefix=MODULE_PREFIX)
         self.declare_global("CPyModule *", static_name)
         self.simple_inits.append((static_name, "Py_None"))
@@ -1126,37 +1127,6 @@ def collect_literals(fn: FuncIR, literals: Literals) -> None:
                 literals.record_literal(op.value)
 
 
-def c_array_initializer(components: list[str]) -> str:
-    """Construct an initializer for a C array variable.
-
-    Components are C expressions valid in an initializer.
-
-    For example, if components are ["1", "2"], the result
-    would be "{1, 2}", which can be used like this:
-
-        int a[] = {1, 2};
-
-    If the result is long, split it into multiple lines.
-    """
-    res = []
-    current: list[str] = []
-    cur_len = 0
-    for c in components:
-        if not current or cur_len + 2 + len(c) < 70:
-            current.append(c)
-            cur_len += len(c) + 2
-        else:
-            res.append(", ".join(current))
-            current = [c]
-            cur_len = len(c)
-    if not res:
-        # Result fits on a single line
-        return "{%s}" % ", ".join(current)
-    # Multi-line result
-    res.append(", ".join(current))
-    return "{\n    " + ",\n    ".join(res) + "\n}"
-
-
 def c_string_array_initializer(components: list[bytes]) -> str:
     result = []
     result.append("{\n")
diff --git a/mypyc/ir/ops.py b/mypyc/ir/ops.py
index adf24de235ff..412d36dd52d7 100644
--- a/mypyc/ir/ops.py
+++ b/mypyc/ir/ops.py
@@ -1348,13 +1348,14 @@ class LoadAddress(RegisterOp):
     Attributes:
       type: Type of the loaded address(e.g. ptr/object_ptr)
       src: Source value (str for globals like 'PyList_Type',
-           Register for temporary values or locals)
+           Register for temporary values or locals, LoadStatic
+           for statics.)
     """
 
     error_kind = ERR_NEVER
     is_borrowed = True
 
-    def __init__(self, type: RType, src: str | Register, line: int = -1) -> None:
+    def __init__(self, type: RType, src: str | Register | LoadStatic, line: int = -1) -> None:
         super().__init__(line)
         self.type = type
         self.src = src
diff --git a/mypyc/ir/pprint.py b/mypyc/ir/pprint.py
index 82e82913c9a6..4d10a91835ca 100644
--- a/mypyc/ir/pprint.py
+++ b/mypyc/ir/pprint.py
@@ -266,6 +266,11 @@ def visit_get_element_ptr(self, op: GetElementPtr) -> str:
     def visit_load_address(self, op: LoadAddress) -> str:
         if isinstance(op.src, Register):
             return self.format("%r = load_address %r", op, op.src)
+        elif isinstance(op.src, LoadStatic):
+            name = op.src.identifier
+            if op.src.module_name is not None:
+                name = f"{op.src.module_name}.{name}"
+            return self.format("%r = load_address %s :: %s", op, name, op.src.namespace)
         else:
             return self.format("%r = load_address %s", op, op.src)
 
diff --git a/mypyc/irbuild/builder.py b/mypyc/irbuild/builder.py
index 2370e9bd1435..16bcfbcca4e5 100644
--- a/mypyc/irbuild/builder.py
+++ b/mypyc/irbuild/builder.py
@@ -89,7 +89,6 @@
     RType,
     RUnion,
     bitmap_rprimitive,
-    c_int_rprimitive,
     c_pyssize_t_rprimitive,
     dict_rprimitive,
     int_rprimitive,
@@ -126,12 +125,7 @@
 from mypyc.primitives.dict_ops import dict_get_item_op, dict_set_item_op
 from mypyc.primitives.generic_ops import iter_op, next_op, py_setattr_op
 from mypyc.primitives.list_ops import list_get_item_unsafe_op, list_pop_last, to_list
-from mypyc.primitives.misc_ops import (
-    check_unpack_count_op,
-    get_module_dict_op,
-    import_extra_args_op,
-    import_op,
-)
+from mypyc.primitives.misc_ops import check_unpack_count_op, get_module_dict_op, import_op
 from mypyc.primitives.registry import CFunctionDescription, function_ops
 
 # These int binary operations can borrow their operands safely, since the
@@ -193,6 +187,8 @@ def __init__(
         self.encapsulating_funcs = pbv.encapsulating_funcs
         self.nested_fitems = pbv.nested_funcs.keys()
         self.fdefs_to_decorators = pbv.funcs_to_decorators
+        self.module_import_groups = pbv.module_import_groups
+
         self.singledispatch_impls = singledispatch_impls
 
         self.visitor = visitor
@@ -394,22 +390,6 @@ def add_to_non_ext_dict(
         key_unicode = self.load_str(key)
         self.call_c(dict_set_item_op, [non_ext.dict, key_unicode, val], line)
 
-    def gen_import_from(
-        self, id: str, globals_dict: Value, imported: list[str], line: int
-    ) -> Value:
-        self.imports[id] = None
-
-        null_dict = Integer(0, dict_rprimitive, line)
-        names_to_import = self.new_list_op([self.load_str(name) for name in imported], line)
-        zero_int = Integer(0, c_int_rprimitive, line)
-        value = self.call_c(
-            import_extra_args_op,
-            [self.load_str(id), globals_dict, null_dict, names_to_import, zero_int],
-            line,
-        )
-        self.add(InitStatic(value, id, namespace=NAMESPACE_MODULE))
-        return value
-
     def gen_import(self, id: str, line: int) -> None:
         self.imports[id] = None
 
diff --git a/mypyc/irbuild/ll_builder.py b/mypyc/irbuild/ll_builder.py
index d41b532f9228..ea4bf63e3463 100644
--- a/mypyc/irbuild/ll_builder.py
+++ b/mypyc/irbuild/ll_builder.py
@@ -853,10 +853,8 @@ def _py_vector_call(
         ):
             if arg_values:
                 # Create a C array containing all arguments as boxed values.
-                array = Register(RArray(object_rprimitive, len(arg_values)))
                 coerced_args = [self.coerce(arg, object_rprimitive, line) for arg in arg_values]
-                self.add(AssignMulti(array, coerced_args))
-                arg_ptr = self.add(LoadAddress(object_pointer_rprimitive, array))
+                arg_ptr = self.setup_rarray(object_rprimitive, coerced_args, object_ptr=True)
             else:
                 arg_ptr = Integer(0, object_pointer_rprimitive)
             num_pos = num_positional_args(arg_values, arg_kinds)
@@ -930,13 +928,10 @@ def _py_vector_method_call(
             not kind.is_star() and not kind.is_optional() for kind in arg_kinds
         ):
             method_name_reg = self.load_str(method_name)
-            array = Register(RArray(object_rprimitive, len(arg_values) + 1))
-            self_arg = self.coerce(obj, object_rprimitive, line)
-            coerced_args = [self_arg] + [
-                self.coerce(arg, object_rprimitive, line) for arg in arg_values
+            coerced_args = [
+                self.coerce(arg, object_rprimitive, line) for arg in [obj] + arg_values
             ]
-            self.add(AssignMulti(array, coerced_args))
-            arg_ptr = self.add(LoadAddress(object_pointer_rprimitive, array))
+            arg_ptr = self.setup_rarray(object_rprimitive, coerced_args, object_ptr=True)
             num_pos = num_positional_args(arg_values, arg_kinds)
             keywords = self._vectorcall_keywords(arg_names)
             value = self.call_c(
@@ -1693,6 +1688,16 @@ def new_list_op(self, values: list[Value], line: int) -> Value:
     def new_set_op(self, values: list[Value], line: int) -> Value:
         return self.call_c(new_set_op, values, line)
 
+    def setup_rarray(
+        self, item_type: RType, values: Sequence[Value], *, object_ptr: bool = False
+    ) -> Value:
+        """Declare and initialize a new RArray, returning its address."""
+        array = Register(RArray(item_type, len(values)))
+        self.add(AssignMulti(array, list(values)))
+        return self.add(
+            LoadAddress(object_pointer_rprimitive if object_ptr else c_pointer_rprimitive, array)
+        )
+
     def shortcircuit_helper(
         self,
         op: str,
diff --git a/mypyc/irbuild/prebuildvisitor.py b/mypyc/irbuild/prebuildvisitor.py
index d99453955002..4d1bb8ad86a6 100644
--- a/mypyc/irbuild/prebuildvisitor.py
+++ b/mypyc/irbuild/prebuildvisitor.py
@@ -5,18 +5,20 @@
     Expression,
     FuncDef,
     FuncItem,
+    Import,
     LambdaExpr,
     MemberExpr,
     MypyFile,
     NameExpr,
+    Node,
     SymbolNode,
     Var,
 )
-from mypy.traverser import TraverserVisitor
+from mypy.traverser import ExtendedTraverserVisitor
 from mypyc.errors import Errors
 
 
-class PreBuildVisitor(TraverserVisitor):
+class PreBuildVisitor(ExtendedTraverserVisitor):
     """Mypy file AST visitor run before building the IR.
 
     This collects various things, including:
@@ -26,6 +28,7 @@ class PreBuildVisitor(TraverserVisitor):
     * Find non-local variables (free variables)
     * Find property setters
     * Find decorators of functions
+    * Find module import groups
 
     The main IR build pass uses this information.
     """
@@ -68,10 +71,29 @@ def __init__(
         # Map function to indices of decorators to remove
         self.decorators_to_remove: dict[FuncDef, list[int]] = decorators_to_remove
 
+        # A mapping of import groups (a series of Import nodes with
+        # nothing inbetween) where each group is keyed by its first
+        # import node.
+        self.module_import_groups: dict[Import, list[Import]] = {}
+        self._current_import_group: Import | None = None
+
         self.errors: Errors = errors
 
         self.current_file: MypyFile = current_file
 
+    def visit(self, o: Node) -> bool:
+        if isinstance(o, Import):
+            if self._current_import_group is not None:
+                self.module_import_groups[self._current_import_group].append(o)
+            else:
+                self.module_import_groups[o] = [o]
+                self._current_import_group = o
+            # Don't recurse into the import's assignments.
+            return False
+
+        self._current_import_group = None
+        return True
+
     def visit_decorator(self, dec: Decorator) -> None:
         if dec.decorators:
             # Only add the function being decorated if there exist
diff --git a/mypyc/irbuild/statement.py b/mypyc/irbuild/statement.py
index b9754ba1a147..84a45a05da28 100644
--- a/mypyc/irbuild/statement.py
+++ b/mypyc/irbuild/statement.py
@@ -43,13 +43,17 @@
     YieldFromExpr,
 )
 from mypyc.ir.ops import (
+    NAMESPACE_MODULE,
     NO_TRACEBACK_LINE_NO,
     Assign,
     BasicBlock,
     Branch,
+    InitStatic,
     Integer,
     LoadAddress,
     LoadErrorValue,
+    LoadLiteral,
+    LoadStatic,
     MethodCall,
     RaiseStandardError,
     Register,
@@ -60,6 +64,7 @@
 )
 from mypyc.ir.rtypes import (
     RInstance,
+    c_pyssize_t_rprimitive,
     exc_rtuple,
     is_tagged,
     none_rprimitive,
@@ -96,7 +101,8 @@
 from mypyc.primitives.misc_ops import (
     check_stop_op,
     coro_op,
-    import_from_op,
+    import_from_many_op,
+    import_many_op,
     send_op,
     type_op,
     yield_from_except_op,
@@ -217,32 +223,69 @@ def transform_operator_assignment_stmt(builder: IRBuilder, stmt: OperatorAssignm
 def transform_import(builder: IRBuilder, node: Import) -> None:
     if node.is_mypy_only:
         return
-    globals = builder.load_globals_dict()
-    for node_id, as_name in node.ids:
-        builder.gen_import(node_id, node.line)
-
-        # Update the globals dict with the appropriate module:
-        # * For 'import foo.bar as baz' we add 'foo.bar' with the name 'baz'
-        # * For 'import foo.bar' we add 'foo' with the name 'foo'
-        # Typically we then ignore these entries and access things directly
-        # via the module static, but we will use the globals version for modules
-        # that mypy couldn't find, since it doesn't analyze module references
-        # from those properly.
-
-        # TODO: Don't add local imports to the global namespace
-
-        # Miscompiling imports inside of functions, like below in import from.
-        if as_name:
-            name = as_name
-            base = node_id
-        else:
-            base = name = node_id.split(".")[0]
 
-        obj = builder.get_module(base, node.line)
+    # Imports (not from imports!) are processed in an odd way so they can be
+    # table-driven and compact. Here's how it works:
+    #
+    # Import nodes are divided in groups (in the prebuild visitor). Each group
+    # consists of consecutive Import nodes:
+    #
+    #   import mod         <| group #1
+    #   import mod2         |
+    #
+    #   def foo() -> None:
+    #       import mod3    <- group #2
+    #
+    #   import mod4        <- group #3
+    #
+    # Every time we encounter the first import of a group, build IR to call a
+    # helper function that will perform all of the group's imports in one go.
+    if node not in builder.module_import_groups:
+        return
 
-        builder.gen_method_call(
-            globals, "__setitem__", [builder.load_str(name), obj], result_type=None, line=node.line
-        )
+    modules = []
+    statics = []
+    # To show the right line number on failure, we have to add the traceback
+    # entry within the helper function (which is admittedly ugly). To drive
+    # this, we'll need the line number corresponding to each import.
+    import_lines = []
+    for import_node in builder.module_import_groups[node]:
+        for mod_id, as_name in import_node.ids:
+            builder.imports[mod_id] = None
+            import_lines.append(Integer(import_node.line, c_pyssize_t_rprimitive))
+
+            module_static = LoadStatic(object_rprimitive, mod_id, namespace=NAMESPACE_MODULE)
+            static_ptr = builder.add(LoadAddress(object_pointer_rprimitive, module_static))
+            statics.append(static_ptr)
+            # TODO: Don't add local imports to the global namespace
+            # Update the globals dict with the appropriate module:
+            # * For 'import foo.bar as baz' we add 'foo.bar' with the name 'baz'
+            # * For 'import foo.bar' we add 'foo' with the name 'foo'
+            # Typically we then ignore these entries and access things directly
+            # via the module static, but we will use the globals version for
+            # modules that mypy couldn't find, since it doesn't analyze module
+            # references from those properly.
+            if as_name or "." not in mod_id:
+                globals_base = None
+            else:
+                globals_base = mod_id.split(".")[0]
+            modules.append((mod_id, as_name, globals_base))
+
+    static_array_ptr = builder.builder.setup_rarray(object_pointer_rprimitive, statics)
+    import_line_ptr = builder.builder.setup_rarray(c_pyssize_t_rprimitive, import_lines)
+    function = "<module>" if builder.fn_info.name == "<top level>" else builder.fn_info.name
+    builder.call_c(
+        import_many_op,
+        [
+            builder.add(LoadLiteral(tuple(modules), object_rprimitive)),
+            static_array_ptr,
+            builder.load_globals_dict(),
+            builder.load_str(builder.module_path),
+            builder.load_str(function),
+            import_line_ptr,
+        ],
+        NO_TRACEBACK_LINE_NO,
+    )
 
 
 def transform_import_from(builder: IRBuilder, node: ImportFrom) -> None:
@@ -258,29 +301,25 @@ def transform_import_from(builder: IRBuilder, node: ImportFrom) -> None:
         module_package = ""
 
     id = importlib.util.resolve_name("." * node.relative + node.id, module_package)
-
-    globals = builder.load_globals_dict()
-    imported_names = [name for name, _ in node.names]
-    module = builder.gen_import_from(id, globals, imported_names, node.line)
-
-    # Copy everything into our module's dict.
+    builder.imports[id] = None
+
+    names = [name for name, _ in node.names]
+    as_names = [as_name for _, as_name in node.names]
+    names_literal = builder.add(LoadLiteral(tuple(names), object_rprimitive))
+    if all(n is None for n in as_names):
+        # Reuse names tuple to reduce verbosity.
+        as_names_literal = names_literal
+    else:
+        as_names_literal = builder.add(LoadLiteral(tuple(as_names), object_rprimitive))
     # Note that we miscompile import from inside of functions here,
-    # since that case *shouldn't* load it into the globals dict.
+    # since that case *shouldn't* load everything into the globals dict.
     # This probably doesn't matter much and the code runs basically right.
-    for name, maybe_as_name in node.names:
-        as_name = maybe_as_name or name
-        obj = builder.call_c(
-            import_from_op,
-            [module, builder.load_str(id), builder.load_str(name), builder.load_str(as_name)],
-            node.line,
-        )
-        builder.gen_method_call(
-            globals,
-            "__setitem__",
-            [builder.load_str(as_name), obj],
-            result_type=None,
-            line=node.line,
-        )
+    module = builder.call_c(
+        import_from_many_op,
+        [builder.load_str(id), names_literal, as_names_literal, builder.load_globals_dict()],
+        node.line,
+    )
+    builder.add(InitStatic(module, id, namespace=NAMESPACE_MODULE))
 
 
 def transform_import_all(builder: IRBuilder, node: ImportAll) -> None:
diff --git a/mypyc/lib-rt/CPy.h b/mypyc/lib-rt/CPy.h
index e01acec4d2f0..7a3e16fe9d65 100644
--- a/mypyc/lib-rt/CPy.h
+++ b/mypyc/lib-rt/CPy.h
@@ -622,8 +622,10 @@ PyObject *CPy_Super(PyObject *builtins, PyObject *self);
 PyObject *CPy_CallReverseOpMethod(PyObject *left, PyObject *right, const char *op,
                                   _Py_Identifier *method);
 
-PyObject *CPyImport_ImportFrom(PyObject *module, PyObject *package_name,
-                               PyObject *import_name, PyObject *as_name);
+bool CPyImport_ImportMany(PyObject *modules, CPyModule **statics[], PyObject *globals,
+                          PyObject *tb_path, PyObject *tb_function, Py_ssize_t *tb_lines);
+PyObject *CPyImport_ImportFromMany(PyObject *mod_id, PyObject *names, PyObject *as_names,
+                                   PyObject *globals);
 
 PyObject *CPySingledispatch_RegisterFunction(PyObject *singledispatch_func, PyObject *cls,
                                              PyObject *func);
diff --git a/mypyc/lib-rt/misc_ops.c b/mypyc/lib-rt/misc_ops.c
index 5fda78704bbc..96d1c32185cc 100644
--- a/mypyc/lib-rt/misc_ops.c
+++ b/mypyc/lib-rt/misc_ops.c
@@ -669,9 +669,71 @@ CPy_Super(PyObject *builtins, PyObject *self) {
     return result;
 }
 
+static bool import_single(PyObject *mod_id,
+                                  PyObject *as_name,
+                                  PyObject **mod_static,
+                                  PyObject *globals_base,
+                                  PyObject *globals) {
+    if (*mod_static == Py_None) {
+        CPyModule *mod = PyImport_Import(mod_id);
+        if (mod == NULL) {
+            return false;
+        }
+        *mod_static = mod;
+    }
+
+    if (as_name == Py_None) {
+        as_name = mod_id;
+    }
+    PyObject *globals_id, *globals_name;
+    if (globals_base == Py_None) {
+        globals_id = mod_id;
+        globals_name = as_name;
+    } else {
+        globals_id = globals_name = globals_base;
+    }
+    PyObject *mod_dict = PyImport_GetModuleDict();
+    CPyModule *globals_mod = CPyDict_GetItem(mod_dict, globals_id);
+    if (globals_mod == NULL) {
+        return false;
+    }
+    int ret = CPyDict_SetItem(globals, globals_name, globals_mod);
+    Py_DECREF(globals_mod);
+    if (ret < 0) {
+        return false;
+    }
+
+    return true;
+}
+
+// Table-driven import helper. See transform_import() in irbuild for the details.
+bool CPyImport_ImportMany(PyObject *modules, CPyModule **statics[], PyObject *globals,
+                          PyObject *tb_path, PyObject *tb_function, Py_ssize_t *tb_lines) {
+    for (Py_ssize_t i = 0; i < PyTuple_GET_SIZE(modules); i++) {
+        PyObject *module = PyTuple_GET_ITEM(modules, i);
+        PyObject *mod_id = PyTuple_GET_ITEM(module, 0);
+        PyObject *as_name = PyTuple_GET_ITEM(module, 1);
+        PyObject *globals_base = PyTuple_GET_ITEM(module, 2);
+
+        if (!import_single(mod_id, as_name, statics[i], globals_base, globals)) {
+            const char *path = PyUnicode_AsUTF8(tb_path);
+            if (path == NULL) {
+                path = "<unable to display>";
+            }
+            const char *function = PyUnicode_AsUTF8(tb_function);
+            if (function == NULL) {
+                function = "<unable to display>";
+            }
+            CPy_AddTraceback(path, function, tb_lines[i], globals);
+            return false;
+        }
+    }
+    return true;
+}
+
 // This helper function is a simplification of cpython/ceval.c/import_from()
-PyObject *CPyImport_ImportFrom(PyObject *module, PyObject *package_name,
-                               PyObject *import_name, PyObject *as_name) {
+static PyObject *CPyImport_ImportFrom(PyObject *module, PyObject *package_name,
+                                      PyObject *import_name, PyObject *as_name) {
     // check if the imported module has an attribute by that name
     PyObject *x = PyObject_GetAttr(module, import_name);
     if (x == NULL) {
@@ -702,6 +764,34 @@ PyObject *CPyImport_ImportFrom(PyObject *module, PyObject *package_name,
     return NULL;
 }
 
+PyObject *CPyImport_ImportFromMany(PyObject *mod_id, PyObject *names, PyObject *as_names,
+                                   PyObject *globals) {
+    PyObject *mod = PyImport_ImportModuleLevelObject(mod_id, globals, 0, names, 0);
+    if (mod == NULL) {
+        return NULL;
+    }
+
+    for (Py_ssize_t i = 0; i < PyTuple_GET_SIZE(names); i++) {
+        PyObject *name = PyTuple_GET_ITEM(names, i);
+        PyObject *as_name = PyTuple_GET_ITEM(as_names, i);
+        if (as_name == Py_None) {
+            as_name = name;
+        }
+        PyObject *obj = CPyImport_ImportFrom(mod, mod_id, name, as_name);
+        if (obj == NULL) {
+            Py_DECREF(mod);
+            return NULL;
+        }
+        int ret = CPyDict_SetItem(globals, as_name, obj);
+        Py_DECREF(obj);
+        if (ret < 0) {
+            Py_DECREF(mod);
+            return NULL;
+        }
+    }
+    return mod;
+}
+
 // From CPython
 static PyObject *
 CPy_BinopTypeError(PyObject *left, PyObject *right, const char *op) {
diff --git a/mypyc/primitives/misc_ops.py b/mypyc/primitives/misc_ops.py
index 07df9c69714b..0e04c0471b8d 100644
--- a/mypyc/primitives/misc_ops.py
+++ b/mypyc/primitives/misc_ops.py
@@ -7,10 +7,10 @@
     bit_rprimitive,
     bool_rprimitive,
     c_int_rprimitive,
+    c_pointer_rprimitive,
     c_pyssize_t_rprimitive,
     dict_rprimitive,
     int_rprimitive,
-    list_rprimitive,
     object_pointer_rprimitive,
     object_rprimitive,
     str_rprimitive,
@@ -112,7 +112,7 @@
     is_borrowed=True,
 )
 
-# Import a module
+# Import a module (plain)
 import_op = custom_op(
     arg_types=[str_rprimitive],
     return_type=object_rprimitive,
@@ -120,25 +120,26 @@
     error_kind=ERR_MAGIC,
 )
 
-# Import with extra arguments (used in from import handling)
-import_extra_args_op = custom_op(
+# Import helper op (handles globals/statics & can import multiple modules)
+import_many_op = custom_op(
     arg_types=[
-        str_rprimitive,
-        dict_rprimitive,
-        dict_rprimitive,
-        list_rprimitive,
-        c_int_rprimitive,
+        object_rprimitive,
+        c_pointer_rprimitive,
+        object_rprimitive,
+        object_rprimitive,
+        object_rprimitive,
+        c_pointer_rprimitive,
     ],
-    return_type=object_rprimitive,
-    c_function_name="PyImport_ImportModuleLevelObject",
-    error_kind=ERR_MAGIC,
+    return_type=bit_rprimitive,
+    c_function_name="CPyImport_ImportMany",
+    error_kind=ERR_FALSE,
 )
 
-# Import-from helper op
-import_from_op = custom_op(
-    arg_types=[object_rprimitive, str_rprimitive, str_rprimitive, str_rprimitive],
+# From import helper op
+import_from_many_op = custom_op(
+    arg_types=[object_rprimitive, object_rprimitive, object_rprimitive, object_rprimitive],
     return_type=object_rprimitive,
-    c_function_name="CPyImport_ImportFrom",
+    c_function_name="CPyImport_ImportFromMany",
     error_kind=ERR_MAGIC,
 )
 
diff --git a/mypyc/test-data/irbuild-basic.test b/mypyc/test-data/irbuild-basic.test
index e6426cdeea53..46002ac1f1ce 100644
--- a/mypyc/test-data/irbuild-basic.test
+++ b/mypyc/test-data/irbuild-basic.test
@@ -682,14 +682,106 @@ L0:
     r5 = unbox(int, r4)
     return r5
 
-[case testFromImport]
-from testmodule import g
+[case testImport_toplevel]
+import sys
+import enum as enum2
+import collections.abc
+import collections.abc as abc2
+_ = "filler"
+import single
+single.hello()
+
+[file single.py]
+def hello() -> None:
+    print("hello, world")
+
+[out]
+def __top_level__():
+    r0, r1 :: object
+    r2 :: bit
+    r3 :: str
+    r4 :: object
+    r5, r6, r7, r8 :: object_ptr
+    r9 :: object_ptr[4]
+    r10 :: c_ptr
+    r11 :: native_int[4]
+    r12 :: c_ptr
+    r13 :: object
+    r14 :: dict
+    r15, r16 :: str
+    r17 :: bit
+    r18 :: str
+    r19 :: dict
+    r20 :: str
+    r21 :: int32
+    r22 :: bit
+    r23 :: object_ptr
+    r24 :: object_ptr[1]
+    r25 :: c_ptr
+    r26 :: native_int[1]
+    r27 :: c_ptr
+    r28 :: object
+    r29 :: dict
+    r30, r31 :: str
+    r32 :: bit
+    r33 :: object
+    r34 :: str
+    r35, r36 :: object
+L0:
+    r0 = builtins :: module
+    r1 = load_address _Py_NoneStruct
+    r2 = r0 != r1
+    if r2 goto L2 else goto L1 :: bool
+L1:
+    r3 = 'builtins'
+    r4 = PyImport_Import(r3)
+    builtins = r4 :: module
+L2:
+    r5 = load_address sys :: module
+    r6 = load_address enum :: module
+    r7 = load_address collections.abc :: module
+    r8 = load_address collections.abc :: module
+    r9 = [r5, r6, r7, r8]
+    r10 = load_address r9
+    r11 = [1, 2, 3, 4]
+    r12 = load_address r11
+    r13 = (('sys', None, None), ('enum', 'enum2', None), ('collections.abc', None, 'collections'), ('collections.abc', 'abc2', None))
+    r14 = __main__.globals :: static
+    r15 = 'main'
+    r16 = '<module>'
+    r17 = CPyImport_ImportMany(r13, r10, r14, r15, r16, r12)
+    r18 = 'filler'
+    r19 = __main__.globals :: static
+    r20 = '_'
+    r21 = CPyDict_SetItem(r19, r20, r18)
+    r22 = r21 >= 0 :: signed
+    r23 = load_address single :: module
+    r24 = [r23]
+    r25 = load_address r24
+    r26 = [6]
+    r27 = load_address r26
+    r28 = (('single', None, None),)
+    r29 = __main__.globals :: static
+    r30 = 'main'
+    r31 = '<module>'
+    r32 = CPyImport_ImportMany(r28, r25, r29, r30, r31, r27)
+    r33 = single :: module
+    r34 = 'hello'
+    r35 = CPyObject_GetAttr(r33, r34)
+    r36 = PyObject_CallFunctionObjArgs(r35, 0)
+    return 1
+
+[case testFromImport_toplevel]
+from testmodule import g, h
+from testmodule import h as two
 
 def f(x: int) -> int:
-    return g(x)
+    return g(x) + h() + two()
 [file testmodule.py]
 def g(x: int) -> int:
     return x + 1
+def h() -> int:
+    return 2
 [out]
 def f(x):
     x :: int
@@ -697,6 +789,14 @@ def f(x):
     r1 :: str
     r2, r3, r4 :: object
     r5 :: int
+    r6 :: dict
+    r7 :: str
+    r8, r9 :: object
+    r10, r11 :: int
+    r12 :: dict
+    r13 :: str
+    r14, r15 :: object
+    r16, r17 :: int
 L0:
     r0 = __main__.globals :: static
     r1 = 'g'
@@ -704,7 +804,52 @@ L0:
     r3 = box(int, x)
     r4 = PyObject_CallFunctionObjArgs(r2, r3, 0)
     r5 = unbox(int, r4)
-    return r5
+    r6 = __main__.globals :: static
+    r7 = 'h'
+    r8 = CPyDict_GetItem(r6, r7)
+    r9 = PyObject_CallFunctionObjArgs(r8, 0)
+    r10 = unbox(int, r9)
+    r11 = CPyTagged_Add(r5, r10)
+    r12 = __main__.globals :: static
+    r13 = 'two'
+    r14 = CPyDict_GetItem(r12, r13)
+    r15 = PyObject_CallFunctionObjArgs(r14, 0)
+    r16 = unbox(int, r15)
+    r17 = CPyTagged_Add(r11, r16)
+    return r17
+def __top_level__():
+    r0, r1 :: object
+    r2 :: bit
+    r3 :: str
+    r4, r5 :: object
+    r6 :: str
+    r7 :: dict
+    r8, r9, r10 :: object
+    r11 :: str
+    r12 :: dict
+    r13 :: object
+L0:
+    r0 = builtins :: module
+    r1 = load_address _Py_NoneStruct
+    r2 = r0 != r1
+    if r2 goto L2 else goto L1 :: bool
+L1:
+    r3 = 'builtins'
+    r4 = PyImport_Import(r3)
+    builtins = r4 :: module
+L2:
+    r5 = ('g', 'h')
+    r6 = 'testmodule'
+    r7 = __main__.globals :: static
+    r8 = CPyImport_ImportFromMany(r6, r5, r5, r7)
+    testmodule = r8 :: module
+    r9 = ('h',)
+    r10 = ('two',)
+    r11 = 'testmodule'
+    r12 = __main__.globals :: static
+    r13 = CPyImport_ImportFromMany(r11, r9, r10, r12)
+    testmodule = r13 :: module
+    return 1
 
 [case testPrintFullname]
 import builtins
@@ -2263,79 +2408,61 @@ def __top_level__():
     r0, r1 :: object
     r2 :: bit
     r3 :: str
-    r4 :: object
-    r5 :: dict
-    r6, r7, r8 :: str
-    r9 :: list
-    r10, r11, r12, r13 :: ptr
+    r4, r5 :: object
+    r6 :: str
+    r7 :: dict
+    r8 :: object
+    r9, r10 :: str
+    r11 :: object
+    r12 :: tuple[str, object]
+    r13 :: object
     r14 :: str
     r15 :: object
-    r16, r17, r18 :: str
+    r16 :: tuple[str, object]
+    r17 :: object
+    r18 :: tuple[object, object]
     r19 :: object
-    r20 :: str
-    r21 :: int32
-    r22 :: bit
-    r23, r24, r25 :: str
-    r26 :: object
-    r27 :: str
-    r28 :: int32
-    r29 :: bit
-    r30, r31, r32 :: str
-    r33 :: object
-    r34 :: str
-    r35 :: int32
-    r36 :: bit
-    r37, r38 :: str
-    r39 :: object
-    r40 :: tuple[str, object]
-    r41 :: object
-    r42 :: str
-    r43 :: object
-    r44 :: tuple[str, object]
-    r45 :: object
-    r46 :: tuple[object, object]
-    r47 :: object
-    r48 :: dict
-    r49 :: str
-    r50, r51 :: object
+    r20 :: dict
+    r21 :: str
+    r22, r23 :: object
+    r24 :: dict
+    r25 :: str
+    r26 :: int32
+    r27 :: bit
+    r28 :: str
+    r29 :: dict
+    r30 :: str
+    r31, r32, r33 :: object
+    r34 :: tuple
+    r35 :: dict
+    r36 :: str
+    r37 :: int32
+    r38 :: bit
+    r39 :: dict
+    r40 :: str
+    r41, r42, r43 :: object
+    r44 :: dict
+    r45 :: str
+    r46 :: int32
+    r47 :: bit
+    r48 :: str
+    r49 :: dict
+    r50 :: str
+    r51 :: object
     r52 :: dict
     r53 :: str
-    r54 :: int32
-    r55 :: bit
-    r56 :: str
-    r57 :: dict
-    r58 :: str
-    r59, r60, r61 :: object
-    r62 :: tuple
-    r63 :: dict
-    r64 :: str
-    r65 :: int32
-    r66 :: bit
-    r67 :: dict
-    r68 :: str
-    r69, r70, r71 :: object
-    r72 :: dict
-    r73 :: str
-    r74 :: int32
-    r75 :: bit
-    r76 :: str
-    r77 :: dict
-    r78 :: str
-    r79 :: object
-    r80 :: dict
-    r81 :: str
-    r82, r83 :: object
-    r84 :: dict
-    r85 :: str
-    r86 :: int32
-    r87 :: bit
-    r88 :: list
-    r89, r90, r91 :: object
-    r92, r93, r94, r95 :: ptr
-    r96 :: dict
-    r97 :: str
-    r98 :: int32
-    r99 :: bit
+    r54, r55 :: object
+    r56 :: dict
+    r57 :: str
+    r58 :: int32
+    r59 :: bit
+    r60 :: list
+    r61, r62, r63 :: object
+    r64, r65, r66, r67 :: ptr
+    r68 :: dict
+    r69 :: str
+    r70 :: int32
+    r71 :: bit
 L0:
     r0 = builtins :: module
     r1 = load_address _Py_NoneStruct
@@ -2346,110 +2473,78 @@ L1:
     r4 = PyImport_Import(r3)
     builtins = r4 :: module
 L2:
-    r5 = __main__.globals :: static
-    r6 = 'List'
-    r7 = 'NewType'
-    r8 = 'NamedTuple'
-    r9 = PyList_New(3)
-    r10 = get_element_ptr r9 ob_item :: PyListObject
-    r11 = load_mem r10 :: ptr*
-    set_mem r11, r6 :: builtins.object*
-    r12 = r11 + WORD_SIZE*1
-    set_mem r12, r7 :: builtins.object*
-    r13 = r11 + WORD_SIZE*2
-    set_mem r13, r8 :: builtins.object*
-    keep_alive r9
-    r14 = 'typing'
-    r15 = PyImport_ImportModuleLevelObject(r14, r5, 0, r9, 0)
-    typing = r15 :: module
-    r16 = 'typing'
-    r17 = 'List'
-    r18 = 'List'
-    r19 = CPyImport_ImportFrom(r15, r16, r17, r18)
-    r20 = 'List'
-    r21 = CPyDict_SetItem(r5, r20, r19)
-    r22 = r21 >= 0 :: signed
-    r23 = 'typing'
-    r24 = 'NewType'
-    r25 = 'NewType'
-    r26 = CPyImport_ImportFrom(r15, r23, r24, r25)
-    r27 = 'NewType'
-    r28 = CPyDict_SetItem(r5, r27, r26)
-    r29 = r28 >= 0 :: signed
-    r30 = 'typing'
-    r31 = 'NamedTuple'
-    r32 = 'NamedTuple'
-    r33 = CPyImport_ImportFrom(r15, r30, r31, r32)
-    r34 = 'NamedTuple'
-    r35 = CPyDict_SetItem(r5, r34, r33)
-    r36 = r35 >= 0 :: signed
-    r37 = 'Lol'
-    r38 = 'a'
-    r39 = load_address PyLong_Type
-    r40 = (r38, r39)
-    r41 = box(tuple[str, object], r40)
-    r42 = 'b'
-    r43 = load_address PyUnicode_Type
-    r44 = (r42, r43)
-    r45 = box(tuple[str, object], r44)
-    r46 = (r41, r45)
-    r47 = box(tuple[object, object], r46)
-    r48 = __main__.globals :: static
-    r49 = 'NamedTuple'
-    r50 = CPyDict_GetItem(r48, r49)
-    r51 = PyObject_CallFunctionObjArgs(r50, r37, r47, 0)
+    r5 = ('List', 'NewType', 'NamedTuple')
+    r6 = 'typing'
+    r7 = __main__.globals :: static
+    r8 = CPyImport_ImportFromMany(r6, r5, r5, r7)
+    typing = r8 :: module
+    r9 = 'Lol'
+    r10 = 'a'
+    r11 = load_address PyLong_Type
+    r12 = (r10, r11)
+    r13 = box(tuple[str, object], r12)
+    r14 = 'b'
+    r15 = load_address PyUnicode_Type
+    r16 = (r14, r15)
+    r17 = box(tuple[str, object], r16)
+    r18 = (r13, r17)
+    r19 = box(tuple[object, object], r18)
+    r20 = __main__.globals :: static
+    r21 = 'NamedTuple'
+    r22 = CPyDict_GetItem(r20, r21)
+    r23 = PyObject_CallFunctionObjArgs(r22, r9, r19, 0)
+    r24 = __main__.globals :: static
+    r25 = 'Lol'
+    r26 = CPyDict_SetItem(r24, r25, r23)
+    r27 = r26 >= 0 :: signed
+    r28 = ''
+    r29 = __main__.globals :: static
+    r30 = 'Lol'
+    r31 = CPyDict_GetItem(r29, r30)
+    r32 = object 1
+    r33 = PyObject_CallFunctionObjArgs(r31, r32, r28, 0)
+    r34 = cast(tuple, r33)
+    r35 = __main__.globals :: static
+    r36 = 'x'
+    r37 = CPyDict_SetItem(r35, r36, r34)
+    r38 = r37 >= 0 :: signed
+    r39 = __main__.globals :: static
+    r40 = 'List'
+    r41 = CPyDict_GetItem(r39, r40)
+    r42 = load_address PyLong_Type
+    r43 = PyObject_GetItem(r41, r42)
+    r44 = __main__.globals :: static
+    r45 = 'Foo'
+    r46 = CPyDict_SetItem(r44, r45, r43)
+    r47 = r46 >= 0 :: signed
+    r48 = 'Bar'
+    r49 = __main__.globals :: static
+    r50 = 'Foo'
+    r51 = CPyDict_GetItem(r49, r50)
     r52 = __main__.globals :: static
-    r53 = 'Lol'
-    r54 = CPyDict_SetItem(r52, r53, r51)
-    r55 = r54 >= 0 :: signed
-    r56 = ''
-    r57 = __main__.globals :: static
-    r58 = 'Lol'
-    r59 = CPyDict_GetItem(r57, r58)
-    r60 = object 1
-    r61 = PyObject_CallFunctionObjArgs(r59, r60, r56, 0)
-    r62 = cast(tuple, r61)
-    r63 = __main__.globals :: static
-    r64 = 'x'
-    r65 = CPyDict_SetItem(r63, r64, r62)
-    r66 = r65 >= 0 :: signed
-    r67 = __main__.globals :: static
-    r68 = 'List'
-    r69 = CPyDict_GetItem(r67, r68)
-    r70 = load_address PyLong_Type
-    r71 = PyObject_GetItem(r69, r70)
-    r72 = __main__.globals :: static
-    r73 = 'Foo'
-    r74 = CPyDict_SetItem(r72, r73, r71)
-    r75 = r74 >= 0 :: signed
-    r76 = 'Bar'
-    r77 = __main__.globals :: static
-    r78 = 'Foo'
-    r79 = CPyDict_GetItem(r77, r78)
-    r80 = __main__.globals :: static
-    r81 = 'NewType'
-    r82 = CPyDict_GetItem(r80, r81)
-    r83 = PyObject_CallFunctionObjArgs(r82, r76, r79, 0)
-    r84 = __main__.globals :: static
-    r85 = 'Bar'
-    r86 = CPyDict_SetItem(r84, r85, r83)
-    r87 = r86 >= 0 :: signed
-    r88 = PyList_New(3)
-    r89 = object 1
-    r90 = object 2
-    r91 = object 3
-    r92 = get_element_ptr r88 ob_item :: PyListObject
-    r93 = load_mem r92 :: ptr*
-    set_mem r93, r89 :: builtins.object*
-    r94 = r93 + WORD_SIZE*1
-    set_mem r94, r90 :: builtins.object*
-    r95 = r93 + WORD_SIZE*2
-    set_mem r95, r91 :: builtins.object*
-    keep_alive r88
-    r96 = __main__.globals :: static
-    r97 = 'y'
-    r98 = CPyDict_SetItem(r96, r97, r88)
-    r99 = r98 >= 0 :: signed
+    r53 = 'NewType'
+    r54 = CPyDict_GetItem(r52, r53)
+    r55 = PyObject_CallFunctionObjArgs(r54, r48, r51, 0)
+    r56 = __main__.globals :: static
+    r57 = 'Bar'
+    r58 = CPyDict_SetItem(r56, r57, r55)
+    r59 = r58 >= 0 :: signed
+    r60 = PyList_New(3)
+    r61 = object 1
+    r62 = object 2
+    r63 = object 3
+    r64 = get_element_ptr r60 ob_item :: PyListObject
+    r65 = load_mem r64 :: ptr*
+    set_mem r65, r61 :: builtins.object*
+    r66 = r65 + 8
+    set_mem r66, r62 :: builtins.object*
+    r67 = r65 + 16
+    set_mem r67, r63 :: builtins.object*
+    keep_alive r60
+    r68 = __main__.globals :: static
+    r69 = 'y'
+    r70 = CPyDict_SetItem(r68, r69, r60)
+    r71 = r70 >= 0 :: signed
     return 1
 
 [case testChainedConditional]
@@ -2773,31 +2868,23 @@ def __top_level__():
     r0, r1 :: object
     r2 :: bit
     r3 :: str
-    r4 :: object
-    r5 :: dict
+    r4, r5 :: object
     r6 :: str
-    r7 :: list
-    r8, r9 :: ptr
+    r7 :: dict
+    r8 :: object
+    r9 :: dict
     r10 :: str
     r11 :: object
-    r12, r13, r14 :: str
-    r15 :: object
-    r16 :: str
-    r17 :: int32
-    r18 :: bit
-    r19 :: dict
-    r20 :: str
-    r21 :: object
-    r22 :: dict
-    r23 :: str
-    r24, r25 :: object
-    r26 :: dict
-    r27 :: str
-    r28, r29 :: object
-    r30 :: dict
-    r31 :: str
-    r32 :: int32
-    r33 :: bit
+    r12 :: dict
+    r13 :: str
+    r14, r15 :: object
+    r16 :: dict
+    r17 :: str
+    r18, r19 :: object
+    r20 :: dict
+    r21 :: str
+    r22 :: int32
+    r23 :: bit
 L0:
     r0 = builtins :: module
     r1 = load_address _Py_NoneStruct
@@ -2808,38 +2895,26 @@ L1:
     r4 = PyImport_Import(r3)
     builtins = r4 :: module
 L2:
-    r5 = __main__.globals :: static
-    r6 = 'Callable'
-    r7 = PyList_New(1)
-    r8 = get_element_ptr r7 ob_item :: PyListObject
-    r9 = load_mem r8 :: ptr*
-    set_mem r9, r6 :: builtins.object*
-    keep_alive r7
-    r10 = 'typing'
-    r11 = PyImport_ImportModuleLevelObject(r10, r5, 0, r7, 0)
-    typing = r11 :: module
-    r12 = 'typing'
-    r13 = 'Callable'
-    r14 = 'Callable'
-    r15 = CPyImport_ImportFrom(r11, r12, r13, r14)
-    r16 = 'Callable'
-    r17 = CPyDict_SetItem(r5, r16, r15)
-    r18 = r17 >= 0 :: signed
-    r19 = __main__.globals :: static
-    r20 = 'c'
-    r21 = CPyDict_GetItem(r19, r20)
-    r22 = __main__.globals :: static
-    r23 = 'b'
-    r24 = CPyDict_GetItem(r22, r23)
-    r25 = PyObject_CallFunctionObjArgs(r24, r21, 0)
-    r26 = __main__.globals :: static
-    r27 = 'a'
-    r28 = CPyDict_GetItem(r26, r27)
-    r29 = PyObject_CallFunctionObjArgs(r28, r25, 0)
-    r30 = __main__.globals :: static
-    r31 = 'c'
-    r32 = CPyDict_SetItem(r30, r31, r29)
-    r33 = r32 >= 0 :: signed
+    r5 = ('Callable',)
+    r6 = 'typing'
+    r7 = __main__.globals :: static
+    r8 = CPyImport_ImportFromMany(r6, r5, r5, r7)
+    typing = r8 :: module
+    r9 = __main__.globals :: static
+    r10 = 'c'
+    r11 = CPyDict_GetItem(r9, r10)
+    r12 = __main__.globals :: static
+    r13 = 'b'
+    r14 = CPyDict_GetItem(r12, r13)
+    r15 = PyObject_CallFunctionObjArgs(r14, r11, 0)
+    r16 = __main__.globals :: static
+    r17 = 'a'
+    r18 = CPyDict_GetItem(r16, r17)
+    r19 = PyObject_CallFunctionObjArgs(r18, r15, 0)
+    r20 = __main__.globals :: static
+    r21 = 'c'
+    r22 = CPyDict_SetItem(r20, r21, r19)
+    r23 = r22 >= 0 :: signed
     return 1
 
 [case testDecoratorsSimple_toplevel]
@@ -2914,18 +2989,10 @@ def __top_level__():
     r0, r1 :: object
     r2 :: bit
     r3 :: str
-    r4 :: object
-    r5 :: dict
+    r4, r5 :: object
     r6 :: str
-    r7 :: list
-    r8, r9 :: ptr
-    r10 :: str
-    r11 :: object
-    r12, r13, r14 :: str
-    r15 :: object
-    r16 :: str
-    r17 :: int32
-    r18 :: bit
+    r7 :: dict
+    r8 :: object
 L0:
     r0 = builtins :: module
     r1 = load_address _Py_NoneStruct
@@ -2936,23 +3003,11 @@ L1:
     r4 = PyImport_Import(r3)
     builtins = r4 :: module
 L2:
-    r5 = __main__.globals :: static
-    r6 = 'Callable'
-    r7 = PyList_New(1)
-    r8 = get_element_ptr r7 ob_item :: PyListObject
-    r9 = load_mem r8 :: ptr*
-    set_mem r9, r6 :: builtins.object*
-    keep_alive r7
-    r10 = 'typing'
-    r11 = PyImport_ImportModuleLevelObject(r10, r5, 0, r7, 0)
-    typing = r11 :: module
-    r12 = 'typing'
-    r13 = 'Callable'
-    r14 = 'Callable'
-    r15 = CPyImport_ImportFrom(r11, r12, r13, r14)
-    r16 = 'Callable'
-    r17 = CPyDict_SetItem(r5, r16, r15)
-    r18 = r17 >= 0 :: signed
+    r5 = ('Callable',)
+    r6 = 'typing'
+    r7 = __main__.globals :: static
+    r8 = CPyImport_ImportFromMany(r6, r5, r5, r7)
+    typing = r8 :: module
     return 1
 
 [case testAnyAllG]
@@ -3422,47 +3477,39 @@ x = 1
 [file p/m.py]
 [out]
 def f():
-    r0 :: dict
-    r1, r2 :: object
-    r3 :: bit
-    r4 :: str
+    r0 :: object_ptr
+    r1 :: object_ptr[1]
+    r2 :: c_ptr
+    r3 :: native_int[1]
+    r4 :: c_ptr
     r5 :: object
     r6 :: dict
-    r7 :: str
-    r8 :: object
-    r9 :: str
-    r10 :: int32
-    r11 :: bit
-    r12 :: dict
+    r7, r8 :: str
+    r9 :: bit
+    r10 :: dict
+    r11 :: str
+    r12 :: object
     r13 :: str
     r14 :: object
-    r15 :: str
-    r16 :: object
-    r17 :: int
-L0:
-    r0 = __main__.globals :: static
-    r1 = p.m :: module
-    r2 = load_address _Py_NoneStruct
-    r3 = r1 != r2
-    if r3 goto L2 else goto L1 :: bool
-L1:
-    r4 = 'p.m'
-    r5 = PyImport_Import(r4)
-    p.m = r5 :: module
-L2:
-    r6 = PyImport_GetModuleDict()
-    r7 = 'p'
-    r8 = CPyDict_GetItem(r6, r7)
-    r9 = 'p'
-    r10 = CPyDict_SetItem(r0, r9, r8)
-    r11 = r10 >= 0 :: signed
-    r12 = PyImport_GetModuleDict()
-    r13 = 'p'
-    r14 = CPyDict_GetItem(r12, r13)
-    r15 = 'x'
-    r16 = CPyObject_GetAttr(r14, r15)
-    r17 = unbox(int, r16)
-    return r17
+    r15 :: int
+L0:
+    r0 = load_address p.m :: module
+    r1 = [r0]
+    r2 = load_address r1
+    r3 = [2]
+    r4 = load_address r3
+    r5 = (('p.m', None, 'p'),)
+    r6 = __main__.globals :: static
+    r7 = 'main'
+    r8 = 'f'
+    r9 = CPyImport_ImportMany(r5, r2, r6, r7, r8, r4)
+    r10 = PyImport_GetModuleDict()
+    r11 = 'p'
+    r12 = CPyDict_GetItem(r10, r11)
+    r13 = 'x'
+    r14 = CPyObject_GetAttr(r12, r13)
+    r15 = unbox(int, r14)
+    return r15
 
 [case testIsinstanceBool]
 def f(x: object) -> bool:
diff --git a/mypyc/test-data/irbuild-classes.test b/mypyc/test-data/irbuild-classes.test
index 0f98fc69e5f3..0a7076e5f0ad 100644
--- a/mypyc/test-data/irbuild-classes.test
+++ b/mypyc/test-data/irbuild-classes.test
@@ -200,84 +200,63 @@ def __top_level__():
     r0, r1 :: object
     r2 :: bit
     r3 :: str
-    r4 :: object
-    r5 :: dict
-    r6, r7 :: str
-    r8 :: list
-    r9, r10, r11 :: ptr
-    r12 :: str
-    r13 :: object
-    r14, r15, r16 :: str
-    r17 :: object
-    r18 :: str
-    r19 :: int32
-    r20 :: bit
-    r21, r22, r23 :: str
-    r24 :: object
-    r25 :: str
-    r26 :: int32
-    r27 :: bit
-    r28 :: dict
-    r29 :: str
-    r30 :: list
-    r31, r32 :: ptr
-    r33 :: str
-    r34 :: object
-    r35, r36, r37 :: str
-    r38 :: object
+    r4, r5 :: object
+    r6 :: str
+    r7 :: dict
+    r8, r9 :: object
+    r10 :: str
+    r11 :: dict
+    r12 :: object
+    r13 :: str
+    r14 :: dict
+    r15 :: str
+    r16, r17 :: object
+    r18 :: dict
+    r19 :: str
+    r20 :: int32
+    r21 :: bit
+    r22 :: object
+    r23 :: str
+    r24, r25 :: object
+    r26 :: bool
+    r27 :: str
+    r28 :: tuple
+    r29 :: int32
+    r30 :: bit
+    r31 :: dict
+    r32 :: str
+    r33 :: int32
+    r34 :: bit
+    r35 :: object
+    r36 :: str
+    r37, r38 :: object
     r39 :: str
-    r40 :: int32
-    r41 :: bit
-    r42 :: str
+    r40 :: tuple
+    r41 :: int32
+    r42 :: bit
     r43 :: dict
     r44 :: str
-    r45, r46 :: object
-    r47 :: dict
-    r48 :: str
-    r49 :: int32
-    r50 :: bit
+    r45 :: int32
+    r46 :: bit
+    r47, r48 :: object
+    r49 :: dict
+    r50 :: str
     r51 :: object
-    r52 :: str
-    r53, r54 :: object
-    r55 :: bool
-    r56 :: str
-    r57 :: tuple
-    r58 :: int32
-    r59 :: bit
-    r60 :: dict
-    r61 :: str
-    r62 :: int32
-    r63 :: bit
-    r64 :: object
-    r65 :: str
-    r66, r67 :: object
-    r68 :: str
-    r69 :: tuple
-    r70 :: int32
-    r71 :: bit
-    r72 :: dict
-    r73 :: str
-    r74 :: int32
-    r75 :: bit
-    r76, r77 :: object
-    r78 :: dict
-    r79 :: str
-    r80 :: object
-    r81 :: dict
-    r82 :: str
-    r83, r84 :: object
-    r85 :: tuple
-    r86 :: str
-    r87, r88 :: object
-    r89 :: bool
-    r90, r91 :: str
-    r92 :: tuple
-    r93 :: int32
-    r94 :: bit
-    r95 :: dict
-    r96 :: str
-    r97 :: int32
-    r98 :: bit
+    r52 :: dict
+    r53 :: str
+    r54, r55 :: object
+    r56 :: tuple
+    r57 :: str
+    r58, r59 :: object
+    r60 :: bool
+    r61, r62 :: str
+    r63 :: tuple
+    r64 :: int32
+    r65 :: bit
+    r66 :: dict
+    r67 :: str
+    r68 :: int32
+    r69 :: bit
 L0:
     r0 = builtins :: module
     r1 = load_address _Py_NoneStruct
@@ -288,110 +267,76 @@ L1:
     r4 = PyImport_Import(r3)
     builtins = r4 :: module
 L2:
-    r5 = __main__.globals :: static
-    r6 = 'TypeVar'
-    r7 = 'Generic'
-    r8 = PyList_New(2)
-    r9 = get_element_ptr r8 ob_item :: PyListObject
-    r10 = load_mem r9 :: ptr*
-    set_mem r10, r6 :: builtins.object*
-    r11 = r10 + WORD_SIZE*1
-    set_mem r11, r7 :: builtins.object*
-    keep_alive r8
-    r12 = 'typing'
-    r13 = PyImport_ImportModuleLevelObject(r12, r5, 0, r8, 0)
-    typing = r13 :: module
-    r14 = 'typing'
+    r5 = ('TypeVar', 'Generic')
+    r6 = 'typing'
+    r7 = __main__.globals :: static
+    r8 = CPyImport_ImportFromMany(r6, r5, r5, r7)
+    typing = r8 :: module
+    r9 = ('trait',)
+    r10 = 'mypy_extensions'
+    r11 = __main__.globals :: static
+    r12 = CPyImport_ImportFromMany(r10, r9, r9, r11)
+    mypy_extensions = r12 :: module
+    r13 = 'T'
+    r14 = __main__.globals :: static
     r15 = 'TypeVar'
-    r16 = 'TypeVar'
-    r17 = CPyImport_ImportFrom(r13, r14, r15, r16)
-    r18 = 'TypeVar'
-    r19 = CPyDict_SetItem(r5, r18, r17)
-    r20 = r19 >= 0 :: signed
-    r21 = 'typing'
-    r22 = 'Generic'
-    r23 = 'Generic'
-    r24 = CPyImport_ImportFrom(r13, r21, r22, r23)
-    r25 = 'Generic'
-    r26 = CPyDict_SetItem(r5, r25, r24)
-    r27 = r26 >= 0 :: signed
-    r28 = __main__.globals :: static
-    r29 = 'trait'
-    r30 = PyList_New(1)
-    r31 = get_element_ptr r30 ob_item :: PyListObject
-    r32 = load_mem r31 :: ptr*
-    set_mem r32, r29 :: builtins.object*
-    keep_alive r30
-    r33 = 'mypy_extensions'
-    r34 = PyImport_ImportModuleLevelObject(r33, r28, 0, r30, 0)
-    mypy_extensions = r34 :: module
-    r35 = 'mypy_extensions'
-    r36 = 'trait'
-    r37 = 'trait'
-    r38 = CPyImport_ImportFrom(r34, r35, r36, r37)
-    r39 = 'trait'
-    r40 = CPyDict_SetItem(r28, r39, r38)
-    r41 = r40 >= 0 :: signed
-    r42 = 'T'
+    r16 = CPyDict_GetItem(r14, r15)
+    r17 = PyObject_CallFunctionObjArgs(r16, r13, 0)
+    r18 = __main__.globals :: static
+    r19 = 'T'
+    r20 = CPyDict_SetItem(r18, r19, r17)
+    r21 = r20 >= 0 :: signed
+    r22 = <error> :: object
+    r23 = '__main__'
+    r24 = __main__.C_template :: type
+    r25 = CPyType_FromTemplate(r24, r22, r23)
+    r26 = C_trait_vtable_setup()
+    r27 = '__mypyc_attrs__'
+    r28 = PyTuple_Pack(0)
+    r29 = PyObject_SetAttr(r25, r27, r28)
+    r30 = r29 >= 0 :: signed
+    __main__.C = r25 :: type
+    r31 = __main__.globals :: static
+    r32 = 'C'
+    r33 = CPyDict_SetItem(r31, r32, r25)
+    r34 = r33 >= 0 :: signed
+    r35 = <error> :: object
+    r36 = '__main__'
+    r37 = __main__.S_template :: type
+    r38 = CPyType_FromTemplate(r37, r35, r36)
+    r39 = '__mypyc_attrs__'
+    r40 = PyTuple_Pack(0)
+    r41 = PyObject_SetAttr(r38, r39, r40)
+    r42 = r41 >= 0 :: signed
+    __main__.S = r38 :: type
     r43 = __main__.globals :: static
-    r44 = 'TypeVar'
-    r45 = CPyDict_GetItem(r43, r44)
-    r46 = PyObject_CallFunctionObjArgs(r45, r42, 0)
-    r47 = __main__.globals :: static
-    r48 = 'T'
-    r49 = CPyDict_SetItem(r47, r48, r46)
-    r50 = r49 >= 0 :: signed
-    r51 = <error> :: object
-    r52 = '__main__'
-    r53 = __main__.C_template :: type
-    r54 = CPyType_FromTemplate(r53, r51, r52)
-    r55 = C_trait_vtable_setup()
-    r56 = '__mypyc_attrs__'
-    r57 = PyTuple_Pack(0)
-    r58 = PyObject_SetAttr(r54, r56, r57)
-    r59 = r58 >= 0 :: signed
-    __main__.C = r54 :: type
-    r60 = __main__.globals :: static
-    r61 = 'C'
-    r62 = CPyDict_SetItem(r60, r61, r54)
-    r63 = r62 >= 0 :: signed
-    r64 = <error> :: object
-    r65 = '__main__'
-    r66 = __main__.S_template :: type
-    r67 = CPyType_FromTemplate(r66, r64, r65)
-    r68 = '__mypyc_attrs__'
-    r69 = PyTuple_Pack(0)
-    r70 = PyObject_SetAttr(r67, r68, r69)
-    r71 = r70 >= 0 :: signed
-    __main__.S = r67 :: type
-    r72 = __main__.globals :: static
-    r73 = 'S'
-    r74 = CPyDict_SetItem(r72, r73, r67)
-    r75 = r74 >= 0 :: signed
-    r76 = __main__.C :: type
-    r77 = __main__.S :: type
-    r78 = __main__.globals :: static
-    r79 = 'Generic'
-    r80 = CPyDict_GetItem(r78, r79)
-    r81 = __main__.globals :: static
-    r82 = 'T'
-    r83 = CPyDict_GetItem(r81, r82)
-    r84 = PyObject_GetItem(r80, r83)
-    r85 = PyTuple_Pack(3, r76, r77, r84)
-    r86 = '__main__'
-    r87 = __main__.D_template :: type
-    r88 = CPyType_FromTemplate(r87, r85, r86)
-    r89 = D_trait_vtable_setup()
-    r90 = '__mypyc_attrs__'
-    r91 = '__dict__'
-    r92 = PyTuple_Pack(1, r91)
-    r93 = PyObject_SetAttr(r88, r90, r92)
-    r94 = r93 >= 0 :: signed
-    __main__.D = r88 :: type
-    r95 = __main__.globals :: static
-    r96 = 'D'
-    r97 = CPyDict_SetItem(r95, r96, r88)
-    r98 = r97 >= 0 :: signed
+    r44 = 'S'
+    r45 = CPyDict_SetItem(r43, r44, r38)
+    r46 = r45 >= 0 :: signed
+    r47 = __main__.C :: type
+    r48 = __main__.S :: type
+    r49 = __main__.globals :: static
+    r50 = 'Generic'
+    r51 = CPyDict_GetItem(r49, r50)
+    r52 = __main__.globals :: static
+    r53 = 'T'
+    r54 = CPyDict_GetItem(r52, r53)
+    r55 = PyObject_GetItem(r51, r54)
+    r56 = PyTuple_Pack(3, r47, r48, r55)
+    r57 = '__main__'
+    r58 = __main__.D_template :: type
+    r59 = CPyType_FromTemplate(r58, r56, r57)
+    r60 = D_trait_vtable_setup()
+    r61 = '__mypyc_attrs__'
+    r62 = '__dict__'
+    r63 = PyTuple_Pack(1, r62)
+    r64 = PyObject_SetAttr(r59, r61, r63)
+    r65 = r64 >= 0 :: signed
+    __main__.D = r59 :: type
+    r66 = __main__.globals :: static
+    r67 = 'D'
+    r68 = CPyDict_SetItem(r66, r67, r59)
+    r69 = r68 >= 0 :: signed
     return 1
 
 [case testIsInstance]
diff --git a/mypyc/test-data/run-imports.test b/mypyc/test-data/run-imports.test
index c6d5bdb3d864..fd289458e786 100644
--- a/mypyc/test-data/run-imports.test
+++ b/mypyc/test-data/run-imports.test
@@ -18,10 +18,12 @@ def test_import_submodule_within_function() -> None:
     import pkg.mod
     assert pkg.x == 1
     assert pkg.mod.y == 2
+    assert "pkg.mod" not in globals(), "the root module should be in globals!"
 
 def test_import_as_submodule_within_function() -> None:
     import pkg.mod as mm
     assert mm.y == 2
+    assert "pkg.mod" not in globals(), "the root module should be in globals!"
 
 # TODO: Don't add local imports to globals()
 #
@@ -192,3 +194,44 @@ a.x = 10
 x = 20
 [file driver.py]
 import native
+
+[case testLazyImport]
+import shared
+
+def do_import() -> None:
+    import a
+
+assert shared.counter == 0
+do_import()
+assert shared.counter == 1
+
+[file a.py]
+import shared
+shared.counter += 1
+
+[file shared.py]
+counter = 0
+
+[case testDelayedImport]
+import a
+print("inbetween")
+import b
+
+[file a.py]
+print("first")
+
+[file b.py]
+print("last")
+
+[out]
+first
+inbetween
+last
+
+[case testImportErrorLineNumber]
+try:
+    import enum
+    import dataclasses, missing  # type: ignore[import]
+except ImportError as e:
+    line = e.__traceback__.tb_lineno # type: ignore[attr-defined]
+    assert line == 3, f"traceback's line number is {line}, expected 3"
diff --git a/mypyc/test-data/run-multimodule.test b/mypyc/test-data/run-multimodule.test
index 418af66ba060..70c73dc2088b 100644
--- a/mypyc/test-data/run-multimodule.test
+++ b/mypyc/test-data/run-multimodule.test
@@ -11,21 +11,23 @@
 -- about how this is specified (e.g. .2 file name suffixes).
 
 [case testMultiModulePackage]
-from p.other import g
+from p.other import g, _i as i
 def f(x: int) -> int:
     from p.other import h
-    return h(g(x + 1))
+    return i(h(g(x + 1)))
 [file p/__init__.py]
 [file p/other.py]
 def g(x: int) -> int:
     return x + 2
 def h(x: int) -> int:
     return x + 1
+def _i(x: int) -> int:
+    return x + 3
 [file driver.py]
 import native
 from native import f
 from p.other import g
-assert f(3) == 7
+assert f(3) == 10
 assert g(2) == 4
 try:
     f(1.1)


Code review comments:
diff_hunk: @@ -669,9 +669,71 @@ CPy_Super(PyObject *builtins, PyObject *self) {
     return result;
 }
 
+static bool import_single(PyObject *mod_id,
+                                  PyObject *as_name,
+                                  PyObject **mod_static,
+                                  PyObject *globals_base,
+                                  PyObject *globals) {
+    if (*mod_static == Py_None) {
+        CPyModule *mod = PyImport_Import(mod_id);
+        if (mod == NULL) {
+            return false;
+        }
+        *mod_static = mod;
+    }
+
+    if (as_name == Py_None) {
+        as_name = mod_id;
+    }
path: mypyc/lib-rt/misc_ops.c
text: @author:
I'm happy to remove this if clause by moving the defaulting to build time. I just implemented it this way first and IMO it makes the generated C a little easier to read (as names stand out more).