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

daisytuner / docc / 23741442572

30 Mar 2026 11:00AM UTC coverage: 64.49% (+0.4%) from 64.123%
23741442572

Pull #617

github

web-flow
Merge 93b1309d5 into 1ebd4fa23
Pull Request #617: [Python] Adds np.einsum support

1403 of 1937 new or added lines in 11 files covered. (72.43%)

28618 of 44376 relevant lines covered (64.49%)

388.35 hits per line

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

78.54
/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
            "sum": self._handle_numpy_reduce,
52
            "max": self._handle_numpy_reduce,
53
            "min": self._handle_numpy_reduce,
54
            "mean": self._handle_numpy_reduce,
55
            "std": self._handle_numpy_reduce,
56
            "matmul": self._handle_numpy_matmul,
57
            "dot": self._handle_numpy_matmul,
58
            "matvec": self._handle_numpy_matmul,
59
            "outer": self._handle_numpy_outer,
60
            "minimum": self._handle_numpy_binary_op,
61
            "maximum": self._handle_numpy_binary_op,
62
            "where": self._handle_numpy_where,
63
            "clip": self._handle_numpy_clip,
64
            "transpose": self._handle_numpy_transpose,
65
            "flip": self._handle_numpy_flip,
66
            "fliplr": self._handle_numpy_fliplr,
67
            "flipud": self._handle_numpy_flipud,
68
            "reshape": self._handle_numpy_reshape,
69
            "einsum": self._handle_numpy_einsum,
70
        }
71

72
    # Expose parent properties for convenience
73
    @property
4✔
74
    def tensor_table(self):
4✔
75
        return self._ev.tensor_table
4✔
76

77
    @property
4✔
78
    def builder(self):
4✔
79
        return self._ev.builder
4✔
80

81
    @property
4✔
82
    def container_table(self):
4✔
83
        return self._ev.container_table
4✔
84

85
    @property
4✔
86
    def globals_dict(self):
4✔
87
        return self._ev.globals_dict
×
88

89
    @property
4✔
90
    def shapes_runtime_info(self):
4✔
91
        return self._ev.shapes_runtime_info
4✔
92

93
    @property
4✔
94
    def memory_handler(self):
4✔
95
        """Access the memory handler owned by the parser."""
96
        return self._ev.memory_handler
4✔
97

98
    def _get_unique_id(self):
4✔
99
        return self._ev._get_unique_id()
4✔
100

101
    def _add_read(self, block, expr_str, debug_info=None):
4✔
102
        return self._ev._add_read(block, expr_str, debug_info)
4✔
103

104
    def _is_int(self, operand):
4✔
105
        return self._ev._is_int(operand)
4✔
106

107
    def visit(self, node):
4✔
108
        return self._ev.visit(node)
4✔
109

110
    # ========== Linear Algebra Helper Methods (from LinearAlgebraHandler) ==========
111

112
    def parse_arg(self, node):
