• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

daisytuner / docc / 28761269778

06 Jul 2026 01:02AM UTC coverage: 62.801% (+0.3%) from 62.469%
28761269778

push

github

web-flow
Merge pull request #835 from daisytuner/pylulesh

[Python] Extends frontend support and adds pylulesh benchmarks

704 of 800 new or added lines in 5 files covered. (88.0%)

4 existing lines in 1 file now uncovered.

40479 of 64456 relevant lines covered (62.8%)

968.2 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

81.19
/python/docc/python/functions/numpy.py
1
import ast
4✔
2
from docc.sdfg import (
4✔
3
    Scalar,
4
    PrimitiveType,
5
    Pointer,
6
    DebugInfo,
7
    TaskletCode,
8
    CMathFunction,
9
    Tensor,
10
)
11
from docc.python.types import (
4✔
12
    element_type_from_ast_node,
13
    promote_element_types,
14
    numpy_promote_types,
15
)
16
from docc.python.ast_utils import get_debug_info
4✔
17
from docc.python.memory import ManagedMemoryHandler
4✔
18

19

20
class NumPyHandler:
4✔
21
    """
22
    Unified handler for NumPy operations including:
23
    - Array creation (empty, zeros, ones, eye, etc.)
24
    - Elementwise operations (add, subtract, multiply, etc.)
25
    - Linear algebra (matmul, dot, outer, gemm)
26
    - Array manipulation (transpose)
27
    - Reductions (sum, max, min, mean, std)
28
    """
29

30
    def __init__(self, expression_visitor):
4✔
31
        self._ev = expression_visitor
4✔
32
        self._unique_counter = 0
4✔
33
        self.function_handlers = {
4✔
34
            "empty": self._handle_numpy_alloc,
35
            "empty_like": self._handle_numpy_empty_like,
36
            "zeros": self._handle_numpy_alloc,
37
            "zeros_like": self._handle_numpy_zeros_like,
38
            "ones": self._handle_numpy_alloc,
39
            "ndarray": self._handle_numpy_alloc,
40
            "eye": self._handle_numpy_eye,
41
            "add": self._handle_numpy_binary_op,
42
            "subtract": self._handle_numpy_binary_op,
43
            "multiply": self._handle_numpy_binary_op,
44
            "divide": self._handle_numpy_binary_op,
45
            "power": self._handle_numpy_binary_op,
46
            "exp": self._handle_numpy_unary_op,
47
            "abs": self._handle_numpy_unary_op,
48
            "absolute": self._handle_numpy_unary_op,
49
            "sqrt": self._handle_numpy_unary_op,
50
            "tanh": self._handle_numpy_unary_op,
51
            "cbrt": self._handle_numpy_cbrt,
52
            "sum": self._handle_numpy_reduce,
53
            "max": self._handle_numpy_reduce,
54
            "min": self._handle_numpy_reduce,
55
            "mean": self._handle_numpy_reduce,
56
            "std": self._handle_numpy_reduce,
57
            "any": self._handle_numpy_any,
58
            "all": self._handle_numpy_all,
59
            "matmul": self._handle_numpy_matmul,
60
            "dot": self._handle_numpy_matmul,
61
            "matvec": self._handle_numpy_matmul,
62
            "outer": self._handle_numpy_outer,
63
            "minimum": self._handle_numpy_binary_op,
64
            "maximum": self._handle_numpy_binary_op,
65
            "where": self._handle_numpy_where,
66
            "select": self._handle_numpy_select,
67
            "clip": self._handle_numpy_clip,
68
            "transpose": self._handle_numpy_transpose,
69
            "flip": self._handle_numpy_flip,
70
            "fliplr": self._handle_numpy_fliplr,
71
            "flipud": self._handle_numpy_flipud,
72
            "reshape": self._handle_numpy_reshape,
73
            "einsum": self._handle_numpy_einsum,
74
            "copy": self._handle_numpy_copy_func,
75
            "split": self._handle_numpy_split,
76
        }
77

78
    # Expose parent properties for convenience
79
    @property
4✔
80
    def tensor_table(self):
4✔
81
        return self._ev.tensor_table
4✔
82

83
    @property
4✔
84
    def builder(self):
4✔
85
        return self._ev.builder
4✔
86

87
    @property
4✔
88
    def container_table(self):
4✔
89
        return self._ev.container_table
4✔
90

91
    @property
4✔
92
    def globals_dict(self):
4✔
93
        return self._ev.globals_dict
4✔
94

95
    @property
4✔
96
    def shapes_runtime_info(self):
4✔
97
        return self._ev.shapes_runtime_info
4✔
98

99
    @property
4✔
100
    def memory_handler(self):
4✔
101
        """Access the memory handler owned by the parser."""
102
        return self._ev.memory_handler
4✔
103

104
    def _get_unique_id(self):
4✔
105
        return self._ev._get_unique_id()
4✔
106

107
    def _add_read(self, block, expr_str, debug_info=None):
4✔
108
        return self._ev._add_read(block, expr_str, debug_info)
4✔
109

110
    def _is_int(self, operand):
4✔
111
        return self._ev._is_int(operand)
4✔
112

113
    def visit(self, node):
4✔
114
        return self._ev.visit(node)
4✔
115

116
    # ========== Linear Algebra Helper Methods (from LinearAlgebraHandler) ==========
117

118
    def parse_arg(self, node):
4✔
119
        """Parse an array argument, returning (name, start_indices, slice_shape, indices).
120

121
        Returns None for 0-d arrays since they are scalars, not valid array operands
122
        for linear algebra operations.
123
        """
124
        if isinstance(node, ast.Name):
4✔
125
            if node.id in self.tensor_table:
4✔
126
                shape = self.tensor_table[node.id].shape
4✔
127
                # Reject 0-d arrays (scalars) - not valid for linalg ops
128
                if len(shape) == 0:
4✔
129
                    return None, None, None, None
×
130
                return node.id, [], shape, []
4✔
131
        elif isinstance(node, ast.Subscript):
4✔
132
            if isinstance(node.value, ast.Name) and node.value.id in self.tensor_table:
4✔
133
                name = node.value.id
4✔
134
                indices = []
4✔
135
                if isinstance(node.slice, ast.Tuple):
4✔
136
                    indices = node.slice.elts
4✔
137
                else:
138
                    indices = [node.slice]
4✔
139

140
                start_indices = []
4✔
141
                slice_shape = []
4✔
142

143
                for i, idx in enumerate(indices):
4✔
144
                    if isinstance(idx, ast.Slice):
4✔
145
                        start = "0"
4✔
146
                        if idx.lower:
4✔
147
                            start = self._ev.visit(idx.lower)
4✔
148
                        start_indices.append(start)
4✔
149

150
                        shapes = self.tensor_table[name].shape
4✔
151
                        dim_size = (
4✔
152
                            shapes[i] if i < len(shapes) else f"_{name}_shape_{i}"
153
                        )
154
                        stop = dim_size
4✔
155
                        if idx.upper:
4✔
156
                            stop = self._ev.visit(idx.upper)
4✔
157

158
                        size = f"({stop} - {start})"
4✔
159
                        slice_shape.append(size)
4✔
160
                    else:
161
                        if isinstance(idx, ast.Name) and idx.id in self.tensor_table:
4✔
162
                            # This is an array index (gather operation)
163
                            return None, None, None, None
4✔
164
                        val = self._ev.visit(idx)
4✔
165
                        start_indices.append(val)
4✔
166

167
                return name, start_indices, slice_shape, indices
4✔
168

169
        return None, None, None, None
4✔
170

171
    def flatten_subset(self, name, start_indices):
4✔
172
        """Convert multi-dimensional start indices to a flattened linear offset."""
173
        if not start_indices:
4✔
174
            return []
4✔
175
        info = self.tensor_table[name]
4✔
176
        shapes = info.shape
4✔
177
        ndim = len(info.shape)
4✔
178

179
        if len(start_indices) != ndim:
4✔
180
            return start_indices
4✔
181

182
        strides = []
4✔
183
        current_stride = "1"
4✔
184
        strides.append(current_stride)
4✔
185
        for i in range(ndim - 1, 0, -1):
4✔
186
            dim_size = shapes[i]
4✔
187
            if current_stride == "1":
4✔
188
                current_stride = str(dim_size)
4✔
189
            else:
190
                current_stride = f"({current_stride} * {dim_size})"
4✔
191
            strides.append(current_stride)
4✔
192
        strides = list(reversed(strides))
4✔
193

194
        offset = "0"
4✔
195
        for i in range(ndim):
4✔
196
            idx = start_indices[i]
4✔
197
            stride = strides[i]
4✔
198
            term = f"({idx} * {stride})" if stride != "1" else idx
4✔
199
            if offset == "0":
4✔
200
                offset = term
4✔
201
            else:
202
                offset = f"({offset} + {term})"
4✔
203

204
        return [offset]
4✔
205

206
    def is_gemm(self, node):
4✔
207
        """Check if a node represents a GEMM operation (matrix multiplication)."""
208
        if isinstance(node, ast.BinOp) and isinstance(node.op, ast.MatMult):
4✔
209
            return True
4✔
210
        if isinstance(node, ast.Call):
4✔
211
            if isinstance(node.func, ast.Attribute) and node.func.attr == "dot":
4✔
212
                return True
×
213
            if isinstance(node.func, ast.Name) and node.func.id == "dot":
4✔
214
                return True
×
215
            if isinstance(node.func, ast.Attribute) and node.func.attr == "matmul":
4✔
216
                return True
×
217
            if isinstance(node.func, ast.Name) and node.func.id == "matmul":
4✔
218
                return True
×
219
        if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add):
4✔
220
            return self.is_gemm(node.left) or self.is_gemm(node.right)
4✔
221
        if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Mult):
4✔
222
            return self.is_gemm(node.left) or self.is_gemm(node.right)
4✔
223
        return False
4✔
224

225
    def _is_stride_1(self, name, indices):
4✔
226
        """Check if the sliced dimension has stride 1 (contiguous access)."""
227
        if name not in self.tensor_table:
4✔
228
            return True
×
229
        info = self.tensor_table[name]
4✔
230
        ndim = len(info.shape)
4✔
231

232
        if not indices:
4✔
233
            return True
4✔
234

235
        sliced_dim = -1
4✔
236
        for i, idx in enumerate(indices):
4✔
237
            if isinstance(idx, ast.Slice):
4✔
238
                sliced_dim = i
4✔
239
                break
4✔
240

241
        if sliced_dim == -1:
4✔
242
            if len(indices) < ndim:
×
243
                sliced_dim = ndim - 1
×
244
            else:
245
                return True
×
246

247
        return sliced_dim == ndim - 1
4✔
248

249
    def _is_target(self, node, target_name):
4✔
250
        """Check if node refers to the target."""
251
        if isinstance(target_name, ast.AST):
4✔
252
            return self._ev.visit(node) == self._ev.visit(target_name)
4✔
253

254
        if isinstance(node, ast.Name) and node.id == target_name:
4✔
255
            return True
×
256
        if isinstance(node, ast.Subscript):
4✔
257
            if isinstance(node.value, ast.Name) and node.value.id == target_name:
4✔
258
                return True
4✔
259
        return False
4✔
260

261
    def _is_dot_call(self, node):
4✔
262
        """Check if node is a dot product call."""
263
        if isinstance(node, ast.Call):
4✔
264
            if isinstance(node.func, ast.Attribute) and node.func.attr == "dot":
×
265
                return True
×
266
            if isinstance(node.func, ast.Name) and node.func.id == "dot":
×
267
                return True
×
268
        if isinstance(node, ast.BinOp) and isinstance(node.op, ast.MatMult):
4✔
269
            return True
4✔
270
        return False
4✔
271

272
    def handle_gemm(self, target, value_node):
4✔
273
        """Handle GEMM (General Matrix Multiply) operations: C = alpha * A @ B + beta * C."""
274
        target_name = None
4✔
275
        target_subset = []
4✔
276

277
        if isinstance(target, str):
4✔
278
            target_name = target
4✔
279
        elif isinstance(target, ast.Name):
4✔
280
            target_name = target.id
4✔
281
        elif isinstance(target, ast.Subscript):
4✔
282
            if isinstance(target.value, ast.Name):
4✔
283
                res = self.parse_arg(target)
4✔
284
                if res[0]:
4✔
285
                    target_name = res[0]
4✔
286
                    target_subset = self.flatten_subset(target_name, res[1])
4✔
287
                else:
288
                    target_name = target.value.id
×
289

290
        if not target_name or target_name not in self.tensor_table:
4✔
291
            return False
4✔
292

293
        alpha = "1.0"
4✔
294
        beta = "0.0"
4✔
295
        A = None
4✔
296
        B = None
4✔
297

298
        def extract_factor(node):
4✔
299
            if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Mult):
4✔
300
                if self.is_gemm(node.left):
×
301
                    return node.left, self._ev.visit(node.right)
×
302
                if self.is_gemm(node.right):
×
303
                    return node.right, self._ev.visit(node.left)
×
304

305
                res = self.parse_arg(node.left)
×
306
                if res[0]:
×
307
                    return node.left, self._ev.visit(node.right)
×
308
                res = self.parse_arg(node.right)
×
309
                if res[0]:
×
310
                    return node.right, self._ev.visit(node.left)
×
311
            return node, "1.0"
4✔
312

313
        def parse_term(node):
4✔
314
            if isinstance(node, ast.BinOp) and isinstance(node.op, ast.MatMult):
4✔
315
                l, l_f = extract_factor(node.left)
4✔
316
                r, r_f = extract_factor(node.right)
4✔
317
                f = "1.0"
4✔
318
                if l_f != "1.0":
4✔
319
                    f = l_f
×
320
                if r_f != "1.0":
4✔
321
                    if f == "1.0":
×
322
                        f = r_f
×
323
                    else:
324
                        f = f"({f} * {r_f})"
×
325
                return l, r, f
4✔
326

327
            if isinstance(node, ast.Call):
×
328
                is_gemm_call = False
×
329
                if isinstance(node.func, ast.Attribute) and node.func.attr in [
×
330
                    "dot",
331
                    "matmul",
332
                ]:
333
                    is_gemm_call = True
×
334
                if isinstance(node.func, ast.Name) and node.func.id in [
×
335
                    "dot",
336
                    "matmul",
337
                ]:
338
                    is_gemm_call = True
×
339

340
                if is_gemm_call and len(node.args) == 2:
×
341
                    return node.args[0], node.args[1], "1.0"
×
342

343
            if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Mult):
×
344
                l, r, a = parse_term(node.left)
×
345
                if l:
×
346
                    return l, r, self._ev.visit(node.right)
×
347
                l, r, a = parse_term(node.right)
×
348
                if l:
×
349
                    return l, r, self._ev.visit(node.left)
×
350

351
            return None, None, None
×
352

353
        if isinstance(value_node, ast.BinOp) and isinstance(value_node.op, ast.Add):
4✔
354
            l, r, a = parse_term(value_node.left)
×
355
            if l:
×
356
                A = l
×
357
                B = r
×
358
                alpha = a
×
359
                if isinstance(value_node.right, ast.BinOp) and isinstance(
×
360
                    value_node.right.op, ast.Mult
361
                ):
362
                    if self._is_target(value_node.right.left, target_name):
×
363
                        beta = self._ev.visit(value_node.right.right)
×
364
                    elif self._is_target(value_node.right.right, target_name):
×
365
                        beta = self._ev.visit(value_node.right.left)
×
366
                elif self._is_target(value_node.right, target_name):
×
367
                    beta = "1.0"
×
368
            else:
369
                l, r, a = parse_term(value_node.right)
×
370
                if l:
×
371
                    A = l
×
372
                    B = r
×
373
                    alpha = a
×
374
                    if isinstance(value_node.left, ast.BinOp) and isinstance(
×
375
                        value_node.left.op, ast.Mult
376
                    ):
377
                        if self._is_target(value_node.left.left, target_name):
×
378
                            beta = self._ev.visit(value_node.left.right)
×
379
                        elif self._is_target(value_node.left.right, target_name):
×
380
                            beta = self._ev.visit(value_node.left.left)
×
381
                    elif self._is_target(value_node.left, target_name):
×
382
                        beta = "1.0"
×
383
        else:
384
            l, r, a = parse_term(value_node)
4✔
385
            if l:
4✔
386
                A = l
4✔
387
                B = r
4✔
388
                alpha = a
4✔
389

390
        if A is None or B is None:
4✔
391
            return False
×
392

393
        def get_name_and_trans(node):
4✔
394
            if isinstance(node, ast.Attribute) and node.attr == "T":
4✔
395
                return node.value, True
×
396
            return node, False
4✔
397

398
        A_node, trans_a = get_name_and_trans(A)
4✔
399
        B_node, trans_b = get_name_and_trans(B)
4✔
400

401
        if self.is_gemm(A_node):
4✔
402
            tmp_name = self._ev.visit(A_node)
×
403
            A_node = ast.Name(id=tmp_name)
×
404

405
        if self.is_gemm(B_node):
4✔
406
            tmp_name = self._ev.visit(B_node)
×
407
            B_node = ast.Name(id=tmp_name)
×
408

409
        res_a = self.parse_arg(A_node)
4✔
410
        res_b = self.parse_arg(B_node)
4✔
411

412
        if not res_a[0] or not res_b[0]:
4✔
413
            return False
4✔
414

415
        A_name, subset_a, shape_a, indices_a = res_a
4✔
416
        B_name, subset_b, shape_b, indices_b = res_b
4✔
417

418
        flat_subset_a = self.flatten_subset(A_name, subset_a)
4✔
419
        flat_subset_b = self.flatten_subset(B_name, subset_b)
4✔
420

421
        def get_ndim(name):
4✔
422
            if name not in self.tensor_table:
4✔
423
                return 1
×
424
            return len(self.tensor_table[name].shape)
4✔
425

426
        if len(shape_a) == 2:
4✔
427
            if not trans_a:
4✔
428
                m = shape_a[0]
4✔
429
                k = shape_a[1]
4✔
430
            else:
431
                m = shape_a[1]
×
432
                k = shape_a[0]
×
433
        else:
434
            m = "1"
4✔
435
            k = shape_a[0]
4✔
436
            if self._is_stride_1(A_name, indices_a):
4✔
437
                if get_ndim(A_name) == 1:
×
438
                    trans_a = True
×
439
                else:
440
                    trans_a = False
×
441
            else:
442
                trans_a = True
4✔
443

444
        if len(shape_b) == 2:
4✔
445
            if not trans_b:
4✔
446
                n = shape_b[1]
4✔
447
            else:
448
                n = shape_b[0]
×
449
        else:
450
            n = "1"
4✔
451
            if self._is_stride_1(B_name, indices_b):
4✔
452
                if get_ndim(B_name) == 1:
4✔
453
                    trans_b = False
4✔
454
                else:
455
                    trans_b = True
×
456
            else:
457
                trans_b = False
×
458

459
        def get_ld(name):
4✔
460
            if name not in self.tensor_table:
4✔
461
                return ""
×
462
            shapes = self.tensor_table[name].shape
4✔
463
            if len(shapes) >= 2:
4✔
464
                return str(shapes[-1])
4✔
465
            return "1"
4✔
466

467
        lda = get_ld(A_name)
4✔
468
        ldb = get_ld(B_name)
4✔
469

470
        ldc = ""
4✔
471
        if target_name:
4✔
472
            if get_ndim(target_name) == 1 and m == "1":
4✔
473
                ldc = n
4✔
474
            else:
475
                ldc = get_ld(target_name)
4✔
476

477
        self.builder.add_gemm(
4✔
478
            A_name,
479
            B_name,
480
            target_name,
481
            alpha,
482
            beta,
483
            m,
484
            n,
485
            k,
486
            trans_a,
487
            trans_b,
488
            flat_subset_a,
489
            flat_subset_b,
490
            target_subset,
491
            lda,
492
            ldb,
493
            ldc,
494
        )
