Repository: cython/cython
Pull request: Avoid parsing failure on cast to ctuple
Fixes #3808

Problem statement:
[BUG] Compiler error when casting to `ctuple`
**Describe the bug**

When attempting to cast to a `ctuple`, Cython runs into a compiler error.

**To Reproduce**

Code to reproduce the behavior:

```cython
# cython: language_level=3

cdef double d
cdef int i

o = (3.0, 2)
d, i = <(double, int)>o  # <--- compiler errors here
```

<details>
<summary>Exception:</summary>

```python
Traceback (most recent call last):
  File "/Users/jkirkham/miniconda/envs/cython/bin/cython", line 11, in <module>
    sys.exit(setuptools_main())
  File "/Users/jkirkham/miniconda/envs/cython/lib/python3.8/site-packages/Cython/Compiler/Main.py", line 840, in setuptools_main
    return main(command_line = 1)
  File "/Users/jkirkham/miniconda/envs/cython/lib/python3.8/site-packages/Cython/Compiler/Main.py", line 858, in main
    result = compile(sources, options)
  File "/Users/jkirkham/miniconda/envs/cython/lib/python3.8/site-packages/Cython/Compiler/Main.py", line 780, in compile
    return compile_multiple(source, options)
  File "/Users/jkirkham/miniconda/envs/cython/lib/python3.8/site-packages/Cython/Compiler/Main.py", line 757, in compile_multiple
    result = run_pipeline(source, options, context=context)
  File "/Users/jkirkham/miniconda/envs/cython/lib/python3.8/site-packages/Cython/Compiler/Main.py", line 515, in run_pipeline
    err, enddata = Pipeline.run_pipeline(pipeline, source)
  File "/Users/jkirkham/miniconda/envs/cython/lib/python3.8/site-packages/Cython/Compiler/Pipeline.py", line 355, in run_pipeline
    data = run(phase, data)
  File "/Users/jkirkham/miniconda/envs/cython/lib/python3.8/site-packages/Cython/Compiler/Pipeline.py", line 335, in run
    return phase(data)
  File "/Users/jkirkham/miniconda/envs/cython/lib/python3.8/site-packages/Cython/Compiler/Pipeline.py", line 34, in parse
    tree = context.parse(source_desc, scope, pxd = 0, full_module_name = full_module_name)
  File "/Users/jkirkham/miniconda/envs/cython/lib/python3.8/site-packages/Cython/Compiler/Main.py", line 369, in parse
    tree = Parsing.p_module(s, pxd, full_module_name)
  File "/Users/jkirkham/miniconda/envs/cython/lib/python3.8/site-packages/Cython/Compiler/Parsing.py", line 3707, in p_module
    body = p_statement_list(s, ctx(level=level), first_statement = 1)
  File "/Users/jkirkham/miniconda/envs/cython/lib/python3.8/site-packages/Cython/Compiler/Parsing.py", line 2341, in p_statement_list
    stat = p_statement(s, ctx, first_statement = first_statement)
  File "/Users/jkirkham/miniconda/envs/cython/lib/python3.8/site-packages/Cython/Compiler/Parsing.py", line 2333, in p_statement
    return p_simple_statement_list(s, ctx, first_statement=first_statement)
  File "/Users/jkirkham/miniconda/envs/cython/lib/python3.8/site-packages/Cython/Compiler/Parsing.py", line 2154, in p_simple_statement_list
    stat = p_simple_statement(s, first_statement = first_statement)
  File "/Users/jkirkham/miniconda/envs/cython/lib/python3.8/site-packages/Cython/Compiler/Parsing.py", line 2148, in p_simple_statement
    node = p_expression_or_assignment(s)
  File "/Users/jkirkham/miniconda/envs/cython/lib/python3.8/site-packages/Cython/Compiler/Parsing.py", line 1523, in p_expression_or_assignment
    expr = p_testlist_star_expr(s)
  File "/Users/jkirkham/miniconda/envs/cython/lib/python3.8/site-packages/Cython/Compiler/Parsing.py", line 1453, in p_testlist_star_expr
    expr = p_test_or_starred_expr(s)
  File "/Users/jkirkham/miniconda/envs/cython/lib/python3.8/site-packages/Cython/Compiler/Parsing.py", line 210, in p_test_or_starred_expr
    return p_test(s)
  File "/Users/jkirkham/miniconda/envs/cython/lib/python3.8/site-packages/Cython/Compiler/Parsing.py", line 142, in p_test
    expr = p_or_test(s)
  File "/Users/jkirkham/miniconda/envs/cython/lib/python3.8/site-packages/Cython/Compiler/Parsing.py", line 163, in p_or_test
    return p_rassoc_binop_expr(s, ('or',), p_and_test)
  File "/Users/jkirkham/miniconda/envs/cython/lib/python3.8/site-packages/Cython/Compiler/Parsing.py", line 166, in p_rassoc_binop_expr
    n1 = p_subexpr(s)
  File "/Users/jkirkham/miniconda/envs/cython/lib/python3.8/site-packages/Cython/Compiler/Parsing.py", line 179, in p_and_test
    return p_rassoc_binop_expr(s, ('and',), p_not_test)
  File "/Users/jkirkham/miniconda/envs/cython/lib/python3.8/site-packages/Cython/Compiler/Parsing.py", line 166, in p_rassoc_binop_expr
    n1 = p_subexpr(s)
  File "/Users/jkirkham/miniconda/envs/cython/lib/python3.8/site-packages/Cython/Compiler/Parsing.py", line 189, in p_not_test
    return p_comparison(s)
  File "/Users/jkirkham/miniconda/envs/cython/lib/python3.8/site-packages/Cython/Compiler/Parsing.py", line 195, in p_comparison
    n1 = p_starred_expr(s)
  File "/Users/jkirkham/miniconda/envs/cython/lib/python3.8/site-packages/Cython/Compiler/Parsing.py", line 219, in p_starred_expr
    expr = p_bit_expr(s)
  File "/Users/jkirkham/miniconda/envs/cython/lib/python3.8/site-packages/Cython/Compiler/Parsing.py", line 261, in p_bit_expr
    return p_binop_expr(s, ('|',), p_xor_expr)
  File "/Users/jkirkham/miniconda/envs/cython/lib/python3.8/site-packages/Cython/Compiler/Parsing.py", line 97, in p_binop_expr
    n1 = p_sub_expr(s)
  File "/Users/jkirkham/miniconda/envs/cython/lib/python3.8/site-packages/Cython/Compiler/Parsing.py", line 266, in p_xor_expr
    return p_binop_expr(s, ('^',), p_and_expr)
  File "/Users/jkirkham/miniconda/envs/cython/lib/python3.8/site-packages/Cython/Compiler/Parsing.py", line 97, in p_binop_expr
    n1 = p_sub_expr(s)
  File "/Users/jkirkham/miniconda/envs/cython/lib/python3.8/site-packages/Cython/Compiler/Parsing.py", line 271, in p_and_expr
    return p_binop_expr(s, ('&',), p_shift_expr)
  File "/Users/jkirkham/miniconda/envs/cython/lib/python3.8/site-packages/Cython/Compiler/Parsing.py", line 97, in p_binop_expr
    n1 = p_sub_expr(s)
  File "/Users/jkirkham/miniconda/envs/cython/lib/python3.8/site-packages/Cython/Compiler/Parsing.py", line 276, in p_shift_expr
    return p_binop_expr(s, ('<<', '>>'), p_arith_expr)
  File "/Users/jkirkham/miniconda/envs/cython/lib/python3.8/site-packages/Cython/Compiler/Parsing.py", line 97, in p_binop_expr
    n1 = p_sub_expr(s)
  File "/Users/jkirkham/miniconda/envs/cython/lib/python3.8/site-packages/Cython/Compiler/Parsing.py", line 281, in p_arith_expr
    return p_binop_expr(s, ('+', '-'), p_term)
  File "/Users/jkirkham/miniconda/envs/cython/lib/python3.8/site-packages/Cython/Compiler/Parsing.py", line 97, in p_binop_expr
    n1 = p_sub_expr(s)
  File "/Users/jkirkham/miniconda/envs/cython/lib/python3.8/site-packages/Cython/Compiler/Parsing.py", line 286, in p_term
    return p_binop_expr(s, ('*', '@', '/', '%', '//'), p_factor)
  File "/Users/jkirkham/miniconda/envs/cython/lib/python3.8/site-packages/Cython/Compiler/Parsing.py", line 97, in p_binop_expr
    n1 = p_sub_expr(s)
  File "/Users/jkirkham/miniconda/envs/cython/lib/python3.8/site-packages/Cython/Compiler/Parsing.py", line 292, in p_factor
    return _p_factor(s)
  File "/Users/jkirkham/miniconda/envs/cython/lib/python3.8/site-packages/Cython/Compiler/Parsing.py", line 308, in _p_factor
    return p_typecast(s)
  File "/Users/jkirkham/miniconda/envs/cython/lib/python3.8/site-packages/Cython/Compiler/Parsing.py", line 322, in p_typecast
    and base_type.name is None):
AttributeError: 'CTupleBaseTypeNode' object has no attribute 'name'
```