4✔
113
        """Parse an array argument, returning (name, start_indices, slice_shape, indices).
114

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

134
                start_indices = []
4✔
135
                slice_shape = []
4✔
136

137
                for i, idx in enumerate(indices):
4✔
138
                    if isinstance(idx, ast.Slice):
4✔
139
                        start = "0"
4✔
140
                        if idx.lower:
4✔
141
                            start = self._ev.visit(idx.lower)
4✔
142
                        start_indices.append(start)
4✔
143

144
                        shapes = self.tensor_table[name].shape
4✔
145
                        dim_size = (
4✔
146
                            shapes[i] if i < len(shapes) else f"_{name}_shape_{i}"
147
                        )
148
                        stop = dim_size
4✔
149
                        if idx.upper:
4✔
150
                            stop = self._ev.visit(idx.upper)
4✔
151

152
                        size = f"({stop} - {start})"
4✔
153
                        slice_shape.append(size)
4✔
154
                    else:
155
                        if isinstance(idx, ast.Name) and idx.id in self.tensor_table:
4✔
156
                            # This is an array index (gather operation)
157
                            return None, None, None, None
×
158
                        val = self._ev.visit(idx)
4✔
159
                        start_indices.append(val)
4✔
160

161
                return name, start_indices, slice_shape, indices
4✔
162

163
        return None, None, None, None
4✔
164

165
    def flatten_subset(self, name, start_indices):
4✔
166
        """Convert multi-dimensional start indices to a flattened linear offset."""
167
        if not start_indices:
4✔
168
            return []
4✔
169
        info = self.tensor_table[name]
4✔
170
        shapes = info.shape
4✔
171
        ndim = len(info.shape)
4✔
172

173
        if len(start_indices) != ndim:
4✔
174
            return start_indices
4✔
175

176
        strides = []
4✔
177
        current_stride = "1"
4✔
178
        strides.append(current_stride)
4✔
179
        for i in range(ndim - 1, 0, -1):
4✔
180
            dim_size = shapes[i]
4✔
181
            if current_stride == "1":
4✔
182
                current_stride = str(dim_size)
4✔
183
            else:
184
                current_stride = f"({current_stride} * {dim_size})"
4✔
185
            strides.append(current_stride)
4✔
186
        strides = list(reversed(strides))
4✔
187

188
        offset = "0"
4✔
189
        for i in range(ndim):
4✔
190
            idx = start_indices[i]
4✔
191
            stride = strides[i]
4✔
192
            term = f"({idx} * {stride})" if stride != "1" else idx
4✔
193
            if offset == "0":
4✔
194
                offset = term
4✔
195
            else:
196
                offset = f"({offset} + {term})"
4✔
197

198
        return [offset]
4✔
199

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

219
    def _is_stride_1(self, name, indices):
4✔
220
        """Check if the sliced dimension has stride 1 (contiguous access)."""
221
        if name not in self.tensor_table:
4✔
222
            return True
×
223
        info = self.tensor_table[name]
4✔
224
        ndim = len(info.shape)
4✔
225

226
        if not indices:
4✔
227
            return True
4✔
228

229
        sliced_dim = -1
×
230
        for i, idx in enumerate(indices):
×
231
            if isinstance(idx, ast.Slice):
×
232
                sliced_dim = i
×
233
                break
×
234

235
        if sliced_dim == -1:
×
236
            if len(indices) < ndim:
×
237
                sliced_dim = ndim - 1
×
238
            else:
239
                return True
×
240

241
        return sliced_dim == ndim - 1
×
242

243
    def _is_target(self, node, target_name):
4✔
244
        """Check if node refers to the target."""
245
        if isinstance(target_name, ast.AST):
4✔
246
            return self._ev.visit(node) == self._ev.visit(target_name)
4✔
247

248
        if isinstance(node, ast.Name) and node.id == target_name:
4✔
249
            return True
×
250
        if isinstance(node, ast.Subscript):
4✔
251
            if isinstance(node.value, ast.Name) and node.value.id == target_name:
4✔
252
                return True
4✔
253
        return False
4✔
254

255
    def _is_dot_call(self, node):
4✔
256
        """Check if node is a dot product call."""
257
        if isinstance(node, ast.Call):
4✔
258
            if isinstance(node.func, ast.Attribute) and node.func.attr == "dot":
×
259
                return True
×
260
            if isinstance(node.func, ast.Name) and node.func.id == "dot":
×
261
                return True
×
262
        if isinstance(node, ast.BinOp) and isinstance(node.op, ast.MatMult):
4✔
263
            return True
4✔
264
        return False
4✔
265

266
    def handle_gemm(self, target, value_node):
4✔
267
        """Handle GEMM (General Matrix Multiply) operations: C = alpha * A @ B + beta * C."""
268
        target_name = None
4✔
269
        target_subset = []
4✔
270

271
        if isinstance(target, str):
4✔
272
            target_name = target
4✔
273
        elif isinstance(target, ast.Name):
4✔
274
            target_name = target.id
4✔
275
        elif isinstance(target, ast.Subscript):
4✔
276
            if isinstance(target.value, ast.Name):
4✔
277
                res = self.parse_arg(target)
4✔
278
                if res[0]:
4✔
279
                    target_name = res[0]
4✔
280
                    target_subset = self.flatten_subset(target_name, res[1])
4✔
281
                else:
282
                    target_name = target.value.id
×
283

284
        if not target_name or target_name not in self.tensor_table:
4✔
285
            return False
4✔
286

287
        alpha = "1.0"
4✔
288
        beta = "0.0"
4✔
289
        A = None
4✔
290
        B = None
4✔
291

292
        def extract_factor(node):
4✔
293
            if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Mult):
4✔
294
                if self.is_gemm(node.left):
×
295
                    return node.left, self._ev.visit(node.right)
×
296
                if self.is_gemm(node.right):
×
297
                    return node.right, self._ev.visit(node.left)
×
298

299
                res = self.parse_arg(node.left)
×
300
                if res[0]:
×
301
                    return node.left, self._ev.visit(node.right)
×
302
                res = self.parse_arg(node.right)
×
303
                if res[0]:
×
304
                    return node.right, self._ev.visit(node.left)
×
305
            return node, "1.0"
4✔
306

307
        def parse_term(node):
4✔
308
            if isinstance(node, ast.BinOp) and isinstance(node.op, ast.MatMult):
4✔
309
                l, l_f = extract_factor(node.left)
4✔
310
                r, r_f = extract_factor(node.right)
4✔
311
                f = "1.0"
4✔
312
                if l_f != "1.0":
4✔
313
                    f = l_f
×
314
                if r_f != "1.0":
4✔
315
                    if f == "1.0":
×
316
                        f = r_f
×
317
                    else:
318
                        f = f"({f} * {r_f})"
×
319
                return l, r, f
4✔
320

321
            if isinstance(node, ast.Call):
×
322
                is_gemm_call = False
×
323
                if isinstance(node.func, ast.Attribute) and node.func.attr in [
×
324
                    "dot",
325
                    "matmul",
326
                ]:
327
                    is_gemm_call = True
×
328
                if isinstance(node.func, ast.Name) and node.func.id in [
×
329
                    "dot",
330
                    "matmul",
331
                ]:
332
                    is_gemm_call = True
×
333

334
                if is_gemm_call and len(node.args) == 2:
×
335
                    return node.args[0], node.args[1], "1.0"
×
336

337
            if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Mult):
×
338
                l, r, a = parse_term(node.left)
×
339
                if l:
×
340
                    return l, r, self._ev.visit(node.right)
×
341
                l, r, a = parse_term(node.right)
×
342
                if l:
×
343
                    return l, r, self._ev.visit(node.left)
×
344

345
            return None, None, None
×
346

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

384
        if A is None or B is None:
4✔
385
            return False
×
386

387
        def get_name_and_trans(node):
4✔
388
            if isinstance(node, ast.Attribute) and node.attr == "T":
4✔
389
                return node.value, True
×
390
            return node, False
4✔
391

392
        A_node, trans_a = get_name_and_trans(A)
4✔
393
        B_node, trans_b = get_name_and_trans(B)
4✔
394

395
        if self.is_gemm(A_node):
4✔
396
            tmp_name = self._ev.visit(A_node)
×
397
            A_node = ast.Name(id=tmp_name)
×
398

399
        if self.is_gemm(B_node):
4✔
400
            tmp_name = self._ev.visit(B_node)
×
401
            B_node = ast.Name(id=tmp_name)
×
402

403
        res_a = self.parse_arg(A_node)
4✔
404
        res_b = self.parse_arg(B_node)
4✔
405

406
        if not res_a[0] or not res_b[0]:
4✔
407
            return False
×
408

409
        A_name, subset_a, shape_a, indices_a = res_a
4✔
410
        B_name, subset_b, shape_b, indices_b = res_b
4✔
411

412
        flat_subset_a = self.flatten_subset(A_name, subset_a)
4✔
413
        flat_subset_b = self.flatten_subset(B_name, subset_b)
4✔
414

415
        def get_ndim(name):
4✔
416
            if name not in self.tensor_table:
4✔
417
                return 1
×
418
            return len(self.tensor_table[name].shape)
4✔
419

420
        if len(shape_a) == 2:
4✔
421
            if not trans_a:
4✔
422
                m = shape_a[0]
4✔
423
                k = shape_a[1]
4✔
424
            else:
425
                m = shape_a[1]
×
426
                k = shape_a[0]
×
427
        else:
428
            m = "1"
×
429
            k = shape_a[0]
×
430
            if self._is_stride_1(A_name, indices_a):
×
431
                if get_ndim(A_name) == 1:
×
432
                    trans_a = True
×
433
                else:
434
                    trans_a = False
×
435
            else:
436
                trans_a = True
×
437

438
        if len(shape_b) == 2:
4✔
439
            if not trans_b:
4✔
440
                n = shape_b[1]
4✔
441
            else:
442
                n = shape_b[0]
×
443
        else:
444
            n = "1"
4✔
445
            if self._is_stride_1(B_name, indices_b):
4✔
446
                if get_ndim(B_name) == 1:
4✔
447
                    trans_b = False
4✔
448
                else:
449
                    trans_b = True
×
450
            else:
451
                trans_b = False
×
452

453
        def get_ld(name):
4✔
454
            if name not in self.tensor_table:
4✔
455
                return ""
×
456
            shapes = self.tensor_table[name].shape
4✔
457
            if len(shapes) >= 2:
4✔
458
                return str(shapes[1])
4✔
459
            return "1"
4✔
460

461
        lda = get_ld(A_name)
4✔
462
        ldb = get_ld(B_name)
4✔
463

464
        ldc = ""
4✔
465
        if target_name:
4✔
466
            if get_ndim(target_name) == 1 and m == "1":
4✔
467
                ldc = n
×
468
            else:
469
                ldc = get_ld(target_name)
4✔
470

471
        self.builder.add_gemm(
4✔
472
            A_name,
473
            B_name,
474
            target_name,
475
            alpha,
476
            beta,
477
            m,
478
            n,
479
            k,
480
            trans_a,
481
            trans_b,
482
            flat_subset_a,
483
            flat_subset_b,
484
            target_subset,
485
            lda,
486
            ldb,
487
            ldc,
488
        )
489
        return True
4✔
490

491
    def handle_dot(self, target, value_node):
4✔
492
        """Handle dot product operations for 1D vectors."""
493
        dot_node = None
4✔
494
        is_accumulate = False
4✔
495

496
        if self._is_dot_call(value_node):
4✔
497
            dot_node = value_node
4✔
498
        elif isinstance(value_node, ast.BinOp) and isinstance(value_node.op, ast.Add):
4✔
499
            if self._is_dot_call(value_node.left):
4✔
500
                dot_node = value_node.left
4✔
501
                if self._is_target(value_node.right, target):
4✔
502
                    is_accumulate = True
×
503
            elif self._is_dot_call(value_node.right):
×
504
                dot_node = value_node.right
×
505
                if self._is_target(value_node.left, target):
×
506
                    is_accumulate = True
×
507

508
        if not dot_node:
4✔
509
            return False
×
510

511
        arg0 = None
4✔
512
        arg1 = None
4✔
513

514
        if isinstance(dot_node, ast.Call):
4✔
515
            args = dot_node.args
×
516
            if len(args) != 2:
×
517
                return False
×
518
            arg0 = args[0]
×
519
            arg1 = args[1]
×
520
        elif isinstance(dot_node, ast.BinOp) and isinstance(dot_node.op, ast.MatMult):
4✔
521
            arg0 = dot_node.left
4✔
522
            arg1 = dot_node.right
4✔
523

524
        res_a = self.parse_arg(arg0)
4✔
525
        res_b = self.parse_arg(arg1)
4✔
526

527
        if not res_a[0] or not res_b[0]:
4✔
528
            return False
×
529

530
        name_a, subset_a, shape_a, indices_a = res_a
4✔
531
        name_b, subset_b, shape_b, indices_b = res_b
4✔
532

533
        if len(shape_a) != 1 or len(shape_b) != 1:
4✔
534
            return False
4✔
535

536
        n = shape_a[0]
4✔
537

538
        def get_stride(name, indices):
4✔
539
            if not indices:
4✔
540
                return "1"
4✔
541
            info = self.tensor_table[name]
4✔
542
            shapes = info.shape
4✔
543
            ndim = len(info.shape)
4✔
544

545
            sliced_dim = -1
4✔
546
            for i, idx in enumerate(indices):
4✔
547
                if isinstance(idx, ast.Slice):
4✔
548
                    sliced_dim = i
4✔
549
                    break
4✔
550

551
            if sliced_dim == -1:
4✔
552
                return "1"
×
553

554
            stride = "1"
4✔
555
            for i in range(sliced_dim + 1, ndim):
4✔
556
                dim_size = shapes[i] if i < len(shapes) else f"_{name}_shape_{i}"
×
557
                if stride == "1":
×
558
                    stride = str(dim_size)
×
559
                else:
560
                    stride = f"({stride} * {dim_size})"
×
561
            return stride
4✔
562

563
        incx = get_stride(name_a, indices_a)
4✔
564
        incy = get_stride(name_b, indices_b)
4✔
565

566
        flat_subset_a = self.flatten_subset(name_a, subset_a)
4✔
567
        flat_subset_b = self.flatten_subset(name_b, subset_b)
4✔
568

569
        tmp_res = f"_dot_res_{self._get_unique_id()}"
4✔
570
        self.builder.add_container(tmp_res, Scalar(PrimitiveType.Double), False)
4✔
571
        block = self.builder.add_block()
4✔
572
        constant = self.builder.add_constant(block, "0.0", Scalar(PrimitiveType.Double))
4✔
573
        tasklet = self.builder.add_tasklet(block, TaskletCode.assign, ["_in"], ["_out"])
4✔
574
        self.builder.add_memlet(
4✔
575
            block, constant, "", tasklet, "_in", "", Scalar(PrimitiveType.Double)
576
        )
577
        access = self.builder.add_access(block, tmp_res)
4✔
578
        self.builder.add_memlet(
4✔
579
            block, tasklet, "_out", access, "", "", Scalar(PrimitiveType.Double)
580
        )
581

582
        self.container_table[tmp_res] = Scalar(PrimitiveType.Double)
4✔
583

584
        self.builder.add_dot(
4✔
585
            name_a, name_b, tmp_res, n, incx, incy, flat_subset_a, flat_subset_b
586
        )
587

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

590
        if not self.builder.exists(target_str):
4✔
591
            self.builder.add_container(target_str, Scalar(PrimitiveType.Double), False)
×
592
            self.container_table[target_str] = Scalar(PrimitiveType.Double)
×
593

594
        if is_accumulate:
4✔
595
            self.builder.add_assignment(target_str, f"{target_str} + {tmp_res}")
×
596
        else:
597
            self.builder.add_assignment(target_str, tmp_res)
4✔
598

599
        return True
4✔
600

601
    def is_outer(self, node):
4✔
602
        """Check if a node represents an outer product operation."""
603
        if isinstance(node, ast.Call):
4✔
604
            if isinstance(node.func, ast.Attribute) and node.func.attr == "outer":
4✔
605
                return True
4✔
606
            if isinstance(node.func, ast.Name) and node.func.id == "outer":
4✔
607
                return True
×
608
        if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add):
4✔
609
            return self.is_outer(node.left) or self.is_outer(node.right)
4✔
610
        return False
4✔
611

612
    def handle_outer(self, target, value_node):
4✔
613
        """Handle outer product operations."""
614
        target_name = None
4✔
615
        target_subset = []
4✔
616

617
        if isinstance(target, str):
4✔
618
            target_name = target
4✔
619
        elif isinstance(target, ast.Name):
4✔
620
            target_name = target.id
4✔
621
        elif isinstance(target, ast.Subscript):
4✔
622
            res = self.parse_arg(target)
4✔
623
            if res[0]:
4✔
624
                target_name = res[0]
4✔
625
                target_subset = self.flatten_subset(target_name, res[1])
4✔
626
            else:
627
                if isinstance(target.value, ast.Name):
×
628
                    target_name = target.value.id
×
629

630
        if not target_name:
4✔
631
            return False
×
632

633
        outer_calls = []
4✔
634
        target_found = False
4✔
635
        terms = []
4✔
636

637
        def collect_terms(node):
4✔
638
            if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add):
4✔
639
                collect_terms(node.left)
4✔
640
                collect_terms(node.right)
4✔
641
            else:
642
                terms.append(node)
4✔
643

644
        collect_terms(value_node)
4✔
645

646
        for term in terms:
4✔
647
            if self._is_target(term, target_name):
4✔
648
                target_found = True
4✔
649
            elif isinstance(term, ast.Call) and (
4✔
650
                (isinstance(term.func, ast.Attribute) and term.func.attr == "outer")
651
                or (isinstance(term.func, ast.Name) and term.func.id == "outer")
652
            ):
653
                if len(term.args) != 2:
4✔
654
                    return False
×
655
                outer_calls.append(term)
4✔
656
            else:
657
                return False
×
658

659
        if not outer_calls:
4✔
660
            return False
×
661

662
        parsed_outers = []
4✔
663
        for outer_node in outer_calls:
4✔
664
            arg0 = outer_node.args[0]
4✔
665
            arg1 = outer_node.args[1]
4✔
666

667
            res_a = self.parse_arg(arg0)
4✔
668
            res_b = self.parse_arg(arg1)
4✔
669

670
            if not res_a[0] or not res_b[0]:
4✔
671
                return False
×
672

673
            parsed_outers.append((res_a, res_b))
4✔
674

675
        alpha = "1.0"
4✔
676
        beta = "1.0" if target_found else "0.0"
4✔
677

678
        def get_flattened_size(name, indices, shapes):
4✔
679
            size_expr = "1"
4✔
680
            for s in shapes:
4✔
681
                if size_expr == "1":
4✔
682
                    size_expr = str(s)
4✔
683
                else:
684
                    size_expr = f"({size_expr} * {str(s)})"
×
685
            return size_expr
4✔
686

687
        def get_ld_2d(name):
4✔
688
            if name in self.tensor_table:
4✔
689
                shapes = self.tensor_table[name].shape
4✔
690
                if len(shapes) >= 2:
4✔
691
                    return str(shapes[1])
4✔
692
            return "1"
4✔
693

694
        ldc = get_ld_2d(target_name)
4✔
695

696
        for res_a, res_b in parsed_outers:
4✔
697
            name_a, subset_a, shape_a, indices_a = res_a
4✔
698
            name_b, subset_b, shape_b, indices_b = res_b
4✔
699

700
            m = get_flattened_size(name_a, indices_a, shape_a)
4✔
701
            n = get_flattened_size(name_b, indices_b, shape_b)
4✔
702
            k = "1"
4✔
703

704
            trans_a = False
4✔
705
            trans_b = True
4✔
706

707
            flat_subset_a = self.flatten_subset(name_a, subset_a)
4✔
708
            flat_subset_b = self.flatten_subset(name_b, subset_b)
4✔
709

710
            lda = "1"
4✔
711
            ldb = "1"
4✔
712

713
            self.builder.add_gemm(
4✔
714
                name_a,
715
                name_b,
716
                target_name,
717
                alpha,
718
                beta,
719
                m,
720
                n,
721
                k,
722
                trans_a,
723
                trans_b,
724
                flat_subset_a,
725
                flat_subset_b,
726
                target_subset,
727
                lda,
728
                ldb,
729
                ldc,
730
            )
731
            beta = "1.0"
4✔
732

733
        return True
4✔
734

735
    # ========== Transpose Operations ==========
736

737
    def _parse_perm(self, node):
4✔
738
        """Parse a permutation list or tuple from an AST node."""
739
        if isinstance(node, (ast.List, ast.Tuple)):
4✔
740
            res = []
4✔
741
            for elt in node.elts:
4✔
742
                val = self._ev.visit(elt)
4✔
743
                res.append(int(val))
4✔
744
            return res
4✔
745
        return []
×
746

747
    def is_transpose(self, node):
4✔
748
        """Check if a node represents a transpose operation."""
749
        # Case 1: np.transpose(arr, ...)
750
        if isinstance(node, ast.Call):
4✔
751
            if isinstance(node.func, ast.Attribute) and node.func.attr == "transpose":
4✔
752
                return True
×
753
            if isinstance(node.func, ast.Name) and node.func.id == "transpose":
4✔
754
                return True
×
755

756
        # Case 2: arr.T
757
        if isinstance(node, ast.Attribute) and node.attr == "T":
4✔
758
            return True
4✔
759

760
        return False
4✔
761

762
    def handle_transpose(self, target, value_node):
4✔
763
        """Handle transpose operations including .T and np.transpose()."""
764
        if not self.is_transpose(value_node):
4✔
765
            return False
×
766

767
        input_node = None
4✔
768
        perm = []
4✔
769

770
        if isinstance(value_node, ast.Attribute) and value_node.attr == "T":
4✔
771
            input_node = value_node.value
4✔
772
            perm = []  # Empty means reverse
4✔
773

774
        elif isinstance(value_node, ast.Call):
×
775
            args = value_node.args
×
776
            keywords = value_node.keywords
×
777

778
            is_numpy_func = False
×
779
            if isinstance(value_node.func, ast.Attribute):
×
780
                caller = ""
×
781
                if isinstance(value_node.func.value, ast.Name):
×
782
                    caller = value_node.func.value.id
×
783
                if caller in ["np", "numpy"]:
×
784
                    is_numpy_func = True
×
785
            elif isinstance(value_node.func, ast.Name):
×
786
                is_numpy_func = True
×
787

788
            if is_numpy_func:
×
789
                if len(args) < 1:
×
790
                    return False
×
791
                input_node = args[0]
×
792
                if len(args) > 1:
×
793
                    perm = self._parse_perm(args[1])
×
794
                for kw in keywords:
×
795
                    if kw.arg == "axes":
×
796
                        perm = self._parse_perm(kw.value)
×
797
            else:
798
                if isinstance(value_node.func, ast.Attribute):
×
799
                    input_node = value_node.func.value
×
800
                else:
801
                    return False
×
802
                if len(args) > 0:
×
803
                    perm = self._parse_perm(args[0])
×
804
                for kw in keywords:
×
805
                    if kw.arg == "axes":
×
806
                        perm = self._parse_perm(kw.value)
×
807

808
        input_name = self._ev.visit(input_node)
4✔
809
        if input_name not in self.tensor_table:
4✔
810
            return False
×
811

812
        in_info = self.tensor_table[input_name]
4✔
813
        in_shape = in_info.shape
4✔
814
        in_strings = [str(s) for s in in_shape]
4✔
815

816
        if not perm:
4✔
817
            perm = list(range(len(in_shape)))[::-1]
4✔
818

819
        out_shape = [in_strings[p] for p in perm]
4✔
820

821
        # Get input strides and check if input is contiguous
822
        in_strides = (
4✔
823
            in_info.strides if hasattr(in_info, "strides") and in_info.strides else None
824
        )
825
        if in_strides is None:
4✔
826
            in_strides = self._compute_strides(in_shape, "C")
×
827

828
        if self._is_contiguous(in_shape, in_strides):
4✔
829
            # For contiguous inputs, output strides are permuted input strides
830
            out_strides = [in_strides[p] for p in perm]
4✔
831
        else:
832
            # For non-contiguous inputs, output is C-order for the new shape
833
            out_strides = self._compute_strides(out_shape, "C")
×
834

835
        target_name = ""
4✔
836
        if isinstance(target, ast.Name):
4✔
837
            target_name = target.id
4✔
838
        elif isinstance(target, str):
×
839
            target_name = target
×
840

841
        dtype = Scalar(PrimitiveType.Double)
4✔
842
        if input_name in self.container_table:
4✔
843
            input_type = self.container_table[input_name]
4✔
844
            if isinstance(input_type, Pointer):
4✔
845
                dtype = input_type.pointee_type
4✔
846
            else:
847
                dtype = input_type
×
848

849
        ptr_type = Pointer(dtype)
4✔
850

851
        # Create target container if it doesn't exist
852
        if not self.builder.exists(target_name):
4✔
853
            self.builder.add_container(target_name, ptr_type, False)
4✔
854
            self.container_table[target_name] = ptr_type
4✔
855
        self.tensor_table[target_name] = Tensor(dtype, out_shape, out_strides)
4✔
856

857
        # Create reference memlet to alias the source array (view, not copy)
858
        block = self.builder.add_block()
4✔
859
        t_src = self.builder.add_access(block, input_name)
4✔
860
        t_dst = self.builder.add_access(block, target_name)
4✔
861
        self.builder.add_reference_memlet(block, t_src, t_dst, "0", ptr_type)
4✔
862

863
        return True
4✔
864

865
    def handle_transpose_expr(self, node):
4✔
866
        """Handle .T attribute access in expressions, returning a temp array name."""
867
        if not isinstance(node, ast.Attribute) or node.attr != "T":
4✔
868
            return None
×
869

870
        input_name = self._ev.visit(node.value)
4✔
871
        if input_name not in self.tensor_table:
4✔
872
            return None
×
873

874
        in_info = self.tensor_table[input_name]
4✔
875
        in_shape = in_info.shape
4✔
876
        perm = list(range(len(in_shape)))[::-1]
4✔
877

878
        return self._create_transpose_view(input_name, perm)
4✔
879

880
    def _handle_numpy_transpose(self, node, func_name):
4✔
881
        """Handle np.transpose(arr, axes=...) function call."""
882
        if len(node.args) < 1:
4✔
883
            raise ValueError("np.transpose requires at least one argument")
×
884

885
        input_node = node.args[0]
4✔
886
        input_name = self.visit(input_node)
4✔
887

888
        if input_name not in self.tensor_table:
4✔
889
            raise ValueError(f"Array {input_name} not found in tensor_table")
×
890

891
        in_info = self.tensor_table[input_name]
4✔
892
        in_shape = in_info.shape
4✔
893

894
        perm = []
4✔
895
        if len(node.args) > 1:
4✔
896
            perm = self._parse_perm(node.args[1])
×
897
        for kw in node.keywords:
4✔
898
            if kw.arg == "axes":
4✔
899
                perm = self._parse_perm(kw.value)
4✔
900

901
        if not perm:
4✔
902
            perm = list(range(len(in_shape)))[::-1]
4✔
903

904
        return self._create_transpose_view(input_name, perm)
4✔
905

906
    def _create_transpose_view(self, input_name, perm):
4✔
907
        in_info = self.tensor_table[input_name]
4✔
908
        in_shape = in_info.shape
4✔
909
        in_strings = [str(s) for s in in_shape]
4✔
910

911
        # Compute output shape by permuting
912
        out_shape = [in_strings[p] for p in perm]
4✔
913

914
        # Get input strides and check if input is contiguous
915
        in_strides = (
4✔
916
            in_info.strides if hasattr(in_info, "strides") and in_info.strides else None
917
        )
918
        if in_strides is None:
4✔
919
            in_strides = self._compute_strides(in_shape, "C")
×
920

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

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

927
        # Create new pointer container
928
        tmp_name = f"_tmp_{self._get_unique_id()}"
4✔
929
        ptr_type = Pointer(in_info.element_type)
4✔
930
        self.builder.add_container(tmp_name, ptr_type, False)
4✔
931
        self.container_table[tmp_name] = ptr_type
4✔
932

933
        # Register tensor with permuted shape, strides, and inherited offset
934
        self.tensor_table[tmp_name] = Tensor(
4✔
935
            in_info.element_type, out_shape, out_strides, in_offset
936
        )
937

938
        # Create reference memlet to alias the source array
939
        block = self.builder.add_block()
4✔
940
        t_src = self.builder.add_access(block, input_name)
4✔
941
        t_dst = self.builder.add_access(block, tmp_name)
4✔
942
        self.builder.add_reference_memlet(block, t_src, t_dst, "0", ptr_type)
4✔
943

944
        return tmp_name
4✔
945

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

949
        Uses negative strides and offset to create a view without copying.
950
        """
