Repository: cython/cython

Issue:
[BUG] Cannot declare fused type for container whose elements are fused types
Declaring a fused type where one element is a `libcpp` container whose elements are fused raises an `AttributeError`. See below for an example. I would have expected the code to compile.

```cython
# distutils: language=c++

from libcpp.vector cimport vector


ctypedef fused fused_element_type:
    int
    long


ctypedef fused error:
    vector[fused_element_type]
```

```
Error compiling Cython file:
------------------------------------------------------------
...
ctypedef fused fused_element_type:
    int
    long


ctypedef fused error:
^
------------------------------------------------------------

/[redacted]/_cython_magic_8449521bb3abea1d93e6323ea9cb8d1a.pyx:11:0: Compiler crash in AnalyseDeclarationsTransform

File 'ModuleNode.py', line 124, in analyse_declarations: ModuleNode(_cython_magic_8449521bb3abea1d93e6323ea9cb8d1a.pyx:1:0,
    full_module_name = '_cython_magic_8449521bb3abea1d93e6323ea9cb8d1a')
File 'Nodes.py', line 431, in analyse_declarations: StatListNode(_cython_magic_8449521bb3abea1d93e6323ea9cb8d1a.pyx:3:0)
File 'Nodes.py', line 1250, in analyse_declarations: FusedTypeNode(_cython_magic_8449521bb3abea1d93e6323ea9cb8d1a.pyx:11:0,
    name = 'error',
    types = [...]/1)
File 'Nodes.py', line 1273, in analyse: FusedTypeNode(_cython_magic_8449521bb3abea1d93e6323ea9cb8d1a.pyx:11:0,
    name = 'error',
    types = [...]/1)

Compiler crash traceback from this point on:
  File "/[redacted]/site-packages/Cython/Compiler/Nodes.py", line 1273, in analyse
    return PyrexTypes.FusedType(types, name=self.name)
  File "/[redacted]/site-packages/Cython/Compiler/PyrexTypes.py", line 1636, in __init__
    for subtype in t.types:
AttributeError: 'CppClassType' object has no attribute 'types'
```

**Environment**
 - OS: macOS Monterey 12.3 (M1 chip)
 - Python version: 3.9.9 (pyenv via brew)
 - Cython version: 0.29.28


Discussion:
I'm not sure if this should work or not.

You should be able to write a function with a templated fused type:

```
cdef f(vector[fused_element_type] vec):
    ...
```

But I don't know whether a `ctypedef` would work, or whether it should be `ctypedef fused`.
Sorry, maybe some context would've been helpful. I'm trying to use fused types of vectors of fused types for some recursive template programming. 

In the C++ world, tensors are often represented as nested vectors, e.g. a matrix is `vector<vector<double>>`. That's arguably not a great choice (a linearized representation is probably better), but, when working with third-party libraries, it's a representation we're stuck with. In the python world, numpy arrays are the de-facto standard, and I wanted to be able to convert between them. The code at the bottom of this comment does that for nested vectors of doubles using a recursive approach so we don't need different implementations for tensors of different rank.

The crux for the templated recursion to work is this fused `ctypedef`.

```cython
ctypedef fused vector_or_double:
    double  # vector
    vector[double]  # matrix
    vector[vector[double]]  # rank-3 tensor
    # Add additional layers for higher-dimensional tensors.
```

But we may also want to convert `int` or `float` tensors, and I tried to replace `double` with `cython.numeric`, giving rise to the original error message.

# Full example code