495
        return True
4✔
496

497
    def handle_dot(self, target, value_node):
4✔
498
        """Handle dot product operations for 1D vectors."""
499
        dot_node = None
4✔
500
        is_accumulate = False
4✔
501

502
        if self._is_dot_call(value_node):
4✔
503
            dot_node = value_node
4✔
504
        elif isinstance(value_node, ast.BinOp) and isinstance(value_node.op, ast.Add):
4✔
505
            if self._is_dot_call(value_node.left):
4✔
506
                dot_node = value_node.left
4✔
507
                if self._is_target(value_node.right, target):
4✔
508
                    is_accumulate = True
×
509
            elif self._is_dot_call(value_node.right):
×
510
                dot_node = value_node.right
×
511
                if self._is_target(value_node.left, target):
×
512
                    is_accumulate = True
×
513

514
        if not dot_node:
4✔
515
            return False
×
516

517
        arg0 = None
4✔
518
        arg1 = None
4✔
519

520
        if isinstance(dot_node, ast.Call):
4✔
521
            args = dot_node.args
×
522
            if len(args) != 2:
×
523
                return False
×
524
            arg0 = args[0]
×
525
            arg1 = args[1]
×
526
        elif isinstance(dot_node, ast.BinOp) and isinstance(dot_node.op, ast.MatMult):
4✔
527
            arg0 = dot_node.left
4✔
528
            arg1 = dot_node.right
4✔
529

530
        res_a = self.parse_arg(arg0)
4✔
531
        res_b = self.parse_arg(arg1)
4✔
532

533
        if not res_a[0] or not res_b[0]:
4✔
534
            return False
4✔
535

536
        name_a, subset_a, shape_a, indices_a = res_a
4✔
537
        name_b, subset_b, shape_b, indices_b = res_b
4✔
538

539
        if len(shape_a) != 1 or len(shape_b) != 1:
4✔
540
            return False
4✔
541

542
        n = shape_a[0]
4✔
543

544
        def get_stride(name, indices):
4✔
545
            info = self.tensor_table[name]
4✔
546
            shapes = info.shape
4✔
547
            ndim = len(shapes)
4✔
548

549
            # Determine which dimension is being iterated over (the 1D axis).
550
            sliced_dim = -1
4✔
551
            for i, idx in enumerate(indices):
4✔
552
                if isinstance(idx, ast.Slice):
4✔
553
                    sliced_dim = i
4✔
554
                    break
4✔
555
            if sliced_dim == -1:
4✔
556
                # Whole-array 1D operand: iterate over dimension 0.
557
                sliced_dim = 0
4✔
558

559
            # Prefer the tensor's actual strides so that views (e.g. np.flip,
560
            # np.transpose) with non-contiguous / negative strides are honored.
561
            strides = getattr(info, "strides", None)
4✔
562
            if strides and sliced_dim < len(strides):
4✔
563
                return str(strides[sliced_dim])
4✔
564

565
            # Fallback: contiguous row-major stride.
566
            stride = "1"
×
567
            for i in range(sliced_dim + 1, ndim):
×
568
                dim_size = shapes[i] if i < len(shapes) else f"_{name}_shape_{i}"
×
569
                if stride == "1":
×
570
                    stride = str(dim_size)
×
571
                else:
572
                    stride = f"({stride} * {dim_size})"
×
573
            return stride
×
574

575
        def add_view_offset(name, flat_subset):
4✔
576
            # Add the tensor's base offset (encodes the start element of views
577
            # such as np.flip) to the flattened start-index offset.
578
            info = self.tensor_table[name]
4✔
579
            offset = getattr(info, "offset", "0") or "0"
4✔
580
            if str(offset) in ("0", ""):
4✔
581
                return flat_subset
4✔
582
            if flat_subset:
×
583
                return [f"({flat_subset[0]} + {offset})"]
×
584
            return [str(offset)]
×
585

586
        incx = get_stride(name_a, indices_a)
4✔
587
        incy = get_stride(name_b, indices_b)
4✔
588

589
        flat_subset_a = add_view_offset(name_a, self.flatten_subset(name_a, subset_a))
4✔
590
        flat_subset_b = add_view_offset(name_b, self.flatten_subset(name_b, subset_b))
4✔
591

592
        tmp_res = f"_dot_res_{self._get_unique_id()}"
4✔
593
        self.builder.add_container(tmp_res, Scalar(PrimitiveType.Double), False)
4✔
594
        block = self.builder.add_block()
4✔
595
        constant = self.builder.add_constant(block, "0.0", Scalar(PrimitiveType.Double))
4✔
596
        tasklet = self.builder.add_tasklet(block, TaskletCode.assign, ["_in"], ["_out"])
4✔
597
        self.builder.add_memlet(
4✔
598
            block, constant, "", tasklet, "_in", "", Scalar(PrimitiveType.Double)
599
        )
600
        access = self.builder.add_access(block, tmp_res)
4✔
601
        self.builder.add_memlet(
4✔
602
            block, tasklet, "_out", access, "", "", Scalar(PrimitiveType.Double)
603
        )
604

605
        self.container_table[tmp_res] = Scalar(PrimitiveType.Double)
4✔
606

607
        self.builder.add_dot(
4✔
608
            name_a, name_b, tmp_res, n, incx, incy, flat_subset_a, flat_subset_b
609
        )
610

611
        target_str = target if isinstance(target, str) else self._ev.visit(target)
4✔
612

613
        if not self.builder.exists(target_str):
4✔
614
            self.builder.add_container(target_str, Scalar(PrimitiveType.Double), False)
×
615
            self.container_table[target_str] = Scalar(PrimitiveType.Double)
×
616

617
        if is_accumulate:
4✔
618
            self.builder.add_assignment(target_str, f"{target_str} + {tmp_res}")
×
619
        else:
620
            self.builder.add_assignment(target_str, tmp_res)
4✔
621

622
        return True
4✔
623

624
    def is_outer(self, node):
4✔
625
        """Check if a node represents an outer *product* operation.
626

627
        Only ``np.outer(...)`` and ``np.multiply.outer(...)`` are genuine outer
628
        products that can be lowered to a GEMM. Other ufunc outers such as
629
        ``np.add.outer`` or ``np.subtract.outer`` are element-wise outer
630
        operations and must not take this (multiplication) path.
631
        """
632
        if isinstance(node, ast.Call):
4✔
633
            if isinstance(node.func, ast.Attribute) and node.func.attr == "outer":
4✔
634
                # np.<ufunc>.outer(...): func.value is the Attribute naming the
635
                # ufunc (e.g. "add", "multiply"). Only multiplication is a true
636
                # outer product.
637
                if isinstance(node.func.value, ast.Attribute):
4✔
638
                    return node.func.value.attr == "multiply"
4✔
639
                # np.outer(...): func.value is the module name (Name).
640
                return True
4✔
641
            if isinstance(node.func, ast.Name) and node.func.id == "outer":
4✔
642
                return True
×
643
        if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add):
4✔
644
            return self.is_outer(node.left) or self.is_outer(node.right)
4✔
645
        return False
4✔
646

647
    def handle_outer(self, target, value_node):
4✔
648
        """Handle outer product operations."""
649
        target_name = None
4✔
650
        target_subset = []
4✔
651

652
        if isinstance(target, str):
4✔
653
            target_name = target
4✔
654
        elif isinstance(target, ast.Name):
4✔
655
            target_name = target.id
×
656
        elif isinstance(target, ast.Subscript):
4✔
657
            res = self.parse_arg(target)
4✔
658
            if res[0]:
4✔
659
                target_name = res[0]
4✔
660
                target_subset = self.flatten_subset(target_name, res[1])
4✔
661
            else:
662
                if isinstance(target.value, ast.Name):
×
663
                    target_name = target.value.id
×
664

665
        if not target_name:
4✔
666
            return False
×
667

668
        outer_calls = []
4✔
669
        target_found = False
4✔
670
        terms = []
4✔
671

672
        def collect_terms(node):
4✔
673
            if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add):
4✔
674
                collect_terms(node.left)
4✔
675
                collect_terms(node.right)
4✔
676
            else:
677
                terms.append(node)
4✔
678

679
        collect_terms(value_node)
4✔
680

681
        for term in terms:
4✔
682
            if self._is_target(term, target_name):
4✔
683
                target_found = True
4✔
684
            elif self.is_outer(term):
4✔
685
                if len(term.args) != 2:
4✔
686
                    return False
×
687
                outer_calls.append(term)
4✔
688
            else:
689
                return False
×
690

691
        if not outer_calls:
4✔
692
            return False
×
693

694
        parsed_outers = []
4✔
695
        for outer_node in outer_calls:
4✔
696
            arg0 = outer_node.args[0]
4✔
697
            arg1 = outer_node.args[1]
4✔
698

699
            res_a = self.parse_arg(arg0)
4✔
700
            res_b = self.parse_arg(arg1)
4✔
701

702
            if not res_a[0] or not res_b[0]:
4✔
703
                return False
×
704

705
            parsed_outers.append((res_a, res_b))
4✔
706

707
        alpha = "1.0"
4✔
708
        beta = "1.0" if target_found else "0.0"
4✔
709

710
        def get_flattened_size(name, indices, shapes):
4✔
711
            size_expr = "1"
4✔
712
            for s in shapes:
4✔
713
                if size_expr == "1":
4✔
714
                    size_expr = str(s)
4✔
715
                else:
716
                    size_expr = f"({size_expr} * {str(s)})"
×
717
            return size_expr
4✔
718

719
        def get_ld_2d(name):
4✔
720
            if name in self.tensor_table:
4✔
721
                shapes = self.tensor_table[name].shape
4✔
722
                if len(shapes) >= 2:
4✔
723
                    return str(shapes[1])
4✔
724
            return "1"
×
725

726
        ldc = get_ld_2d(target_name)
4✔
727

728
        for res_a, res_b in parsed_outers:
4✔
729
            name_a, subset_a, shape_a, indices_a = res_a
4✔
730
            name_b, subset_b, shape_b, indices_b = res_b
4✔
731

732
            m = get_flattened_size(name_a, indices_a, shape_a)
4✔
733
            n = get_flattened_size(name_b, indices_b, shape_b)
4✔
734
            k = "1"
4✔
735

736
            trans_a = False
4✔
737
            trans_b = True
4✔
738

739
            flat_subset_a = self.flatten_subset(name_a, subset_a)
4✔
740
            flat_subset_b = self.flatten_subset(name_b, subset_b)
4✔
741

742
            lda = "1"
4✔
743
            ldb = "1"
4✔
744

745
            self.builder.add_gemm(
4✔
746
                name_a,
747
                name_b,
748
                target_name,
749
                alpha,
750
                beta,
751
                m,
752
                n,
753
                k,
754
                trans_a,
755
                trans_b,
756
                flat_subset_a,
757
                flat_subset_b,
758
                target_subset,
759
                lda,
760
                ldb,
761
                ldc,
762
            )
763
            beta = "1.0"
4✔
764

765
        return True
4✔
766

767
    # ========== Transpose Operations ==========
768

769
    def _parse_perm(self, node):
4✔
770
        """Parse a permutation list or tuple from an AST node."""
771
        if isinstance(node, (ast.List, ast.Tuple)):
4✔
772
            res = []
4✔
773
            for elt in node.elts:
4✔
774
                val = self._ev.visit(elt)
4✔
775
                res.append(int(val))
4✔
776
            return res
4✔
777
        return []
×
778

779
    def is_transpose(self, node):
4✔
780
        """Check if a node represents a transpose operation."""
781
        # Case 1: np.transpose(arr, ...)
782
        if isinstance(node, ast.Call):
4✔
783
            if isinstance(node.func, ast.Attribute) and node.func.attr == "transpose":
4✔
784
                return True
×
785
            if isinstance(node.func, ast.Name) and node.func.id == "transpose":
4✔
786
                return True
×
787

788
        # Case 2: arr.T
789
        if isinstance(node, ast.Attribute) and node.attr == "T":
4✔
790
            return True
4✔
791

792
        return False
4✔
793

794
    def handle_transpose(self, target, value_node):
4✔
795
        """Handle transpose operations including .T and np.transpose()."""
796
        if not self.is_transpose(value_node):
4✔
797
            return False
×
798

799
        input_node = None
4✔
800
        perm = []
4✔
801

802
        if isinstance(value_node, ast.Attribute) and value_node.attr == "T":
4✔
803
            input_node = value_node.value
4✔
804
            perm = []  # Empty means reverse
4✔
805

806
        elif isinstance(value_node, ast.Call):
×
807
            args = value_node.args
×
808
            keywords = value_node.keywords
×
809

810
            is_numpy_func = False
×
811
            if isinstance(value_node.func, ast.Attribute):
×
812
                caller = ""
×
813
                if isinstance(value_node.func.value, ast.Name):
×
814
                    caller = value_node.func.value.id
×
815
                if caller in ["np", "numpy"]:
×
816
                    is_numpy_func = True
×
817
            elif isinstance(value_node.func, ast.Name):
×
818
                is_numpy_func = True
×
819

820
            if is_numpy_func:
×
821
                if len(args) < 1:
×
822
                    return False
×
823
                input_node = args[0]
×
824
                if len(args) > 1:
×
825
                    perm = self._parse_perm(args[1])
×
826
                for kw in keywords:
×
827
                    if kw.arg == "axes":
×
828
                        perm = self._parse_perm(kw.value)
×
829
            else:
830
                if isinstance(value_node.func, ast.Attribute):
×
831
                    input_node = value_node.func.value
×
832
                else:
833
                    return False
×
834
                if len(args) > 0:
×
835
                    perm = self._parse_perm(args[0])
×
836
                for kw in keywords:
×
837
                    if kw.arg == "axes":
×
838
                        perm = self._parse_perm(kw.value)
×
839

840
        input_name = self._ev.visit(input_node)
4✔
841
        if input_name not in self.tensor_table:
4✔
842
            return False
×
843

844
        in_info = self.tensor_table[input_name]
4✔
845
        in_shape = in_info.shape
4✔
846
        in_strings = [str(s) for s in in_shape]
4✔
847

848
        if not perm:
4✔
849
            perm = list(range(len(in_shape)))[::-1]
4✔
850

851
        out_shape = [in_strings[p] for p in perm]
4✔
852

853
        # Get input strides and check if input is contiguous
854
        in_strides = (
4✔
855
            in_info.strides if hasattr(in_info, "strides") and in_info.strides else None
856
        )
857
        if in_strides is None:
4✔
858
            in_strides = self._compute_strides(in_shape, "C")
×
859

860
        if self._is_contiguous(in_shape, in_strides):
4✔
861
            # For contiguous inputs, output strides are permuted input strides
862
            out_strides = [in_strides[p] for p in perm]
4✔
863
        else:
864
            # For non-contiguous inputs, output is C-order for the new shape
865
            out_strides = self._compute_strides(out_shape, "C")
×
866

867
        target_name = ""
4✔
868
        if isinstance(target, ast.Name):
4✔
869
            target_name = target.id
4✔
870
        elif isinstance(target, str):
×
871
            target_name = target
×
872

873
        dtype = Scalar(PrimitiveType.Double)
4✔
874
        if input_name in self.container_table:
4✔
875
            input_type = self.container_table[input_name]
4✔
876
            if isinstance(input_type, Pointer):
4✔
877
                dtype = input_type.pointee_type
4✔
878
            else:
879
                dtype = input_type
×
880

881
        ptr_type = Pointer(dtype)
4✔
882

883
        # Create target container if it doesn't exist
884
        if not self.builder.exists(target_name):
4✔
885
            self.builder.add_container(target_name, ptr_type, False)
4✔
886
            self.container_table[target_name] = ptr_type
4✔
887
        self.tensor_table[target_name] = Tensor(dtype, out_shape, out_strides)
4✔
888

889
        # Create reference memlet to alias the source array (view, not copy)
890
        block = self.builder.add_block()
4✔
891
        t_src = self.builder.add_access(block, input_name)
4✔
892
        t_dst = self.builder.add_access(block, target_name)
4✔
893
        self.builder.add_reference_memlet(block, t_src, t_dst, "0", ptr_type)
4✔
894

895
        return True
4✔
896

897
    def handle_transpose_expr(self, node):
4✔
898
        """Handle .T attribute access in expressions, returning a temp array name."""
899
        if not isinstance(node, ast.Attribute) or node.attr != "T":
4✔
900
            return None
×
901

902
        input_name = self._ev.visit(node.value)
4✔
903
        if input_name not in self.tensor_table:
4✔
904
            return None
×
905

906
        in_info = self.tensor_table[input_name]
4✔
907
        in_shape = in_info.shape
4✔
908
        perm = list(range(len(in_shape)))[::-1]
4✔
909

910
        return self._create_transpose_view(input_name, perm)
4✔
911

912
    def _handle_numpy_transpose(self, node, func_name):
4✔
913
        """Handle np.transpose(arr, axes=...) function call."""
914
        if len(node.args) < 1:
4✔
915
            raise ValueError("np.transpose requires at least one argument")
×
916

917
        input_node = node.args[0]
4✔
918
        input_name = self.visit(input_node)
4✔
919

920
        if input_name not in self.tensor_table:
4✔
921
            raise ValueError(f"Array {input_name} not found in tensor_table")
×
922

923
        in_info = self.tensor_table[input_name]
4✔
924
        in_shape = in_info.shape
4✔
925

926
        perm = []
4✔
927
        if len(node.args) > 1:
4✔
928
            perm = self._parse_perm(node.args[1])
×
929
        for kw in node.keywords:
4✔
930
            if kw.arg == "axes":
4✔
931
                perm = self._parse_perm(kw.value)
4✔
932

933
        if not perm:
4✔
934
            perm = list(range(len(in_shape)))[::-1]
4✔
935

936
        return self._create_transpose_view(input_name, perm)
4✔
937

938
    def _create_transpose_view(self, input_name, perm):
4✔
939
        in_info = self.tensor_table[input_name]
4✔
940
        in_shape = in_info.shape
4✔
941
        in_strings = [str(s) for s in in_shape]
4✔
942

943
        # Compute output shape by permuting
944
        out_shape = [in_strings[p] for p in perm]
4✔
945

946
        # Get input strides and check if input is contiguous
947
        in_strides = (
4✔
948
            in_info.strides if hasattr(in_info, "strides") and in_info.strides else None
949
        )
950
        if in_strides is None:
4✔
951
            in_strides = self._compute_strides(in_shape, "C")
×
952

953
        # Always permute input strides (works for both contiguous and view inputs)
954
        out_strides = [in_strides[p] for p in perm]
4✔
955

956
        # Inherit offset from input tensor (for chained views like flip->transpose)
957
        in_offset = getattr(in_info, "offset", "0") or "0"
4✔
958

959
        # Create new pointer container
960
        tmp_name = f"_tmp_{self._get_unique_id()}"
4✔
961
        ptr_type = Pointer(in_info.element_type)
4✔
962
        self.builder.add_container(tmp_name, ptr_type, False)
4✔
963
        self.container_table[tmp_name] = ptr_type
4✔
964

965
        # Register tensor with permuted shape, strides, and inherited offset
966
        self.tensor_table[tmp_name] = Tensor(
4✔
967
            in_info.element_type, out_shape, out_strides, in_offset
968
        )
969

970
        # Create reference memlet to alias the source array
971
        block = self.builder.add_block()
4✔
972
        t_src = self.builder.add_access(block, input_name)
4✔
973
        t_dst = self.builder.add_access(block, tmp_name)
4✔
974
        self.builder.add_reference_memlet(block, t_src, t_dst, "0", ptr_type)
4✔
975

976
        return tmp_name
4✔
977

978
    def _handle_numpy_flip(self, node, func_name):