951
        if len(node.args) < 1:
4✔
952
            raise ValueError("np.flip requires at least one argument")
×
953

954
        input_name = self.visit(node.args[0])
4✔
955
        if input_name not in self.tensor_table:
4✔
956
            raise ValueError(f"Array {input_name} not found in tensor_table")
×
957

958
        in_info = self.tensor_table[input_name]
4✔
959
        in_shape = in_info.shape
4✔
960
        ndim = len(in_shape)
4✔
961

962
        # Parse axis argument
963
        axis = None
4✔
964
        if len(node.args) > 1:
4✔
965
            axis_node = node.args[1]
×
966
            if isinstance(axis_node, ast.Constant):
×
967
                axis = axis_node.value
×
968
            elif isinstance(axis_node, ast.UnaryOp) and isinstance(
×
969
                axis_node.op, ast.USub
970
            ):
971
                if isinstance(axis_node.operand, ast.Constant):
×
972
                    axis = -axis_node.operand.value
×
973
        for kw in node.keywords:
4✔
974
            if kw.arg == "axis":
4✔
975
                if isinstance(kw.value, ast.Constant):
4✔
976
                    axis = kw.value.value
4✔
977
                elif isinstance(kw.value, ast.UnaryOp) and isinstance(
4✔
978
                    kw.value.op, ast.USub
979
                ):
980
                    if isinstance(kw.value.operand, ast.Constant):
4✔
981
                        axis = -kw.value.operand.value
4✔
982

983
        # Determine which axes to flip
984
        if axis is None:
4✔
985
            # Flip all axes
986
            axes_to_flip = list(range(ndim))
4✔
987
        else:
988
            if axis < 0:
4✔
989
                axis = ndim + axis
4✔
990
            axes_to_flip = [axis]
4✔
991

992
        return self._create_flip_view(input_name, axes_to_flip)
4✔
993

994
    def _handle_numpy_fliplr(self, node, func_name):
4✔
995
        """Handle np.fliplr(arr) - flip array left-right (axis=1)."""
996
        if len(node.args) < 1:
4✔
997
            raise ValueError("np.fliplr requires one argument")
×
998

999
        input_name = self.visit(node.args[0])
4✔
1000
        if input_name not in self.tensor_table:
4✔
1001
            raise ValueError(f"Array {input_name} not found in tensor_table")
×
1002

1003
        in_info = self.tensor_table[input_name]
4✔
1004
        if len(in_info.shape) < 2:
4✔
1005
            raise ValueError("np.fliplr requires array with ndim >= 2")
×
1006

1007
        return self._create_flip_view(input_name, [1])
4✔
1008

1009
    def _handle_numpy_flipud(self, node, func_name):
4✔
1010
        """Handle np.flipud(arr) - flip array up-down (axis=0)."""
1011
        if len(node.args) < 1:
4✔
1012
            raise ValueError("np.flipud requires one argument")
×
1013

1014
        input_name = self.visit(node.args[0])
4✔
1015
        if input_name not in self.tensor_table:
4✔
1016
            raise ValueError(f"Array {input_name} not found in tensor_table")
×
1017

1018
        return self._create_flip_view(input_name, [0])
4✔
1019

1020
    def _create_flip_view(self, input_name, axes_to_flip):
4✔
1021
        """Create a flipped view of an array using Tensor.flip().
1022

1023
        Uses the Tensor type's flip() method which computes the correct
1024
        negative strides and offset adjustment.
1025
        """
1026
        in_tensor = self.tensor_table[input_name]
4✔
1027

1028
        # Apply flip for each axis
1029
        flipped_tensor = in_tensor
4✔
1030
        for axis in axes_to_flip:
4✔
1031
            flipped_tensor = flipped_tensor.flip(axis)
4✔
1032

1033
        # Create new pointer container pointing to same data
1034
        tmp_name = f"_tmp_{self._get_unique_id()}"
4✔
1035
        ptr_type = Pointer(in_tensor.element_type)
4✔
1036
        self.builder.add_container(tmp_name, ptr_type, False)
4✔
1037
        self.container_table[tmp_name] = ptr_type
4✔
1038

1039
        # Store the flipped tensor with its offset in tensor_table
1040
        self.tensor_table[tmp_name] = flipped_tensor
4✔
1041

1042
        # Create reference memlet (offset is handled by tensor's offset property)
1043
        block = self.builder.add_block()
4✔
1044
        t_src = self.builder.add_access(block, input_name)
4✔
1045
        t_dst = self.builder.add_access(block, tmp_name)
4✔
1046
        self.builder.add_reference_memlet(block, t_src, t_dst, "0", ptr_type)
4✔
1047

1048
        return tmp_name
4✔
1049

1050
    def _handle_numpy_reshape(self, node, func_name):
4✔
1051
        """Handle np.reshape(arr, newshape) - reshape array without copying.
1052

1053
        Only works for contiguous arrays; creates a view with new shape/strides.
1054
        """
1055
        if len(node.args) < 2:
4✔
1056
            raise ValueError("np.reshape requires array and new shape")
×
1057

1058
        input_name = self.visit(node.args[0])
4✔
1059
        if input_name not in self.tensor_table:
4✔
1060
            raise ValueError(f"Array {input_name} not found in tensor_table")
×
1061

1062
        in_info = self.tensor_table[input_name]
4✔
1063
        in_shape = in_info.shape
4✔
1064

1065
        # Parse new shape
1066
        new_shape = self._parse_shape(node.args[1])
4✔
1067

1068
        # Get input strides
1069
        in_strides = (
4✔
1070
            in_info.strides if hasattr(in_info, "strides") and in_info.strides else None
1071
        )
1072
        if in_strides is None:
4✔
1073
            in_strides = self._compute_strides(in_shape, "C")
×
1074

1075
        # Check if input is contiguous (C or F order)
1076
        c_contig = self._is_contiguous(in_shape, in_strides)
4✔
1077
        f_contig = self._is_contiguous_f(in_shape, in_strides)
4✔
1078

1079
        if c_contig:
4✔
1080
            out_strides = self._compute_strides(new_shape, "C")
4✔
1081
        elif f_contig:
×
1082
            out_strides = self._compute_strides(new_shape, "F")
×
1083
        else:
1084
            # Non-contiguous array cannot be reshaped without copy
1085
            raise NotImplementedError(
×
1086
                "np.reshape on non-contiguous array not supported (would require copy)"
1087
            )
1088

1089
        # Create new pointer container
1090
        tmp_name = f"_tmp_{self._get_unique_id()}"
4✔
1091
        ptr_type = Pointer(in_info.element_type)
4✔
1092
        self.builder.add_container(tmp_name, ptr_type, False)
4✔
1093
        self.container_table[tmp_name] = ptr_type
4✔
1094

1095
        # Register tensor with new shape and computed strides
1096
        self.tensor_table[tmp_name] = Tensor(
4✔
1097
            in_info.element_type, new_shape, out_strides
1098
        )
1099

1100
        # Create reference memlet to alias the source array (view, no copy)
1101
        block = self.builder.add_block()
4✔
1102
        t_src = self.builder.add_access(block, input_name)
4✔
1103
        t_dst = self.builder.add_access(block, tmp_name)
4✔
1104
        self.builder.add_reference_memlet(block, t_src, t_dst, "0", ptr_type)
4✔
1105

1106
        return tmp_name
4✔
1107

1108
    def _parse_shape(self, shape_node):
4✔
1109
        """Parse a shape argument (tuple, list, or single int)."""
1110
        if isinstance(shape_node, ast.Tuple) or isinstance(shape_node, ast.List):
4✔
1111
            result = []
4✔
1112
            for elt in shape_node.elts:
4✔
1113
                if isinstance(elt, ast.Constant):
4✔
1114
                    result.append(str(elt.value))
4✔
1115
                elif isinstance(elt, ast.Name):
×
1116
                    result.append(elt.id)
×
1117
                elif isinstance(elt, ast.UnaryOp) and isinstance(elt.op, ast.USub):
×
1118
                    if isinstance(elt.operand, ast.Constant):
×
1119
                        result.append(str(-elt.operand.value))
×
1120
                else:
1121
                    result.append(self._shape_to_runtime_expr(elt))
×
1122
            return result
4✔
1123
        elif isinstance(shape_node, ast.Constant):
×
1124
            return [str(shape_node.value)]
×
1125
        elif isinstance(shape_node, ast.Name):
×
1126
            # Could be a variable holding a shape tuple - not supported yet
1127
            raise NotImplementedError("Shape variable not supported, use literal tuple")
×
1128
        else:
1129
            raise ValueError(f"Cannot parse shape: {ast.dump(shape_node)}")
×
1130

1131
    def _is_contiguous_f(self, shape, strides):
4✔
1132
        """Check if array is F-order contiguous."""
1133
        if not shape or not strides:
4✔
1134
            return True
×
1135
        f_strides = self._compute_strides(shape, "F")
4✔
1136
        return [str(s) for s in strides] == [str(s) for s in f_strides]
4✔
1137

1138
    def handle_numpy_call(self, node, func_name):
4✔
1139
        if func_name in self.function_handlers:
4✔
1140
            return self.function_handlers[func_name](node, func_name)
4✔
1141
        raise NotImplementedError(f"NumPy function {func_name} not supported")
×
1142

1143
    def has_handler(self, func_name):
4✔
1144
        return func_name in self.function_handlers
4✔
1145

1146
    def handle_array_unary_op(self, op_type, operand):
4✔
1147
        dtype = self._ev._element_type(operand)
4✔
1148
        if operand in self.tensor_table:
4✔
1149
            tensor = self.tensor_table[operand]
4✔
1150
        else:
1151
            tensor = Tensor(dtype, [])
4✔
1152

1153
        if len(tensor.shape) == 0:
4✔
1154
            tmp_name = self._create_array_temp([], dtype)
4✔
1155

1156
            func_map = {
4✔
1157
                "sqrt": CMathFunction.sqrt,
1158
                "abs": CMathFunction.fabs,
1159
                "absolute": CMathFunction.fabs,
1160
                "exp": CMathFunction.exp,
1161
                "tanh": CMathFunction.tanh,
1162
            }
1163

1164
            block = self.builder.add_block()
4✔
1165
            t_src = self.builder.add_access(block, operand)
4✔
1166
            t_dst = self.builder.add_access(block, tmp_name)
4✔
1167
            t_task = self.builder.add_cmath(
4✔
1168
                block, func_map[op_type], dtype.primitive_type
1169
            )
1170

1171
            self.builder.add_memlet(block, t_src, "void", t_task, "_in1", "", dtype)
4✔
1172
            self.builder.add_memlet(block, t_task, "_out", t_dst, "void", "", dtype)
4✔
1173

1174
            return tmp_name
4✔
1175

1176
        output_strides = self._get_contiguous_output_strides(
4✔
1177
            tensor.shape, tensor.strides
1178
        )
1179
        tmp_name = self._create_array_temp(tensor.shape, dtype, strides=output_strides)
4✔
1180
        tmp_tensor = self.tensor_table[tmp_name]
4✔
1181
        self.builder.add_elementwise_unary_op(
4✔
1182
            op_type, operand, tensor, tmp_name, tmp_tensor
1183
        )
1184

1185
        return tmp_name
4✔
1186

1187
    def handle_array_binary_op(self, op_type, left, right):
4✔
1188
        # Determine if operands are arrays or scalars
1189
        # NumPy 0-d arrays (shape=[]) ARE arrays for promotion purposes
1190
        # Only literals and Python scalars (not in tensor_table) are treated as scalars
1191
        left_is_array = left in self.tensor_table
4✔
1192
        right_is_array = right in self.tensor_table
4✔
1193

1194
        dtype_left = self._ev._element_type(left)
4✔
1195
        dtype_right = self._ev._element_type(right)
4✔
1196

1197
        # Use NumPy promotion rules: scalars adapt to arrays
1198
        dtype = numpy_promote_types(
4✔
1199
            dtype_left, left_is_array, dtype_right, right_is_array
1200
        )
1201

1202
        # Cast operands to result type if needed
1203
        real_left = self._cast_to_type(left, dtype)
4✔
1204
        real_right = self._cast_to_type(right, dtype)
4✔
1205

1206
        # Get tensor info for the (possibly casted) operands
1207
        if real_left in self.tensor_table:
4✔
1208
            left_tensor = self.tensor_table[real_left]
4✔
1209
        else:
1210
            left_tensor = Tensor(dtype, [])
4✔
1211

1212
        if real_right in self.tensor_table:
4✔
1213
            right_tensor = self.tensor_table[real_right]
4✔
1214
        else:
1215
            right_tensor = Tensor(dtype, [])
4✔
1216

1217
        left_shape = left_tensor.shape
4✔
1218
        right_shape = right_tensor.shape
4✔
1219

1220
        # Compute broadcast output shape
1221
        output_shape = self._compute_broadcast_shape(left_shape, right_shape)
4✔
1222

1223
        # Check if broadcasting is needed
1224
        left_needs_broadcast = (
4✔
1225
            self._needs_broadcast(left_shape, output_shape) if left_shape else False
1226
        )
1227
        right_needs_broadcast = (
4✔
1228
            self._needs_broadcast(right_shape, output_shape) if right_shape else False
1229
        )
1230

1231
        real_left_tensor = left_tensor
4✔
1232
        real_right_tensor = right_tensor
4✔
1233

1234
        # Broadcast left operand if needed (stride-based, no copy)
1235
        if left_needs_broadcast:
4✔
1236
            left_strides = left_tensor.strides if left_tensor.strides else []
×
1237
            broadcast_strides = self._compute_broadcast_strides(
×
1238
                left_shape, left_strides, output_shape
1239
            )
1240
            # Create a new tensor view with broadcast shape and strides
1241
            # Preserve the offset from the original tensor (important for views like flip)
1242
            left_offset = left_tensor.offset if left_tensor.offset else "0"
×
1243
            real_left_tensor = Tensor(
×
1244
                dtype, output_shape, broadcast_strides, left_offset
1245
            )
1246

1247
        # Broadcast right operand if needed (stride-based, no copy)
1248
        if right_needs_broadcast:
4✔
1249
            right_strides = right_tensor.strides if right_tensor.strides else []
4✔
1250
            broadcast_strides = self._compute_broadcast_strides(
4✔
1251
                right_shape, right_strides, output_shape
1252
            )
1253
            # Create a new tensor view with broadcast shape and strides
1254
            # Preserve the offset from the original tensor (important for views like flip)
1255
            right_offset = right_tensor.offset if right_tensor.offset else "0"
4✔
1256
            real_right_tensor = Tensor(
4✔
1257
                dtype, output_shape, broadcast_strides, right_offset
1258
            )
1259

1260
        # Create output array with broadcast shape
1261
        # Preserve F-order if both inputs are F-order and no broadcasting needed
1262
        if not left_needs_broadcast and not right_needs_broadcast:
4✔
1263
            # Use left tensor strides to determine output order
1264
            output_strides = self._get_contiguous_output_strides(
4✔
1265
                output_shape, left_tensor.strides
1266
            )
1267
        else:
1268
            output_strides = self._compute_strides(output_shape, "C")
4✔
1269
        tmp_name = self._create_array_temp(output_shape, dtype, strides=output_strides)
4✔
1270
        tmp_tensor = self.tensor_table[tmp_name]
4✔
1271

1272
        self.builder.add_elementwise_op(
4✔
1273
            op_type,
1274
            real_left,
1275
            real_left_tensor,
1276
            real_right,
1277
            real_right_tensor,
1278
            tmp_name,
1279
            tmp_tensor,
1280
        )
1281

1282
        return tmp_name
4✔
1283

