Repository: python/mypy
Pull request: [mypyc] Compile away NewType type calls
For example, here the call to ID is simply a no-op at runtime, returning 1 unchanged.

    ID = NewType('ID', int)
    person = ID(1)

Resolves https://github.com/mypyc/mypyc/issues/958



Problem statement:
Compile calls to NewType types into no-ops
Calling a NewType type is currently slow. Consider this code:
```py
from typing import NewType, cast

class C: pass

N = NewType("N", C)

def f(c: C) -> N:
    return N(c)  # Slow!
```

The IR for the call `N(c)` looks like this:
```
L0:
    r0 = tt.globals :: static
    r1 = 'N'
    r2 = CPyDict_GetItem(r0, r1)
    if is_error(r2) goto L3 (error at f:10) else goto L1
L1:
    r3 = [c]
    r4 = load_address r3
    r5 = _PyObject_Vectorcall(r2, r4, 1, 0)
    dec_ref r2
    if is_error(r5) goto L3 (error at f:10) else goto L2
```

Since calling a NewType type is a no-op at runtime, we can simply compile the call away.

Hints from issue discussion:

Commit message: [mypyc] Compile away NewType type calls

For example, here the call to ID is simply a no-op at runtime,
returning 1 unchanged.

    ID = NewType('ID', int)
    person = ID(1)

Patch to review:
diff --git a/mypyc/irbuild/expression.py b/mypyc/irbuild/expression.py
index b7d093cde7ee..4fe71a294162 100644
--- a/mypyc/irbuild/expression.py
+++ b/mypyc/irbuild/expression.py
@@ -266,13 +266,20 @@ def transform_super_expr(builder: IRBuilder, o: SuperExpr) -> Value:
 
 
 def transform_call_expr(builder: IRBuilder, expr: CallExpr) -> Value:
+    callee = expr.callee
     if isinstance(expr.analyzed, CastExpr):
         return translate_cast_expr(builder, expr.analyzed)
     elif isinstance(expr.analyzed, AssertTypeExpr):
         # Compile to a no-op.
         return builder.accept(expr.analyzed.expr)
+    elif (
+        isinstance(callee, (NameExpr, MemberExpr))
+        and isinstance(callee.node, TypeInfo)
+        and callee.node.is_newtype
+    ):
+        # Calls to a NewType type is a no-op at runtime.
+        return builder.accept(expr.args[0])
 
-    callee = expr.callee
     if isinstance(callee, IndexExpr) and isinstance(callee.analyzed, TypeApplication):
         callee = callee.analyzed.expr  # Unwrap type application
 
diff --git a/mypyc/test-data/irbuild-basic.test b/mypyc/test-data/irbuild-basic.test
index 16b085ad4927..f72720e59b18 100644
--- a/mypyc/test-data/irbuild-basic.test
+++ b/mypyc/test-data/irbuild-basic.test
@@ -2338,11 +2338,8 @@ def __top_level__():
     r92, r93, r94, r95 :: ptr
     r96 :: dict
     r97 :: str
-    r98, r99 :: object
-    r100 :: dict
-    r101 :: str
-    r102 :: int32
-    r103 :: bit
+    r98 :: int32
+    r99 :: bit
 L0:
     r0 = builtins :: module
     r1 = load_address _Py_NoneStruct
@@ -2454,13 +2451,9 @@ L2:
     set_mem r95, r91 :: builtins.object*
     keep_alive r88
     r96 = __main__.globals :: static
-    r97 = 'Bar'
-    r98 = CPyDict_GetItem(r96, r97)
-    r99 = PyObject_CallFunctionObjArgs(r98, r88, 0)
-    r100 = __main__.globals :: static
-    r101 = 'y'
-    r102 = CPyDict_SetItem(r100, r101, r99)
-    r103 = r102 >= 0 :: signed
+    r97 = 'y'
+    r98 = CPyDict_SetItem(r96, r97, r88)
+    r99 = r98 >= 0 :: signed
     return 1
 
 [case testChainedConditional]
@@ -3584,3 +3577,18 @@ L0:
     r3 = 0.0
     i__redef____redef__ = r3
     return 1
+
+[case testNewType]
+from typing import NewType
+
+class A: pass
+
+N = NewType("N", A)
+
+def f(arg: A) -> N:
+    return N(arg)
+[out]
+def f(arg):
+    arg :: __main__.A
+L0:
+    return arg


Code review comments:
diff_hunk: @@ -266,13 +266,20 @@ def transform_super_expr(builder: IRBuilder, o: SuperExpr) -> Value:
 
 
 def transform_call_expr(builder: IRBuilder, expr: CallExpr) -> Value:
+    callee = expr.callee
     if isinstance(expr.analyzed, CastExpr):
         return translate_cast_expr(builder, expr.analyzed)
     elif isinstance(expr.analyzed, AssertTypeExpr):
         # Compile to a no-op.
         return builder.accept(expr.analyzed.expr)
+    elif (
+        isinstance(callee, (NameExpr, MemberExpr))
+        and isinstance(callee.node, TypeInfo)
+        and callee.node.is_newtype
+    ):
+        # Calls to a NewType type is a no-op at runtime.
path: mypyc/irbuild/expression.py
text: @user1:
```suggestion
        # A call to a NewType type is a no-op at runtime.
```
grammar nit

@author:
I'll commit this after CI finishes. I rely on CI to fully test my patches since my laptop takes forever to run the mypyc tests. I'll probably switch to Codespaces, but I haven't done that yet!

@user2:
@author I may look into speeding up mypyc tests on macOS using Docker. Are you using a mac by any chance?

@author:
I'm using a Linux laptop so that won't be helpful. However, I've gotten around to setting up a Codespaces instance. A 8-core VM runs the whole mypyc test suite in ~2 minutes which is more than fast enough!

![image](https://user-images.githubusercontent.com/63936253/211074340-7c0655c9-f22e-4062-a261-c808753b4ec8.png)