</details>

Workaround (`ctypedef` of `ctuple`):

```cython
ctypedef (double, int) double_int

cdef double d
cdef int i

o = (3.0, 2)
d, i = <double_int>o
```

**Expected behavior**

The compiler error does not occur.

**Environment (please complete the following information):**
 - OS: macOS
 - Python version 3.8.5
 - Cython version 0.29.21

**Additional context**

NA

Hints from issue discussion:
I realize this is probably a minimized example to demonstrate the issue, but I can't see why the cast to a ctuple is useful here? I'd think `d, i = o` should work just as efficiently?
Yeah it’s an oversimplified example just to demonstrate the issue.
> `d, i = <(double, int)>o  # <--- compiler errors here`

You didn't say what error occurred, but it probably fails to parse it, right? Such a cast is not really beautiful syntax, and most likely not something that was considered when ctuples were implemented.

Is there really a use case for such a cast? In an assignment, you could always type the left hand side rather than casting the right hand side. In what kind of code have you noticed this?
Yeah it seems to be a parsing error. Have added the exception to the detail in the OP.

Fair enough. FWIW it seems readable and seems comparable in complexity to things like `<float[:n:1]>`.

Only recently learned about `ctuple`s in Cython (which is a neat feature btw :), so was doing some exploration with some existing code that could be made a little clearer by bundling a few related values together. Stumbled across this issue in the process.