1284
    def handle_array_negate(self, operand):
4✔
1285
        operand_tensor = self.tensor_table[operand]
4✔
1286
        dtype = self._ev._element_type(operand)
4✔
1287

1288
        output_strides = self._get_contiguous_output_strides(
4✔
1289
            operand_tensor.shape, operand_tensor.strides
1290
        )
1291
        tmp_name = self._create_array_temp(
4✔
1292
            operand_tensor.shape, dtype, strides=output_strides
1293
        )
1294
        tmp_tensor = self.tensor_table[tmp_name]
4✔
1295

1296
        zero_name = f"_tmp_{self._get_unique_id()}"
4✔
1297
        self.builder.add_container(zero_name, dtype, False)
4✔
1298
        self.container_table[zero_name] = dtype
4✔
1299
        self.tensor_table[zero_name] = Tensor(dtype, [])
4✔
1300

1301
        zero_block = self.builder.add_block()
4✔
1302
        t_const = self.builder.add_constant(
4✔
1303
            zero_block,
1304
            "0.0" if dtype.primitive_type == PrimitiveType.Double else "0",
1305
            dtype,
1306
        )
1307
        t_zero = self.builder.add_access(zero_block, zero_name)
4✔
1308
        t_assign = self.builder.add_tasklet(
4✔
1309
            zero_block, TaskletCode.assign, ["_in"], ["_out"]
1310
        )
1311
        self.builder.add_memlet(zero_block, t_const, "void", t_assign, "_in", "")
4✔
1312
        self.builder.add_memlet(zero_block, t_assign, "_out", t_zero, "void", "")
4✔
1313

1314
        zero_tensor = self.tensor_table[zero_name]
4✔
1315
        self.builder.add_elementwise_op(
4✔
1316
            "sub", zero_name, zero_tensor, operand, operand_tensor, tmp_name, tmp_tensor
1317
        )
1318

1319
        return tmp_name
4✔
1320

1321
    def handle_array_compare(self, left, op, right, left_is_array, right_is_array):
4✔
1322
        """Handle elementwise comparison of arrays, returning a boolean array."""
1323
        if left_is_array:
4✔
1324
            shape = self.tensor_table[left].shape
4✔
1325
            arr_name = left
4✔
1326
        else:
1327
            shape = self.tensor_table[right].shape
×
1328
            arr_name = right
×
1329

1330
        use_int_cmp = False
4✔
1331
        arr_dtype = self._ev._element_type(arr_name)
4✔
1332
        if arr_dtype.primitive_type in (PrimitiveType.Int32, PrimitiveType.Int64):
4✔
1333
            use_int_cmp = True
×
1334

1335
        dtype = Scalar(PrimitiveType.Bool)
4✔
1336
        tmp_name = self._create_array_temp(shape, dtype)
4✔
1337

1338
        if use_int_cmp:
4✔
1339
            cmp_ops = {
×
1340
                ">": TaskletCode.int_sgt,
1341
                ">=": TaskletCode.int_sge,
1342
                "<": TaskletCode.int_slt,
1343
                "<=": TaskletCode.int_sle,
1344
                "==": TaskletCode.int_eq,
1345
                "!=": TaskletCode.int_ne,
1346
            }
1347
        else:
1348
            cmp_ops = {
4✔
1349
                ">": TaskletCode.fp_ogt,
1350
                ">=": TaskletCode.fp_oge,
1351
                "<": TaskletCode.fp_olt,
1352
                "<=": TaskletCode.fp_ole,
1353
                "==": TaskletCode.fp_oeq,
1354
                "!=": TaskletCode.fp_one,
1355
            }
1356

1357
        if op not in cmp_ops:
4✔
1358
            raise NotImplementedError(
×
1359
                f"Comparison operator {op} not supported for arrays"
1360
            )
1361

1362
        tasklet_code = cmp_ops[op]
4✔
1363

1364
        scalar_name = None
4✔
1365
        if not left_is_array:
4✔
1366
            scalar_name = left
×
1367
        elif not right_is_array:
4✔
1368
            scalar_name = right
4✔
1369

1370
        if scalar_name is not None and not use_int_cmp:
4✔
1371
            if self._is_int(scalar_name):
4✔
1372
                float_name = f"_tmp_{self._get_unique_id()}"
4✔
1373
                self.builder.add_container(
4✔
1374
                    float_name, Scalar(PrimitiveType.Double), False
1375
                )
1376
                self.container_table[float_name] = Scalar(PrimitiveType.Double)
4✔
1377

1378
                block_conv = self.builder.add_block()
4✔
1379
                t_const = self.builder.add_constant(
4✔
1380
                    block_conv, f"{scalar_name}.0", Scalar(PrimitiveType.Double)
1381
                )
1382
                t_float = self.builder.add_access(block_conv, float_name)
4✔
1383
                t_assign = self.builder.add_tasklet(
4✔
1384
                    block_conv, TaskletCode.assign, ["_in"], ["_out"]
1385
                )
1386
                self.builder.add_memlet(
4✔
1387
                    block_conv, t_const, "void", t_assign, "_in", ""
1388
                )
1389
                self.builder.add_memlet(
4✔
1390
                    block_conv, t_assign, "_out", t_float, "void", ""
1391
                )
1392

1393
                if not left_is_array:
4✔
1394
                    left = float_name
×
1395
                else:
1396
                    right = float_name
4✔
1397

1398
        # Get tensor info for array operands
1399
        left_tensor = self.tensor_table.get(left) if left_is_array else None
4✔
1400
        right_tensor = self.tensor_table.get(right) if right_is_array else None
4✔
1401
        tmp_tensor = self.tensor_table[tmp_name]
4✔
1402

1403
        loop_vars = []
4✔
1404
        for i, dim in enumerate(shape):
4✔
1405
            loop_var = f"_cmp_i{i}_{self._get_unique_id()}"
4✔
1406
            if not self.builder.exists(loop_var):
4✔
1407
                self.builder.add_container(loop_var, Scalar(PrimitiveType.Int64), False)
4✔
1408
                self.container_table[loop_var] = Scalar(PrimitiveType.Int64)
4✔
1409
            loop_vars.append(loop_var)
4✔
1410
            self.builder.begin_for(loop_var, "0", str(dim), "1")
4✔
1411

1412
        # Multi-dimensional subset - TensorToPointerConversion handles strides/offset
1413
        multi_dim_subset = ",".join(loop_vars)
4✔
1414

1415
        block = self.builder.add_block()
4✔
1416

1417
        if left_is_array:
4✔
1418
            t_left = self.builder.add_access(block, left)
4✔
1419
            left_sub = multi_dim_subset
4✔
1420
        else:
1421
            t_left, left_sub = self._add_read(block, left)
×
1422

1423
        if right_is_array:
4✔
1424
            t_right = self.builder.add_access(block, right)
×
1425
            right_sub = multi_dim_subset
×
1426
        else:
1427
            t_right, right_sub = self._add_read(block, right)
4✔
1428

1429
        t_out = self.builder.add_access(block, tmp_name)
4✔
1430

1431
        t_task = self.builder.add_tasklet(
4✔
1432
            block, tasklet_code, ["_in1", "_in2"], ["_out"]
1433
        )
1434

1435
        # Pass tensor type so TensorToPointerConversion uses correct strides/offset
1436
        if left_is_array and left_tensor:
4✔
1437
            self.builder.add_memlet(
4✔
1438
                block, t_left, "void", t_task, "_in1", left_sub, left_tensor
1439
            )
1440
        else:
1441
            self.builder.add_memlet(block, t_left, "void", t_task, "_in1", left_sub)
×
1442

1443
        if right_is_array and right_tensor:
4✔
1444
            self.builder.add_memlet(
×
1445
                block, t_right, "void", t_task, "_in2", right_sub, right_tensor
1446
            )
1447
        else:
1448
            self.builder.add_memlet(block, t_right, "void", t_task, "_in2", right_sub)
4✔
1449

1450
        self.builder.add_memlet(
4✔
1451
            block, t_task, "_out", t_out, "void", multi_dim_subset, tmp_tensor
1452
        )
1453

1454
        for _ in loop_vars:
4✔
1455
            self.builder.end_for()
4✔
1456

1457
        return tmp_name
4✔
1458

1459
    # ========== NumPy Function Handlers ==========
1460

1461
    def _handle_numpy_alloc(self, node, func_name):
4✔
1462
        """Handle np.empty, np.zeros, np.ones, np.ndarray."""
1463
        shape_arg = node.args[0]
4✔
1464
        dims = []
4✔
1465
        dims_runtime = []
4✔
1466
        if isinstance(shape_arg, ast.Tuple):
4✔
1467
            dims = [self.visit(elt) for elt in shape_arg.elts]
4✔
1468
            dims_runtime = [self._shape_to_runtime_expr(elt) for elt in shape_arg.elts]
4✔
1469
        elif isinstance(shape_arg, ast.List):
4✔
1470
            dims = [self.visit(elt) for elt in shape_arg.elts]
×
1471
            dims_runtime = [self._shape_to_runtime_expr(elt) for elt in shape_arg.elts]
×
1472
        else:
1473
            val = self.visit(shape_arg)
4✔
1474
            runtime_val = self._shape_to_runtime_expr(shape_arg)
4✔
1475
            if val.startswith("_shape_proxy_"):
4✔
1476
                array_name = val[len("_shape_proxy_") :]
×
1477
                if array_name in self.tensor_table:
×
1478
                    info = self.tensor_table[array_name]
×
1479
                    dims = info.shape
×
1480
                    dims_runtime = self.shapes_runtime_info.get(array_name, dims)
×
1481
                else:
1482
                    dims = [val]
×
1483
                    dims_runtime = [runtime_val]
×
1484
            else:
1485
                dims = [val]
4✔
1486
                dims_runtime = [runtime_val]
4✔
1487

1488
        dtype_arg = None
4✔
1489
        order = "C"  # Default to C-order (row-major)
4✔
1490
        explicit_strides = None
4✔
1491
        if len(node.args) > 1:
4✔
1492
            dtype_arg = node.args[1]
×
1493

1494
        for kw in node.keywords:
4✔
1495
            if kw.arg == "dtype":
4✔
1496
                dtype_arg = kw.value
4✔
1497
            elif kw.arg == "order":
4✔
1498
                if isinstance(kw.value, ast.Constant):
4✔
1499
                    order = kw.value.value
4✔
1500
            elif kw.arg == "strides":
4✔
1501
                # Parse explicit strides tuple/list
1502
                if isinstance(kw.value, (ast.Tuple, ast.List)):
4✔
1503
                    explicit_strides = [
4✔
1504
                        self._shape_to_runtime_expr(elt) for elt in kw.value.elts
1505
                    ]
1506

1507
        element_type = element_type_from_ast_node(dtype_arg, self.container_table)
4✔
1508

1509
        # Use explicit strides if provided, otherwise compute from order
1510
        if explicit_strides is not None:
4✔
1511
            # Convert byte strides to element strides by dividing by element size
1512
            element_size = self.builder.get_sizeof(element_type)
4✔
1513
            strides = [f"(({s}) / {element_size})" for s in explicit_strides]
4✔
1514
        else:
1515
            strides = self._compute_strides(dims, order)
4✔
1516

1517
        return self._create_array_temp(
4✔
1518
            dims,
1519
            element_type,
1520
            zero_init=(func_name == "zeros"),
1521
            ones_init=(func_name == "ones"),
1522
            shapes_runtime=dims_runtime,
1523
            strides=strides,
1524
        )
1525

1526
    def _handle_numpy_empty_like(self, node, func_name):
4✔
1527
        """Handle np.empty_like."""
1528
        prototype_arg = node.args[0]
4✔
1529
        prototype_name = self.visit(prototype_arg)
4✔
1530

1531
        dims = []
4✔
1532
        if prototype_name in self.tensor_table:
4✔
1533
            dims = self.tensor_table[prototype_name].shape
4✔
1534

1535
        dtype_arg = None
4✔
1536
        order = "C"  # Default to C-order
4✔
1537
        if len(node.args) > 1:
4✔
1538
            dtype_arg = node.args[1]
×
1539

1540
        for kw in node.keywords:
4✔
1541
            if kw.arg == "dtype":
4✔
1542
                dtype_arg = kw.value
4✔
1543
            elif kw.arg == "order":
4✔
1544
                if isinstance(kw.value, ast.Constant):
4✔
1545
                    order = kw.value.value
4✔
1546

1547
        element_type = None
4✔
1548
        if dtype_arg:
4✔
1549
            element_type = element_type_from_ast_node(dtype_arg, self.container_table)
4✔
1550
        else:
1551
            if prototype_name in self.container_table:
4✔
1552
                sym_type = self.container_table[prototype_name]
4✔
1553
                if isinstance(sym_type, Pointer) and sym_type.has_pointee_type():
4✔
1554
                    element_type = sym_type.pointee_type
4✔
1555

1556
        if element_type is None:
4✔
1557
            element_type = Scalar(PrimitiveType.Double)
×
1558

1559
        strides = self._compute_strides(dims, order)
4✔
1560
        return self._create_array_temp(
4✔
1561
            dims, element_type, zero_init=False, ones_init=False, strides=strides
1562
        )
1563

1564
    def _handle_numpy_zeros_like(self, node, func_name):
4✔
1565
        """Handle np.zeros_like."""
1566
        prototype_arg = node.args[0]
4✔
1567
        prototype_name = self.visit(prototype_arg)
4✔
1568

1569
        dims = []
4✔
1570
        if prototype_name in self.tensor_table:
4✔
1571
            dims = self.tensor_table[prototype_name].shape
4✔
1572

1573
        dtype_arg = None
4✔
1574
        order = "C"  # Default to C-order
4✔
1575
        if len(node.args) > 1:
4✔
1576
            dtype_arg = node.args[1]
×
1577

1578
        for kw in node.keywords:
4✔
1579
            if kw.arg == "dtype":
4✔
1580
                dtype_arg = kw.value
4✔
1581
            elif kw.arg == "order":
4✔
1582
                if isinstance(kw.value, ast.Constant):
4✔
1583
                    order = kw.value.value
4✔
1584

1585
        element_type = None
4✔
1586
        if dtype_arg:
4✔
1587
            element_type = element_type_from_ast_node(dtype_arg, self.container_table)
4✔
1588
        else:
1589
            if prototype_name in self.container_table:
4✔
1590
                sym_type = self.container_table[prototype_name]
4✔
1591
                if isinstance(sym_type, Pointer) and sym_type.has_pointee_type():
4✔
1592
                    element_type = sym_type.pointee_type
4✔
1593

1594
        if element_type is None:
4✔
1595
            element_type = Scalar(PrimitiveType.Double)
×
1596

1597
        strides = self._compute_strides(dims, order)
4✔
1598
        return self._create_array_temp(
4✔
1599
            dims, element_type, zero_init=True, ones_init=False, strides=strides
1600
        )
1601

1602
    def _handle_numpy_eye(self, node, func_name):
4✔
1603
        """Handle np.eye."""
1604
        N_arg = node.args[0]
4✔
1605
        N_str = self.visit(N_arg)
4✔
1606
        N_runtime = self._shape_to_runtime_expr(N_arg)
4✔
1607

1608
        M_str = N_str
4✔
1609
        M_arg = N_arg  # Default M = N
4✔
1610
        if len(node.args) > 1:
4✔
1611
            M_arg = node.args[1]
×
1612
            M_str = self.visit(M_arg)
×
1613

1614
        k_str = "0"
4✔
1615
        if len(node.args) > 2:
4✔
1616
            k_str = self.visit(node.args[2])
×
1617

1618
        dtype_arg = None
4✔
1619
        for kw in node.keywords:
4✔
1620
            if kw.arg == "M":
4✔
1621
                M_arg = kw.value
4✔
1622
                M_str = self.visit(M_arg)
4✔
1623
                if M_str == "None":