4✔
979
        """Handle np.flip(arr, axis=None) - flip array along specified axis.
980

981
        Uses negative strides and offset to create a view without copying.
982
        """
983
        if len(node.args) < 1:
4✔
984
            raise ValueError("np.flip requires at least one argument")
×
985

986
        input_name = self.visit(node.args[0])
4✔
987
        if input_name not in self.tensor_table:
4✔
988
            raise ValueError(f"Array {input_name} not found in tensor_table")
×
989

990
        in_info = self.tensor_table[input_name]
4✔
991
        in_shape = in_info.shape
4✔
992
        ndim = len(in_shape)
4✔
993

994
        # Parse axis argument
995
        axis = None
4✔
996
        if len(node.args) > 1:
4✔
997
            axis_node = node.args[1]
×
998
            if isinstance(axis_node, ast.Constant):
×
999
                axis = axis_node.value
×
1000
            elif isinstance(axis_node, ast.UnaryOp) and isinstance(
×
1001
                axis_node.op, ast.USub
1002
            ):
1003
                if isinstance(axis_node.operand, ast.Constant):
×
1004
                    axis = -axis_node.operand.value
×
1005
        for kw in node.keywords:
4✔
1006
            if kw.arg == "axis":
4✔
1007
                if isinstance(kw.value, ast.Constant):
4✔
1008
                    axis = kw.value.value
4✔
1009
                elif isinstance(kw.value, ast.UnaryOp) and isinstance(
4✔
1010
                    kw.value.op, ast.USub
1011
                ):
1012
                    if isinstance(kw.value.operand, ast.Constant):
4✔
1013
                        axis = -kw.value.operand.value
4✔
1014

1015
        # Determine which axes to flip
1016
        if axis is None:
4✔
1017
            # Flip all axes
1018
            axes_to_flip = list(range(ndim))
4✔
1019
        else:
1020
            if axis < 0:
4✔
1021
                axis = ndim + axis
4✔
1022
            axes_to_flip = [axis]
4✔
1023

1024
        return self._create_flip_view(input_name, axes_to_flip)
4✔
1025

1026
    def _handle_numpy_fliplr(self, node, func_name):
4✔
1027
        """Handle np.fliplr(arr) - flip array left-right (axis=1)."""
1028
        if len(node.args) < 1:
4✔
1029
            raise ValueError("np.fliplr requires one argument")
×
1030

1031
        input_name = self.visit(node.args[0])
4✔
1032
        if input_name not in self.tensor_table:
4✔
1033
            raise ValueError(f"Array {input_name} not found in tensor_table")
×
1034

1035
        in_info = self.tensor_table[input_name]
4✔
1036
        if len(in_info.shape) < 2:
4✔
1037
            raise ValueError("np.fliplr requires array with ndim >= 2")
×
1038

1039
        return self._create_flip_view(input_name, [1])
4✔
1040

1041
    def _handle_numpy_flipud(self, node, func_name):
4✔
1042
        """Handle np.flipud(arr) - flip array up-down (axis=0)."""
1043
        if len(node.args) < 1:
4✔
1044
            raise ValueError("np.flipud requires one argument")
×
1045

1046
        input_name = self.visit(node.args[0])
4✔
1047
        if input_name not in self.tensor_table:
4✔
1048
            raise ValueError(f"Array {input_name} not found in tensor_table")
×
1049

1050
        return self._create_flip_view(input_name, [0])
4✔
1051

1052
    def _create_flip_view(self, input_name, axes_to_flip):
4✔
1053
        """Create a flipped view of an array using Tensor.flip().
1054

1055
        Uses the Tensor type's flip() method which computes the correct
1056
        negative strides and offset adjustment.
1057
        """
1058
        in_tensor = self.tensor_table[input_name]
4✔
1059

1060
        # Apply flip for each axis
1061
        flipped_tensor = in_tensor
4✔
1062
        for axis in axes_to_flip:
4✔
1063
            flipped_tensor = flipped_tensor.flip(axis)
4✔
1064

1065
        # Create new pointer container pointing to same data
1066
        tmp_name = f"_tmp_{self._get_unique_id()}"
4✔
1067
        ptr_type = Pointer(in_tensor.element_type)
4✔
1068
        self.builder.add_container(tmp_name, ptr_type, False)
4✔
1069
        self.container_table[tmp_name] = ptr_type
4✔
1070

1071
        # Store the flipped tensor with its offset in tensor_table
1072
        self.tensor_table[tmp_name] = flipped_tensor
4✔
1073

1074
        # Create reference memlet (offset is handled by tensor's offset property)
1075
        block = self.builder.add_block()
4✔
1076
        t_src = self.builder.add_access(block, input_name)
4✔
1077
        t_dst = self.builder.add_access(block, tmp_name)
4✔
1078
        self.builder.add_reference_memlet(block, t_src, t_dst, "0", ptr_type)
4✔
1079

1080
        return tmp_name
4✔
1081

1082
    def _handle_numpy_reshape(self, node, func_name):
4✔
1083
        """Handle np.reshape(arr, newshape) - reshape array without copying.
1084

1085
        Only works for contiguous arrays; creates a view with new shape/strides.
1086
        """
1087
        if len(node.args) < 2:
4✔
1088
            raise ValueError("np.reshape requires array and new shape")
×
1089

1090
        input_name = self.visit(node.args[0])
4✔
1091
        if input_name not in self.tensor_table:
4✔
1092
            raise ValueError(f"Array {input_name} not found in tensor_table")
×
1093

1094
        in_info = self.tensor_table[input_name]
4✔
1095
        in_shape = in_info.shape
4✔
1096

1097
        # Parse new shape
1098
        new_shape = self._parse_shape(node.args[1])
4✔
1099

1100
        # Get input strides
1101
        in_strides = (
4✔
1102
            in_info.strides if hasattr(in_info, "strides") and in_info.strides else None
1103
        )
1104
        if in_strides is None:
4✔
1105
            in_strides = self._compute_strides(in_shape, "C")
×
1106

1107
        # Check if input is contiguous (C or F order)
1108
        c_contig = self._is_contiguous(in_shape, in_strides)
4✔
1109
        f_contig = self._is_contiguous_f(in_shape, in_strides)
4✔
1110

1111
        if c_contig:
4✔
1112
            out_strides = self._compute_strides(new_shape, "C")
4✔
1113
        elif f_contig:
×
1114
            out_strides = self._compute_strides(new_shape, "F")
×
1115
        else:
1116
            # Non-contiguous array cannot be reshaped without copy
1117
            raise NotImplementedError(
×
1118
                "np.reshape on non-contiguous array not supported (would require copy)"
1119
            )
1120

1121
        # Create new pointer container
1122
        tmp_name = f"_tmp_{self._get_unique_id()}"
4✔
1123
        ptr_type = Pointer(in_info.element_type)
4✔
1124
        self.builder.add_container(tmp_name, ptr_type, False)
4✔
1125
        self.container_table[tmp_name] = ptr_type
4✔
1126

1127
        # Register tensor with new shape and computed strides
1128
        self.tensor_table[tmp_name] = Tensor(
4✔
1129
            in_info.element_type, new_shape, out_strides
1130
        )
1131

1132
        # Create reference memlet to alias the source array (view, no copy)
1133
        block = self.builder.add_block()
4✔
1134
        t_src = self.builder.add_access(block, input_name)
4✔
1135
        t_dst = self.builder.add_access(block, tmp_name)
4✔
1136
        self.builder.add_reference_memlet(block, t_src, t_dst, "0", ptr_type)
4✔
1137

1138
        return tmp_name
4✔
1139

1140
    def _handle_numpy_split(self, node, func_name):
4✔
1141
        """Handle np.split(ary, sections, axis=0) -> list of sub-array views.
1142

1143
        Only the "integer number of equal sections" form is supported (which is
1144
        what pylulesh uses, e.g. ``np.split(x[:, idx], 6, axis=1)``). Each
1145
        section is produced as a strided slice view along ``axis`` (no copy),
1146
        keeping the split dimension (matching NumPy semantics). Splitting at an
1147
        explicit list of indices, a non-constant number of sections, a
1148
        non-constant axis, or an axis length not evenly divisible by the number
1149
        of sections is not supported and raises clearly.
1150
        """
1151
        if len(node.args) < 2:
4✔
NEW
1152
            raise NotImplementedError(
×
1153
                "np.split requires (array, sections[, axis]) arguments"
1154
            )
1155

1156
        def _const_int(n):
4✔
1157
            if isinstance(n, ast.Constant) and isinstance(n.value, int):
4✔
1158
                return n.value
4✔
1159
            if isinstance(n, ast.UnaryOp) and isinstance(n.op, ast.USub):
4✔
1160
                inner = _const_int(n.operand)
4✔
1161
                return None if inner is None else -inner
4✔
1162
            return None
4✔
1163

1164
        # Parse the number of sections (must be a constant integer).
1165
        sections = _const_int(node.args[1])
4✔
1166
        if sections is None:
4✔
1167
            raise NotImplementedError(
4✔
1168
                "np.split is only supported with a constant integer number of "
1169
                "equal sections (splitting at an explicit list of indices is "
1170
                "not supported)"
1171
            )
1172
        if sections <= 0:
4✔
NEW
1173
            raise NotImplementedError("np.split: number of sections must be positive")
×
1174

1175
        # Parse the axis (positional 3rd arg or `axis=` keyword), default 0.
1176
        axis_node = node.args[2] if len(node.args) >= 3 else None
4✔
1177
        for kw in node.keywords:
4✔
1178
            if kw.arg == "axis":
4✔
1179
                axis_node = kw.value
4✔
1180
            else:
NEW
1181
                raise NotImplementedError(
×
1182
                    f"np.split keyword argument '{kw.arg}' is not supported"
1183
                )
1184
        axis = 0
4✔
1185
        if axis_node is not None:
4✔
1186
            axis = _const_int(axis_node)
4✔
1187
            if axis is None:
4✔
NEW
1188
                raise NotImplementedError("np.split axis must be a constant integer")
×
1189

1190
        # Materialize the array operand once, then slice it repeatedly.
1191
        ary_name = self.visit(node.args[0])
4✔
1192
        if ary_name not in self.tensor_table:
4✔
NEW
1193
            raise NotImplementedError("np.split argument must be an array")
×
1194
        tensor = self.tensor_table[ary_name]
4✔
1195
        shape = tensor.shape
4✔
1196
        ndim = len(shape)
4✔
1197
        if axis < 0:
4✔
1198
            axis += ndim
4✔
1199
        if axis < 0 or axis >= ndim:
4✔
NEW
1200
            raise NotImplementedError(
×
1201
                f"np.split axis {axis} out of range for {ndim}-D array"
1202
            )
1203

1204
        # The axis length may be a compile-time constant or a runtime-symbolic
1205
        # expression. NumPy requires it to be evenly divisible by `sections`
1206
        # (raising otherwise at runtime); we assume that holds and build each
1207
        # section as a zero-copy view of shape `[..., step, ...]` where
1208
        # `step = axis_len // sections`, offset by `j*step` along `axis`. Shapes
1209
        # and offsets are kept symbolic in terms of the source array's own
1210
        # shape/stride symbols so the (return-)shape marshaling can evaluate
1211
        # them. If the axis length is a known constant, verify divisibility
1212
        # eagerly for a clearer error.
1213
        axis_len_str = str(shape[axis])
4✔
1214
        if not axis_len_str:
4✔
NEW
1215
            raise NotImplementedError("np.split requires a determinable axis length")
×
1216
        try:
4✔
1217
            axis_len_int = int(axis_len_str)
4✔
1218
        except ValueError:
4✔
1219
            axis_len_int = None
4✔
1220
        if axis_len_int is not None and axis_len_int % sections != 0:
4✔
1221
            raise NotImplementedError(
4✔
1222
                f"np.split: axis length {axis_len_int} is not evenly divisible "
1223
                f"into {sections} sections (uneven split / np.array_split is not "
1224
                "supported)"
1225
            )
1226
        step_str = f"idiv({axis_len_str}, {sections})"
4✔
1227

1228
        dtype = tensor.element_type
4✔
1229
        in_strides = (
4✔
1230
            tensor.strides
1231
            if hasattr(tensor, "strides") and tensor.strides
1232
            else self._compute_strides(shape, "C")
1233
        )
1234
        in_offset = getattr(tensor, "offset", "0") or "0"
4✔
1235
        stride_axis = in_strides[axis]
4✔
1236

1237
        results = []
4✔
1238
        for j in range(sections):
4✔
1239
            out_shape = list(shape)
4✔
1240
            out_shape[axis] = step_str
4✔
1241
            out_strides = list(in_strides)
4✔
1242

1243
            if j == 0:
4✔
1244
                out_offset = in_offset
4✔
1245
            else:
1246
                term = f"(({j}) * {step_str} * {stride_axis})"
4✔
1247
                out_offset = (
4✔
1248
                    term if in_offset in ("0", "") else f"({in_offset} + {term})"
1249
                )
1250

1251
            tmp_name = f"_tmp_{self._get_unique_id()}"
4✔
1252
            ptr_type = Pointer(dtype)
4✔
1253
            self.builder.add_container(tmp_name, ptr_type, False)
4✔
1254
            self.container_table[tmp_name] = ptr_type
4✔
1255
            self.tensor_table[tmp_name] = Tensor(
4✔
1256
                dtype, out_shape, out_strides, out_offset
1257
            )
1258

1259
            block = self.builder.add_block()
4✔
1260
            t_src = self.builder.add_access(block, ary_name)
4✔
1261
            t_dst = self.builder.add_access(block, tmp_name)
4✔
1262
            self.builder.add_reference_memlet(block, t_src, t_dst, "0", ptr_type)
4✔
1263

1264
            results.append(tmp_name)
4✔
1265

1266
        return results
4✔
1267

1268
    def _parse_shape(self, shape_node):
4✔
1269
        """Parse a shape argument (tuple, list, single int, or a variable
1270
        bound to a tuple/list)."""
1271
        elts = self._ev._resolve_tuple(shape_node)
4✔
1272
        if elts is not None:
4✔
1273
            result = []
4✔
1274
            for elt in elts:
4✔
1275
                if isinstance(elt, ast.Constant):
4✔
1276
                    result.append(str(elt.value))
4✔
1277
                elif isinstance(elt, ast.Name):
4✔
1278
                    result.append(elt.id)
×
1279
                elif isinstance(elt, ast.UnaryOp) and isinstance(elt.op, ast.USub):
4✔
1280
                    if isinstance(elt.operand, ast.Constant):
×
1281
                        result.append(str(-elt.operand.value))
×
1282
                else:
1283
                    result.append(self._shape_to_runtime_expr(elt))
4✔
1284
            return result
4✔
1285
        elif isinstance(shape_node, ast.Constant):
×
1286
            return [str(shape_node.value)]
×
1287
        elif isinstance(shape_node, ast.Name):
×
NEW
1288
            raise NotImplementedError(
×
1289
                "Shape variable not supported unless it holds a tuple/list literal"
1290
            )
1291
        else:
1292
            raise ValueError(f"Cannot parse shape: {ast.dump(shape_node)}")
×
1293

1294
    def _is_contiguous_f(self, shape, strides):
4✔
1295
        """Check if array is F-order contiguous."""
1296
        if not shape or not strides:
4✔
1297
            return True
×
1298
        f_strides = self._compute_strides(shape, "F")
4✔
1299
        return self._strides_equal(strides, f_strides)
4✔
1300

1301
    def handle_numpy_call(self, node, func_name):
4✔
1302
        if func_name in self.function_handlers:
4✔
1303
            return self.function_handlers[func_name](node, func_name)
4✔
1304
        raise NotImplementedError(f"NumPy function {func_name} not supported")
×
1305

1306
    def has_handler(self, func_name):
4✔
1307
        return func_name in self.function_handlers
4✔
1308

1309
    def handle_array_unary_op(self, op_type, operand):
4✔
1310
        dtype = self._ev._element_type(operand)
4✔
1311
        if operand in self.tensor_table:
4✔
1312
            tensor = self.tensor_table[operand]
4✔
1313
        else:
1314
            tensor = Tensor(dtype, [])
4✔
1315

1316
        if len(tensor.shape) == 0:
4✔
1317
            tmp_name = self._create_array_temp([], dtype)
4✔
1318

1319
            func_map = {
4✔
1320
                "sqrt": CMathFunction.sqrt,
1321
                "abs": CMathFunction.fabs,
1322
                "absolute": CMathFunction.fabs,
1323
                "exp": CMathFunction.exp,
1324
                "tanh": CMathFunction.tanh,
1325
                "cbrt": CMathFunction.cbrt,
1326
            }
1327

1328
            block = self.builder.add_block()
4✔
1329
            t_src = self.builder.add_access(block, operand)
4✔
1330
            t_dst = self.builder.add_access(block, tmp_name)
4✔
1331
            t_task = self.builder.add_cmath(
4✔
1332
                block, func_map[op_type], dtype.primitive_type
1333
            )
1334

1335
            self.builder.add_memlet(block, t_src, "void", t_task, "_in1", "", dtype)
4✔
1336
            self.builder.add_memlet(block, t_task, "_out", t_dst, "void", "", dtype)
4✔
1337

1338
            return tmp_name
4✔
1339

1340
        output_strides = self._get_contiguous_output_strides(
4✔
1341
            tensor.shape, tensor.strides
1342
        )
1343
        tmp_name = self._create_array_temp(tensor.shape, dtype, strides=output_strides)
4✔
1344
        tmp_tensor = self.tensor_table[tmp_name]
4✔
1345
        self.builder.add_elementwise_unary_op(
4✔
1346
            op_type, operand, tensor, tmp_name, tmp_tensor
1347
        )
1348

1349
        return tmp_name
4✔
1350

1351
    def handle_array_binary_op(self, op_type, left, right):
4✔
1352
        # Determine if operands are arrays or scalars
1353
        # NumPy 0-d arrays (shape=[]) ARE arrays for promotion purposes
1354
        # Only literals and Python scalars (not in tensor_table) are treated as scalars
1355
        left_is_array = left in self.tensor_table
4✔
1356
        right_is_array = right in self.tensor_table
4✔
1357

1358
        dtype_left = self._ev._element_type(left)
4✔
1359
        dtype_right = self._ev._element_type(right)
4✔
1360

1361
        # Use NumPy promotion rules: scalars adapt to arrays
1362
        dtype = numpy_promote_types(
4✔
1363
            dtype_left, left_is_array, dtype_right, right_is_array
1364
        )
1365

1366
        # Cast operands to result type if needed
1367
        real_left = self._cast_to_type(left, dtype)
4✔
1368
        real_right = self._cast_to_type(right, dtype)
4✔
1369

1370
        # Get tensor info for the (possibly casted) operands
1371
        if real_left in self.tensor_table:
4✔
1372
            left_tensor = self.tensor_table[real_left]
4✔
1373
        else:
1374
            left_tensor = Tensor(dtype, [])
4✔
1375

1376
        if real_right in self.tensor_table:
4✔
1377
            right_tensor = self.tensor_table[real_right]
4✔
1378
        else:
1379
            right_tensor = Tensor(dtype, [])
4✔
1380

1381
        left_shape = left_tensor.shape
4✔
1382
        right_shape = right_tensor.shape
4✔
1383

1384
        # Compute broadcast output shape
1385
        output_shape = self._compute_broadcast_shape(left_shape, right_shape)
4✔
1386

1387
        # Check if broadcasting is needed
1388
        left_needs_broadcast = (
4✔
1389
            self._needs_broadcast(left_shape, output_shape) if left_shape else False
1390
        )
1391
        right_needs_broadcast = (
4✔
1392
            self._needs_broadcast(right_shape, output_shape) if right_shape else False
1393
        )
1394

1395
        real_left_tensor = left_tensor
4✔
1396
        real_right_tensor = right_tensor
4✔
1397