No strong feelings on the resolution here (including closing if that is what we deem appropriate).

Commit message: Avoid parsing failure on cast to ctuple

Patch to review:
diff --git a/Cython/Compiler/Parsing.py b/Cython/Compiler/Parsing.py
index 4755f9ad171..2e3241b6b17 100644
--- a/Cython/Compiler/Parsing.py
+++ b/Cython/Compiler/Parsing.py
@@ -318,7 +318,8 @@ def p_typecast(s):
     is_memslice = isinstance(base_type, Nodes.MemoryViewSliceTypeNode)
     is_template = isinstance(base_type, Nodes.TemplatedTypeNode)
     is_const_volatile = isinstance(base_type, Nodes.CConstOrVolatileTypeNode)
-    if not is_memslice and not is_template and not is_const_volatile and base_type.name is None:
+    is_ctuple = isinstance(base_type, Nodes.CTupleBaseTypeNode)
+    if not is_memslice and not is_template and not is_const_volatile and not is_ctuple and base_type.name is None:
         s.error("Unknown type")
     declarator = p_c_declarator(s, empty = 1)
     if s.sy == '?':
diff --git a/tests/run/ctuple.pyx b/tests/run/ctuple.pyx
index 235265bb1f5..4053df42b93 100644
--- a/tests/run/ctuple.pyx
+++ b/tests/run/ctuple.pyx
@@ -142,6 +142,17 @@ cpdef (int, double) cpdef_ctuple_return_type(int x, double y):
     return x, y
 
 
+def cast_to_ctuple(*o):
+    """
+    >>> cast_to_ctuple(1, 2.)
+    (1, 2.0)
+    """
+    cdef int x
+    cdef double y
+    x, y = <(int, double)>o
+    return x, y
+
+
 @cython.infer_types(True)
 def test_type_inference():
     """


Code review comments:
diff_hunk: @@ -318,7 +318,8 @@ def p_typecast(s):
     is_memslice = isinstance(base_type, Nodes.MemoryViewSliceTypeNode)
     is_template = isinstance(base_type, Nodes.TemplatedTypeNode)
     is_const_volatile = isinstance(base_type, Nodes.CConstOrVolatileTypeNode)
-    if not is_memslice and not is_template and not is_const_volatile and base_type.name is None:
+    is_ctuple = isinstance(base_type, Nodes.CTupleBaseTypeNode)
+    if not is_memslice and not is_template and not is_const_volatile and not is_ctuple and base_type.name is None:
path: Cython/Compiler/Parsing.py
text: @user1:
I think this has become long enough to start making it easier to read.
```suggestion
    if not (is_memslice or is_template or is_const_volatile or is_ctuple) and base_type.name is None:
```
And given that, I wonder if it's not better to just say
```cython
isinstance(base_type, (
    list,
    exceptional,
    types,
    here,
))
```