4✔
1624
                    M_str = N_str
4✔
1625
                    M_arg = N_arg
4✔
1626
            elif kw.arg == "k":
4✔
1627
                k_str = self.visit(kw.value)
4✔
1628
            elif kw.arg == "dtype":
4✔
1629
                dtype_arg = kw.value
4✔
1630

1631
        M_runtime = self._shape_to_runtime_expr(M_arg)
4✔
1632

1633
        element_type = element_type_from_ast_node(dtype_arg, self.container_table)
4✔
1634

1635
        ptr_name = self._create_array_temp(
4✔
1636
            [N_str, M_str],
1637
            element_type,
1638
            zero_init=True,
1639
            shapes_runtime=[N_runtime, M_runtime],
1640
        )
1641

1642
        loop_var = f"_i_{self._get_unique_id()}"
4✔
1643
        if not self.builder.exists(loop_var):
4✔
1644
            self.builder.add_container(loop_var, Scalar(PrimitiveType.Int64), False)
4✔
1645
            self.container_table[loop_var] = Scalar(PrimitiveType.Int64)
4✔
1646

1647
        self.builder.begin_for(loop_var, "0", N_str, "1")
4✔
1648

1649
        cond = f"(({loop_var} + {k_str}) >= 0) & (({loop_var} + {k_str}) < {M_str})"
4✔
1650
        self.builder.begin_if(cond)
4✔
1651

1652
        val = "1.0"
4✔
1653
        if element_type.primitive_type in [
4✔
1654
            PrimitiveType.Int64,
1655
            PrimitiveType.Int32,
1656
            PrimitiveType.Int8,
1657
            PrimitiveType.Int16,
1658
            PrimitiveType.UInt64,
1659
            PrimitiveType.UInt32,
1660
            PrimitiveType.UInt8,
1661
            PrimitiveType.UInt16,
1662
        ]:
1663
            val = "1"
×
1664

1665
        block_assign = self.builder.add_block()
4✔
1666
        t_const = self.builder.add_constant(block_assign, val, element_type)
4✔
1667
        t_arr = self.builder.add_access(block_assign, ptr_name)
4✔
1668
        flat_index = f"(({loop_var}) * ({M_str}) + ({loop_var}) + ({k_str}))"
4✔
1669
        subset = flat_index
4✔
1670

1671
        t_task = self.builder.add_tasklet(
4✔
1672
            block_assign, TaskletCode.assign, ["_in"], ["_out"]
1673
        )
1674
        self.builder.add_memlet(
4✔
1675
            block_assign, t_const, "void", t_task, "_in", "", element_type
1676
        )
1677
        self.builder.add_memlet(block_assign, t_task, "_out", t_arr, "void", subset)
4✔
1678

1679
        self.builder.end_if()
4✔
1680
        self.builder.end_for()
4✔
1681

1682
        return ptr_name
4✔
1683

1684
    def _handle_numpy_binary_op(self, node, func_name):
4✔
1685
        """Handle np.add, np.subtract, np.multiply, np.divide, etc."""
1686
        args = [self.visit(arg) for arg in node.args]
4✔
1687
        if len(args) != 2:
4✔
1688
            raise NotImplementedError(
×
1689
                f"Numpy function {func_name} requires 2 arguments"
1690
            )
1691

1692
        op_map = {
4✔
1693
            "add": "add",
1694
            "subtract": "sub",
1695
            "multiply": "mul",
1696
            "divide": "div",
1697
            "power": "pow",
1698
            "minimum": "min",
1699
            "maximum": "max",
1700
        }
1701
        return self.handle_array_binary_op(op_map[func_name], args[0], args[1])
4✔
1702

1703
    def _handle_numpy_unary_op(self, node, func_name):
4✔
1704
        """Handle np.exp, np.sqrt, np.abs, etc."""
1705
        args = [self.visit(arg) for arg in node.args]
4✔
1706
        if len(args) != 1:
4✔
1707
            raise NotImplementedError(f"Numpy function {func_name} requires 1 argument")
×
1708

1709
        op_name = func_name
4✔
1710
        if op_name == "absolute":
4✔
1711
            op_name = "abs"
×
1712

1713
        return self.handle_array_unary_op(op_name, args[0])
4✔
1714

1715
    def _handle_numpy_where(self, node, func_name):
4✔
1716
        """Handle np.where(condition, x, y) - elementwise ternary selection."""
1717
        if len(node.args) != 3:
4✔
1718
            raise NotImplementedError("np.where requires 3 arguments (condition, x, y)")
×
1719

1720
        cond_name = self.visit(node.args[0])
4✔
1721
        x_name = self.visit(node.args[1])
4✔
1722
        y_name = self.visit(node.args[2])
4✔
1723

1724
        shape = []
4✔
1725
        dtype = Scalar(PrimitiveType.Double)
4✔
1726

1727
        if cond_name in self.tensor_table:
4✔
1728
            shape = self.tensor_table[cond_name].shape
4✔
1729

1730
        if not shape and y_name in self.tensor_table:
4✔
1731
            shape = self.tensor_table[y_name].shape
×
1732

1733
        if not shape and x_name in self.tensor_table:
4✔
1734
            shape = self.tensor_table[x_name].shape
×
1735

1736
        if not shape:
4✔
1737
            raise NotImplementedError("np.where requires at least one array argument")
×
1738

1739
        if y_name in self.container_table:
4✔
1740
            y_type = self.container_table[y_name]
4✔
1741
            if isinstance(y_type, Pointer) and y_type.has_pointee_type():
4✔
1742
                dtype = y_type.pointee_type
4✔
1743
            elif isinstance(y_type, Scalar):
×
1744
                dtype = y_type
×
1745

1746
        tmp_name = self._create_array_temp(shape, dtype)
4✔
1747
        tmp_tensor = self.tensor_table[tmp_name]
4✔
1748

1749
        loop_vars = []
4✔
1750
        for i, dim in enumerate(shape):
4✔
1751
            loop_var = f"_where_i{i}_{self._get_unique_id()}"
4✔
1752
            if not self.builder.exists(loop_var):
4✔
1753
                self.builder.add_container(loop_var, Scalar(PrimitiveType.Int64), False)
4✔
1754
                self.container_table[loop_var] = Scalar(PrimitiveType.Int64)
4✔
1755
            loop_vars.append(loop_var)
4✔
1756
            self.builder.begin_for(loop_var, "0", str(dim), "1")
4✔
1757
        multi_dim_subset = ",".join(loop_vars)
4✔
1758

1759
        cond_tmp = f"_where_cond_{self._get_unique_id()}"
4✔
1760
        self.builder.add_container(cond_tmp, Scalar(PrimitiveType.Bool), False)
4✔
1761
        self.container_table[cond_tmp] = Scalar(PrimitiveType.Bool)
4✔
1762

1763
        block_cond = self.builder.add_block()
4✔
1764
        if cond_name in self.tensor_table:
4✔
1765
            cond_tensor = self.tensor_table[cond_name]
4✔
1766
            t_cond_arr = self.builder.add_access(block_cond, cond_name)
4✔
1767
            t_cond_out = self.builder.add_access(block_cond, cond_tmp)
4✔
1768
            t_cond_task = self.builder.add_tasklet(
4✔
1769
                block_cond, TaskletCode.assign, ["_in"], ["_out"]
1770
            )
1771
            self.builder.add_memlet(
4✔
1772
                block_cond,
1773
                t_cond_arr,
1774
                "void",
1775
                t_cond_task,
1776
                "_in",
1777
                multi_dim_subset,
1778
                cond_tensor,
1779
            )
1780
            self.builder.add_memlet(
4✔
1781
                block_cond, t_cond_task, "_out", t_cond_out, "void", ""
1782
            )
1783
        else:
1784
            t_cond_src, cond_sub = self._add_read(block_cond, cond_name)
×
1785
            t_cond_out = self.builder.add_access(block_cond, cond_tmp)
×
1786
            t_cond_task = self.builder.add_tasklet(
×
1787
                block_cond, TaskletCode.assign, ["_in"], ["_out"]
1788
            )
1789
            self.builder.add_memlet(
×
1790
                block_cond, t_cond_src, "void", t_cond_task, "_in", cond_sub
1791
            )
1792
            self.builder.add_memlet(
×
1793
                block_cond, t_cond_task, "_out", t_cond_out, "void", ""
1794
            )
1795

1796
        self.builder.begin_if(f"{cond_tmp} == true")
4✔
1797

1798
        block_true = self.builder.add_block()
4✔
1799
        t_out_true = self.builder.add_access(block_true, tmp_name)
4✔
1800
        if x_name in self.tensor_table:
4✔
1801
            x_tensor = self.tensor_table[x_name]
4✔
1802
            t_x = self.builder.add_access(block_true, x_name)
4✔
1803
            t_task_true = self.builder.add_tasklet(
4✔
1804
                block_true, TaskletCode.assign, ["_in"], ["_out"]
1805
            )
1806
            self.builder.add_memlet(
4✔
1807
                block_true, t_x, "void", t_task_true, "_in", multi_dim_subset, x_tensor
1808
            )
1809
        else:
1810
            t_x, x_sub = self._add_read(block_true, x_name)
4✔
1811
            t_task_true = self.builder.add_tasklet(
4✔
1812
                block_true, TaskletCode.assign, ["_in"], ["_out"]
1813
            )
1814
            self.builder.add_memlet(block_true, t_x, "void", t_task_true, "_in", x_sub)
4✔
1815
        self.builder.add_memlet(
4✔
1816
            block_true,
1817
            t_task_true,
1818
            "_out",
1819
            t_out_true,
1820
            "void",
1821
            multi_dim_subset,
1822
            tmp_tensor,
1823
        )
1824

1825
        self.builder.begin_else()
4✔
1826

1827
        # False branch: read from y, write to output
1828
        block_false = self.builder.add_block()
4✔
1829
        t_out_false = self.builder.add_access(block_false, tmp_name)
4✔
1830
        if y_name in self.tensor_table:
4✔
1831
            y_tensor = self.tensor_table[y_name]
4✔
1832
            t_y = self.builder.add_access(block_false, y_name)
4✔
1833
            t_task_false = self.builder.add_tasklet(
4✔
1834
                block_false, TaskletCode.assign, ["_in"], ["_out"]
1835
            )
1836
            self.builder.add_memlet(
4✔
1837
                block_false,
1838
                t_y,
1839
                "void",
1840
                t_task_false,
1841
                "_in",
1842
                multi_dim_subset,
1843
                y_tensor,
1844
            )
1845
        else:
1846
            t_y, y_sub = self._add_read(block_false, y_name)
4✔
1847
            t_task_false = self.builder.add_tasklet(
4✔
1848
                block_false, TaskletCode.assign, ["_in"], ["_out"]
1849
            )
1850
            self.builder.add_memlet(
4✔
1851
                block_false, t_y, "void", t_task_false, "_in", y_sub
1852
            )
1853
        self.builder.add_memlet(
4✔
1854
            block_false,
1855
            t_task_false,
1856
            "_out",
1857
            t_out_false,
1858
            "void",
1859
            multi_dim_subset,
1860
            tmp_tensor,
1861
        )
1862

1863
        self.builder.end_if()
4✔
1864

1865
        for _ in loop_vars:
4✔
1866
            self.builder.end_for()
4✔
1867

1868
        return tmp_name
4✔
1869

1870
    def _handle_numpy_clip(self, node, func_name):
4✔
1871
        """Handle np.clip(a, a_min, a_max) - elementwise clipping."""
1872
        if len(node.args) != 3:
4✔
1873
            raise NotImplementedError("np.clip requires 3 arguments (a, a_min, a_max)")
×
1874

1875
        arr_name = self.visit(node.args[0])
4✔
1876
        a_min = self.visit(node.args[1])
4✔
1877
        a_max = self.visit(node.args[2])
4✔
1878

1879
        tmp1 = self.handle_array_binary_op("max", arr_name, a_min)
4✔
1880
        result = self.handle_array_binary_op("min", tmp1, a_max)
4✔
1881

1882
        return result
4✔
1883

1884
    def _handle_numpy_matmul(self, node, func_name):
4✔
1885
        """Handle np.matmul, np.dot."""
1886
        if len(node.args) != 2:
4✔
1887
            raise NotImplementedError("matmul/dot requires 2 arguments")
×
1888
        return self._handle_matmul_helper(node.args[0], node.args[1])
4✔
1889

1890
    def handle_numpy_matmul_op(self, left_node, right_node):
4✔
1891
        """Handle the @ operator for matrix multiplication."""
1892
        return self._handle_matmul_helper(left_node, right_node)
4✔
1893

1894
    def _handle_matmul_helper(self, left_node, right_node):
4✔
1895
        """Helper for matrix multiplication operations."""
1896
        res_a = self.parse_arg(left_node)
4✔
1897
        res_b = self.parse_arg(right_node)
4✔
1898

1899
        if not res_a[0]:
4✔
1900
            left_name = self.visit(left_node)
4✔
1901
            left_node = ast.Name(id=left_name)
4✔
1902
            res_a = self.parse_arg(left_node)
4✔
1903

1904
        if not res_b[0]:
4✔
1905
            right_name = self.visit(right_node)
×
1906
            right_node = ast.Name(id=right_name)
×
1907
            res_b = self.parse_arg(right_node)
×
1908

1909
        name_a, subset_a, shape_a, indices_a = res_a
4✔
1910
        name_b, subset_b, shape_b, indices_b = res_b
4✔
1911

1912
        if not name_a or not name_b:
4✔
1913
            raise NotImplementedError("Could not resolve matmul operands")
×
1914

1915
        real_shape_a = shape_a
4✔
1916
        real_shape_b = shape_b
4✔
1917

1918
        ndim_a = len(real_shape_a)
4✔
1919
        ndim_b = len(real_shape_b)
4✔
1920

1921
        output_shape = []
4✔
1922
        is_scalar = False
4✔
1923

1924
        if ndim_a == 1 and ndim_b == 1:
4✔
1925
            is_scalar = True
4✔
1926
            output_shape = []
4✔
1927
        elif ndim_a == 2 and ndim_b == 2:
4✔
1928
            output_shape = [real_shape_a[0], real_shape_b[1]]
4✔
1929
        elif ndim_a == 2 and ndim_b == 1:
4✔
1930
            output_shape = [real_shape_a[0]]
4✔
1931
        elif ndim_a == 1 and ndim_b == 2:
4✔
1932
            output_shape = [real_shape_b[1]]
×
1933
        elif ndim_a > 2 or ndim_b > 2:
4✔
1934
            if ndim_a == ndim_b:
4✔
1935
                output_shape = list(real_shape_a[:-2]) + [
4✔
1936
                    real_shape_a[-2],
1937
                    real_shape_b[-1],
1938
                ]
1939
            else:
1940
                raise NotImplementedError(
×
1941
                    "Broadcasting with different ranks not fully supported yet"
1942
                )
1943
        else:
1944
            raise NotImplementedError(
×
1945
                f"Matmul with ranks {ndim_a} and {ndim_b} not supported"
1946
            )
1947

1948
        dtype_a = self._ev._element_type(name_a)
4✔
1949
        dtype_b = self._ev._element_type(name_b)
4✔
1950
        dtype = promote_element_types(dtype_a, dtype_b)
4✔
1951

1952
        if is_scalar:
4✔
1953
            tmp_name = f"_tmp_{self._get_unique_id()}"
4✔
1954
            self.builder.add_container(tmp_name, dtype, False)
4✔
1955
            self.container_table[tmp_name] = dtype
4✔
1956
        else:
1957
            tmp_name = self._create_array_temp(output_shape, dtype)
4✔
1958

1959
        if ndim_a > 2 or ndim_b > 2:
4✔
1960
            batch_dims = ndim_a - 2
4✔
1961
            loop_vars = []
4✔
1962

1963
            for i in range(batch_dims):