1398
        # Broadcast left operand if needed (stride-based, no copy)
1399
        if left_needs_broadcast:
4✔
1400
            left_strides = left_tensor.strides if left_tensor.strides else []
4✔
1401
            broadcast_strides = self._compute_broadcast_strides(
4✔
1402
                left_shape, left_strides, output_shape
1403
            )
1404
            # Create a new tensor view with broadcast shape and strides
1405
            # Preserve the offset from the original tensor (important for views like flip)
1406
            left_offset = left_tensor.offset if left_tensor.offset else "0"
4✔
1407
            real_left_tensor = Tensor(
4✔
1408
                dtype, output_shape, broadcast_strides, left_offset
1409
            )
1410

1411
        # Broadcast right operand if needed (stride-based, no copy)
1412
        if right_needs_broadcast:
4✔
1413
            right_strides = right_tensor.strides if right_tensor.strides else []
4✔
1414
            broadcast_strides = self._compute_broadcast_strides(
4✔
1415
                right_shape, right_strides, output_shape
1416
            )
1417
            # Create a new tensor view with broadcast shape and strides
1418
            # Preserve the offset from the original tensor (important for views like flip)
1419
            right_offset = right_tensor.offset if right_tensor.offset else "0"
4✔
1420
            real_right_tensor = Tensor(
4✔
1421
                dtype, output_shape, broadcast_strides, right_offset
1422
            )
1423

1424
        # Create output array with broadcast shape
1425
        # Preserve F-order if both inputs are F-order and no broadcasting needed
1426
        if not left_needs_broadcast and not right_needs_broadcast:
4✔
1427
            # Use left tensor strides to determine output order
1428
            output_strides = self._get_contiguous_output_strides(
4✔
1429
                output_shape, left_tensor.strides
1430
            )
1431
        else:
1432
            output_strides = self._compute_strides(output_shape, "C")
4✔
1433
        tmp_name = self._create_array_temp(output_shape, dtype, strides=output_strides)
4✔
1434
        tmp_tensor = self.tensor_table[tmp_name]
4✔
1435

1436
        self.builder.add_elementwise_op(
4✔
1437
            op_type,
1438
            real_left,
1439
            real_left_tensor,
1440
            real_right,
1441
            real_right_tensor,
1442
            tmp_name,
1443
            tmp_tensor,
1444
        )
1445

1446
        return tmp_name
4✔
1447

1448
    def handle_array_negate(self, operand):
4✔
1449
        operand_tensor = self.tensor_table[operand]
4✔
1450
        dtype = self._ev._element_type(operand)
4✔
1451

1452
        output_strides = self._get_contiguous_output_strides(
4✔
1453
            operand_tensor.shape, operand_tensor.strides
1454
        )
1455
        tmp_name = self._create_array_temp(
4✔
1456
            operand_tensor.shape, dtype, strides=output_strides
1457
        )
1458
        tmp_tensor = self.tensor_table[tmp_name]
4✔
1459

1460
        zero_name = f"_tmp_{self._get_unique_id()}"
4✔
1461
        self.builder.add_container(zero_name, dtype, False)
4✔
1462
        self.container_table[zero_name] = dtype
4✔
1463
        self.tensor_table[zero_name] = Tensor(dtype, [])
4✔
1464

1465
        zero_block = self.builder.add_block()
4✔
1466
        t_const = self.builder.add_constant(
4✔
1467
            zero_block,
1468
            "0.0" if dtype.primitive_type == PrimitiveType.Double else "0",
1469
            dtype,
1470
        )
1471
        t_zero = self.builder.add_access(zero_block, zero_name)
4✔
1472
        t_assign = self.builder.add_tasklet(
4✔
1473
            zero_block, TaskletCode.assign, ["_in"], ["_out"]
1474
        )
1475
        self.builder.add_memlet(zero_block, t_const, "void", t_assign, "_in", "")
4✔
1476
        self.builder.add_memlet(zero_block, t_assign, "_out", t_zero, "void", "")
4✔
1477

1478
        zero_tensor = self.tensor_table[zero_name]
4✔
1479
        self.builder.add_elementwise_op(
4✔
1480
            "sub", zero_name, zero_tensor, operand, operand_tensor, tmp_name, tmp_tensor
1481
        )
1482

1483
        return tmp_name
4✔
1484

1485
    def handle_array_compare(self, left, op, right, left_is_array, right_is_array):
4✔
1486
        """Handle elementwise comparison of arrays, returning a boolean array."""
1487
        if left_is_array:
4✔
1488
            shape = self.tensor_table[left].shape
4✔
1489
            arr_name = left
4✔
1490
        else:
1491
            shape = self.tensor_table[right].shape
×
1492
            arr_name = right
×
1493

1494
        use_int_cmp = False
4✔
1495
        arr_dtype = self._ev._element_type(arr_name)
4✔
1496
        if arr_dtype.primitive_type in (PrimitiveType.Int32, PrimitiveType.Int64):
4✔
1497
            use_int_cmp = True
4✔
1498

1499
        dtype = Scalar(PrimitiveType.Bool)
4✔
1500
        tmp_name = self._create_array_temp(shape, dtype)
4✔
1501

1502
        if use_int_cmp:
4✔
1503
            cmp_ops = {
4✔
1504
                ">": TaskletCode.int_sgt,
1505
                ">=": TaskletCode.int_sge,
1506
                "<": TaskletCode.int_slt,
1507
                "<=": TaskletCode.int_sle,
1508
                "==": TaskletCode.int_eq,
1509
                "!=": TaskletCode.int_ne,
1510
            }
1511
        else:
1512
            cmp_ops = {
4✔
1513
                ">": TaskletCode.fp_ogt,
1514
                ">=": TaskletCode.fp_oge,
1515
                "<": TaskletCode.fp_olt,
1516
                "<=": TaskletCode.fp_ole,
1517
                "==": TaskletCode.fp_oeq,
1518
                "!=": TaskletCode.fp_one,
1519
            }
1520

1521
        if op not in cmp_ops:
4✔
1522
            raise NotImplementedError(
×
1523
                f"Comparison operator {op} not supported for arrays"
1524
            )
1525

1526
        tasklet_code = cmp_ops[op]
4✔
1527

1528
        scalar_name = None
4✔
1529
        if not left_is_array:
4✔
1530
            scalar_name = left
×
1531
        elif not right_is_array:
4✔
1532
            scalar_name = right
4✔
1533

1534
        if scalar_name is not None and not use_int_cmp:
4✔
1535
            if self._is_int(scalar_name):
4✔
1536
                float_name = f"_tmp_{self._get_unique_id()}"
4✔
1537
                self.builder.add_container(
4✔
1538
                    float_name, Scalar(PrimitiveType.Double), False
1539
                )
1540
                self.container_table[float_name] = Scalar(PrimitiveType.Double)
4✔
1541

1542
                block_conv = self.builder.add_block()
4✔
1543
                t_const = self.builder.add_constant(
4✔
1544
                    block_conv, f"{scalar_name}.0", Scalar(PrimitiveType.Double)
1545
                )
1546
                t_float = self.builder.add_access(block_conv, float_name)
4✔
1547
                t_assign = self.builder.add_tasklet(
4✔
1548
                    block_conv, TaskletCode.assign, ["_in"], ["_out"]
1549
                )
1550
                self.builder.add_memlet(
4✔
1551
                    block_conv, t_const, "void", t_assign, "_in", ""
1552
                )
1553
                self.builder.add_memlet(
4✔
1554
                    block_conv, t_assign, "_out", t_float, "void", ""
1555
                )
1556

1557
                if not left_is_array:
4✔
1558
                    left = float_name
×
1559
                else:
1560
                    right = float_name
4✔
1561

1562
        # Get tensor info for array operands
1563
        left_tensor = self.tensor_table.get(left) if left_is_array else None
4✔
1564
        right_tensor = self.tensor_table.get(right) if right_is_array else None
4✔
1565
        tmp_tensor = self.tensor_table[tmp_name]
4✔
1566

1567
        loop_vars = []
4✔
1568
        for i, dim in enumerate(shape):
4✔
1569
            loop_var = f"_cmp_i{i}_{self._get_unique_id()}"
4✔
1570
            if not self.builder.exists(loop_var):
4✔
1571
                self.builder.add_container(loop_var, Scalar(PrimitiveType.Int64), False)
4✔
1572
                self.container_table[loop_var] = Scalar(PrimitiveType.Int64)
4✔
1573
            loop_vars.append(loop_var)
4✔
1574
            self.builder.begin_for(loop_var, "0", str(dim), "1")
4✔
1575

1576
        # Multi-dimensional subset - TensorToPointerConversion handles strides/offset
1577
        multi_dim_subset = ",".join(loop_vars)
4✔
1578

1579
        block = self.builder.add_block()
4✔
1580

1581
        if left_is_array:
4✔
1582
            t_left = self.builder.add_access(block, left)
4✔
1583
            left_sub = multi_dim_subset
4✔
1584
        else:
1585
            t_left, left_sub = self._add_read(block, left)
×
1586

1587
        if right_is_array:
4✔
1588
            t_right = self.builder.add_access(block, right)
×
1589
            right_sub = multi_dim_subset
×
1590
        else:
1591
            t_right, right_sub = self._add_read(block, right)
4✔
1592

1593
        t_out = self.builder.add_access(block, tmp_name)
4✔
1594

1595
        t_task = self.builder.add_tasklet(
4✔
1596
            block, tasklet_code, ["_in1", "_in2"], ["_out"]
1597
        )
1598

1599
        # Pass tensor type so TensorToPointerConversion uses correct strides/offset
1600
        if left_is_array and left_tensor:
4✔
1601
            self.builder.add_memlet(
4✔
1602
                block, t_left, "void", t_task, "_in1", left_sub, left_tensor
1603
            )
1604
        else:
1605
            self.builder.add_memlet(block, t_left, "void", t_task, "_in1", left_sub)
×
1606

1607
        if right_is_array and right_tensor:
4✔
1608
            self.builder.add_memlet(
×
1609
                block, t_right, "void", t_task, "_in2", right_sub, right_tensor
1610
            )
1611
        else:
1612
            self.builder.add_memlet(block, t_right, "void", t_task, "_in2", right_sub)
4✔
1613

1614
        self.builder.add_memlet(
4✔
1615
            block, t_task, "_out", t_out, "void", multi_dim_subset, tmp_tensor
1616
        )
1617

1618
        for _ in loop_vars:
4✔
1619
            self.builder.end_for()
4✔
1620

1621
        return tmp_name
4✔
1622

1623
    def handle_array_bitwise_op(self, op, left, right):
4✔
1624
        """Elementwise integer bitwise op (``&``, ``|``, ``^``, ``<<``, ``>>``)
1625
        for arrays. Supports array-op-array (same shape) and array-op-scalar.
1626
        Returns the name of the result array."""
1627
        left_is_array = left in self.tensor_table
4✔
1628
        right_is_array = right in self.tensor_table
4✔
1629

1630
        if left_is_array:
4✔
1631
            shape = self.tensor_table[left].shape
4✔
1632
            arr_name = left
4✔
1633
        else:
NEW
1634
            shape = self.tensor_table[right].shape
×
NEW
1635
            arr_name = right
×
1636

1637
        dtype = self._ev._element_type(arr_name)
4✔
1638
        if dtype.primitive_type not in (
4✔
1639
            PrimitiveType.Int32,
1640
            PrimitiveType.Int64,
1641
            PrimitiveType.Bool,
1642
        ):
NEW
1643
            raise NotImplementedError(
×
1644
                f"Bitwise operator {op} requires integer/boolean arrays"
1645
            )
1646

1647
        is_signed = dtype.primitive_type in (PrimitiveType.Int32, PrimitiveType.Int64)
4✔
1648
        bit_ops = {
4✔
1649
            "&": TaskletCode.int_and,
1650
            "|": TaskletCode.int_or,
1651
            "^": TaskletCode.int_xor,
1652
            "<<": TaskletCode.int_shl,
1653
            ">>": TaskletCode.int_ashr if is_signed else TaskletCode.int_lshr,
1654
        }
1655
        if op not in bit_ops:
4✔
NEW
1656
            raise NotImplementedError(f"Bitwise operator {op} not supported for arrays")
×
1657
        tasklet_code = bit_ops[op]
4✔
1658

1659
        tmp_name = self._create_array_temp(shape, dtype)
4✔
1660
        left_tensor = self.tensor_table.get(left) if left_is_array else None
4✔
1661
        right_tensor = self.tensor_table.get(right) if right_is_array else None
4✔
1662
        tmp_tensor = self.tensor_table[tmp_name]
4✔
1663

1664
        loop_vars = []
4✔
1665
        for i, dim in enumerate(shape):
4✔
1666
            loop_var = f"_bit_i{i}_{self._get_unique_id()}"
4✔
1667
            if not self.builder.exists(loop_var):
4✔
1668
                self.builder.add_container(loop_var, Scalar(PrimitiveType.Int64), False)
4✔
1669
                self.container_table[loop_var] = Scalar(PrimitiveType.Int64)
4✔
1670
            loop_vars.append(loop_var)
4✔
1671
            self.builder.begin_for(loop_var, "0", str(dim), "1")
4✔
1672

1673
        multi_dim_subset = ",".join(loop_vars)
4✔
1674
        block = self.builder.add_block()
4✔
1675

1676
        if left_is_array:
4✔
1677
            t_left = self.builder.add_access(block, left)
4✔
1678
            left_sub = multi_dim_subset
4✔
1679
        else:
NEW
1680
            t_left, left_sub = self._add_read(block, left)
×
1681

1682
        if right_is_array:
4✔
1683
            t_right = self.builder.add_access(block, right)
4✔
1684
            right_sub = multi_dim_subset
4✔
1685
        else:
1686
            t_right, right_sub = self._add_read(block, right)
4✔
1687

1688
        t_out = self.builder.add_access(block, tmp_name)
4✔
1689
        t_task = self.builder.add_tasklet(
4✔
1690
            block, tasklet_code, ["_in1", "_in2"], ["_out"]
1691
        )
1692

1693
        if left_is_array and left_tensor:
4✔
1694
            self.builder.add_memlet(
4✔
1695
                block, t_left, "void", t_task, "_in1", left_sub, left_tensor
1696
            )
1697
        else:
NEW
1698
            self.builder.add_memlet(block, t_left, "void", t_task, "_in1", left_sub)
×
1699

1700
        if right_is_array and right_tensor:
4✔
1701
            self.builder.add_memlet(
4✔
1702
                block, t_right, "void", t_task, "_in2", right_sub, right_tensor
1703
            )
1704
        else:
1705
            self.builder.add_memlet(block, t_right, "void", t_task, "_in2", right_sub)
4✔
1706

1707
        self.builder.add_memlet(
4✔
1708
            block, t_task, "_out", t_out, "void", multi_dim_subset, tmp_tensor
1709
        )
1710

1711
        for _ in loop_vars:
4✔
1712
            self.builder.end_for()
4✔
1713

1714
        return tmp_name
4✔
1715

1716
    # ========== NumPy Function Handlers ==========
1717

1718
    def _element_type_for_dtype_arg(self, dtype_arg):
4✔
1719
        """Resolve the element type for a ``dtype=`` argument.
1720

1721
        Extends ``element_type_from_ast_node`` (which handles ``np.float64``,
1722
        builtins, global aliases, and ``<name>.dtype``) with ``.dtype`` on an
1723
        arbitrary array *expression* -- e.g. a struct member ``domain.x.dtype``
1724
        -- by resolving the array and reading its element type.
1725
        """
1726
        if (
4✔
1727
            isinstance(dtype_arg, ast.Attribute)
1728
            and dtype_arg.attr == "dtype"
1729
            and not isinstance(dtype_arg.value, ast.Name)
1730
        ):
NEW
1731
            arr = self.visit(dtype_arg.value)
×
NEW
1732
            if arr in self.tensor_table:
×
NEW
1733
                return self.tensor_table[arr].element_type
×
1734
        return element_type_from_ast_node(
4✔
1735
            dtype_arg, self.container_table, self.globals_dict
1736
        )
1737

1738
    def _handle_numpy_alloc(self, node, func_name):
4✔
1739
        """Handle np.empty, np.zeros, np.ones, np.ndarray."""
1740
        shape_arg = node.args[0]
4✔
1741
        dims = []
4✔
1742
        dims_runtime = []
4✔
1743
        # A tuple/list shape -- either a literal or a variable bound to one
1744
        # (e.g. `shp = (a.shape[0], a.shape[1]); np.ndarray(shp, ...)`).
1745
        shape_elts = self._ev._resolve_tuple(shape_arg)
4✔
1746
        if shape_elts is not None:
4✔
1747
            dims = [self.visit(elt) for elt in shape_elts]
4✔
1748
            dims_runtime = [self._shape_to_runtime_expr(elt) for elt in shape_elts]
4✔
1749
        else:
1750
            val = self.visit(shape_arg)
4✔
1751
            runtime_val = self._shape_to_runtime_expr(shape_arg)
4✔
1752
            if val.startswith("_shape_proxy_"):
4✔
1753
                array_name = val[len("_shape_proxy_") :]
×
1754
                if array_name in self.tensor_table:
×
1755
                    info = self.tensor_table[array_name]
×
1756
                    dims = info.shape
×
1757
                    dims_runtime = self.shapes_runtime_info.get(array_name, dims)
×
1758
                else:
1759
                    dims = [val]
×
1760
                    dims_runtime = [runtime_val]
×
1761
            else:
1762
                dims = [val]
4✔
1763
                dims_runtime = [runtime_val]
4✔
1764

1765
        dtype_arg = None
4✔
1766
        order = "C"  # Default to C-order (row-major)
4✔
1767
        explicit_strides = None
4✔
1768
        if len(node.args) > 1:
4✔
1769
            dtype_arg = node.args[1]
4✔
1770

1771
        for kw in node.keywords:
4✔
1772
            if kw.arg == "dtype":
4✔
1773
                dtype_arg = kw.value
4✔
1774
            elif kw.arg == "order":
4✔
1775
                if isinstance(kw.value, ast.Constant):
4✔
1776
                    order = kw.value.value
4✔
1777
            elif kw.arg == "strides":
4✔
1778
                # Parse explicit strides tuple/list
1779
                if isinstance(kw.value, (ast.Tuple, ast.List)):
4✔
1780
                    explicit_strides = [
4✔
1781
                        self._shape_to_runtime_expr(elt) for elt in kw.value.elts
1782
                    ]
1783

1784
        element_type = self._element_type_for_dtype_arg(dtype_arg)
4✔
1785

1786
        # Use explicit strides if provided, otherwise compute from order
1787
        if explicit_strides is not None:
4✔
1788
            # Convert byte strides to element strides by dividing by element size
1789
            element_size = self.builder.get_sizeof(element_type)
4✔
1790
            strides = [f"(({s}) / {element_size})" for s in explicit_strides]
4✔
1791
        else:
1792
            strides = self._compute_strides(dims, order)
4✔
1793

1794
        return self._create_array_temp(
4✔
1795
            dims,
1796
            element_type,
1797
            zero_init=(func_name == "zeros"),
1798
            ones_init=(func_name == "ones"),
1799
            shapes_runtime=dims_runtime,
1800
            strides=strides,
1801
        )
1802

1803
    def _handle_numpy_empty_like(self, node, func_name):
4✔
1804
        """Handle np.empty_like."""
1805
        prototype_arg = node.args[0]
4✔
1806
        prototype_name = self.visit(prototype_arg)
4✔
1807

1808
        dims = []
4✔
1809
        if prototype_name in self.tensor_table:
4✔
1810
            dims = self.tensor_table[prototype_name].shape
4✔
1811

1812
        dtype_arg = None
4✔
1813
        order = "C"  # Default to C-order
4✔
1814
        if len(node.args) > 1:
4✔
1815
            dtype_arg = node.args[1]
×
1816

1817
        for kw in node.keywords:
4✔
1818
            if kw.arg == "dtype":
4✔
1819
                dtype_arg = kw.value
4✔
1820
            elif kw.arg == "order":
4✔
1821
                if isinstance(kw.value, ast.Constant):
4✔
1822
                    order = kw.value.value
4✔
1823

1824
        element_type = None
4✔
1825
        if dtype_arg:
4✔
1826
            element_type = element_type_from_ast_node(
4✔
1827
                dtype_arg, self.container_table, self.globals_dict
1828
            )
1829
        else:
1830
            if prototype_name in self.container_table:
4✔
1831
                sym_type = self.container_table[prototype_name]
4✔
1832
                if isinstance(sym_type, Pointer) and sym_type.has_pointee_type():
4✔
1833
                    element_type = sym_type.pointee_type
4✔
1834

1835
        if element_type is None:
4✔
1836
            element_type = Scalar(PrimitiveType.Double)
×
1837

1838
        strides = self._compute_strides(dims, order)
4✔
1839
        return self._create_array_temp(
4✔
1840
            dims, element_type, zero_init=False, ones_init=False, strides=strides
1841
        )
1842

1843
    def _handle_numpy_zeros_like(self, node, func_name):
4✔
1844
        """Handle np.zeros_like."""
1845
        prototype_arg = node.args[0]
4✔
1846
        prototype_name = self.visit(prototype_arg)
4✔
1847

1848
        dims = []
4✔
1849
        if prototype_name in self.tensor_table:
4✔
1850
            dims = self.tensor_table[prototype_name].shape
4✔
1851

1852
        dtype_arg = None
4✔
1853
        order = "C"  # Default to C-order
4✔
1854
        if len(node.args) > 1:
4✔
1855
            dtype_arg = node.args[1]
×
1856

1857
        for kw in node.keywords:
4✔
1858
            if kw.arg == "dtype":
4✔
1859
                dtype_arg = kw.value
4✔
1860
            elif kw.arg == "order":
4✔
1861
                if isinstance(kw.value, ast.Constant):
4✔
1862
                    order = kw.value.value
4✔
1863

1864
        element_type = None
4✔
1865
        if dtype_arg:
4✔
1866
            element_type = element_type_from_ast_node(
4✔
1867
                dtype_arg, self.container_table, self.globals_dict
1868
            )
1869
        else:
1870
            if prototype_name in self.container_table:
4✔
1871
                sym_type = self.container_table[prototype_name]
4✔
1872
                if isinstance(sym_type, Pointer) and sym_type.has_pointee_type():
4✔
1873
                    element_type = sym_type.pointee_type
4✔
1874

1875
        if element_type is None:
4✔
1876
            element_type = Scalar(PrimitiveType.Double)
×
1877

1878
        strides = self._compute_strides(dims, order)
4✔
1879
        return self._create_array_temp(
4✔
1880
            dims, element_type, zero_init=True, ones_init=False, strides=strides
1881
        )
1882

1883
    def _handle_numpy_eye(self, node, func_name):
4✔
1884
        """Handle np.eye."""
1885
        N_arg = node.args[0]
4✔
1886
        N_str = self.visit(N_arg)
4✔
1887
        N_runtime = self._shape_to_runtime_expr(N_arg)
4✔
1888

1889
        M_str = N_str
4✔
1890
        M_arg = N_arg  # Default M = N
4✔
1891
        if len(node.args) > 1:
4✔
1892
            M_arg = node.args[1]
×
1893
            M_str = self.visit(M_arg)
×
1894

1895
        k_str = "0"
4✔
1896
        if len(node.args) > 2:
4✔
1897
            k_str = self.visit(node.args[2])
×
1898

1899
        dtype_arg = None
4✔
1900
        for kw in node.keywords:
4✔
1901
            if kw.arg == "M":
4✔
1902
                M_arg = kw.value
4✔
1903
                M_str = self.visit(M_arg)
4✔
1904
                if M_str == "None":
4✔
1905
                    M_str = N_str
4✔
1906
                    M_arg = N_arg
4✔
1907
            elif kw.arg == "k":
4✔
1908
                k_str = self.visit(kw.value)
4✔
1909
            elif kw.arg == "dtype":
4✔
1910
                dtype_arg = kw.value
4✔
1911

1912
        M_runtime = self._shape_to_runtime_expr(M_arg)
4✔
1913

1914
        element_type = element_type_from_ast_node(
4✔
1915
            dtype_arg, self.container_table, self.globals_dict
1916
        )
1917

1918
        ptr_name = self._create_array_temp(
4✔
1919
            [N_str, M_str],
1920
            element_type,
1921
            zero_init=True,
1922
            shapes_runtime=[N_runtime, M_runtime],
1923
        )
1924

1925
        loop_var = f"_i_{self._get_unique_id()}"
4✔
1926
        if not self.builder.exists(loop_var):
4✔
1927
            self.builder.add_container(loop_var, Scalar(PrimitiveType.Int64), False)
4✔
1928
            self.container_table[loop_var] = Scalar(PrimitiveType.Int64)
4✔
1929

1930
        self.builder.begin_for(loop_var, "0", N_str, "1")
4✔
1931

1932
        cond = f"(({loop_var} + {k_str}) >= 0) & (({loop_var} + {k_str}) < {M_str})"
4✔
1933
        self.builder.begin_if(cond)
4✔
1934

1935
        val = "1.0"
4✔
1936
        if element_type.primitive_type in [
4✔
1937
            PrimitiveType.Int64,
1938
            PrimitiveType.Int32,
1939
            PrimitiveType.Int8,
1940
            PrimitiveType.Int16,
1941
            PrimitiveType.UInt64,
1942
            PrimitiveType.UInt32,
1943
            PrimitiveType.UInt8,
1944
            PrimitiveType.UInt16,
1945
        ]:
1946
            val = "1"
×
1947

1948
        block_assign = self.builder.add_block()
4✔
1949
        t_const = self.builder.add_constant(block_assign, val, element_type)
4✔
1950
        t_arr = self.builder.add_access(block_assign, ptr_name)
4✔
1951
        flat_index = f"(({loop_var}) * ({M_str}) + ({loop_var}) + ({k_str}))"
4✔
1952
        subset = flat_index
4✔
1953

1954
        t_task = self.builder.add_tasklet(
4✔
1955
            block_assign, TaskletCode.assign, ["_in"], ["_out"]
1956
        )
1957
        self.builder.add_memlet(
4✔
1958
            block_assign, t_const, "void", t_task, "_in", "", element_type
1959
        )
1960
        self.builder.add_memlet(block_assign, t_task, "_out", t_arr, "void", subset)
4✔
1961

1962
        self.builder.end_if()
4✔
1963
        self.builder.end_for()
4✔
1964

1965
        return ptr_name
4✔
1966

1967
    def _handle_numpy_binary_op(self, node, func_name):
4✔
1968
        """Handle np.add, np.subtract, np.multiply, np.divide, etc."""
1969
        args = [self.visit(arg) for arg in node.args]
4✔
1970
        if len(args) != 2:
4✔
1971
            raise NotImplementedError(
×
1972
                f"Numpy function {func_name} requires 2 arguments"
1973
            )
1974

1975
        op_map = {
4✔
1976
            "add": "add",
1977
            "subtract": "sub",
1978
            "multiply": "mul",
1979
            "divide": "div",
1980
            "power": "pow",
1981
            "minimum": "min",
1982
            "maximum": "max",
1983
        }
1984
        return self.handle_array_binary_op(op_map[func_name], args[0], args[1])
4✔
1985

1986
    def _handle_numpy_unary_op(self, node, func_name):
4✔
1987
        """Handle np.exp, np.sqrt, np.abs, etc."""
1988
        args = [self.visit(arg) for arg in node.args]
4✔
1989
        if len(args) != 1:
4✔
1990
            raise NotImplementedError(f"Numpy function {func_name} requires 1 argument")
×
1991

1992
        op_name = func_name
4✔
1993
        if op_name == "absolute":
4✔
1994
            op_name = "abs"
×
1995

1996
        return self.handle_array_unary_op(op_name, args[0])
4✔
1997

1998
    def _handle_numpy_cbrt(self, node, func_name):
4✔
1999
        """Handle np.cbrt (cube root).
2000

2001
        There is no tensor library node for cube root, so an array operand is
2002
        lowered to an explicit elementwise loop that applies the ``cbrt`` C math
2003
        function per element. A scalar (0-d) operand reuses the scalar cmath
2004
        path. Unlike ``x ** (1/3)`` this is correct for negative inputs.
2005
        """
2006
        if len(node.args) != 1:
4✔
NEW
2007
            raise NotImplementedError("np.cbrt requires exactly one argument")
×
2008

2009
        operand = self.visit(node.args[0])
4✔
2010

2011
        # Scalar / 0-d operand: reuse the scalar cmath path.
2012
        if (
4✔
2013
            operand not in self.tensor_table
2014
            or len(self.tensor_table[operand].shape) == 0
2015
        ):
2016
            return self.handle_array_unary_op("cbrt", operand)
4✔
2017

2018
        in_tensor = self.tensor_table[operand]
4✔
2019

2020
        # The loop uses flat pointer indexing, so ensure the source is
2021
        # contiguous with zero offset; otherwise copy it first.
2022
        in_strides = getattr(in_tensor, "strides", None)
4✔
2023
        in_offset = getattr(in_tensor, "offset", "0") or "0"
4✔
2024
        needs_copy = str(in_offset) != "0"
4✔
2025
        if in_strides is not None and not (
4✔
2026
            self._is_contiguous(in_tensor.shape, in_strides)
2027
            or self._is_contiguous_f(in_tensor.shape, in_strides)
2028
        ):
NEW
2029
            needs_copy = True
×
2030
        if needs_copy:
4✔
NEW
2031
            operand = self.handle_numpy_copy(None, operand)
×
NEW
2032
            in_tensor = self.tensor_table[operand]
×
2033

2034
        dtype = in_tensor.element_type
4✔
2035
        shape = in_tensor.shape
4✔
2036
        tmp_name = self._create_array_temp(
4✔
2037
            shape, dtype, strides=self._compute_strides(shape, "C")
2038
        )
2039

2040
        total = "1"
4✔
2041
        for d in shape:
4✔
2042
            total = f"({total} * ({d}))"
4✔
2043

2044
        loop_var = self.builder.find_new_name("_cbrt_i_")
4✔
2045
        self.builder.add_container(loop_var, Scalar(PrimitiveType.Int64), False)
4✔
2046
        self.container_table[loop_var] = Scalar(PrimitiveType.Int64)
4✔
2047

2048
        self.builder.begin_for(loop_var, "0", total, "1")
4✔
2049
        block = self.builder.add_block()
4✔
2050
        t_src = self.builder.add_access(block, operand)
4✔
2051
        t_dst = self.builder.add_access(block, tmp_name)
4✔
2052
        t_task = self.builder.add_cmath(block, CMathFunction.cbrt, dtype.primitive_type)
4✔
2053
        self.builder.add_memlet(block, t_src, "void", t_task, "_in1", loop_var, None)
4✔
2054
        self.builder.add_memlet(block, t_task, "_out", t_dst, "void", loop_var, None)
4✔
2055
        self.builder.end_for()
4✔
2056

2057
        return tmp_name
4✔
2058

2059
    def _handle_numpy_select(self, node, func_name):
4✔
2060
        """Handle np.select(condlist, choicelist, default=0).
2061

2062
        Lowered to a chain of nested np.where so that the first matching
2063
        condition wins: np.select([c0,c1],[v0,v1],default=d) becomes
2064
        np.where(c0, v0, np.where(c1, v1, d)).
2065
        """
2066
        if len(node.args) < 2:
4✔
NEW
2067
            raise NotImplementedError(
×
2068
                "np.select requires condlist and choicelist arguments"
2069
            )
2070

2071
        conds = self._ev._resolve_tuple(node.args[0])
4✔
2072
        choices = self._ev._resolve_tuple(node.args[1])
4✔
2073
        if conds is None or choices is None:
4✔
NEW
2074
            raise NotImplementedError(
×
2075
                "np.select condlist/choicelist must be list/tuple literals"
2076
            )
2077
        if len(conds) != len(choices):
4✔
NEW
2078
            raise ValueError("np.select condlist and choicelist must be same length")
×
2079

2080
        default_node = None
4✔
2081
        if len(node.args) >= 3:
4✔
2082
            default_node = node.args[2]
4✔
2083
        for kw in node.keywords:
4✔
2084
            if kw.arg == "default":
4✔
2085
                default_node = kw.value
4✔
2086
        if default_node is None:
4✔
NEW
2087
            default_node = ast.Constant(value=0)
×
2088

2089
        # Build nested where bottom-up; process reversed so condlist[0] is the
2090
        # outermost (highest-priority) selection.
2091
        result_node = default_node
4✔
2092
        result_name = None
4✔
2093
        for cond_ast, choice_ast in zip(reversed(conds), reversed(choices)):
4✔
2094
            where_node = ast.Call(
4✔
2095
                func=node.func,
2096
                args=[cond_ast, choice_ast, result_node],
2097
                keywords=[],
2098
            )
2099
            result_name = self._handle_numpy_where(where_node, "where")
4✔
2100
            result_node = ast.Name(id=result_name, ctx=ast.Load())
4✔
2101
        return result_name
4✔
2102

2103
    def _handle_numpy_where(self, node, func_name):
4✔
2104
        """Handle np.where(condition, x, y) - elementwise ternary selection."""
2105
        if len(node.args) != 3:
4✔
2106
            raise NotImplementedError("np.where requires 3 arguments (condition, x, y)")
×
2107

2108
        cond_name = self.visit(node.args[0])
4✔
2109
        x_name = self.visit(node.args[1])
4✔
2110
        y_name = self.visit(node.args[2])
4✔
2111

2112
        shape = []
4✔
2113
        dtype = Scalar(PrimitiveType.Double)
4✔
2114

2115
        if cond_name in self.tensor_table:
4✔
2116
            shape = self.tensor_table[cond_name].shape
4✔
2117

2118
        if not shape and y_name in self.tensor_table:
4✔
2119
            shape = self.tensor_table[y_name].shape
×
2120

2121
        if not shape and x_name in self.tensor_table:
4✔
2122
            shape = self.tensor_table[x_name].shape
×
2123

2124
        if not shape:
4✔
2125
            raise NotImplementedError("np.where requires at least one array argument")
×
2126

2127
        if y_name in self.container_table:
4✔
2128
            y_type = self.container_table[y_name]
4✔
2129
            if isinstance(y_type, Pointer) and y_type.has_pointee_type():
4✔
2130
                dtype = y_type.pointee_type
4✔
2131
            elif isinstance(y_type, Scalar):
4✔
2132
                dtype = y_type
4✔
2133

2134
        tmp_name = self._create_array_temp(shape, dtype)
4✔
2135
        tmp_tensor = self.tensor_table[tmp_name]
4✔
2136

2137
        loop_vars = []
4✔
2138
        for i, dim in enumerate(shape):
4✔
2139
            loop_var = f"_where_i{i}_{self._get_unique_id()}"
4✔
2140
            if not self.builder.exists(loop_var):
4✔
2141
                self.builder.add_container(loop_var, Scalar(PrimitiveType.Int64), False)
4✔
2142
                self.container_table[loop_var] = Scalar(PrimitiveType.Int64)
4✔
2143
            loop_vars.append(loop_var)
4✔
2144
            self.builder.begin_for(loop_var, "0", str(dim), "1")
4✔
2145
        multi_dim_subset = ",".join(loop_vars)
4✔
2146

2147
        cond_tmp = f"_where_cond_{self._get_unique_id()}"
4✔
2148
        self.builder.add_container(cond_tmp, Scalar(PrimitiveType.Bool), False)
4✔
2149
        self.container_table[cond_tmp] = Scalar(PrimitiveType.Bool)
4✔
2150

2151
        block_cond = self.builder.add_block()
4✔
2152
        if cond_name in self.tensor_table:
4✔
2153
            cond_tensor = self.tensor_table[cond_name]
4✔
2154
            t_cond_arr = self.builder.add_access(block_cond, cond_name)
4✔
2155
            t_cond_out = self.builder.add_access(block_cond, cond_tmp)
4✔
2156
            t_cond_task = self.builder.add_tasklet(
4✔
2157
                block_cond, TaskletCode.assign, ["_in"], ["_out"]
2158
            )
2159
            self.builder.add_memlet(
4✔
2160
                block_cond,
2161
                t_cond_arr,
2162
                "void",
2163
                t_cond_task,
2164
                "_in",
2165
                multi_dim_subset,
2166
                cond_tensor,
2167
            )
2168
            self.builder.add_memlet(
4✔
2169
                block_cond, t_cond_task, "_out", t_cond_out, "void", ""
2170
            )
2171
        else:
2172
            t_cond_src, cond_sub = self._add_read(block_cond, cond_name)
×
2173
            t_cond_out = self.builder.add_access(block_cond, cond_tmp)
×
2174
            t_cond_task = self.builder.add_tasklet(
×
2175
                block_cond, TaskletCode.assign, ["_in"], ["_out"]
2176
            )
2177
            self.builder.add_memlet(
×
2178
                block_cond, t_cond_src, "void", t_cond_task, "_in", cond_sub
2179
            )
2180
            self.builder.add_memlet(
×
2181
                block_cond, t_cond_task, "_out", t_cond_out, "void", ""
2182
            )
2183

2184
        self.builder.begin_if(f"{cond_tmp} == true")
4✔
2185

2186
        block_true = self.builder.add_block()
4✔
2187
        t_out_true = self.builder.add_access(block_true, tmp_name)
4✔
2188
        if x_name in self.tensor_table:
4✔
2189
            x_tensor = self.tensor_table[x_name]
4✔
2190
            t_x = self.builder.add_access(block_true, x_name)
4✔
2191
            t_task_true = self.builder.add_tasklet(
4✔
2192
                block_true, TaskletCode.assign, ["_in"], ["_out"]
2193
            )
2194
            self.builder.add_memlet(
4✔
2195
                block_true, t_x, "void", t_task_true, "_in", multi_dim_subset, x_tensor
2196
            )
2197
        else:
2198
            t_x, x_sub = self._add_read(block_true, x_name)
4✔
2199
            t_task_true = self.builder.add_tasklet(
4✔
2200
                block_true, TaskletCode.assign, ["_in"], ["_out"]
2201
            )
2202
            self.builder.add_memlet(block_true, t_x, "void", t_task_true, "_in", x_sub)
4✔
2203
        self.builder.add_memlet(
4✔
2204
            block_true,
2205
            t_task_true,
2206
            "_out",
2207
            t_out_true,
2208
            "void",
2209
            multi_dim_subset,
2210
            tmp_tensor,
2211
        )
2212

2213
        self.builder.begin_else()
4✔
2214

2215
        # False branch: read from y, write to output
2216
        block_false = self.builder.add_block()
4✔
2217
        t_out_false = self.builder.add_access(block_false, tmp_name)
4✔
2218
        if y_name in self.tensor_table:
4✔
2219
            y_tensor = self.tensor_table[y_name]
4✔
2220
            t_y = self.builder.add_access(block_false, y_name)
4✔
2221
            t_task_false = self.builder.add_tasklet(
4✔
2222
                block_false, TaskletCode.assign, ["_in"], ["_out"]
2223
            )
2224
            self.builder.add_memlet(
4✔
2225
                block_false,
2226
                t_y,
2227
                "void",
2228
                t_task_false,
2229
                "_in",
2230
                multi_dim_subset,
2231
                y_tensor,
2232
            )
2233
        else:
2234
            t_y, y_sub = self._add_read(block_false, y_name)
4✔
2235
            t_task_false = self.builder.add_tasklet(
4✔
2236
                block_false, TaskletCode.assign, ["_in"], ["_out"]
2237
            )