```cython
# distutils: language=c++
# cython: boundscheck=False
# cython: cdivision=True

from libcpp.vector cimport vector
import numpy as np
cimport cython
from cython.operator cimport dereference, preincrement


ctypedef fused vector_or_double:
    double
    vector[double]
    vector[vector[double]]
    # Add additional layers for higher-dimensional tensors.
    
    
cdef nested_to_ndarray(vector[vector_or_double] nested):
    """
    Convert nested vectors to a numpy array.
    """
    cdef:
        long size
        vector[long] shape = vector[long]()
        double[:] target
        
    size = _infer_shape(nested, shape)
    target = np.empty(size)
    _recursive_fill(nested, &target[0], size)
    return np.reshape(target, shape)
    

cdef void _recursive_fill(vector[vector_or_double] source, double* target, long stride):
    """
    Fill nested source vectors into a preallocated target.
    
    Args:
        source: Nested source vectors.
        target: Pointer to preallocated memory.
        stride: Stride for preallocated memory.
    """
    stride = stride // source.size()
    if vector_or_double is double:
        iterator = source.begin()
        while iterator != source.end():
            target[0] = dereference(iterator)
            preincrement(iterator)
            target += 1
    else:
        iterator = source.begin()
        while iterator != source.end():
            _recursive_fill(dereference(iterator), target, stride)
            preincrement(iterator)
            target += stride
            
            
cdef long _infer_shape(vector[vector_or_double] nested, vector[long]& shape):
    """
    Infer the shape of nested vectors.
    
    Args:
        nested: Nested vectors.
        shape: Shape vector to populate.
    
    Returns:
        size: Total number of elements.
    """
    cdef long size = nested.size()
    # Push the current dimension.
    shape.push_back(size)
    # This is the lowest level or there are no elements. We are done here.
    if vector_or_double is double or size == 0:
        return size
    # Recurse down one level.
    return size * _infer_shape(nested.at(0), shape)
```
Ok - I follow what you're trying to do. I'm not sure if I'd expect it to work or not, but I can see why it'd be useful.
What might work at a guess: go to the first line of the traceback and replace `t.types` (which only works for a first-level fused type) with `t.get_fused_types()` (which should work with any type).

Edit: this does work...

Resolving patch:
diff --git a/Cython/Compiler/PyrexTypes.py b/Cython/Compiler/PyrexTypes.py
--- a/Cython/Compiler/PyrexTypes.py
+++ b/Cython/Compiler/PyrexTypes.py
@@ -13,6 +13,7 @@
 except NameError:
     from functools import reduce
 from functools import partial
+from itertools import product
 
 from Cython.Utils import cached_function
 from .Code import UtilityCode, LazyUtilityCode, TempitaUtilityCode
@@ -1835,7 +1836,27 @@ def __init__(self, types, name=None):
         for t in types:
             if t.is_fused:
                 # recursively merge in subtypes
-                for subtype in t.types:
+                if isinstance(t, FusedType):
+                    t_types = t.types
+                else:
+                    # handle types that aren't a fused type themselves but contain fused types
+                    # for example a C++ template where the template type is fused.
+                    t_fused_types = t.get_fused_types()
+                    t_types = []
+                    for substitution in product(
+                        *[fused_type.types for fused_type in t_fused_types]
+                    ):
+                        t_types.append(
+                            t.specialize(
+                                {
+                                    fused_type: sub
+                                    for fused_type, sub in zip(
+                                        t_fused_types, substitution
+                                    )
+                                }
+                            )
+                        )
+                for subtype in t_types:
                     if subtype not in flattened_types:
                         flattened_types.append(subtype)
             elif t not in flattened_types:


Test patch:
diff --git a/tests/run/fused_cpp.pyx b/tests/run/fused_cpp.pyx
--- a/tests/run/fused_cpp.pyx
+++ b/tests/run/fused_cpp.pyx
@@ -2,6 +2,7 @@
 
 cimport cython
 from libcpp.vector cimport vector
+from libcpp.map cimport map
 from libcpp.typeinfo cimport type_info
 from cython.operator cimport typeid
 
@@ -51,3 +52,39 @@ def test_fused_ref(int x):
     (10, 10)
     """
     return fused_ref(x), fused_ref[int](x)
+
+ctypedef fused nested_fused:
+    vector[cython.integral]
+
+cdef vec_of_fused(nested_fused v):
+    x = v[0]
+    return cython.typeof(x)
+
+def test_nested_fused():
+    """
+    >>> test_nested_fused()
+    int
+    long
+    """
+    cdef vector[int] vi = [0,1]
+    cdef vector[long] vl = [0,1]
+    print vec_of_fused(vi)
+    print vec_of_fused(vl)
+
+ctypedef fused nested_fused2:
+    map[cython.integral, cython.floating]
+
+cdef map_of_fused(nested_fused2 m):
+    for pair in m:
+        return cython.typeof(pair.first), cython.typeof(pair.second)
+
+def test_nested_fused2():
+    """
+    >>> test_nested_fused2()
+    ('int', 'float')
+    ('long', 'double')
+    """
+    cdef map[int, float] mif = { 0: 0.0 }
+    cdef map[long, double] mld = { 0: 0.0 }
+    print map_of_fused(mif)
+    print map_of_fused(mld)