4✔
1964
                loop_var = f"_i{self._get_unique_id()}"
4✔
1965
                self.builder.add_container(loop_var, Scalar(PrimitiveType.Int64), False)
4✔
1966
                loop_vars.append(loop_var)
4✔
1967
                dim_size = real_shape_a[i]
4✔
1968
                self.builder.begin_for(loop_var, "0", str(dim_size), "1")
4✔
1969

1970
            def make_slice(name, indices):
4✔
1971
                elts = []
4✔
1972
                for idx in indices:
4✔
1973
                    if idx == ":":
4✔
1974
                        elts.append(ast.Slice())
4✔
1975
                    else:
1976
                        elts.append(ast.Name(id=idx))
4✔
1977

1978
                return ast.Subscript(
4✔
1979
                    value=ast.Name(id=name), slice=ast.Tuple(elts=elts), ctx=ast.Load()
1980
                )
1981

1982
            indices = loop_vars + [":", ":"]
4✔
1983
            slice_a = make_slice(name_a, indices)
4✔
1984
            slice_b = make_slice(name_b, indices)
4✔
1985
            slice_c = make_slice(tmp_name, indices)
4✔
1986

1987
            self.handle_gemm(
4✔
1988
                slice_c, ast.BinOp(left=slice_a, op=ast.MatMult(), right=slice_b)
1989
            )
1990

1991
            for _ in range(batch_dims):
4✔
1992
                self.builder.end_for()
4✔
1993
        else:
1994
            if is_scalar:
4✔
1995
                self.handle_dot(
4✔
1996
                    tmp_name,
1997
                    ast.BinOp(left=left_node, op=ast.MatMult(), right=right_node),
1998
                )
1999
            else:
2000
                self.handle_gemm(
4✔
2001
                    tmp_name,
2002
                    ast.BinOp(left=left_node, op=ast.MatMult(), right=right_node),
2003
                )
2004

2005
        return tmp_name
4✔
2006

2007
    def _handle_numpy_outer(self, node, func_name):
4✔
2008
        """Handle np.outer."""
2009
        if len(node.args) != 2:
4✔
2010
            raise NotImplementedError("outer requires 2 arguments")
×
2011

2012
        arg0 = node.args[0]
4✔
2013
        arg1 = node.args[1]
4✔
2014

2015
        res_a = self.parse_arg(arg0)
4✔
2016
        res_b = self.parse_arg(arg1)
4✔
2017

2018
        if not res_a[0]:
4✔
2019
            left_name = self.visit(arg0)
×
2020
            arg0 = ast.Name(id=left_name)
×
2021
            res_a = self.parse_arg(arg0)
×
2022

2023
        if not res_b[0]:
4✔
2024
            right_name = self.visit(arg1)
×
2025
            arg1 = ast.Name(id=right_name)
×
2026
            res_b = self.parse_arg(arg1)
×
2027

2028
        name_a, subset_a, shape_a, indices_a = res_a
4✔
2029
        name_b, subset_b, shape_b, indices_b = res_b
4✔
2030

2031
        if not name_a or not name_b:
4✔
2032
            raise NotImplementedError("Could not resolve outer operands")
×
2033

2034
        def get_flattened_size_expr(name, indices, shapes):
4✔
2035
            size_expr = "1"
4✔
2036
            for s in shapes:
4✔
2037
                if size_expr == "1":
4✔
2038
                    size_expr = str(s)
4✔
2039
                else:
2040
                    size_expr = f"({size_expr} * {str(s)})"
×
2041
            return size_expr
4✔
2042

2043
        m_expr = get_flattened_size_expr(name_a, indices_a, shape_a)
4✔
2044
        n_expr = get_flattened_size_expr(name_b, indices_b, shape_b)
4✔
2045

2046
        dtype_a = self._ev._element_type(name_a)
4✔
2047
        dtype_b = self._ev._element_type(name_b)
4✔
2048
        dtype = promote_element_types(dtype_a, dtype_b)
4✔
2049

2050
        tmp_name = self._create_array_temp([m_expr, n_expr], dtype)
4✔
2051

2052
        new_call_node = ast.Call(
4✔
2053
            func=node.func, args=[arg0, arg1], keywords=node.keywords
2054
        )
2055

2056
        self.handle_outer(tmp_name, new_call_node)
4✔
2057

2058
        return tmp_name
4✔
2059

2060
    def handle_ufunc_outer(self, node, ufunc_name):
4✔
2061
        """Handle np.add.outer, np.subtract.outer, np.multiply.outer, etc."""
2062
        if len(node.args) != 2:
4✔
2063
            raise NotImplementedError(f"{ufunc_name}.outer requires 2 arguments")
×
2064

2065
        if ufunc_name == "multiply":
4✔
2066
            return self._handle_numpy_outer(node, "outer")
4✔
2067

2068
        op_map = {
4✔
2069
            "add": ("add", TaskletCode.fp_add, TaskletCode.int_add),
2070
            "subtract": ("sub", TaskletCode.fp_sub, TaskletCode.int_sub),
2071
            "divide": ("div", TaskletCode.fp_div, TaskletCode.int_sdiv),
2072
            "minimum": ("min", CMathFunction.fmin, TaskletCode.int_smin),
2073
            "maximum": ("max", CMathFunction.fmax, TaskletCode.int_smax),
2074
        }
2075

2076
        if ufunc_name not in op_map:
4✔
2077
            raise NotImplementedError(f"{ufunc_name}.outer not supported")
×
2078

2079
        op_name, fp_opcode, int_opcode = op_map[ufunc_name]
4✔
2080

2081
        arg0 = node.args[0]
4✔
2082
        arg1 = node.args[1]
4✔
2083

2084
        res_a = self.parse_arg(arg0)
4✔
2085
        res_b = self.parse_arg(arg1)
4✔
2086

2087
        if not res_a[0]:
4✔
2088
            left_name = self.visit(arg0)
×
2089
            arg0 = ast.Name(id=left_name)
×
2090
            res_a = self.parse_arg(arg0)
×
2091

2092
        if not res_b[0]:
4✔
2093
            right_name = self.visit(arg1)
×
2094
            arg1 = ast.Name(id=right_name)
×
2095
            res_b = self.parse_arg(arg1)
×
2096

2097
        name_a, subset_a, shape_a, indices_a = res_a
4✔
2098
        name_b, subset_b, shape_b, indices_b = res_b
4✔
2099

2100
        if not name_a or not name_b:
4✔
2101
            raise NotImplementedError("Could not resolve ufunc outer operands")
×
2102

2103
        def get_flattened_size_expr(shapes):
4✔
2104
            if not shapes:
4✔
2105
                return "1"
×
2106
            size_expr = str(shapes[0])
4✔
2107
            for s in shapes[1:]:
4✔
2108
                size_expr = f"({size_expr} * {str(s)})"
×
2109
            return size_expr
4✔
2110

2111
        m_expr = get_flattened_size_expr(shape_a)
4✔
2112
        n_expr = get_flattened_size_expr(shape_b)
4✔
2113

2114
        dtype_left = self._ev._element_type(name_a)
4✔
2115
        dtype_right = self._ev._element_type(name_b)
4✔
2116
        dtype = promote_element_types(dtype_left, dtype_right)
4✔
2117

2118
        is_int = dtype.primitive_type in [
4✔
2119
            PrimitiveType.Int64,
2120
            PrimitiveType.Int32,
2121
            PrimitiveType.Int8,
2122
            PrimitiveType.Int16,
2123
            PrimitiveType.UInt64,
2124
            PrimitiveType.UInt32,
2125
            PrimitiveType.UInt8,
2126
            PrimitiveType.UInt16,
2127
        ]
2128

2129
        tmp_name = self._create_array_temp([m_expr, n_expr], dtype)
4✔
2130

2131
        i_var = self.builder.find_new_name("_outer_i_")
4✔
2132
        j_var = self.builder.find_new_name("_outer_j_")
4✔
2133

2134
        if not self.builder.exists(i_var):
4✔
2135
            self.builder.add_container(i_var, Scalar(PrimitiveType.Int64), False)
4✔
2136
            self.container_table[i_var] = Scalar(PrimitiveType.Int64)
4✔
2137
        if not self.builder.exists(j_var):
4✔
2138
            self.builder.add_container(j_var, Scalar(PrimitiveType.Int64), False)
4✔
2139
            self.container_table[j_var] = Scalar(PrimitiveType.Int64)
4✔
2140

2141
        def compute_linear_index(name, subset, indices, loop_var):
4✔
2142
            if not indices:
4✔
2143
                return loop_var
4✔
2144

2145
            if name in self.tensor_table:
4✔
2146
                info = self.tensor_table[name]
4✔
2147
                shapes = info.shape
4✔
2148
                ndim = len(shapes)
4✔
2149
            else:
2150
                shapes = []
×
2151
                ndim = 0
×
2152

2153
            if ndim == 0:
4✔
2154
                return loop_var
×
2155

2156
            strides = []
4✔
2157
            current_stride = "1"
4✔
2158
            for i in range(ndim - 1, -1, -1):
4✔
2159
                strides.insert(0, current_stride)
4✔
2160
                if i > 0:
4✔
2161
                    dim_size = shapes[i] if i < len(shapes) else f"_{name}_shape_{i}"
4✔
2162
                    if current_stride == "1":
4✔
2163
                        current_stride = str(dim_size)
4✔
2164
                    else:
2165
                        current_stride = f"({current_stride} * {dim_size})"
×
2166

2167
            terms = []
4✔
2168
            loop_var_used = False
4✔
2169

2170
            for i, idx in enumerate(indices):
4✔
2171
                stride = strides[i] if i < len(strides) else "1"
4✔
2172
                start = subset[i] if i < len(subset) else "0"
4✔
2173

2174
                if isinstance(idx, ast.Slice):
4✔
2175
                    if stride == "1":
4✔
2176
                        term = f"({start} + {loop_var})"
4✔
2177
                    else:
2178
                        term = f"(({start} + {loop_var}) * {stride})"
4✔
2179
                    loop_var_used = True
4✔
2180
                else:
2181
                    if stride == "1":
4✔
2182
                        term = start
4✔
2183
                    else:
2184
                        term = f"({start} * {stride})"
4✔
2185

2186
                terms.append(term)
4✔
2187

2188
            if not terms:
4✔
2189
                return loop_var
×
2190

2191
            result = terms[0]
4✔
2192
            for t in terms[1:]:
4✔
2193
                result = f"({result} + {t})"
4✔
2194

2195
            return result
4✔
2196

2197
        self.builder.begin_for(i_var, "0", m_expr, "1")
4✔
2198
        self.builder.begin_for(j_var, "0", n_expr, "1")
4✔
2199

2200
        block = self.builder.add_block()
4✔
2201

2202
        t_a = self.builder.add_access(block, name_a)
4✔
2203
        t_b = self.builder.add_access(block, name_b)
4✔
2204
        t_c = self.builder.add_access(block, tmp_name)
4✔
2205

2206
        if ufunc_name in ["minimum", "maximum"]:
4✔
2207
            if is_int:
4✔
2208
                t_task = self.builder.add_tasklet(
4✔
2209
                    block, int_opcode, ["_in1", "_in2"], ["_out"]
2210
                )
2211
            else:
2212
                t_task = self.builder.add_cmath(block, fp_opcode, dtype.primitive_type)
4✔
2213
        else:
2214
            tasklet_code = int_opcode if is_int else fp_opcode
4✔
2215
            t_task = self.builder.add_tasklet(
4✔
2216
                block, tasklet_code, ["_in1", "_in2"], ["_out"]
2217
            )
2218

2219
        a_index = compute_linear_index(name_a, subset_a, indices_a, i_var)
4✔
2220
        b_index = compute_linear_index(name_b, subset_b, indices_b, j_var)
4✔
2221

2222
        self.builder.add_memlet(block, t_a, "void", t_task, "_in1", a_index)
4✔
2223
        self.builder.add_memlet(block, t_b, "void", t_task, "_in2", b_index)
4✔
2224

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

2228
        self.builder.end_for()
4✔
2229
        self.builder.end_for()
4✔
2230

2231
        return tmp_name
4✔
2232

2233
    def _handle_numpy_reduce(self, node, func_name):
4✔
2234
        """Handle np.sum, np.max, np.min, np.mean, np.std."""
2235
        args = node.args
4✔
2236
        keywords = {kw.arg: kw.value for kw in node.keywords}
4✔
2237

2238
        array_node = args[0]
4✔
2239
        array_name = self.visit(array_node)
4✔
2240

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

2244
        # For mean and std, we need float64 input and output (NumPy behavior)
2245
        # Cast input to float64 if needed
2246
        if func_name in ("mean", "std"):
4✔
2247
            float64_type = Scalar(PrimitiveType.Double)
4✔
2248
            array_name = self._cast_array(array_name, float64_type)
4✔
2249

2250
        input_tensor = self.tensor_table[array_name]
4✔
2251
        input_shape = input_tensor.shape
4✔
2252
        ndim = len(input_shape)
4✔
2253

2254
        axis = None
4✔
2255
        if len(args) > 1:
4✔
2256
            axis = args[1]
×
2257
        elif "axis" in keywords:
4✔
2258
            axis = keywords["axis"]
4✔
2259

2260
        keepdims = False
4✔
2261
        if "keepdims" in keywords:
4✔
2262
            keepdims_node = keywords["keepdims"]
4✔
2263
            if isinstance(keepdims_node, ast.Constant):
4✔
2264
                keepdims = bool(keepdims_node.value)
4✔
2265

2266
        axes = []
4✔
2267
        if axis is None:
4✔
2268
            axes = list(range(ndim))
4✔
2269
        elif isinstance(axis, ast.Constant):
4✔
2270
            val = axis.value
4✔
2271
            if val < 0:
4✔
2272
                val += ndim
×
2273
            axes = [val]
4✔
2274
        elif isinstance(axis, ast.Tuple):
4✔
2275
            for elt in axis.elts:
×
2276
                if isinstance(elt, ast.Constant):
×
2277
                    val = elt.value
×
2278
                    if val < 0:
×
2279
                        val += ndim
×
2280
                    axes.append(val)
×
2281
        elif (
4✔
2282
            isinstance(axis, ast.UnaryOp)
2283
            and isinstance(axis.op, ast.USub)
2284
            and isinstance(axis.operand, ast.Constant)
2285
        ):
2286
            val = -axis.operand.value
4✔
2287
            if val < 0:
4✔
2288
                val += ndim
4✔
2289
            axes = [val]
4✔
2290
        else:
2291
            try:
×
2292
                val = int(self.visit(axis))
×
2293
                if val < 0:
×
2294
                    val += ndim
×
2295
                axes = [val]
×
2296
            except:
×
2297
                raise NotImplementedError("Dynamic axis not supported")
×
2298

2299
        output_shape = []
4✔
2300
        for i in range(ndim):
4✔
2301
            if i in axes:
4✔
2302
                if keepdims:
4✔
2303
                    output_shape.append("1")
4✔
2304
            else:
2305
                output_shape.append(input_shape[i])
4✔
2306

2307
        dtype = self._ev._element_type(array_name)
4✔
2308

2309
        if not output_shape:
4✔
2310
            tmp_name = f"_tmp_{self._get_unique_id()}"
4✔
2311
            self.builder.add_container(tmp_name, dtype, False)
4✔
2312
            self.container_table[tmp_name] = dtype
4✔
2313
            self.tensor_table[tmp_name] = Tensor(dtype, [])
4✔
2314
        else:
2315
            output_strides = self._compute_strides(output_shape, "C")
4✔
2316
            tmp_name = self._create_array_temp(
4✔
2317
                output_shape, dtype, strides=output_strides
2318
            )
2319

2320
        output_tensor = self.tensor_table[tmp_name]
4✔
2321
        self.builder.add_reduce_op(
4✔
2322
            func_name, array_name, input_tensor, tmp_name, output_tensor, axes, keepdims
2323
        )