2238
            self.builder.add_memlet(
4✔
2239
                block_false, t_y, "void", t_task_false, "_in", y_sub
2240
            )
2241
        self.builder.add_memlet(
4✔
2242
            block_false,
2243
            t_task_false,
2244
            "_out",
2245
            t_out_false,
2246
            "void",
2247
            multi_dim_subset,
2248
            tmp_tensor,
2249
        )
2250

2251
        self.builder.end_if()
4✔
2252

2253
        for _ in loop_vars:
4✔
2254
            self.builder.end_for()
4✔
2255

2256
        return tmp_name
4✔
2257

2258
    def _handle_numpy_clip(self, node, func_name):
4✔
2259
        """Handle np.clip(a, a_min, a_max) - elementwise clipping."""
2260
        if len(node.args) != 3:
4✔
2261
            raise NotImplementedError("np.clip requires 3 arguments (a, a_min, a_max)")
×
2262

2263
        arr_name = self.visit(node.args[0])
4✔
2264
        a_min = self.visit(node.args[1])
4✔
2265
        a_max = self.visit(node.args[2])
4✔
2266

2267
        tmp1 = self.handle_array_binary_op("max", arr_name, a_min)
4✔
2268
        result = self.handle_array_binary_op("min", tmp1, a_max)
4✔
2269

2270
        return result
4✔
2271

2272
    def _contiguous_matmul_operand(self, name, node):
4✔
2273
        """Copy a matmul operand to contiguous storage if it has a base offset.
2274

2275
        The GEMM/GEMV code generation ignores an operand's base offset, so an
2276
        offset view (e.g. a row `gamma[i]` of a 2-D array) would otherwise be
2277
        read from the wrong location. Returns (name, node), replacing both with
2278
        a contiguous copy when the operand's tensor has a non-zero offset.
2279
        """
2280
        if name in self.tensor_table:
4✔
2281
            tensor = self.tensor_table[name]
4✔
2282
            offset = getattr(tensor, "offset", "0") or "0"
4✔
2283
            if str(offset) != "0":
4✔
2284
                new_name = self.handle_numpy_copy(None, name)
4✔
2285
                return new_name, ast.Name(id=new_name)
4✔
2286
        return name, node
4✔
2287

2288
    def _handle_numpy_matmul(self, node, func_name):
4✔
2289
        """Handle np.matmul, np.dot."""
2290
        if len(node.args) != 2:
4✔
2291
            raise NotImplementedError("matmul/dot requires 2 arguments")
×
2292
        return self._handle_matmul_helper(node.args[0], node.args[1])
4✔
2293

2294
    def handle_numpy_matmul_op(self, left_node, right_node):
4✔
2295
        """Handle the @ operator for matrix multiplication."""
2296
        return self._handle_matmul_helper(left_node, right_node)
4✔
2297

2298
    def _handle_matmul_helper(self, left_node, right_node):
4✔
2299
        """Helper for matrix multiplication operations."""
2300
        res_a = self.parse_arg(left_node)
4✔
2301
        res_b = self.parse_arg(right_node)
4✔
2302
        if not res_a[0]:
4✔
2303
            left_name = self.visit(left_node)
4✔
2304
            left_node = ast.Name(id=left_name)
4✔
2305
            res_a = self.parse_arg(left_node)
4✔
2306

2307
        if not res_b[0]:
4✔
2308
            right_name = self.visit(right_node)
4✔
2309
            right_node = ast.Name(id=right_name)
4✔
2310
            res_b = self.parse_arg(right_node)
4✔
2311

2312
        name_a, subset_a, shape_a, indices_a = res_a
4✔
2313
        name_b, subset_b, shape_b, indices_b = res_b
4✔
2314

2315
        if not name_a or not name_b:
4✔
2316
            raise NotImplementedError("Could not resolve matmul operands")
×
2317

2318
        # The GEMM/GEMV lowering does not honor a non-zero base offset on an
2319
        # operand (e.g. a row view `gamma[i]`). Materialize a contiguous copy
2320
        # of any offset operand so the multiplication reads the correct data.
2321
        name_a, left_node = self._contiguous_matmul_operand(name_a, left_node)
4✔
2322
        name_b, right_node = self._contiguous_matmul_operand(name_b, right_node)
4✔
2323

2324
        real_shape_a = shape_a
4✔
2325
        real_shape_b = shape_b
4✔
2326

2327
        ndim_a = len(real_shape_a)
4✔
2328
        ndim_b = len(real_shape_b)
4✔
2329

2330
        output_shape = []
4✔
2331
        is_scalar = False
4✔
2332

2333
        if ndim_a == 1 and ndim_b == 1:
4✔
2334
            is_scalar = True
4✔
2335
            output_shape = []
4✔
2336
        elif ndim_a == 2 and ndim_b == 2:
4✔
2337
            output_shape = [real_shape_a[0], real_shape_b[1]]
4✔
2338
        elif ndim_a == 2 and ndim_b == 1:
4✔
2339
            output_shape = [real_shape_a[0]]
4✔
2340
        elif ndim_a == 1 and ndim_b == 2:
4✔
2341
            output_shape = [real_shape_b[1]]
4✔
2342
        elif ndim_a > 2 or ndim_b > 2:
4✔
2343
            # Batched matmul with NumPy broadcasting of the leading (batch)
2344
            # dimensions. The trailing two dimensions are the matrix dims; the
2345
            # leading dims are broadcast (aligned to the right, size-1 dims and
2346
            # missing dims broadcast).
2347
            batch_a = [str(s) for s in real_shape_a[:-2]]
4✔
2348
            batch_b = [str(s) for s in real_shape_b[:-2]]
4✔
2349
            nb = max(len(batch_a), len(batch_b))
4✔
2350
            batch_a = ["1"] * (nb - len(batch_a)) + batch_a
4✔
2351
            batch_b = ["1"] * (nb - len(batch_b)) + batch_b
4✔
2352
            batch_out = []
4✔
2353
            for da, db in zip(batch_a, batch_b):
4✔
2354
                if da == "1":
4✔
2355
                    batch_out.append(db)
×
2356
                else:
2357
                    batch_out.append(da)
4✔
2358
            output_shape = batch_out + [real_shape_a[-2], real_shape_b[-1]]
4✔
2359
        else:
2360
            raise NotImplementedError(
×
2361
                f"Matmul with ranks {ndim_a} and {ndim_b} not supported"
2362
            )
2363

2364
        dtype_a = self._ev._element_type(name_a)
4✔
2365
        dtype_b = self._ev._element_type(name_b)
4✔
2366
        dtype = promote_element_types(dtype_a, dtype_b)
4✔
2367

2368
        if is_scalar:
4✔
2369
            tmp_name = f"_tmp_{self._get_unique_id()}"
4✔
2370
            self.builder.add_container(tmp_name, dtype, False)
4✔
2371
            self.container_table[tmp_name] = dtype
4✔
2372
        else:
2373
            tmp_name = self._create_array_temp(output_shape, dtype)
4✔
2374

2375
        if ndim_a > 2 or ndim_b > 2:
4✔
2376
            batch_dims = len(output_shape) - 2
4✔
2377
            batch_shape = output_shape[:batch_dims]
4✔
2378

2379
            loop_vars = []
4✔
2380
            for i in range(batch_dims):
4✔
2381
                loop_var = f"_i{self._get_unique_id()}"
4✔
2382
                self.builder.add_container(loop_var, Scalar(PrimitiveType.Int64), False)
4✔
2383
                loop_vars.append(loop_var)
4✔
2384
                dim_size = batch_shape[i]
4✔
2385
                self.builder.begin_for(loop_var, "0", str(dim_size), "1")
4✔
2386

2387
            def make_operand_slice(name, op_shape):
4✔
2388
                # Build name[b0, b1, ..., :, :] where each batch index is either
2389
                # the corresponding output loop var, or 0 when this operand
2390
                # broadcasts that dimension (size-1 or a missing leading dim).
2391
                op_batch = len(op_shape) - 2
4✔
2392
                elts = []
4✔
2393
                for b in range(op_batch):
4✔
2394
                    out_b = batch_dims - op_batch + b
4✔
2395
                    op_dim = str(op_shape[b])
4✔
2396
                    out_dim = str(batch_shape[out_b])
4✔
2397
                    if op_dim == "1" and out_dim != "1":
4✔
2398
                        elts.append(ast.Name(id="0"))
×
2399
                    else:
2400
                        elts.append(ast.Name(id=loop_vars[out_b]))
4✔
2401
                elts.append(ast.Slice())
4✔
2402
                elts.append(ast.Slice())
4✔
2403
                return ast.Subscript(
4✔
2404
                    value=ast.Name(id=name), slice=ast.Tuple(elts=elts), ctx=ast.Load()
2405
                )
2406

2407
            slice_a = make_operand_slice(name_a, real_shape_a)
4✔
2408
            slice_b = make_operand_slice(name_b, real_shape_b)
4✔
2409
            slice_c = make_operand_slice(tmp_name, output_shape)
4✔
2410

2411
            self.handle_gemm(
4✔
2412
                slice_c, ast.BinOp(left=slice_a, op=ast.MatMult(), right=slice_b)
2413
            )
2414

2415
            for _ in range(batch_dims):
4✔
2416
                self.builder.end_for()
4✔
2417
        else:
2418
            if is_scalar:
4✔
2419
                self.handle_dot(
4✔
2420
                    tmp_name,
2421
                    ast.BinOp(left=left_node, op=ast.MatMult(), right=right_node),
2422
                )
2423
            else:
2424
                self.handle_gemm(
4✔
2425
                    tmp_name,
2426
                    ast.BinOp(left=left_node, op=ast.MatMult(), right=right_node),
2427
                )
2428

2429
        return tmp_name
4✔
2430

2431
    def _handle_numpy_outer(self, node, func_name):
4✔
2432
        """Handle np.outer."""
2433
        if len(node.args) != 2:
4✔
2434
            raise NotImplementedError("outer requires 2 arguments")
×
2435

2436
        arg0 = node.args[0]
4✔
2437
        arg1 = node.args[1]
4✔
2438

2439
        res_a = self.parse_arg(arg0)
4✔
2440
        res_b = self.parse_arg(arg1)
4✔
2441

2442
        if not res_a[0]:
4✔
2443
            left_name = self.visit(arg0)
×
2444
            arg0 = ast.Name(id=left_name)
×
2445
            res_a = self.parse_arg(arg0)
×
2446

2447
        if not res_b[0]:
4✔
2448
            right_name = self.visit(arg1)
×
2449
            arg1 = ast.Name(id=right_name)
×
2450
            res_b = self.parse_arg(arg1)
×
2451

2452
        name_a, subset_a, shape_a, indices_a = res_a
4✔
2453
        name_b, subset_b, shape_b, indices_b = res_b
4✔
2454

2455
        if not name_a or not name_b:
4✔
2456
            raise NotImplementedError("Could not resolve outer operands")
×
2457

2458
        def get_flattened_size_expr(name, indices, shapes):
4✔
2459
            size_expr = "1"
4✔
2460
            for s in shapes:
4✔
2461
                if size_expr == "1":
4✔
2462
                    size_expr = str(s)
4✔
2463
                else:
2464
                    size_expr = f"({size_expr} * {str(s)})"
×
2465
            return size_expr
4✔
2466

2467
        m_expr = get_flattened_size_expr(name_a, indices_a, shape_a)
4✔
2468
        n_expr = get_flattened_size_expr(name_b, indices_b, shape_b)
4✔
2469

2470
        dtype_a = self._ev._element_type(name_a)
4✔
2471
        dtype_b = self._ev._element_type(name_b)
4✔
2472
        dtype = promote_element_types(dtype_a, dtype_b)
4✔
2473

2474
        tmp_name = self._create_array_temp([m_expr, n_expr], dtype)
4✔
2475

2476
        new_call_node = ast.Call(
4✔
2477
            func=node.func, args=[arg0, arg1], keywords=node.keywords
2478
        )
2479

2480
        self.handle_outer(tmp_name, new_call_node)
4✔
2481

2482
        return tmp_name
4✔
2483

2484
    def handle_ufunc_outer(self, node, ufunc_name):
4✔
2485
        """Handle np.add.outer, np.subtract.outer, np.multiply.outer, etc."""
2486
        if len(node.args) != 2:
4✔
2487
            raise NotImplementedError(f"{ufunc_name}.outer requires 2 arguments")
×
2488

2489
        if ufunc_name == "multiply":
4✔
2490
            return self._handle_numpy_outer(node, "outer")
4✔
2491

2492
        op_map = {
4✔
2493
            "add": ("add", TaskletCode.fp_add, TaskletCode.int_add),
2494
            "subtract": ("sub", TaskletCode.fp_sub, TaskletCode.int_sub),
2495
            "divide": ("div", TaskletCode.fp_div, TaskletCode.int_sdiv),
2496
            "minimum": ("min", CMathFunction.fmin, TaskletCode.int_smin),
2497
            "maximum": ("max", CMathFunction.fmax, TaskletCode.int_smax),
2498
        }
2499

2500
        if ufunc_name not in op_map:
4✔
2501
            raise NotImplementedError(f"{ufunc_name}.outer not supported")
×
2502

2503
        op_name, fp_opcode, int_opcode = op_map[ufunc_name]
4✔
2504

2505
        arg0 = node.args[0]
4✔
2506
        arg1 = node.args[1]
4✔
2507

2508
        res_a = self.parse_arg(arg0)
4✔
2509
        res_b = self.parse_arg(arg1)
4✔
2510

2511
        if not res_a[0]:
4✔
2512
            left_name = self.visit(arg0)
×
2513
            arg0 = ast.Name(id=left_name)
×
2514
            res_a = self.parse_arg(arg0)
×
2515

2516
        if not res_b[0]:
4✔
2517
            right_name = self.visit(arg1)
×
2518
            arg1 = ast.Name(id=right_name)
×
2519
            res_b = self.parse_arg(arg1)
×
2520

2521
        name_a, subset_a, shape_a, indices_a = res_a
4✔
2522
        name_b, subset_b, shape_b, indices_b = res_b
4✔
2523

2524
        if not name_a or not name_b:
4✔
2525
            raise NotImplementedError("Could not resolve ufunc outer operands")
×
2526

2527
        def get_flattened_size_expr(shapes):
4✔
2528
            if not shapes:
4✔
2529
                return "1"
×
2530
            size_expr = str(shapes[0])
4✔
2531
            for s in shapes[1:]:
4✔
2532
                size_expr = f"({size_expr} * {str(s)})"
×
2533
            return size_expr
4✔
2534

2535
        m_expr = get_flattened_size_expr(shape_a)
4✔
2536
        n_expr = get_flattened_size_expr(shape_b)
4✔
2537

2538
        dtype_left = self._ev._element_type(name_a)
4✔
2539
        dtype_right = self._ev._element_type(name_b)
4✔
2540
        dtype = promote_element_types(dtype_left, dtype_right)
4✔
2541

2542
        is_int = dtype.primitive_type in [
4✔
2543
            PrimitiveType.Int64,
2544
            PrimitiveType.Int32,
2545
            PrimitiveType.Int8,
2546
            PrimitiveType.Int16,
2547
            PrimitiveType.UInt64,
2548
            PrimitiveType.UInt32,
2549
            PrimitiveType.UInt8,
2550
            PrimitiveType.UInt16,
2551
        ]
2552

2553
        tmp_name = self._create_array_temp([m_expr, n_expr], dtype)
4✔
2554

2555
        i_var = self.builder.find_new_name("_outer_i_")
4✔
2556
        j_var = self.builder.find_new_name("_outer_j_")
4✔
2557

2558
        if not self.builder.exists(i_var):
4✔
2559
            self.builder.add_container(i_var, Scalar(PrimitiveType.Int64), False)
4✔
2560
            self.container_table[i_var] = Scalar(PrimitiveType.Int64)
4✔
2561
        if not self.builder.exists(j_var):
4✔
2562
            self.builder.add_container(j_var, Scalar(PrimitiveType.Int64), False)
4✔
2563
            self.container_table[j_var] = Scalar(PrimitiveType.Int64)
4✔
2564

2565
        def compute_linear_index(name, subset, indices, loop_var):
4✔
2566
            if not indices:
4✔
2567
                return loop_var
4✔
2568

2569
            if name in self.tensor_table:
4✔
2570
                info = self.tensor_table[name]
4✔
2571
                shapes = info.shape
4✔
2572
                ndim = len(shapes)
4✔
2573
            else:
2574
                shapes = []
×
2575
                ndim = 0
×
2576

2577
            if ndim == 0:
4✔
2578
                return loop_var
×
2579

2580
            strides = []
4✔
2581
            current_stride = "1"
4✔
2582
            for i in range(ndim - 1, -1, -1):
4✔
2583
                strides.insert(0, current_stride)
4✔
2584
                if i > 0:
4✔
2585
                    dim_size = shapes[i] if i < len(shapes) else f"_{name}_shape_{i}"
4✔
2586
                    if current_stride == "1":
4✔
2587
                        current_stride = str(dim_size)
4✔
2588
                    else:
2589
                        current_stride = f"({current_stride} * {dim_size})"
×
2590

2591
            terms = []
4✔
2592
            loop_var_used = False
4✔
2593

2594
            for i, idx in enumerate(indices):
4✔
2595
                stride = strides[i] if i < len(strides) else "1"
4✔
2596
                start = subset[i] if i < len(subset) else "0"
4✔
2597

2598
                if isinstance(idx, ast.Slice):
4✔
2599
                    if stride == "1":
4✔
2600
                        term = f"({start} + {loop_var})"
4✔
2601
                    else:
2602
                        term = f"(({start} + {loop_var}) * {stride})"
4✔
2603
                    loop_var_used = True
4✔
2604
                else:
2605
                    if stride == "1":
4✔
2606
                        term = start
4✔
2607
                    else:
2608
                        term = f"({start} * {stride})"
4✔
2609

2610
                terms.append(term)
4✔
2611

2612
            if not terms:
4✔
2613
                return loop_var
×
2614

2615
            result = terms[0]
4✔
2616
            for t in terms[1:]:
4✔
2617
                result = f"({result} + {t})"
4✔
2618

2619
            return result
4✔
2620

2621
        self.builder.begin_for(i_var, "0", m_expr, "1")
4✔
2622
        self.builder.begin_for(j_var, "0", n_expr, "1")
4✔
2623

2624
        block = self.builder.add_block()
4✔
2625

2626
        t_a = self.builder.add_access(block, name_a)
4✔
2627
        t_b = self.builder.add_access(block, name_b)
4✔
2628
        t_c = self.builder.add_access(block, tmp_name)
4✔
2629

2630
        if ufunc_name in ["minimum", "maximum"]:
4✔
2631
            if is_int:
4✔
2632
                t_task = self.builder.add_tasklet(
4✔
2633
                    block, int_opcode, ["_in1", "_in2"], ["_out"]
2634
                )
2635
            else:
2636
                t_task = self.builder.add_cmath(block, fp_opcode, dtype.primitive_type)
4✔
2637
        else:
2638
            tasklet_code = int_opcode if is_int else fp_opcode
4✔
2639
            t_task = self.builder.add_tasklet(
4✔
2640
                block, tasklet_code, ["_in1", "_in2"], ["_out"]
2641
            )
2642

2643
        a_index = compute_linear_index(name_a, subset_a, indices_a, i_var)
4✔
2644
        b_index = compute_linear_index(name_b, subset_b, indices_b, j_var)
4✔
2645

2646
        self.builder.add_memlet(block, t_a, "void", t_task, "_in1", a_index)
4✔
2647
        self.builder.add_memlet(block, t_b, "void", t_task, "_in2", b_index)
4✔
2648

2649
        flat_index = f"(({i_var}) * ({n_expr}) + ({j_var}))"