2324

2325
        return tmp_name
4✔
2326

2327
    # ========== Einsum Operations ==========
2328

2329
    def _parse_einsum_subscripts(self, subscripts, operand_shapes):
4✔
2330
        """Parse einsum subscripts string and return parsed components.
2331

2332
        Args:
2333
            subscripts: Einsum notation string, e.g., "ij,jk->ik" or "ij,jk"
2334
            operand_shapes: List of shapes for each operand
2335

2336
        Returns:
2337
            Tuple of (input_subscripts, output_subscripts, index_to_dim)
2338
            - input_subscripts: List of index strings per operand, e.g., ["ij", "jk"]
2339
            - output_subscripts: Output index string, e.g., "ik"
2340
            - index_to_dim: Dict mapping index char to dimension size string
2341
        """
2342
        # Remove whitespace
2343
        subscripts = subscripts.replace(" ", "")
4✔
2344

2345
        # Split into inputs and output
2346
        if "->" in subscripts:
4✔
2347
            input_part, output_subscripts = subscripts.split("->")
4✔
2348
        else:
NEW
2349
            input_part = subscripts
×
NEW
2350
            output_subscripts = None  # Implicit output
×
2351

2352
        # Split inputs by comma
2353
        input_subscripts = input_part.split(",")
4✔
2354

2355
        if len(input_subscripts) != len(operand_shapes):
4✔
NEW
2356
            raise ValueError(
×
2357
                f"Number of operands ({len(operand_shapes)}) does not match "
2358
                f"number of subscripts ({len(input_subscripts)})"
2359
            )
2360

2361
        # Map each index to its dimension size
2362
        index_to_dim = {}
4✔
2363
        for subscript, shape in zip(input_subscripts, operand_shapes):
4✔
2364
            if len(subscript) != len(shape):
4✔
NEW
2365
                raise ValueError(
×
2366
                    f"Subscript '{subscript}' has {len(subscript)} indices but "
2367
                    f"operand has {len(shape)} dimensions"
2368
                )
2369
            for idx_char, dim_size in zip(subscript, shape):
4✔
2370
                if idx_char in index_to_dim:
4✔
2371
                    # Validate dimensions match (at least symbolically)
2372
                    existing = index_to_dim[idx_char]
4✔
2373
                    if str(existing) != str(dim_size):
4✔
2374
                        # Could be symbolic - just warn or trust the user
NEW
2375
                        pass
×
2376
                else:
2377
                    index_to_dim[idx_char] = dim_size
4✔
2378

2379
        # Compute implicit output if not provided
2380
        if output_subscripts is None:
4✔
NEW
2381
            output_subscripts = self._compute_implicit_output(input_subscripts)
×
2382

2383
        return input_subscripts, output_subscripts, index_to_dim
4✔
2384

2385
    def _compute_implicit_output(self, input_subscripts):
4✔
2386
        """Compute implicit output indices (sorted indices appearing exactly once).
2387

2388
        Args:
2389
            input_subscripts: List of index strings, e.g., ["ij", "jk"]
2390

2391
        Returns:
2392
            Output index string with sorted non-contracted indices, e.g., "ik"
2393
        """
NEW
2394
        counts = {}
×
NEW
2395
        for subscript in input_subscripts:
×
NEW
2396
            for idx in subscript:
×
NEW
2397
                counts[idx] = counts.get(idx, 0) + 1
×
2398

2399
        # Output = sorted indices with count == 1 (non-contracted)
NEW
2400
        return "".join(sorted(idx for idx, cnt in counts.items() if cnt == 1))
×
2401

2402
    def _handle_numpy_einsum(self, node, func_name):
4✔
2403
        """Handle np.einsum(subscripts, *operands) calls.
2404

2405
        Parses the subscripts string to extract index structure, computes output
2406
        shape, and emits an EinsumNode to the IR.
2407
        """
2408
        if len(node.args) < 2:
4✔
NEW
2409
            raise ValueError("np.einsum requires at least subscripts and one operand")
×
2410

2411
        # First argument is the subscripts string
2412
        subscripts_arg = node.args[0]
4✔
2413
        if not isinstance(subscripts_arg, ast.Constant) or not isinstance(
4✔
2414
            subscripts_arg.value, str
2415
        ):
NEW
2416
            raise NotImplementedError("np.einsum subscripts must be a string literal")
×
2417
        subscripts = subscripts_arg.value
4✔
2418

2419
        # Remaining arguments are operands
2420
        operand_nodes = node.args[1:]
4✔
2421
        operand_names = [self.visit(op) for op in operand_nodes]
4✔
2422

2423
        # Validate all operands are in tensor_table
2424
        for name in operand_names:
4✔
2425
            if name not in self.tensor_table:
4✔
NEW
2426
                raise ValueError(f"Einsum operand '{name}' not found in tensor_table")
×
2427

2428
        # Get shapes for all operands
2429
        operand_shapes = [self.tensor_table[name].shape for name in operand_names]
4✔
2430

2431
        # Parse subscripts
2432
        input_subscripts, output_subscripts, index_to_dim = (
4✔
2433
            self._parse_einsum_subscripts(subscripts, operand_shapes)
2434
        )
2435

2436
        # Build dimension specs: (indvar, init, bound) for each unique index
2437
        # Collect all unique indices in order of first appearance
2438
        seen_indices = []
4✔
2439
        for subscript in input_subscripts:
4✔
2440
            for idx in subscript:
4✔
2441
                if idx not in seen_indices:
4✔
2442
                    seen_indices.append(idx)
4✔
2443

2444
        dims = []
4✔
2445
        for idx in seen_indices:
4✔
2446
            dims.append((idx, "0", str(index_to_dim[idx])))
4✔
2447

2448
        # Build output indices (the index variables for output dimensions)
2449
        out_indices = list(output_subscripts)
4✔
2450

2451
        # Build input indices for each operand
2452
        in_indices = [list(subscript) for subscript in input_subscripts]
4✔
2453

2454
        # Compute output shape from output subscripts
2455
        output_shape = [str(index_to_dim[idx]) for idx in output_subscripts]
4✔
2456

2457
        # Determine element type (promote from inputs)
2458
        dtypes = [self._ev._element_type(name) for name in operand_names]
4✔
2459
        dtype = dtypes[0]
4✔
2460
        for dt in dtypes[1:]:
4✔
2461
            dtype = promote_element_types(dtype, dt)
4✔
2462

2463
        # Create output container
2464
        if output_shape:
4✔
2465
            output_strides = self._compute_strides(output_shape, "C")
4✔
2466
            tmp_name = self._create_array_temp(
4✔
2467
                output_shape, dtype, strides=output_strides
2468
            )
2469
        else:
2470
            # Scalar output
2471
            tmp_name = f"_tmp_{self._get_unique_id()}"
4✔
2472
            self.builder.add_container(tmp_name, dtype, False)
4✔
2473
            self.container_table[tmp_name] = dtype
4✔
2474
            self.tensor_table[tmp_name] = Tensor(dtype, [])
4✔
2475

2476
        # Get tensor types for builder call
2477
        input_types = [self.tensor_table[name] for name in operand_names]
4✔
2478
        output_type = self.tensor_table[tmp_name]
4✔
2479

2480
        # Call builder.add_einsum
2481
        self.builder.add_einsum(
4✔
2482
            operand_names,
2483
            tmp_name,
2484
            dims,
2485
            out_indices,
2486
            in_indices,
2487
            input_types,
2488
            output_type,
2489
        )
2490

2491
        return tmp_name
4✔
2492

2493
    def handle_numpy_astype(self, node, array_name):
4✔
2494
        """Handle numpy array.astype(dtype) method calls."""
2495
        if len(node.args) < 1:
4✔
2496
            raise ValueError("astype requires at least one argument (dtype)")
×
2497

2498
        # Check for copy=False which we don't support (we always copy)
2499
        for kw in node.keywords:
4✔
2500
            if kw.arg == "copy":
4✔
2501
                if isinstance(kw.value, ast.Constant) and kw.value.value is False:
4✔
2502
                    raise NotImplementedError("astype with copy=False is not supported")
4✔
2503

2504
        dtype_arg = node.args[0]
4✔
2505
        target_dtype = element_type_from_ast_node(dtype_arg, self.container_table)
4✔
2506

2507
        if array_name not in self.tensor_table:
4✔
2508
            raise ValueError(f"Array {array_name} not found in tensor_table")
×
2509

2510
        input_tensor = self.tensor_table[array_name]
4✔
2511
        input_shape = input_tensor.shape
4✔
2512
        input_strides = getattr(input_tensor, "strides", None)
4✔
2513

2514
        # Determine output order: preserve F-order if input is F-contiguous
2515
        order = "C"
4✔
2516
        if input_strides and len(input_strides) >= 2 and len(input_shape) >= 2:
4✔
2517
            # F-order: first stride is 1, subsequent strides are products of preceding dims
2518
            f_strides = self._compute_strides(input_shape, "F")
4✔
2519
            if input_strides == f_strides:
4✔
2520
                order = "F"
×
2521

2522
        output_strides = self._compute_strides(input_shape, order)
4✔
2523
        tmp_name = self._create_array_temp(
4✔
2524
            input_shape, target_dtype, strides=output_strides
2525
        )
2526

2527
        output_tensor = self.tensor_table[tmp_name]
4✔
2528
        self.builder.add_cast_op(array_name, input_tensor, tmp_name, output_tensor)
4✔
2529

2530
        return tmp_name
4✔
2531

2532
    def handle_numpy_copy(self, node, array_name):
4✔
2533
        """Handle numpy array.copy() method calls using memcpy."""
2534
        if array_name not in self.tensor_table:
4✔
2535
            raise ValueError(f"Array {array_name} not found in tensor_table")
×
2536

2537
        input_tensor = self.tensor_table[array_name]
4✔
2538
        input_shape = input_tensor.shape
4✔
2539
        input_strides = getattr(input_tensor, "strides", None)
4✔
2540

2541
        element_type = Scalar(PrimitiveType.Double)
4✔
2542
        if array_name in self.container_table:
4✔
2543
            sym_type = self.container_table[array_name]
4✔
2544
            if isinstance(sym_type, Pointer) and sym_type.has_pointee_type():
4✔
2545
                element_type = sym_type.pointee_type
4✔
2546

2547
        # Determine output order: preserve F-order if input is F-contiguous
2548
        order = "C"
4✔
2549
        if input_strides and len(input_strides) >= 2 and len(input_shape) >= 2:
4✔
2550
            f_strides = self._compute_strides(input_shape, "F")
4✔
2551
            if input_strides == f_strides:
4✔
2552
                order = "F"
×
2553

2554
        output_strides = self._compute_strides(input_shape, order)
4✔
2555
        tmp_name = self._create_array_temp(
4✔
2556
            input_shape, element_type, strides=output_strides
2557
        )
2558

2559
        output_tensor = self.tensor_table[tmp_name]
4✔
2560
        # Workaround: "assign-op"
2561
        self.builder.add_cast_op(array_name, input_tensor, tmp_name, output_tensor)
4✔
2562

2563
        return tmp_name
4✔
2564

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

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

2571
        Args:
2572
            shape: Output shape
2573
            input_strides: Strides from input tensor
2574

2575
        Returns:
2576
            List of stride expressions for a contiguous output array
2577
        """
2578
        if not shape or not input_strides:
4✔
2579
            return self._compute_strides(shape, "C")
4✔
2580

2581
        # Preserve order if contiguous, otherwise default to C-order
2582
        c_strides = self._compute_strides(shape, "C")
4✔
2583
        if input_strides == c_strides:
4✔
2584
            return c_strides
4✔
2585
        f_strides = self._compute_strides(shape, "F")
4✔
2586
        if input_strides == f_strides:
4✔
2587
            return f_strides
×
2588
        return c_strides
4✔
2589

2590
    def _compute_strides(self, shape, order="C"):
4✔
2591
        """Compute strides for a given shape and memory order.
2592

2593
        Args:
2594
            shape: List of dimension sizes
2595
            order: "C" for row-major (default), "F" for column-major
2596

2597
        Returns:
2598
            List of stride expressions as strings
2599
        """
2600
        if not shape:
4✔
2601
            return []
4✔
2602

2603
        ndim = len(shape)
4✔
2604
        strides = []
4✔
2605

2606
        if order == "F":
4✔
2607
            # Column-major (Fortran order): stride[i] = product of shape[:i]
2608
            for dim_idx in range(ndim):
4✔
2609
                if dim_idx == 0:
4✔
2610
                    strides.append("1")
4✔
2611
                else:
2612
                    # Wrap each shape in parens to ensure correct precedence
2613
                    prefix_shapes = [f"({s})" for s in shape[:dim_idx]]
4✔
2614
                    if len(prefix_shapes) == 1:
4✔
2615
                        strides.append(prefix_shapes[0])
4✔
2616
                    else:
2617
                        strides.append("(" + " * ".join(prefix_shapes) + ")")
×
2618
        else:
2619
            # Row-major (C order): stride[i] = product of shape[i+1:]
2620
            for dim_idx in range(ndim):
4✔
2621
                if dim_idx == ndim - 1:
4✔
2622
                    strides.append("1")
4✔
2623
                else:
2624
                    # Wrap each shape in parens to ensure correct precedence
2625
                    suffix_shapes = [f"({s})" for s in shape[dim_idx + 1 :]]
4✔
2626
                    if len(suffix_shapes) == 1:
4✔
2627
                        strides.append(suffix_shapes[0])
4✔
2628
                    else:
2629
                        strides.append("(" + " * ".join(suffix_shapes) + ")")
4✔
2630

2631
        return strides
4✔
2632

2633
    def _is_contiguous(self, shape, strides):
4✔
2634
        """Check if strides represent a contiguous (C or F order) layout."""
2635
        if not shape or not strides:
4✔
2636
            return True
×
2637

2638
        def normalize(s):
4✔
2639
            # Normalize stride expression by removing spaces and outer parens
2640
            s = s.replace(" ", "")
4✔
2641
            while s.startswith("(") and s.endswith(")"):
4✔
2642
                # Only strip if balanced parens
2643
                inner = s[1:-1]
4✔
2644
                depth = 0
4✔
2645
                balanced = True
4✔
2646
                for c in inner:
4✔
2647
                    if c == "(":
4✔
2648
                        depth += 1
×
2649
                    elif c == ")":
4✔
2650
                        depth -= 1
×
2651
                        if depth < 0:
×
2652
                            balanced = False
×
2653
                            break
×
2654
                if balanced and depth == 0:
4✔
2655
                    s = inner
4✔
2656
                else:
2657
                    break
×
2658
            return s
4✔
2659

2660
        c_strides = self._compute_strides(shape, "C")
4✔
2661
        if all(
4✔
2662
            normalize(str(a)) == normalize(str(b)) for a, b in zip(strides, c_strides)
2663
        ):
2664
            return True
4✔
2665
        f_strides = self._compute_strides(shape, "F")
×
2666
        return all(
×
2667
            normalize(str(a)) == normalize(str(b)) for a, b in zip(strides, f_strides)
2668
        )
2669

2670
    def _create_array_temp(
4✔
2671
        self,
2672
        shape,
2673
        dtype,
2674
        zero_init=False,
2675
        ones_init=False,
2676
        shapes_runtime=None,
2677
        strides=None,
2678
    ):
2679
        """Create a temporary array."""
2680
        tmp_name = f"_tmp_{self._get_unique_id()}"
4✔
2681

2682
        # Handle 0-dimensional arrays as scalars
2683
        if not shape or (len(shape) == 0):
4✔
2684
            self.builder.add_container(tmp_name, dtype, False)
4✔
2685
            self.container_table[tmp_name] = dtype
4✔
2686
            self.tensor_table[tmp_name] = Tensor(dtype, [])
4✔
2687

2688
            if zero_init:
4✔
2689
                self.builder.add_assignment(
×
2690
                    tmp_name,
2691
                    "0.0" if dtype.primitive_type == PrimitiveType.Double else "0",
2692
                )
2693
            elif ones_init:
4✔
2694
                self.builder.add_assignment(
×
2695
                    tmp_name,
2696
                    "1.0" if dtype.primitive_type == PrimitiveType.Double else "1",
2697
                )
2698

2699
            return tmp_name
4✔
2700

2701
        # Calculate size - wrap each dimension in parentheses to ensure correct
2702
        # parsing when dimensions are expressions like "-2 + _s0"
2703
        size_str = "1"
4✔
2704
        for dim in shape:
4✔
2705
            size_str = f"({size_str} * ({dim}))"
4✔
2706

2707
        element_size = self.builder.get_sizeof(dtype)
4✔
2708
        total_size = f"({size_str} * {element_size})"
4✔
2709

2710
        # Use provided strides or compute C-order strides
2711
        if strides is None:
4✔
2712
            strides = self._compute_strides(shape, "C")
4✔
2713

2714
        # Create pointer
2715
        ptr_type = Pointer(dtype)
4✔
2716
        self.builder.add_container(tmp_name, ptr_type, False)
4✔
2717
        self.container_table[tmp_name] = ptr_type
4✔
2718
        tensor_entry = Tensor(dtype, shape, strides, "0")
4✔
2719
        if shapes_runtime is not None:
4✔
2720
            self.shapes_runtime_info[tmp_name] = shapes_runtime
4✔
2721
        self.tensor_table[tmp_name] = tensor_entry
4✔
2722

2723
        # Try to hoist allocation to function entry
2724
        init_type = (
4✔
2725
            ManagedMemoryHandler.INIT_ZERO
2726
            if zero_init
2727
            else ManagedMemoryHandler.INIT_NONE
2728
        )
2729
        if not ones_init and self.memory_handler.allocate(
4✔
2730
            tmp_name, ptr_type, total_size, init=init_type
2731
        ):
2732
            pass  # Allocation registered for hoisting
4✔
2733
        else:
2734
            # Emit allocation immediately (size depends on loop variables or needs loop init)
2735
            self._emit_malloc(
4✔
2736
                tmp_name, total_size, ptr_type, zero_init, ones_init, size_str, dtype
2737
            )
2738

2739
        return tmp_name
4✔
2740

2741
    def _emit_malloc(
4✔
2742
        self, tmp_name, total_size, ptr_type, zero_init, ones_init, size_str, dtype
2743
    ):
2744
        """Emit malloc and optional initialization for a temporary array."""
2745
        block1 = self.builder.add_block()
4✔
2746
        t_malloc = self.builder.add_malloc(block1, total_size)
4✔
2747
        t_ptr1 = self.builder.add_access(block1, tmp_name)
4✔
2748
        self.builder.add_memlet(block1, t_malloc, "_ret", t_ptr1, "void", "", ptr_type)
4✔
2749

2750
        if zero_init:
4✔
2751
            block2 = self.builder.add_block()
4✔
2752
            t_memset = self.builder.add_memset(block2, "0", total_size)
4✔
2753
            t_ptr2 = self.builder.add_access(block2, tmp_name)
4✔
2754
            self.builder.add_memlet(
4✔
2755
                block2, t_memset, "_ptr", t_ptr2, "void", "", ptr_type
2756
            )
2757
        elif ones_init:
4✔
2758
            loop_var = f"_i_{self._get_unique_id()}"
4✔
2759
            if not self.builder.exists(loop_var):
4✔
2760
                self.builder.add_container(loop_var, Scalar(PrimitiveType.Int64), False)
4✔
2761
                self.container_table[loop_var] = Scalar(PrimitiveType.Int64)
4✔
2762

2763
            self.builder.begin_for(loop_var, "0", size_str, "1")
4✔
2764

2765
            val = "1.0"
4✔
2766
            if dtype.primitive_type in [
4✔
2767
                PrimitiveType.Int64,
2768
                PrimitiveType.Int32,
2769
                PrimitiveType.Int8,
2770
                PrimitiveType.Int16,
2771
                PrimitiveType.UInt64,
2772
                PrimitiveType.UInt32,
2773
                PrimitiveType.UInt8,
2774
                PrimitiveType.UInt16,
2775
            ]:
2776
                val = "1"
4✔
2777

2778
            block_assign = self.builder.add_block()
4✔
2779
            t_const = self.builder.add_constant(block_assign, val, dtype)
4✔
2780
            t_arr = self.builder.add_access(block_assign, tmp_name)
4✔
2781

2782
            t_task = self.builder.add_tasklet(
4✔
2783
                block_assign, TaskletCode.assign, ["_in"], ["_out"]
2784
            )
2785
            self.builder.add_memlet(
4✔
2786
                block_assign, t_const, "void", t_task, "_in", "", dtype
2787
            )
2788
            self.builder.add_memlet(
4✔
2789
                block_assign, t_task, "_out", t_arr, "void", loop_var
2790
            )
2791

2792
            self.builder.end_for()
4✔
2793

2794
    def _compute_linear_index(self, indices, shapes, array_name, ndim):
4✔
2795
        """Compute linear index from multi-dimensional indices.
2796

2797
        Uses strides from tensor_table if available (supporting F-order arrays),
2798
        otherwise falls back to computing strides assuming C-order.
2799
        """
2800
        if ndim == 0:
×
2801
            return "0"
×
2802

2803
        # Try to get strides from tensor_table
2804
        strides = None
×
2805
        if array_name in self.tensor_table:
×
2806
            tensor_info = self.tensor_table[array_name]
×
2807
            if hasattr(tensor_info, "strides") and tensor_info.strides:
×
2808
                strides = tensor_info.strides
×
2809

2810
        if strides and len(strides) == ndim:
×
2811
            # Use explicit strides from tensor_table
2812
            linear_index = ""
×
2813
            for i in range(ndim):
×
2814
                stride = strides[i]
×
2815
                if stride == "1":
×
2816
                    term = str(indices[i])
×
2817
                else:
2818
                    term = f"(({indices[i]}) * ({stride}))"
×
2819

2820
                if i == 0:
×
2821
                    linear_index = term
×
2822
                else:
2823
                    linear_index = f"({linear_index} + {term})"
×
2824
            return linear_index
×
2825
        else:
2826
            # Fall back to C-order (row-major) stride computation
2827
            linear_index = ""
×
2828
            for i in range(ndim):
×
2829
                term = str(indices[i])
×
2830
                for j in range(i + 1, ndim):
×
2831
                    shape_val = (
×
2832
                        shapes[j] if j < len(shapes) else f"_{array_name}_shape_{j}"
2833
                    )
2834
                    term = f"(({term}) * {shape_val})"
×
2835

2836
                if i == 0:
×
2837
                    linear_index = term
×
2838
                else:
2839
                    linear_index = f"({linear_index} + {term})"
×
2840

2841
            return linear_index
×
2842

2843
    def _compute_broadcast_shape(self, shape_a, shape_b):
4✔
2844
        """Compute the broadcast output shape following NumPy broadcasting rules."""
2845
        if not shape_a:
4✔
2846
            return shape_b
4✔
2847
        if not shape_b:
4✔
2848
            return shape_a
4✔
2849

2850
        max_ndim = max(len(shape_a), len(shape_b))
4✔
2851
        padded_a = ["1"] * (max_ndim - len(shape_a)) + [str(s) for s in shape_a]
4✔
2852
        padded_b = ["1"] * (max_ndim - len(shape_b)) + [str(s) for s in shape_b]
4✔
2853

2854
        result = []
4✔
2855
        for a, b in zip(padded_a, padded_b):
4✔
2856
            if a == "1":
4✔
2857
                result.append(b)
4✔
2858
            elif b == "1":
4✔
2859
                result.append(a)
4✔
2860
            elif a == b:
4✔
2861
                result.append(a)
4✔
2862
            else:
2863
                result.append(a)
4✔
2864

2865
        return result
4✔
2866

2867
    def _needs_broadcast(self, input_shape, output_shape):
4✔
2868
        """Check if input shape needs broadcasting to match output shape."""
2869
        if len(input_shape) != len(output_shape):
4✔
2870
            return True
4✔
2871
        for in_dim, out_dim in zip(input_shape, output_shape):
4✔
2872
            if str(in_dim) != str(out_dim):
4✔
2873
                return True
4✔
2874
        return False
4✔
2875

2876
    def _compute_broadcast_strides(self, input_shape, input_strides, output_shape):
4✔
2877
        """Compute strides for broadcasting input to output shape.
2878

2879
        For broadcast dimensions (size 1), stride is set to 0 so the same
2880
        value is repeated. This enables stride-based broadcasting without copying.
2881
        """
2882
        # Pad input shape and strides on the left to match output ndim
2883
        ndim_diff = len(output_shape) - len(input_shape)
4✔
2884
        padded_shape = ["1"] * ndim_diff + [str(s) for s in input_shape]
4✔
2885
        padded_strides = ["0"] * ndim_diff + [str(s) for s in input_strides]
4✔
2886

2887
        broadcast_strides = []
4✔
2888
        for in_dim, in_stride, out_dim in zip(
4✔
2889
            padded_shape, padded_strides, output_shape
2890
        ):
2891
            # Only use stride 0 when input dimension is exactly "1" (broadcast case).
2892
            # For other cases (including symbolic dimensions that may be equal at runtime),
2893
            # keep the original stride.
2894
            if str(in_dim) == "1" and str(out_dim) != "1":
4✔
2895
                # Broadcast dimension: use stride 0
2896
                broadcast_strides.append("0")
4✔
2897
            else:
2898
                # Non-broadcast dimension or potentially equal symbolic dimensions:
2899
                # keep original stride
2900
                broadcast_strides.append(in_stride)
4✔
2901

2902
        return broadcast_strides
4✔
2903

2904
    def _shape_to_runtime_expr(self, shape_node):
4✔
2905
        """Convert a shape expression AST node to a runtime-evaluable string."""
2906
        if isinstance(shape_node, ast.Constant):
4✔
2907
            return str(shape_node.value)
4✔
2908
        elif isinstance(shape_node, ast.Name):
4✔
2909
            return shape_node.id
4✔
2910
        elif isinstance(shape_node, ast.BinOp):
4✔
2911
            left = self._shape_to_runtime_expr(shape_node.left)
4✔
2912
            right = self._shape_to_runtime_expr(shape_node.right)
4✔
2913
            op = self.visit(shape_node.op)
4✔
2914
            return f"({left} {op} {right})"
4✔
2915
        elif isinstance(shape_node, ast.UnaryOp):
4✔
2916
            operand = self._shape_to_runtime_expr(shape_node.operand)
×
2917
            if isinstance(shape_node.op, ast.USub):
×
2918
                return f"(-{operand})"
×
2919
            elif isinstance(shape_node.op, ast.UAdd):
×
2920
                return operand
×
2921
            else:
2922
                return self.visit(shape_node)
×
2923
        elif isinstance(shape_node, ast.Subscript):
4✔
2924
            val = shape_node.value
4✔
2925
            if isinstance(val, ast.Attribute) and val.attr == "shape":
4✔
2926
                if isinstance(val.value, ast.Name):
4✔
2927
                    arr_name = val.value.id
4✔
2928
                    if isinstance(shape_node.slice, ast.Constant):
4✔
2929
                        idx = shape_node.slice.value
4✔
2930
                        if arr_name in self.tensor_table:
4✔
2931
                            shapes = self.tensor_table[arr_name].shape
4✔
2932
                            if idx < len(shapes):
4✔
2933
                                return shapes[idx]
4✔
2934
                        return f"{arr_name}.shape[{idx}]"
×
2935
            return self.visit(shape_node)
×
2936
        elif isinstance(shape_node, ast.Tuple):
×
2937
            return [self._shape_to_runtime_expr(elt) for elt in shape_node.elts]
×
2938
        elif isinstance(shape_node, ast.List):
×
2939
            return [self._shape_to_runtime_expr(elt) for elt in shape_node.elts]
×
2940
        else:
2941
            return self.visit(shape_node)
×
2942

2943
    # ========== Type Casting Helpers ==========
2944

2945
    def _cast_scalar(self, name, target_type):
4✔
2946
        """
2947
        Cast a scalar value to a different type using an assign tasklet.
2948

2949
        The backend detects the specific conversion (fpext, sitofp, etc.)
2950
        from the type mismatch between input and output.
2951

2952
        Args:
2953
            name: Name of the scalar to cast
2954
            target_type: Target element type (Scalar)
2955

2956
        Returns:
2957
            Name of the casted scalar (or original if no cast needed)
2958
        """
2959
        current_type = self._ev._element_type(name)
4✔
2960
        if current_type.primitive_type == target_type.primitive_type:
4✔
2961
            return name
4✔
2962

2963
        cast_name = f"_cast_{self._get_unique_id()}"
4✔
2964
        self.builder.add_container(cast_name, target_type, False)
4✔
2965
        self.container_table[cast_name] = target_type
4✔
2966
        self.tensor_table[cast_name] = Tensor(target_type, [])
4✔
2967

2968
        block = self.builder.add_block()
4✔
2969
        t_src, src_sub = self._add_read(block, name)
4✔
2970
        t_dst = self.builder.add_access(block, cast_name)
4✔
2971
        t_task = self.builder.add_tasklet(block, TaskletCode.assign, ["_in"], ["_out"])
4✔
2972
        self.builder.add_memlet(block, t_src, "void", t_task, "_in", src_sub)
4✔
2973
        self.builder.add_memlet(block, t_task, "_out", t_dst, "void", "")
4✔
2974

2975
        return cast_name
4✔
2976

2977
    def _cast_array(self, name, target_type):
4✔
2978
        """
2979
        Cast an array to a different element type using the CastNode library node.
2980

2981
        This is an elementwise cast operation that creates a new array.
2982
        Reuses the same infrastructure as handle_numpy_astype().
2983

2984
        Args:
2985
            name: Name of the array to cast
2986
            target_type: Target element type (Scalar)
2987

2988
        Returns:
2989
            Name of the casted array (or original if no cast needed)
2990
        """
2991
        current_type = self._ev._element_type(name)
4✔
2992
        if current_type.primitive_type == target_type.primitive_type:
4✔
2993
            return name
4✔
2994

2995
        src_tensor = self.tensor_table[name]
4✔
2996

2997
        # Create output array with same shape but new dtype
2998
        # Preserve strides order (C or F contiguous)
2999
        output_strides = self._get_contiguous_output_strides(
4✔
3000
            src_tensor.shape, src_tensor.strides
3001
        )
3002
        tmp_name = self._create_array_temp(
4✔
3003
            src_tensor.shape, target_type, strides=output_strides
3004
        )
3005
        tmp_tensor = self.tensor_table[tmp_name]
4✔
3006

3007
        # Use existing cast infrastructure (CastNode)
3008
        self.builder.add_cast_op(name, src_tensor, tmp_name, tmp_tensor)
4✔
3009

3010
        return tmp_name
4✔
3011

3012
    def _cast_to_type(self, name, target_type):
4✔
3013
        """
3014
        Cast an operand (scalar or array) to the target type.
3015

3016
        Dispatches to _cast_scalar or _cast_array based on whether
3017
        the operand is in tensor_table (includes 0-d arrays).
3018

3019
        Args:
3020
            name: Name of the operand to cast
3021
            target_type: Target element type (Scalar)
3022

3023
        Returns:
3024
            Name of the casted operand (or original if no cast needed)
3025
        """
3026
        if name in self.tensor_table:
4✔
3027
            # In tensor_table means it's an array (including 0-d arrays)
3028
            return self._cast_array(name, target_type)
4✔
3029
        else:
3030
            # Not in tensor_table means it's a literal or Python scalar
3031
            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