4✔
2650
        self.builder.add_memlet(block, t_task, "_out", t_c, "void", flat_index)
4✔
2651

2652
        self.builder.end_for()
4✔
2653
        self.builder.end_for()
4✔
2654

2655
        return tmp_name
4✔
2656

2657
    def _handle_numpy_any(self, node, func_name):
4✔
2658
        """Handle np.any(arr): truthy if any element is truthy.
2659

2660
        Implemented as the maximum over the mask cast to float (1.0/0.0). The
2661
        cast is required because the max reduction's identity element is -inf,
2662
        which is not a valid value for a boolean/integer array.
2663
        """
2664
        return self._reduce_mask(node, "max")
4✔
2665

2666
    def _handle_numpy_all(self, node, func_name):
4✔
2667
        """Handle np.all(arr): truthy iff all elements are truthy.
2668

2669
        Implemented as the minimum over the mask cast to float (1.0/0.0).
2670
        """
2671
        return self._reduce_mask(node, "min")
4✔
2672

2673
    def _reduce_mask(self, node, reduce_op):
4✔
2674
        if len(node.args) < 1:
4✔
NEW
2675
            raise ValueError("np.any/np.all require an array argument")
×
2676
        mask_name = self.visit(node.args[0])
4✔
2677
        if mask_name not in self.tensor_table:
4✔
NEW
2678
            raise ValueError(
×
2679
                f"np.any/np.all argument must resolve to an array, got {mask_name}"
2680
            )
2681
        float_name = self._cast_array(mask_name, Scalar(PrimitiveType.Double))
4✔
2682
        reduce_node = ast.Call(
4✔
2683
            func=node.func,
2684
            args=[ast.Name(id=float_name, ctx=ast.Load())],
2685
            keywords=[],
2686
        )
2687
        return self._handle_numpy_reduce(reduce_node, reduce_op)
4✔
2688

2689
    def _handle_numpy_reduce(self, node, func_name):
4✔
2690
        """Handle np.sum, np.max, np.min, np.mean, np.std."""
2691
        args = node.args
4✔
2692
        keywords = {kw.arg: kw.value for kw in node.keywords}
4✔
2693

2694
        array_node = args[0]
4✔
2695
        array_name = self.visit(array_node)
4✔
2696

2697
        if array_name not in self.tensor_table:
4✔
2698
            raise ValueError(f"Reduction input must be an array, got {array_name}")
×
2699

2700
        # For mean and std, we need float64 input and output (NumPy behavior)
2701
        # Cast input to float64 if needed
2702
        if func_name in ("mean", "std"):
4✔
2703
            float64_type = Scalar(PrimitiveType.Double)
4✔
2704
            array_name = self._cast_array(array_name, float64_type)
4✔
2705

2706
        input_tensor = self.tensor_table[array_name]
4✔
2707
        input_shape = input_tensor.shape
4✔
2708
        ndim = len(input_shape)
4✔
2709

2710
        axis = None
4✔
2711
        if len(args) > 1:
4✔
2712
            axis = args[1]
×
2713
        elif "axis" in keywords:
4✔
2714
            axis = keywords["axis"]
4✔
2715

2716
        keepdims = False
4✔
2717
        if "keepdims" in keywords:
4✔
2718
            keepdims_node = keywords["keepdims"]
4✔
2719
            if isinstance(keepdims_node, ast.Constant):
4✔
2720
                keepdims = bool(keepdims_node.value)
4✔
2721

2722
        axes = []
4✔
2723
        if axis is None:
4✔
2724
            axes = list(range(ndim))
4✔
2725
        elif isinstance(axis, ast.Constant):
4✔
2726
            val = axis.value
4✔
2727
            if val < 0:
4✔
2728
                val += ndim
×
2729
            axes = [val]
4✔
2730
        elif isinstance(axis, ast.Tuple):
4✔
2731
            for elt in axis.elts:
×
2732
                if isinstance(elt, ast.Constant):
×
2733
                    val = elt.value
×
2734
                    if val < 0:
×
2735
                        val += ndim
×
2736
                    axes.append(val)
×
2737
        elif (
4✔
2738
            isinstance(axis, ast.UnaryOp)
2739
            and isinstance(axis.op, ast.USub)
2740
            and isinstance(axis.operand, ast.Constant)
2741
        ):
2742
            val = -axis.operand.value
4✔
2743
            if val < 0:
4✔
2744
                val += ndim
4✔
2745
            axes = [val]
4✔
2746
        else:
2747
            try:
×
2748
                val = int(self.visit(axis))
×
2749
                if val < 0:
×
2750
                    val += ndim
×
2751
                axes = [val]
×
2752
            except:
×
2753
                raise NotImplementedError("Dynamic axis not supported")
×
2754

2755
        output_shape = []
4✔
2756
        for i in range(ndim):
4✔
2757
            if i in axes:
4✔
2758
                if keepdims:
4✔
2759
                    output_shape.append("1")
4✔
2760
            else:
2761
                output_shape.append(input_shape[i])
4✔
2762

2763
        dtype = self._ev._element_type(array_name)
4✔
2764

2765
        if not output_shape:
4✔
2766
            tmp_name = f"_tmp_{self._get_unique_id()}"
4✔
2767
            self.builder.add_container(tmp_name, dtype, False)
4✔
2768
            self.container_table[tmp_name] = dtype
4✔
2769
            self.tensor_table[tmp_name] = Tensor(dtype, [])
4✔
2770
        else:
2771
            output_strides = self._compute_strides(output_shape, "C")
4✔
2772
            tmp_name = self._create_array_temp(
4✔
2773
                output_shape, dtype, strides=output_strides
2774
            )
2775

2776
        output_tensor = self.tensor_table[tmp_name]
4✔
2777
        self.builder.add_reduce_op(
4✔
2778
            func_name, array_name, input_tensor, tmp_name, output_tensor, axes, keepdims
2779
        )
2780

2781
        return tmp_name
4✔
2782

2783
    # ========== Einsum Operations ==========
2784

2785
    def _parse_einsum_subscripts(self, subscripts, operand_shapes):
4✔
2786
        """Parse einsum subscripts string and return parsed components.
2787

2788
        Args:
2789
            subscripts: Einsum notation string, e.g., "ij,jk->ik" or "ij,jk"
2790
            operand_shapes: List of shapes for each operand
2791

2792
        Returns:
2793
            Tuple of (input_subscripts, output_subscripts, index_to_dim)
2794
            - input_subscripts: List of index strings per operand, e.g., ["ij", "jk"]
2795
            - output_subscripts: Output index string, e.g., "ik"
2796
            - index_to_dim: Dict mapping index char to dimension size string
2797
        """
2798
        # Remove whitespace
2799
        subscripts = subscripts.replace(" ", "")
4✔
2800

2801
        # Split into inputs and output
2802
        if "->" in subscripts:
4✔
2803
            input_part, output_subscripts = subscripts.split("->")
4✔
2804
        else:
2805
            input_part = subscripts
×
2806
            output_subscripts = None  # Implicit output
×
2807

2808
        # Split inputs by comma
2809
        input_subscripts = input_part.split(",")
4✔
2810

2811
        if len(input_subscripts) != len(operand_shapes):
4✔
2812
            raise ValueError(
×
2813
                f"Number of operands ({len(operand_shapes)}) does not match "
2814
                f"number of subscripts ({len(input_subscripts)})"
2815
            )
2816

2817
        # Map each index to its dimension size
2818
        index_to_dim = {}
4✔
2819
        for subscript, shape in zip(input_subscripts, operand_shapes):
4✔
2820
            if len(subscript) != len(shape):
4✔
2821
                raise ValueError(
×
2822
                    f"Subscript '{subscript}' has {len(subscript)} indices but "
2823
                    f"operand has {len(shape)} dimensions"
2824
                )
2825
            for idx_char, dim_size in zip(subscript, shape):
4✔
2826
                if idx_char in index_to_dim:
4✔
2827
                    # Validate dimensions match (at least symbolically)
2828
                    existing = index_to_dim[idx_char]
4✔
2829
                    if str(existing) != str(dim_size):
4✔
2830
                        # Could be symbolic - just warn or trust the user
2831
                        pass
×
2832
                else:
2833
                    index_to_dim[idx_char] = dim_size
4✔
2834

2835
        # Compute implicit output if not provided
2836
        if output_subscripts is None:
4✔
2837
            output_subscripts = self._compute_implicit_output(input_subscripts)
×
2838

2839
        return input_subscripts, output_subscripts, index_to_dim
4✔
2840

2841
    def _compute_implicit_output(self, input_subscripts):
4✔
2842
        """Compute implicit output indices (sorted indices appearing exactly once).
2843

2844
        Args:
2845
            input_subscripts: List of index strings, e.g., ["ij", "jk"]
2846

2847
        Returns:
2848
            Output index string with sorted non-contracted indices, e.g., "ik"
2849
        """
2850
        counts = {}
×
2851
        for subscript in input_subscripts:
×
2852
            for idx in subscript:
×
2853
                counts[idx] = counts.get(idx, 0) + 1
×
2854

2855
        # Output = sorted indices with count == 1 (non-contracted)
2856
        return "".join(sorted(idx for idx, cnt in counts.items() if cnt == 1))
×
2857

2858
    def _handle_numpy_einsum(self, node, func_name):
4✔
2859
        """Handle np.einsum(subscripts, *operands) calls.
2860

2861
        Parses the subscripts string to extract index structure, computes output
2862
        shape, and emits an EinsumNode to the IR.
2863
        """
2864
        if len(node.args) < 2:
4✔
2865
            raise ValueError("np.einsum requires at least subscripts and one operand")
×
2866

2867
        # First argument is the subscripts string
2868
        subscripts_arg = node.args[0]
4✔
2869
        if not isinstance(subscripts_arg, ast.Constant) or not isinstance(
4✔
2870
            subscripts_arg.value, str
2871
        ):
2872
            raise NotImplementedError("np.einsum subscripts must be a string literal")
×
2873
        subscripts = subscripts_arg.value
4✔
2874

2875
        # Remaining arguments are operands
2876
        operand_nodes = node.args[1:]
4✔
2877
        operand_names = [self.visit(op) for op in operand_nodes]
4✔
2878

2879
        # Validate all operands are in tensor_table
2880
        for name in operand_names:
4✔
2881
            if name not in self.tensor_table:
4✔
2882
                raise ValueError(f"Einsum operand '{name}' not found in tensor_table")
×
2883

2884
        # Get shapes for all operands
2885
        operand_shapes = [self.tensor_table[name].shape for name in operand_names]
4✔
2886

2887
        # Parse subscripts
2888
        input_subscripts, output_subscripts, index_to_dim = (
4✔
2889
            self._parse_einsum_subscripts(subscripts, operand_shapes)
2890
        )
2891

2892
        # Build dimension specs: (indvar, init, bound) for each unique index
2893
        # Collect all unique indices in order of first appearance
2894
        seen_indices = []
4✔
2895
        for subscript in input_subscripts:
4✔
2896
            for idx in subscript:
4✔
2897
                if idx not in seen_indices:
4✔
2898
                    seen_indices.append(idx)
4✔
2899

2900
        # Remap the (single-letter) einsum indices to collision-safe symbol
2901
        # names. A bare letter like 'e' or 'i' is parsed by the symbolic engine
2902
        # as a reserved constant (Euler's number / imaginary unit) rather than a
2903
        # symbol, which breaks the EinsumNode index/indvar matching. Use unique
2904
        # opaque names instead.
2905
        idx_map = {c: f"__einidx_{k}" for k, c in enumerate(seen_indices)}
4✔
2906

2907
        dims = []
4✔
2908
        for idx in seen_indices:
4✔
2909
            dims.append((idx_map[idx], "0", str(index_to_dim[idx])))
4✔
2910

2911
        # Build output indices (the index variables for output dimensions)
2912
        out_indices = [idx_map[idx] for idx in output_subscripts]
4✔
2913

2914
        # Build input indices for each operand
2915
        in_indices = [
4✔
2916
            [idx_map[idx] for idx in subscript] for subscript in input_subscripts
2917
        ]
2918

2919
        # Compute output shape from output subscripts
2920
        output_shape = [str(index_to_dim[idx]) for idx in output_subscripts]
4✔
2921

2922
        # Determine element type (promote from inputs)
2923
        dtypes = [self._ev._element_type(name) for name in operand_names]
4✔
2924
        dtype = dtypes[0]
4✔
2925
        for dt in dtypes[1:]:
4✔
2926
            dtype = promote_element_types(dtype, dt)
4✔
2927

2928
        # Create output container
2929
        if output_shape:
4✔
2930
            output_strides = self._compute_strides(output_shape, "C")
4✔
2931
            tmp_name = self._create_array_temp(
4✔
2932
                output_shape, dtype, strides=output_strides, zero_init=True
2933
            )
2934
        else:
2935
            # Scalar output
2936
            tmp_name = f"_tmp_{self._get_unique_id()}"
4✔
2937
            self.builder.add_container(tmp_name, dtype, False)
4✔
2938
            self.container_table[tmp_name] = dtype
4✔
2939
            self.tensor_table[tmp_name] = Tensor(dtype, [])
4✔
2940

2941
        # Get tensor types for builder call
2942
        input_types = [self.tensor_table[name] for name in operand_names]
4✔
2943
        output_type = self.tensor_table[tmp_name]
4✔
2944

2945
        # Call builder.add_einsum
2946
        self.builder.add_einsum(
4✔
2947
            operand_names,
2948
            tmp_name,
2949
            dims,
2950
            out_indices,
2951
            in_indices,
2952
            input_types,
2953
            output_type,
2954
        )
2955

2956
        return tmp_name
4✔
2957

2958
    def handle_numpy_astype(self, node, array_name):
4✔
2959
        """Handle numpy array.astype(dtype) method calls."""
2960
        if len(node.args) < 1:
4✔
2961
            raise ValueError("astype requires at least one argument (dtype)")
×
2962

2963
        # Check for copy=False which we don't support (we always copy)
2964
        for kw in node.keywords:
4✔
2965
            if kw.arg == "copy":
4✔
2966
                if isinstance(kw.value, ast.Constant) and kw.value.value is False:
4✔
2967
                    raise NotImplementedError("astype with copy=False is not supported")
4✔
2968

2969
        dtype_arg = node.args[0]
4✔
2970
        target_dtype = element_type_from_ast_node(
4✔
2971
            dtype_arg, self.container_table, self.globals_dict
2972
        )
2973

2974
        if array_name not in self.tensor_table:
4✔
2975
            raise ValueError(f"Array {array_name} not found in tensor_table")
×
2976

2977
        input_tensor = self.tensor_table[array_name]
4✔
2978
        input_shape = input_tensor.shape
4✔
2979
        input_strides = getattr(input_tensor, "strides", None)
4✔
2980

2981
        # Determine output order: preserve F-order if input is F-contiguous
2982
        order = "C"
4✔
2983
        if input_strides and len(input_strides) >= 2 and len(input_shape) >= 2:
4✔
2984
            # F-order: first stride is 1, subsequent strides are products of preceding dims
2985
            f_strides = self._compute_strides(input_shape, "F")
4✔
2986
            if input_strides == f_strides:
4✔
2987
                order = "F"
×
2988

2989
        output_strides = self._compute_strides(input_shape, order)
4✔
2990
        tmp_name = self._create_array_temp(
4✔
2991
            input_shape, target_dtype, strides=output_strides
2992
        )
2993

2994
        output_tensor = self.tensor_table[tmp_name]
4✔
2995
        self.builder.add_cast_op(array_name, input_tensor, tmp_name, output_tensor)
4✔
2996

2997
        return tmp_name
4✔
2998

2999
    def _handle_numpy_copy_func(self, node, func_name):
4✔
3000
        """Handle np.copy(arr) - function form of the array .copy() method.
3001

3002
        The argument may be an arbitrary array expression (e.g. a gather like
3003
        ``np.copy(domain.e[region_elems])``); resolve it to an array container
3004
        first, then reuse the method-form copy.
3005
        """
3006
        if len(node.args) < 1:
4✔
NEW
3007
            raise ValueError("np.copy requires an array argument")
×
3008
        array_name = self.visit(node.args[0])
4✔
3009
        if array_name not in self.tensor_table:
4✔
NEW
3010
            raise ValueError(
×
3011
                f"np.copy argument must resolve to an array, got {array_name}"
3012
            )
3013
        return self.handle_numpy_copy(node, array_name)
4✔
3014

3015
    def handle_numpy_copy(self, node, array_name):
4✔
3016
        """Handle numpy array.copy() method calls using memcpy."""
3017
        if array_name not in self.tensor_table:
4✔
3018
            raise ValueError(f"Array {array_name} not found in tensor_table")
×
3019
        input_tensor = self.tensor_table[array_name]
4✔
3020
        input_shape = input_tensor.shape
4✔
3021
        input_strides = getattr(input_tensor, "strides", None)
4✔
3022

3023
        element_type = Scalar(PrimitiveType.Double)
4✔
3024
        if array_name in self.container_table:
4✔
3025
            sym_type = self.container_table[array_name]
4✔
3026
            if isinstance(sym_type, Pointer) and sym_type.has_pointee_type():
4✔
3027
                element_type = sym_type.pointee_type
4✔
3028

3029
        # Determine output order: preserve F-order if input is F-contiguous
3030
        order = "C"
4✔
3031
        if input_strides and len(input_strides) >= 2 and len(input_shape) >= 2:
4✔
3032
            f_strides = self._compute_strides(input_shape, "F")
4✔
3033
            if input_strides == f_strides:
4✔
3034
                order = "F"
×
3035

3036
        output_strides = self._compute_strides(input_shape, order)
4✔
3037
        tmp_name = self._create_array_temp(
4✔
3038
            input_shape, element_type, strides=output_strides
3039
        )
3040

3041
        output_tensor = self.tensor_table[tmp_name]
4✔
3042
        # Workaround: "assign-op"
3043
        self.builder.add_cast_op(array_name, input_tensor, tmp_name, output_tensor)
4✔
3044

3045
        return tmp_name
4✔
3046

3047
    def _get_contiguous_output_strides(self, shape, input_strides):
4✔
3048
        """Get contiguous output strides, preserving C or F order if input is contiguous.
3049

3050
        For non-contiguous input strides (e.g., from slices), returns C-order strides.
3051
        This ensures output allocation matches the stride pattern.
3052

3053
        Args:
3054
            shape: Output shape
3055
            input_strides: Strides from input tensor
3056

3057
        Returns:
3058
            List of stride expressions for a contiguous output array
3059
        """
3060
        if not shape or not input_strides:
4✔
3061
            return self._compute_strides(shape, "C")
4✔
3062

3063
        # Preserve order if contiguous, otherwise default to C-order
3064
        c_strides = self._compute_strides(shape, "C")
4✔
3065
        if input_strides == c_strides:
4✔
3066
            return c_strides
4✔
3067
        f_strides = self._compute_strides(shape, "F")
4✔
3068
        if input_strides == f_strides:
4✔
3069
            return f_strides
×
3070
        return c_strides
4✔
3071

3072
    def _compute_strides(self, shape, order="C"):
4✔
3073
        """Compute strides for a given shape and memory order.
3074

3075
        Args:
3076
            shape: List of dimension sizes
3077
            order: "C" for row-major (default), "F" for column-major
3078

3079
        Returns:
3080
            List of stride expressions as strings
3081
        """
3082
        if not shape:
4✔
3083
            return []
4✔
3084

3085
        ndim = len(shape)
4✔
3086
        strides = []
4✔
3087

3088
        if order == "F":
4✔
3089
            # Column-major (Fortran order): stride[i] = product of shape[:i]
3090
            for dim_idx in range(ndim):
4✔
3091
                if dim_idx == 0:
4✔
3092
                    strides.append("1")
4✔
3093
                else:
3094
                    # Wrap each shape in parens to ensure correct precedence
3095
                    prefix_shapes = [f"({s})" for s in shape[:dim_idx]]
4✔
3096
                    if len(prefix_shapes) == 1:
4✔
3097
                        strides.append(prefix_shapes[0])
4✔
3098
                    else:
3099
                        strides.append("(" + " * ".join(prefix_shapes) + ")")
4✔
3100
        else:
3101
            # Row-major (C order): stride[i] = product of shape[i+1:]
3102
            for dim_idx in range(ndim):
4✔
3103
                if dim_idx == ndim - 1:
4✔
3104
                    strides.append("1")
4✔
3105
                else:
3106
                    # Wrap each shape in parens to ensure correct precedence
3107
                    suffix_shapes = [f"({s})" for s in shape[dim_idx + 1 :]]
4✔
3108
                    if len(suffix_shapes) == 1:
4✔
3109
                        strides.append(suffix_shapes[0])
4✔
3110
                    else:
3111
                        strides.append("(" + " * ".join(suffix_shapes) + ")")
4✔
3112

3113
        return strides
4✔
3114

3115
    def _exprs_equal(self, a, b):
4✔
3116
        """Return True if two stride/shape expressions are mathematically equal.
3117

3118
        Stride expressions are produced by different code paths with different
3119
        formatting (e.g. ``"(_s1 * _s2)"`` vs ``"((_s1) * (_s2))"``) and may
3120
        contain additions, so a purely textual comparison is insufficient. Fall
3121
        back to symbolic comparison via sympy, forcing every identifier to a
3122
        plain symbol so reserved names (``N``, ``E``, ``I``, ...) are not given
3123
        special meaning.
3124
        """
3125
        a, b = str(a), str(b)
4✔
3126
        if a.replace(" ", "") == b.replace(" ", ""):
4✔
3127
            return True
4✔
3128
        try:
4✔
3129
            import re
4✔
3130
            import sympy
4✔
3131

3132
            names = set(re.findall(r"[A-Za-z_]\w*", a + " " + b))
4✔
3133
            local_dict = {n: sympy.Symbol(n) for n in names}
4✔
3134
            expr_a = sympy.sympify(a, locals=local_dict)
4✔
3135
            expr_b = sympy.sympify(b, locals=local_dict)
4✔
3136
            return sympy.simplify(expr_a - expr_b) == 0
4✔
3137
        except Exception:
×
3138
            return a.replace(" ", "") == b.replace(" ", "")
×
3139

3140
    def _strides_equal(self, a_strides, b_strides):
4✔
3141
        """Compare two stride lists element-wise up to mathematical equality."""
3142
        if len(a_strides) != len(b_strides):
4✔
3143
            return False
×
3144
        return all(self._exprs_equal(a, b) for a, b in zip(a_strides, b_strides))
4✔
3145

3146
    def _is_contiguous(self, shape, strides):
4✔
3147
        """Check if strides represent a contiguous (C or F order) layout."""
3148
        if not shape or not strides:
4✔
3149
            return True
×
3150

3151
        c_strides = self._compute_strides(shape, "C")
4✔
3152
        if self._strides_equal(strides, c_strides):
4✔
3153
            return True
4✔
3154
        f_strides = self._compute_strides(shape, "F")
×
3155
        return self._strides_equal(strides, f_strides)
×
3156

3157
    def _create_array_temp(
4✔
3158
        self,
3159
        shape,
3160
        dtype,
3161
        zero_init=False,
3162
        ones_init=False,
3163
        shapes_runtime=None,
3164
        strides=None,
3165
    ):
3166
        """Create a temporary array."""
3167
        tmp_name = f"_tmp_{self._get_unique_id()}"
4✔
3168

3169
        # Handle 0-dimensional arrays as scalars
3170
        if not shape or (len(shape) == 0):
4✔
3171
            self.builder.add_container(tmp_name, dtype, False)
4✔
3172
            self.container_table[tmp_name] = dtype
4✔
3173
            self.tensor_table[tmp_name] = Tensor(dtype, [])
4✔
3174

3175
            if zero_init:
4✔
3176
                self.builder.add_assignment(
×
3177
                    tmp_name,
3178
                    "0.0" if dtype.primitive_type == PrimitiveType.Double else "0",
3179
                )
3180
            elif ones_init:
4✔
3181
                self.builder.add_assignment(
×
3182
                    tmp_name,
3183
                    "1.0" if dtype.primitive_type == PrimitiveType.Double else "1",
3184
                )
3185

3186
            return tmp_name
4✔
3187

3188
        # Calculate size - wrap each dimension in parentheses to ensure correct
3189
        # parsing when dimensions are expressions like "-2 + _s0"
3190
        size_str = "1"
4✔
3191
        for dim in shape:
4✔
3192
            size_str = f"({size_str} * ({dim}))"
4✔
3193

3194
        element_size = self.builder.get_sizeof(dtype)
4✔
3195
        total_size = f"({size_str} * {element_size})"
4✔
3196

3197
        # Use provided strides or compute C-order strides
3198
        if strides is None:
4✔
3199
            strides = self._compute_strides(shape, "C")
4✔
3200

3201
        # Create pointer
3202
        ptr_type = Pointer(dtype)
4✔
3203
        self.builder.add_container(tmp_name, ptr_type, False)
4✔
3204
        self.container_table[tmp_name] = ptr_type
4✔
3205
        tensor_entry = Tensor(dtype, shape, strides, "0")
4✔
3206
        if shapes_runtime is not None:
4✔
3207
            self.shapes_runtime_info[tmp_name] = shapes_runtime
4✔
3208
        self.tensor_table[tmp_name] = tensor_entry
4✔
3209

3210
        # Try to hoist allocation to function entry
3211
        init_type = (
4✔
3212
            ManagedMemoryHandler.INIT_ZERO
3213
            if zero_init
3214
            else ManagedMemoryHandler.INIT_NONE
3215
        )
3216
        if not ones_init and self.memory_handler.allocate(
4✔
3217
            tmp_name, ptr_type, total_size, init=init_type
3218
        ):
3219
            pass  # Allocation registered for hoisting
4✔
3220
        else:
3221
            # Emit allocation immediately (size depends on loop variables or needs loop init)
3222
            self._emit_malloc(
4✔
3223
                tmp_name, total_size, ptr_type, zero_init, ones_init, size_str, dtype
3224
            )
3225

3226
        return tmp_name
4✔
3227

3228
    def _emit_malloc(
4✔
3229
        self, tmp_name, total_size, ptr_type, zero_init, ones_init, size_str, dtype
3230
    ):
3231
        """Emit malloc and optional initialization for a temporary array."""
3232
        block1 = self.builder.add_block()
4✔
3233
        t_malloc = self.builder.add_malloc(block1, total_size)
4✔
3234
        t_ptr1 = self.builder.add_access(block1, tmp_name)
4✔
3235
        self.builder.add_memlet(block1, t_malloc, "_ret", t_ptr1, "void", "", ptr_type)
4✔
3236

3237
        if zero_init:
4✔
3238
            block2 = self.builder.add_block()
4✔
3239
            t_memset = self.builder.add_memset(block2, "0", total_size)
4✔
3240
            t_ptr2 = self.builder.add_access(block2, tmp_name)
4✔
3241
            self.builder.add_memlet(
4✔
3242
                block2, t_ptr2, "void", t_memset, "_ptr", "", ptr_type
3243
            )
3244
        elif ones_init:
4✔
3245
            loop_var = f"_i_{self._get_unique_id()}"
4✔
3246
            if not self.builder.exists(loop_var):
4✔
3247
                self.builder.add_container(loop_var, Scalar(PrimitiveType.Int64), False)
4✔
3248
                self.container_table[loop_var] = Scalar(PrimitiveType.Int64)
4✔
3249

3250
            self.builder.begin_for(loop_var, "0", size_str, "1")
4✔
3251

3252
            val = "1.0"
4✔
3253
            if dtype.primitive_type in [
4✔
3254
                PrimitiveType.Int64,
3255
                PrimitiveType.Int32,
3256
                PrimitiveType.Int8,
3257
                PrimitiveType.Int16,
3258
                PrimitiveType.UInt64,
3259
                PrimitiveType.UInt32,
3260
                PrimitiveType.UInt8,
3261
                PrimitiveType.UInt16,
3262
            ]:
3263
                val = "1"
4✔
3264

3265
            block_assign = self.builder.add_block()
4✔
3266
            t_const = self.builder.add_constant(block_assign, val, dtype)
4✔
3267
            t_arr = self.builder.add_access(block_assign, tmp_name)
4✔
3268

3269
            t_task = self.builder.add_tasklet(
4✔
3270
                block_assign, TaskletCode.assign, ["_in"], ["_out"]
3271
            )
3272
            self.builder.add_memlet(
4✔
3273
                block_assign, t_const, "void", t_task, "_in", "", dtype
3274
            )
3275
            self.builder.add_memlet(
4✔
3276
                block_assign, t_task, "_out", t_arr, "void", loop_var
3277
            )
3278

3279
            self.builder.end_for()
4✔
3280

3281
    def _compute_linear_index(self, indices, shapes, array_name, ndim):
4✔
3282
        """Compute linear index from multi-dimensional indices.
3283

3284
        Uses strides from tensor_table if available (supporting F-order arrays),
3285
        otherwise falls back to computing strides assuming C-order.
3286
        """
3287
        if ndim == 0:
×
3288
            return "0"
×
3289

3290
        # Try to get strides from tensor_table
3291
        strides = None
×
3292
        if array_name in self.tensor_table:
×
3293
            tensor_info = self.tensor_table[array_name]
×
3294
            if hasattr(tensor_info, "strides") and tensor_info.strides:
×
3295
                strides = tensor_info.strides
×
3296

3297
        if strides and len(strides) == ndim:
×
3298
            # Use explicit strides from tensor_table
3299
            linear_index = ""
×
3300
            for i in range(ndim):
×
3301
                stride = strides[i]
×
3302
                if stride == "1":
×
3303
                    term = str(indices[i])
×
3304
                else:
3305
                    term = f"(({indices[i]}) * ({stride}))"
×
3306

3307
                if i == 0:
×
3308
                    linear_index = term
×
3309
                else:
3310
                    linear_index = f"({linear_index} + {term})"
×
3311
            return linear_index
×
3312
        else:
3313
            # Fall back to C-order (row-major) stride computation
3314
            linear_index = ""
×
3315
            for i in range(ndim):
×
3316
                term = str(indices[i])
×
3317
                for j in range(i + 1, ndim):
×
3318
                    shape_val = (
×
3319
                        shapes[j] if j < len(shapes) else f"_{array_name}_shape_{j}"
3320
                    )
3321
                    term = f"(({term}) * {shape_val})"
×
3322

3323
                if i == 0:
×
3324
                    linear_index = term
×
3325
                else:
3326
                    linear_index = f"({linear_index} + {term})"
×
3327

3328
            return linear_index
×
3329

3330
    def _compute_broadcast_shape(self, shape_a, shape_b):
4✔
3331
        """Compute the broadcast output shape following NumPy broadcasting rules."""
3332
        if not shape_a:
4✔
3333
            return shape_b
4✔
3334
        if not shape_b:
4✔
3335
            return shape_a
4✔
3336

3337
        max_ndim = max(len(shape_a), len(shape_b))
4✔
3338
        padded_a = ["1"] * (max_ndim - len(shape_a)) + [str(s) for s in shape_a]
4✔
3339
        padded_b = ["1"] * (max_ndim - len(shape_b)) + [str(s) for s in shape_b]
4✔
3340

3341
        result = []
4✔
3342
        for a, b in zip(padded_a, padded_b):
4✔
3343
            if a == "1":
4✔
3344
                result.append(b)
4✔
3345
            elif b == "1":
4✔
3346
                result.append(a)
4✔
3347
            elif a == b:
4✔
3348
                result.append(a)
4✔
3349
            else:
3350
                result.append(a)
4✔
3351

3352
        return result
4✔
3353

3354
    def _needs_broadcast(self, input_shape, output_shape):
4✔
3355
        """Check if input shape needs broadcasting to match output shape."""
3356
        if len(input_shape) != len(output_shape):
4✔
3357
            return True
4✔
3358
        for in_dim, out_dim in zip(input_shape, output_shape):
4✔
3359
            if str(in_dim) != str(out_dim):
4✔
3360
                return True
4✔
3361
        return False
4✔
3362

3363
    def _compute_broadcast_strides(self, input_shape, input_strides, output_shape):
4✔
3364
        """Compute strides for broadcasting input to output shape.
3365

3366
        For broadcast dimensions (size 1), stride is set to 0 so the same
3367
        value is repeated. This enables stride-based broadcasting without copying.
3368
        """
3369
        # Pad input shape and strides on the left to match output ndim
3370
        ndim_diff = len(output_shape) - len(input_shape)
4✔
3371
        padded_shape = ["1"] * ndim_diff + [str(s) for s in input_shape]
4✔
3372
        padded_strides = ["0"] * ndim_diff + [str(s) for s in input_strides]
4✔
3373

3374
        broadcast_strides = []
4✔
3375
        for in_dim, in_stride, out_dim in zip(
4✔
3376
            padded_shape, padded_strides, output_shape
3377
        ):
3378
            # Only use stride 0 when input dimension is exactly "1" (broadcast case).
3379
            # For other cases (including symbolic dimensions that may be equal at runtime),
3380
            # keep the original stride.
3381
            if str(in_dim) == "1" and str(out_dim) != "1":
4✔
3382
                # Broadcast dimension: use stride 0
3383
                broadcast_strides.append("0")
4✔
3384
            else:
3385
                # Non-broadcast dimension or potentially equal symbolic dimensions:
3386
                # keep original stride
3387
                broadcast_strides.append(in_stride)
4✔
3388

3389
        return broadcast_strides
4✔
3390

3391
    def _shape_to_runtime_expr(self, shape_node):
4✔
3392
        """Convert a shape expression AST node to a runtime-evaluable string."""
3393
        if isinstance(shape_node, ast.Constant):
4✔
3394
            return str(shape_node.value)
4✔
3395
        elif isinstance(shape_node, ast.Name):
4✔
3396
            return shape_node.id
4✔
3397
        elif isinstance(shape_node, ast.BinOp):
4✔
3398
            left = self._shape_to_runtime_expr(shape_node.left)
4✔
3399
            right = self._shape_to_runtime_expr(shape_node.right)
4✔
3400
            op = self.visit(shape_node.op)
4✔
3401
            return f"({left} {op} {right})"
4✔
3402
        elif isinstance(shape_node, ast.UnaryOp):
4✔
3403
            operand = self._shape_to_runtime_expr(shape_node.operand)
×
3404
            if isinstance(shape_node.op, ast.USub):
×
3405
                return f"(-{operand})"
×
3406
            elif isinstance(shape_node.op, ast.UAdd):
×
3407
                return operand
×
3408
            else:
3409
                return self.visit(shape_node)
×
3410
        elif isinstance(shape_node, ast.Subscript):
4✔
3411
            val = shape_node.value
4✔
3412
            if isinstance(val, ast.Attribute) and val.attr == "shape":
4✔
3413
                if isinstance(val.value, ast.Name):
4✔
3414
                    arr_name = val.value.id
4✔
3415
                    if isinstance(shape_node.slice, ast.Constant):
4✔
3416
                        idx = shape_node.slice.value
4✔
3417
                        if arr_name in self.tensor_table:
4✔
3418
                            shapes = self.tensor_table[arr_name].shape
4✔
3419
                            if idx < len(shapes):
4✔
3420
                                return shapes[idx]
4✔
3421
                        return f"{arr_name}.shape[{idx}]"
×
3422
            return self.visit(shape_node)
×
3423
        elif isinstance(shape_node, ast.Tuple):
×
3424
            return [self._shape_to_runtime_expr(elt) for elt in shape_node.elts]
×
3425
        elif isinstance(shape_node, ast.List):
×
3426
            return [self._shape_to_runtime_expr(elt) for elt in shape_node.elts]
×
3427
        else:
3428
            return self.visit(shape_node)
×
3429

3430
    # ========== Type Casting Helpers ==========
3431

3432
    def _cast_scalar(self, name, target_type):
4✔
3433
        """
3434
        Cast a scalar value to a different type using an assign tasklet.
3435

3436
        The backend detects the specific conversion (fpext, sitofp, etc.)
3437
        from the type mismatch between input and output.
3438

3439
        Args:
3440
            name: Name of the scalar to cast
3441
            target_type: Target element type (Scalar)
3442

3443
        Returns:
3444
            Name of the casted scalar (or original if no cast needed)
3445
        """
3446
        current_type = self._ev._element_type(name)
4✔
3447
        if current_type.primitive_type == target_type.primitive_type:
4✔
3448
            return name
4✔
3449

3450
        cast_name = f"_cast_{self._get_unique_id()}"
4✔
3451
        self.builder.add_container(cast_name, target_type, False)
4✔
3452
        self.container_table[cast_name] = target_type
4✔
3453
        self.tensor_table[cast_name] = Tensor(target_type, [])
4✔
3454

3455
        block = self.builder.add_block()
4✔
3456
        t_src, src_sub = self._add_read(block, name)
4✔
3457
        t_dst = self.builder.add_access(block, cast_name)
4✔
3458
        t_task = self.builder.add_tasklet(block, TaskletCode.assign, ["_in"], ["_out"])
4✔
3459
        self.builder.add_memlet(block, t_src, "void", t_task, "_in", src_sub)
4✔
3460
        self.builder.add_memlet(block, t_task, "_out", t_dst, "void", "")
4✔
3461

3462
        return cast_name
4✔
3463

3464
    def _cast_array(self, name, target_type):
4✔
3465
        """
3466
        Cast an array to a different element type using the CastNode library node.
3467

3468
        This is an elementwise cast operation that creates a new array.
3469
        Reuses the same infrastructure as handle_numpy_astype().
3470

3471
        Args:
3472
            name: Name of the array to cast
3473
            target_type: Target element type (Scalar)
3474

3475
        Returns:
3476
            Name of the casted array (or original if no cast needed)
3477
        """
3478
        current_type = self._ev._element_type(name)
4✔
3479
        if current_type.primitive_type == target_type.primitive_type:
4✔
3480
            return name
4✔
3481

3482
        src_tensor = self.tensor_table[name]
4✔
3483

3484
        # Create output array with same shape but new dtype
3485
        # Preserve strides order (C or F contiguous)
3486
        output_strides = self._get_contiguous_output_strides(
4✔
3487
            src_tensor.shape, src_tensor.strides
3488
        )
3489
        tmp_name = self._create_array_temp(
4✔
3490
            src_tensor.shape, target_type, strides=output_strides
3491
        )
3492
        tmp_tensor = self.tensor_table[tmp_name]
4✔
3493

3494
        # Use existing cast infrastructure (CastNode)
3495
        self.builder.add_cast_op(name, src_tensor, tmp_name, tmp_tensor)
4✔
3496

3497
        return tmp_name
4✔
3498

3499
    def _cast_to_type(self, name, target_type):
4✔
3500
        """
3501
        Cast an operand (scalar or array) to the target type.
3502

3503
        Dispatches to _cast_scalar or _cast_array based on whether
3504
        the operand is in tensor_table (includes 0-d arrays).
3505

3506
        Args:
3507
            name: Name of the operand to cast
3508
            target_type: Target element type (Scalar)
3509

3510
        Returns:
3511
            Name of the casted operand (or original if no cast needed)
3512
        """
3513
        if name in self.tensor_table:
4✔
3514
            # In tensor_table means it's an array (including 0-d arrays)
3515
            return self._cast_array(name, target_type)
4✔
3516
        else:
3517
            # Not in tensor_table means it's a literal or Python scalar
3518
            return self._cast_scalar(name, target_type)
4✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc