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

daisytuner / docc / 21577584338

01 Feb 2026 09:34PM UTC coverage: 66.487% (+0.008%) from 66.479%
21577584338

push

github

web-flow
Merge pull request #499 from daisytuner/np-clip

adds support for np.clip to python frontend

8 of 9 new or added lines in 1 file covered. (88.89%)

23099 of 34742 relevant lines covered (66.49%)

376.24 hits per line

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

81.61
/python/docc/python/expression_visitor.py
1
import ast
4✔
2
import inspect
4✔
3
import textwrap
4✔
4
from docc.sdfg import (
4✔
5
    Scalar,
6
    PrimitiveType,
7
    Pointer,
8
    Type,
9
    DebugInfo,
10
    Structure,
11
    TaskletCode,
12
    CMathFunction,
13
)
14

15

16
class ExpressionVisitor(ast.NodeVisitor):
4✔
17
    def __init__(
4✔
18
        self,
19
        array_info=None,
20
        builder=None,
21
        symbol_table=None,
22
        globals_dict=None,
23
        inliner=None,
24
        unique_counter_ref=None,
25
        structure_member_info=None,
26
    ):
27
        self.array_info = array_info if array_info is not None else {}
4✔
28
        self.builder = builder
4✔
29
        self.symbol_table = symbol_table if symbol_table is not None else {}
4✔
30
        self.globals_dict = globals_dict if globals_dict is not None else {}
4✔
31
        self.inliner = inliner
4✔
32
        self._unique_counter_ref = (
4✔
33
            unique_counter_ref if unique_counter_ref is not None else [0]
34
        )
35
        self._access_cache = {}
4✔
36
        self.la_handler = None
4✔
37
        self.structure_member_info = (
4✔
38
            structure_member_info if structure_member_info is not None else {}
39
        )
40
        self._init_numpy_handlers()
4✔
41

42
    def _get_unique_id(self):
4✔
43
        self._unique_counter_ref[0] += 1
4✔
44
        return self._unique_counter_ref[0]
4✔
45

46
    def _get_temp_name(self, prefix="_tmp_"):
4✔
47
        if hasattr(self.builder, "find_new_name"):
4✔
48
            return self.builder.find_new_name(prefix)
×
49
        return f"{prefix}{self._get_unique_id()}"
4✔
50

51
    def _is_indirect_access(self, node):
4✔
52
        """Check if a node represents an indirect array access (e.g., A[B[i]]).
53

54
        Returns True if the node is a subscript where the index itself is a subscript
55
        into an array (indirect access pattern).
56
        """
57
        if not isinstance(node, ast.Subscript):
×
58
            return False
×
59
        # Check if value is a subscripted array access
60
        if isinstance(node.value, ast.Name):
×
61
            arr_name = node.value.id
×
62
            if arr_name in self.array_info:
×
63
                # Check if slice/index is itself an array access
64
                if isinstance(node.slice, ast.Subscript):
×
65
                    if isinstance(node.slice.value, ast.Name):
×
66
                        idx_arr_name = node.slice.value.id
×
67
                        if idx_arr_name in self.array_info:
×
68
                            return True
×
69
        return False
×
70

71
    def _contains_indirect_access(self, node):
4✔
72
        """Check if an AST node contains any indirect array access.
73

74
        Used to detect expressions like A_row[i] that would be used as slice bounds.
75
        """
76
        if isinstance(node, ast.Subscript):
4✔
77
            if isinstance(node.value, ast.Name):
4✔
78
                arr_name = node.value.id
4✔
79
                if arr_name in self.array_info:
4✔
80
                    return True
4✔
81
        elif isinstance(node, ast.BinOp):
4✔
82
            return self._contains_indirect_access(
4✔
83
                node.left
84
            ) or self._contains_indirect_access(node.right)
85
        elif isinstance(node, ast.UnaryOp):
4✔
86
            return self._contains_indirect_access(node.operand)
4✔
87
        return False
4✔
88

89
    def _materialize_indirect_access(
4✔
90
        self, node, debug_info=None, return_original_expr=False
91
    ):
92
        """Materialize an array access into a scalar variable using tasklet+memlets.
93

94
        For indirect memory access patterns in SDFGs, we need to:
95
        1. Create a scalar container for the result
96
        2. Create a tasklet that performs the assignment
97
        3. Use memlets to read from the array and write to the scalar
98
        4. Return the scalar name (which can be used as a symbolic expression)
99

100
        This is the canonical SDFG pattern for indirect access.
101

102
        If return_original_expr is True, also returns the original array access
103
        expression using parentheses notation (e.g., "A_row(0)") which is consistent
104
        with SDFG subset notation. The runtime evaluator will convert this to
105
        bracket notation for Python evaluation.
106
        """
107
        if not self.builder:
4✔
108
            # Without builder, just return the expression string
109
            expr = self.visit(node)
×
110
            return (expr, expr) if return_original_expr else expr
×
111

112
        if debug_info is None:
4✔
113
            debug_info = DebugInfo()
4✔
114

115
        if not isinstance(node, ast.Subscript):
4✔
116
            expr = self.visit(node)
×
117
            return (expr, expr) if return_original_expr else expr
×
118

119
        if not isinstance(node.value, ast.Name):
4✔
120
            expr = self.visit(node)
×
121
            return (expr, expr) if return_original_expr else expr
×
122

123
        arr_name = node.value.id
4✔
124
        if arr_name not in self.array_info:
4✔
125
            expr = self.visit(node)
×
126
            return (expr, expr) if return_original_expr else expr
×
127

128
        # Determine the element type
129
        dtype = Scalar(PrimitiveType.Int64)  # Default for indices
4✔
130
        if arr_name in self.symbol_table:
4✔
131
            t = self.symbol_table[arr_name]
4✔
132
            if isinstance(t, Pointer) and t.has_pointee_type():
4✔
133
                dtype = t.pointee_type
4✔
134

135
        # Create scalar container for the result
136
        tmp_name = self._get_temp_name("_idx_")
4✔
137
        self.builder.add_container(tmp_name, dtype, False)
4✔
138
        self.symbol_table[tmp_name] = dtype
4✔
139

140
        # Get the index expression
141
        ndim = self.array_info[arr_name]["ndim"]
4✔
142
        shapes = self.array_info[arr_name].get("shapes", [])
4✔
143

144
        # Compute linear index from the subscript
145
        if isinstance(node.slice, ast.Tuple):
4✔
146
            indices = [self.visit(elt) for elt in node.slice.elts]
×
147
        else:
148
            indices = [self.visit(node.slice)]
4✔
149

150
        # Handle cases where we need recursive materialization
151
        materialized_indices = []
4✔
152
        for i, idx_str in enumerate(indices):
4✔
153
            # Check if the index itself needs materialization (nested indirect)
154
            # This happens when idx_str looks like an array access e.g., "arr(i)"
155
            if "(" in idx_str and idx_str.endswith(")"):
4✔
156
                # This is an array access, it should already be a valid symbolic expression
157
                # or a scalar variable name
158
                materialized_indices.append(idx_str)
×
159
            else:
160
                materialized_indices.append(idx_str)
4✔
161

162
        # Compute linear index
163
        linear_index = self._compute_linear_index(
4✔
164
            materialized_indices, shapes, arr_name, ndim
165
        )
166

167
        # Create block with tasklet and memlets
168
        block = self.builder.add_block(debug_info)
4✔
169
        t_src = self.builder.add_access(block, arr_name, debug_info)
4✔
170
        t_dst = self.builder.add_access(block, tmp_name, debug_info)
4✔
171
        t_task = self.builder.add_tasklet(
4✔
172
            block, TaskletCode.assign, ["_in"], ["_out"], debug_info
173
        )
174

175
        self.builder.add_memlet(
4✔
176
            block, t_src, "void", t_task, "_in", linear_index, None, debug_info
177
        )
178
        self.builder.add_memlet(
4✔
179
            block, t_task, "_out", t_dst, "void", "", None, debug_info
180
        )
181

182
        if return_original_expr:
4✔
183
            # Return both the materialized variable name and the original array access expression
184
            # Use parentheses notation which is consistent with SDFG subset syntax
185
            original_expr = f"{arr_name}({linear_index})"
4✔
186
            return (tmp_name, original_expr)
4✔
187

188
        return tmp_name
×
189

190
    def _init_numpy_handlers(self):
4✔
191
        self.numpy_handlers = {
4✔
192
            "empty": self._handle_numpy_alloc,
193
            "empty_like": self._handle_numpy_empty_like,
194
            "zeros": self._handle_numpy_alloc,
195
            "zeros_like": self._handle_numpy_zeros_like,
196
            "ones": self._handle_numpy_alloc,
197
            "ndarray": self._handle_numpy_alloc,  # np.ndarray() constructor
198
            "eye": self._handle_numpy_eye,
199
            "add": self._handle_numpy_binary_op,
200
            "subtract": self._handle_numpy_binary_op,
201
            "multiply": self._handle_numpy_binary_op,
202
            "divide": self._handle_numpy_binary_op,
203
            "power": self._handle_numpy_binary_op,
204
            "exp": self._handle_numpy_unary_op,
205
            "abs": self._handle_numpy_unary_op,
206
            "absolute": self._handle_numpy_unary_op,
207
            "sqrt": self._handle_numpy_unary_op,
208
            "tanh": self._handle_numpy_unary_op,
209
            "sum": self._handle_numpy_reduce,
210
            "max": self._handle_numpy_reduce,
211
            "min": self._handle_numpy_reduce,
212
            "mean": self._handle_numpy_reduce,
213
            "std": self._handle_numpy_reduce,
214
            "matmul": self._handle_numpy_matmul,
215
            "dot": self._handle_numpy_matmul,
216
            "matvec": self._handle_numpy_matmul,
217
            "outer": self._handle_numpy_outer,
218
            "minimum": self._handle_numpy_binary_op,
219
            "maximum": self._handle_numpy_binary_op,
220
            "where": self._handle_numpy_where,
221
            "clip": self._handle_numpy_clip,
222
        }
223

224
    def generic_visit(self, node):
4✔
225
        return super().generic_visit(node)
×
226

227
    def visit_Constant(self, node):
4✔
228
        if isinstance(node.value, bool):
4✔
229
            return "true" if node.value else "false"
×
230
        return str(node.value)
4✔
231

232
    def visit_Name(self, node):
4✔
233
        name = node.id
4✔
234
        # Check if it's a global constant (not a local variable/array)
235
        if name not in self.symbol_table and self.globals_dict is not None:
4✔
236
            if name in self.globals_dict:
4✔
237
                val = self.globals_dict[name]
4✔
238
                # Only substitute simple numeric constants
239
                if isinstance(val, (int, float)):
4✔
240
                    return str(val)
4✔
241
        return name
4✔
242

243
    def _map_numpy_dtype(self, dtype_node):
4✔
244
        # Default to double
245
        if dtype_node is None:
4✔
246
            return Scalar(PrimitiveType.Double)
×
247

248
        if isinstance(dtype_node, ast.Name):
4✔
249
            if dtype_node.id == "float":
4✔
250
                return Scalar(PrimitiveType.Double)
4✔
251
            if dtype_node.id == "int":
4✔
252
                return Scalar(PrimitiveType.Int64)
4✔
253
            if dtype_node.id == "bool":
×
254
                return Scalar(PrimitiveType.Bool)
×
255

256
        if isinstance(dtype_node, ast.Attribute):
4✔
257
            # Handle array.dtype
258
            if (
4✔
259
                isinstance(dtype_node.value, ast.Name)
260
                and dtype_node.value.id in self.symbol_table
261
                and dtype_node.attr == "dtype"
262
            ):
263
                sym_type = self.symbol_table[dtype_node.value.id]
4✔
264
                if isinstance(sym_type, Pointer) and sym_type.has_pointee_type():
4✔
265
                    return sym_type.pointee_type
4✔
266

267
            if isinstance(dtype_node.value, ast.Name) and dtype_node.value.id in [
4✔
268
                "numpy",
269
                "np",
270
            ]:
271
                if dtype_node.attr == "float64":
4✔
272
                    return Scalar(PrimitiveType.Double)
4✔
273
                if dtype_node.attr == "float32":
4✔
274
                    return Scalar(PrimitiveType.Float)
4✔
275
                if dtype_node.attr == "int64":
4✔
276
                    return Scalar(PrimitiveType.Int64)
4✔
277
                if dtype_node.attr == "int32":
4✔
278
                    return Scalar(PrimitiveType.Int32)
4✔
279
                if dtype_node.attr == "bool_":
×
280
                    return Scalar(PrimitiveType.Bool)
×
281

282
        # Fallback
283
        return Scalar(PrimitiveType.Double)
×
284

285
    def _is_int(self, operand):
4✔
286
        try:
4✔
287
            if operand.lstrip("-").isdigit():
4✔
288
                return True
4✔
289
        except ValueError:
×
290
            pass
×
291

292
        name = operand
4✔
293
        if "(" in operand and operand.endswith(")"):
4✔
294
            name = operand.split("(")[0]
4✔
295

296
        if name in self.symbol_table:
4✔
297
            t = self.symbol_table[name]
4✔
298

299
            def is_int_ptype(pt):
4✔
300
                return pt in [
4✔
301
                    PrimitiveType.Int64,
302
                    PrimitiveType.Int32,
303
                    PrimitiveType.Int8,
304
                    PrimitiveType.Int16,
305
                    PrimitiveType.UInt64,
306
                    PrimitiveType.UInt32,
307
                    PrimitiveType.UInt8,
308
                    PrimitiveType.UInt16,
309
                ]
310

311
            if isinstance(t, Scalar):
4✔
312
                return is_int_ptype(t.primitive_type)
4✔
313

314
            if type(t).__name__ == "Array" and hasattr(t, "element_type"):
4✔
315
                et = t.element_type
×
316
                if callable(et):
×
317
                    et = et()
×
318
                if isinstance(et, Scalar):
×
319
                    return is_int_ptype(et.primitive_type)
×
320

321
            if type(t).__name__ == "Pointer":
4✔
322
                if hasattr(t, "pointee_type"):
4✔
323
                    et = t.pointee_type
4✔
324
                    if callable(et):
4✔
325
                        et = et()
×
326
                    if isinstance(et, Scalar):
4✔
327
                        return is_int_ptype(et.primitive_type)
4✔
328
                # Fallback: check if it has element_type (maybe alias?)
329
                if hasattr(t, "element_type"):
×
330
                    et = t.element_type
×
331
                    if callable(et):
×
332
                        et = et()
×
333
                    if isinstance(et, Scalar):
×
334
                        return is_int_ptype(et.primitive_type)
×
335

336
        return False
4✔
337

338
    def _add_read(self, block, expr_str, debug_info=None):
4✔
339
        # Try to reuse access node
340
        try:
4✔
341
            if (block, expr_str) in self._access_cache:
4✔
342
                return self._access_cache[(block, expr_str)]
4✔
343
        except TypeError:
×
344
            # block might not be hashable
345
            pass
×
346

347
        if debug_info is None:
4✔
348
            debug_info = DebugInfo()
4✔
349

350
        if "(" in expr_str and expr_str.endswith(")"):
4✔
351
            name = expr_str.split("(")[0]
4✔
352
            subset = expr_str[expr_str.find("(") + 1 : -1]
4✔
353
            access = self.builder.add_access(block, name, debug_info)
4✔
354
            try:
4✔
355
                self._access_cache[(block, expr_str)] = (access, subset)
4✔
356
            except TypeError:
×
357
                pass
×
358
            return access, subset
4✔
359

360
        if self.builder.exists(expr_str):
4✔
361
            access = self.builder.add_access(block, expr_str, debug_info)
4✔
362
            # For pointer types representing 0-D arrays, dereference with "0"
363
            subset = ""
4✔
364
            if expr_str in self.symbol_table:
4✔
365
                sym_type = self.symbol_table[expr_str]
4✔
366
                if isinstance(sym_type, Pointer):
4✔
367
                    # Check if it's a 0-D array (scalar wrapped in pointer)
368
                    if expr_str in self.array_info:
×
369
                        ndim = self.array_info[expr_str].get("ndim", 0)
×
370
                        if ndim == 0:
×
371
                            subset = "0"
×
372
                    else:
373
                        # Pointer without array_info is treated as 0-D
374
                        subset = "0"
×
375
            try:
4✔
376
                self._access_cache[(block, expr_str)] = (access, subset)
4✔
377
            except TypeError:
×
378
                pass
×
379
            return access, subset
4✔
380

381
        dtype = Scalar(PrimitiveType.Double)
4✔
382
        if self._is_int(expr_str):
4✔
383
            dtype = Scalar(PrimitiveType.Int64)
4✔
384
        elif expr_str == "true" or expr_str == "false":
4✔
385
            dtype = Scalar(PrimitiveType.Bool)
×
386

387
        const_node = self.builder.add_constant(block, expr_str, dtype, debug_info)
4✔
388
        try:
4✔
389
            self._access_cache[(block, expr_str)] = (const_node, "")
4✔
390
        except TypeError:
×
391
            pass
×
392
        return const_node, ""
4✔
393

394
    def _handle_min_max(self, node, func_name):
4✔
395
        args = [self.visit(arg) for arg in node.args]
4✔
396
        if len(args) != 2:
4✔
397
            raise NotImplementedError(f"{func_name} only supported with 2 arguments")
×
398

399
        # Check types
400
        is_float = False
4✔
401
        arg_types = []
4✔
402

403
        for arg in args:
4✔
404
            name = arg
4✔
405
            if "(" in arg and arg.endswith(")"):
4✔
406
                name = arg.split("(")[0]
×
407

408
            if name in self.symbol_table:
4✔
409
                t = self.symbol_table[name]
4✔
410
                if isinstance(t, Pointer):
4✔
411
                    t = t.base_type
×
412

413
                if t.primitive_type == PrimitiveType.Double:
4✔
414
                    is_float = True
4✔
415
                    arg_types.append(PrimitiveType.Double)
4✔
416
                else:
417
                    arg_types.append(PrimitiveType.Int64)
4✔
418
            elif self._is_int(arg):
×
419
                arg_types.append(PrimitiveType.Int64)
×
420
            else:
421
                # Assume float constant
422
                is_float = True
×
423
                arg_types.append(PrimitiveType.Double)
×
424

425
        dtype = Scalar(PrimitiveType.Double if is_float else PrimitiveType.Int64)
4✔
426

427
        tmp_name = self._get_temp_name("_tmp_")
4✔
428
        self.builder.add_container(tmp_name, dtype, False)
4✔
429
        self.symbol_table[tmp_name] = dtype
4✔
430

431
        if is_float:
4✔
432
            # Cast args if necessary
433
            casted_args = []
4✔
434
            for i, arg in enumerate(args):
4✔
435
                if arg_types[i] != PrimitiveType.Double:
4✔
436
                    # Create temp double
437
                    tmp_cast = self._get_temp_name("_cast_")
4✔
438
                    self.builder.add_container(
4✔
439
                        tmp_cast, Scalar(PrimitiveType.Double), False
440
                    )
441
                    self.symbol_table[tmp_cast] = Scalar(PrimitiveType.Double)
4✔
442

443
                    # Assign int to double (implicit cast)
444
                    self.builder.add_assignment(tmp_cast, arg)
4✔
445
                    casted_args.append(tmp_cast)
4✔
446
                else:
447
                    casted_args.append(arg)
4✔
448

449
            block = self.builder.add_block()
4✔
450
            t_out = self.builder.add_access(block, tmp_name)
4✔
451

452
            intrinsic_name = (
4✔
453
                CMathFunction.fmax if func_name == "max" else CMathFunction.fmin
454
            )
455
            t_task = self.builder.add_cmath(block, intrinsic_name)
4✔
456

457
            for i, arg in enumerate(casted_args):
4✔
458
                t_arg, arg_sub = self._add_read(block, arg)
4✔
459
                self.builder.add_memlet(
4✔
460
                    block, t_arg, "void", t_task, f"_in{i+1}", arg_sub
461
                )
462
        else:
463
            block = self.builder.add_block()
4✔
464
            t_out = self.builder.add_access(block, tmp_name)
4✔
465

466
            # Use int_smax/int_smin tasklet
467
            opcode = None
4✔
468
            if func_name == "max":
4✔
469
                opcode = TaskletCode.int_smax
4✔
470
            else:
471
                opcode = TaskletCode.int_smin
4✔
472
            t_task = self.builder.add_tasklet(block, opcode, ["_in1", "_in2"], ["_out"])
4✔
473

474
            for i, arg in enumerate(args):
4✔
475
                t_arg, arg_sub = self._add_read(block, arg)
4✔
476
                self.builder.add_memlet(
4✔
477
                    block, t_arg, "void", t_task, f"_in{i+1}", arg_sub
478
                )
479

480
        self.builder.add_memlet(block, t_task, "_out", t_out, "void", "")
4✔
481
        return tmp_name
4✔
482

483
    def _handle_python_cast(self, node, func_name):
4✔
484
        """Handle Python type casts: int(), float(), bool()"""
485
        if len(node.args) != 1:
4✔
486
            raise NotImplementedError(f"{func_name}() cast requires exactly 1 argument")
×
487

488
        arg = self.visit(node.args[0])
4✔
489

490
        # Determine target type based on cast function
491
        if func_name == "int":
4✔
492
            target_dtype = Scalar(PrimitiveType.Int64)
4✔
493
        elif func_name == "float":
4✔
494
            target_dtype = Scalar(PrimitiveType.Double)
4✔
495
        elif func_name == "bool":
4✔
496
            target_dtype = Scalar(PrimitiveType.Bool)
4✔
497
        else:
498
            raise NotImplementedError(f"Cast to {func_name} not supported")
×
499

500
        # Determine source type
501
        source_dtype = None
4✔
502
        name = arg
4✔
503
        if "(" in arg and arg.endswith(")"):
4✔
504
            name = arg.split("(")[0]
×
505

506
        if name in self.symbol_table:
4✔
507
            source_dtype = self.symbol_table[name]
4✔
508
            if isinstance(source_dtype, Pointer):
4✔
509
                source_dtype = source_dtype.base_type
×
510
        elif self._is_int(arg):
×
511
            source_dtype = Scalar(PrimitiveType.Int64)
×
512
        elif arg == "true" or arg == "false":
×
513
            source_dtype = Scalar(PrimitiveType.Bool)
×
514
        else:
515
            # Assume float constant
516
            source_dtype = Scalar(PrimitiveType.Double)
×
517

518
        # Create temporary variable for result
519
        tmp_name = self._get_temp_name("_tmp_")
4✔
520
        self.builder.add_container(tmp_name, target_dtype, False)
4✔
521
        self.symbol_table[tmp_name] = target_dtype
4✔
522

523
        # Use tasklet assign opcode for casting (as specified in problem statement)
524
        block = self.builder.add_block()
4✔
525
        t_src, src_sub = self._add_read(block, arg)
4✔
526
        t_dst = self.builder.add_access(block, tmp_name)
4✔
527
        t_task = self.builder.add_tasklet(block, TaskletCode.assign, ["_in"], ["_out"])
4✔
528
        self.builder.add_memlet(block, t_src, "void", t_task, "_in", src_sub)
4✔
529
        self.builder.add_memlet(block, t_task, "_out", t_dst, "void", "")
4✔
530

531
        return tmp_name
4✔
532

533
    def visit_Call(self, node):
4✔
534
        func_name = ""
4✔
535
        module_name = ""
4✔
536
        if isinstance(node.func, ast.Attribute):
4✔
537
            if isinstance(node.func.value, ast.Name):
4✔
538
                if node.func.value.id == "math":
4✔
539
                    module_name = "math"
4✔
540
                    func_name = node.func.attr
4✔
541
                elif node.func.value.id in ["numpy", "np"]:
4✔
542
                    module_name = "numpy"
4✔
543
                    func_name = node.func.attr
4✔
544
                else:
545
                    # Check if it's a method call on an array (e.g., arr.astype(...), arr.copy())
546
                    array_name = node.func.value.id
4✔
547
                    method_name = node.func.attr
4✔
548
                    if array_name in self.array_info and method_name == "astype":
4✔
549
                        return self._handle_numpy_astype(node, array_name)
4✔
550
                    elif array_name in self.array_info and method_name == "copy":
4✔
551
                        return self._handle_numpy_copy(node, array_name)
4✔
552
            elif isinstance(node.func.value, ast.Attribute):
4✔
553
                if (
4✔
554
                    isinstance(node.func.value.value, ast.Name)
555
                    and node.func.value.value.id == "scipy"
556
                    and node.func.value.attr == "special"
557
                ):
558
                    if node.func.attr == "softmax":
4✔
559
                        return self._handle_scipy_softmax(node, "softmax")
4✔
560
                # Handle np.add.outer, np.subtract.outer, np.multiply.outer, etc.
561
                elif (
4✔
562
                    isinstance(node.func.value.value, ast.Name)
563
                    and node.func.value.value.id in ["numpy", "np"]
564
                    and node.func.attr == "outer"
565
                ):
566
                    ufunc_name = node.func.value.attr  # "add", "subtract", etc.
4✔
567
                    return self._handle_ufunc_outer(node, ufunc_name)
4✔
568

569
        elif isinstance(node.func, ast.Name):
4✔
570
            func_name = node.func.id
4✔
571

572
        if module_name == "numpy":
4✔
573
            if func_name in self.numpy_handlers:
4✔
574
                return self.numpy_handlers[func_name](node, func_name)
4✔
575

576
        if func_name in ["max", "min"]:
4✔
577
            return self._handle_min_max(node, func_name)
4✔
578

579
        # Handle Python type casts (int, float, bool)
580
        if func_name in ["int", "float", "bool"]:
4✔
581
            return self._handle_python_cast(node, func_name)
4✔
582

583
        math_funcs = {
4✔
584
            # Trigonometric functions
585
            "sin": CMathFunction.sin,
586
            "cos": CMathFunction.cos,
587
            "tan": CMathFunction.tan,
588
            "asin": CMathFunction.asin,
589
            "acos": CMathFunction.acos,
590
            "atan": CMathFunction.atan,
591
            "atan2": CMathFunction.atan2,
592
            # Hyperbolic functions
593
            "sinh": CMathFunction.sinh,
594
            "cosh": CMathFunction.cosh,
595
            "tanh": CMathFunction.tanh,
596
            "asinh": CMathFunction.asinh,
597
            "acosh": CMathFunction.acosh,
598
            "atanh": CMathFunction.atanh,
599
            # Exponential and logarithmic functions
600
            "exp": CMathFunction.exp,
601
            "exp2": CMathFunction.exp2,
602
            "expm1": CMathFunction.expm1,
603
            "log": CMathFunction.log,
604
            "log2": CMathFunction.log2,
605
            "log10": CMathFunction.log10,
606
            "log1p": CMathFunction.log1p,
607
            # Power functions
608
            "pow": CMathFunction.pow,
609
            "sqrt": CMathFunction.sqrt,
610
            "cbrt": CMathFunction.cbrt,
611
            "hypot": CMathFunction.hypot,
612
            # Rounding and remainder functions
613
            "abs": CMathFunction.fabs,
614
            "fabs": CMathFunction.fabs,
615
            "ceil": CMathFunction.ceil,
616
            "floor": CMathFunction.floor,
617
            "trunc": CMathFunction.trunc,
618
            "fmod": CMathFunction.fmod,
619
            "remainder": CMathFunction.remainder,
620
            # Floating-point manipulation functions
621
            "copysign": CMathFunction.copysign,
622
            # Other functions
623
            "fma": CMathFunction.fma,
624
        }
625

626
        if func_name in math_funcs:
4✔
627
            args = [self.visit(arg) for arg in node.args]
4✔
628

629
            tmp_name = self._get_temp_name("_tmp_")
4✔
630
            dtype = Scalar(PrimitiveType.Double)
4✔
631
            self.builder.add_container(tmp_name, dtype, False)
4✔
632
            self.symbol_table[tmp_name] = dtype
4✔
633

634
            block = self.builder.add_block()
4✔
635
            t_out = self.builder.add_access(block, tmp_name)
4✔
636

637
            t_task = self.builder.add_cmath(block, math_funcs[func_name])
4✔
638

639
            for i, arg in enumerate(args):
4✔
640
                t_arg, arg_sub = self._add_read(block, arg)
4✔
641
                self.builder.add_memlet(
4✔
642
                    block, t_arg, "void", t_task, f"_in{i+1}", arg_sub
643
                )
644

645
            self.builder.add_memlet(block, t_task, "_out", t_out, "void", "")
4✔
646
            return tmp_name
4✔
647

648
        if func_name in self.globals_dict:
4✔
649
            obj = self.globals_dict[func_name]
4✔
650
            if inspect.isfunction(obj):
4✔
651
                return self._handle_inline_call(node, obj)
4✔
652

653
        raise NotImplementedError(f"Function call {func_name} not supported")
×
654

655
    def _handle_inline_call(self, node, func_obj):
4✔
656
        # 1. Parse function source
657
        try:
4✔
658
            source_lines, start_line = inspect.getsourcelines(func_obj)
4✔
659
            source = textwrap.dedent("".join(source_lines))
4✔
660
            tree = ast.parse(source)
4✔
661
            func_def = tree.body[0]
4✔
662
        except Exception as e:
×
663
            raise NotImplementedError(
×
664
                f"Could not parse function {func_obj.__name__}: {e}"
665
            )
666

667
        # 2. Evaluate arguments
668
        arg_vars = [self.visit(arg) for arg in node.args]
4✔
669

670
        if len(arg_vars) != len(func_def.args.args):
4✔
671
            raise NotImplementedError(
×
672
                f"Argument count mismatch for {func_obj.__name__}"
673
            )
674

675
        # 3. Generate unique suffix
676
        suffix = f"_{func_obj.__name__}_{self._get_unique_id()}"
4✔
677
        res_name = f"_res{suffix}"
4✔
678

679
        # Assume Int64 for now as match returns 0/1
680
        dtype = Scalar(PrimitiveType.Int64)
4✔
681
        self.builder.add_container(res_name, dtype, False)
4✔
682
        self.symbol_table[res_name] = dtype
4✔
683

684
        # 4. Rename variables
685
        class VariableRenamer(ast.NodeTransformer):
4✔
686
            # Builtins that should not be renamed
687
            BUILTINS = {
4✔
688
                "range",
689
                "len",
690
                "int",
691
                "float",
692
                "bool",
693
                "str",
694
                "list",
695
                "dict",
696
                "tuple",
697
                "set",
698
                "print",
699
                "abs",
700
                "min",
701
                "max",
702
                "sum",
703
                "enumerate",
704
                "zip",
705
                "map",
706
                "filter",
707
                "sorted",
708
                "reversed",
709
                "True",
710
                "False",
711
                "None",
712
            }
713

714
            def __init__(self, suffix, globals_dict):
4✔
715
                self.suffix = suffix
4✔
716
                self.globals_dict = globals_dict
4✔
717

718
            def visit_Name(self, node):
4✔
719
                # Don't rename builtins or globals
720
                if node.id in self.globals_dict or node.id in self.BUILTINS:
4✔
721
                    return node
4✔
722
                return ast.Name(id=f"{node.id}{self.suffix}", ctx=node.ctx)
4✔
723

724
            def visit_Return(self, node):
4✔
725
                if node.value:
4✔
726
                    val = self.visit(node.value)
4✔
727
                    return ast.Assign(
4✔
728
                        targets=[ast.Name(id=res_name, ctx=ast.Store())],
729
                        value=val,
730
                    )
731
                return node
×
732

733
        renamer = VariableRenamer(suffix, self.globals_dict)
4✔
734
        new_body = [renamer.visit(stmt) for stmt in func_def.body]
4✔
735

736
        # 5. Assign arguments to parameters
737
        param_assignments = []
4✔
738
        for arg_def, arg_val in zip(func_def.args.args, arg_vars):
4✔
739
            param_name = f"{arg_def.arg}{suffix}"
4✔
740

741
            # Infer type and create container
742
            if arg_val in self.symbol_table:
4✔
743
                self.symbol_table[param_name] = self.symbol_table[arg_val]
4✔
744
                self.builder.add_container(
4✔
745
                    param_name, self.symbol_table[arg_val], False
746
                )
747
                val_node = ast.Name(id=arg_val, ctx=ast.Load())
4✔
748
            elif self._is_int(arg_val):
×
749
                self.symbol_table[param_name] = Scalar(PrimitiveType.Int64)
×
750
                self.builder.add_container(
×
751
                    param_name, Scalar(PrimitiveType.Int64), False
752
                )
753
                val_node = ast.Constant(value=int(arg_val))
×
754
            else:
755
                # Assume float constant
756
                try:
×
757
                    val = float(arg_val)
×
758
                    self.symbol_table[param_name] = Scalar(PrimitiveType.Double)
×
759
                    self.builder.add_container(
×
760
                        param_name, Scalar(PrimitiveType.Double), False
761
                    )
762
                    val_node = ast.Constant(value=val)
×
763
                except ValueError:
×
764
                    # Fallback to Name, might fail later if not in symbol table
765
                    val_node = ast.Name(id=arg_val, ctx=ast.Load())
×
766

767
            assign = ast.Assign(
4✔
768
                targets=[ast.Name(id=param_name, ctx=ast.Store())], value=val_node
769
            )
770
            param_assignments.append(assign)
4✔
771

772
        final_body = param_assignments + new_body
4✔
773

774
        # 6. Visit new body using ASTParser
775
        from .ast_parser import ASTParser
4✔
776

777
        parser = ASTParser(
4✔
778
            self.builder,
779
            self.array_info,
780
            self.symbol_table,
781
            globals_dict=self.globals_dict,
782
            unique_counter_ref=self._unique_counter_ref,
783
        )
784

785
        for stmt in final_body:
4✔
786
            parser.visit(stmt)
4✔
787

788
        return res_name
4✔
789

790
    def visit_BinOp(self, node):
4✔
791
        if isinstance(node.op, ast.MatMult):
4✔
792
            return self._handle_numpy_matmul_op(node.left, node.right)
4✔
793

794
        left = self.visit(node.left)
4✔
795
        op = self.visit(node.op)
4✔
796
        right = self.visit(node.right)
4✔
797

798
        # Check if left or right are arrays
799
        left_is_array = left in self.array_info
4✔
800
        right_is_array = right in self.array_info
4✔
801

802
        if left_is_array or right_is_array:
4✔
803
            op_map = {"+": "add", "-": "sub", "*": "mul", "/": "div", "**": "pow"}
4✔
804
            if op in op_map:
4✔
805
                return self._handle_array_binary_op(op_map[op], left, right)
4✔
806
            else:
807
                raise NotImplementedError(f"Array operation {op} not supported")
×
808

809
        tmp_name = f"_tmp_{self._get_unique_id()}"
4✔
810

811
        dtype = Scalar(PrimitiveType.Double)  # Default
4✔
812

813
        left_is_int = self._is_int(left)
4✔
814
        right_is_int = self._is_int(right)
4✔
815

816
        if left_is_int and right_is_int and op not in ["/", "**"]:
4✔
817
            dtype = Scalar(PrimitiveType.Int64)
4✔
818

819
        self.builder.add_container(tmp_name, dtype, False)
4✔
820
        self.symbol_table[tmp_name] = dtype
4✔
821

822
        real_left = left
4✔
823
        real_right = right
4✔
824

825
        if dtype.primitive_type == PrimitiveType.Double:
4✔
826
            if left_is_int:
4✔
827
                left_cast = f"_tmp_{self._get_unique_id()}"
4✔
828
                self.builder.add_container(
4✔
829
                    left_cast, Scalar(PrimitiveType.Double), False
830
                )
831
                self.symbol_table[left_cast] = Scalar(PrimitiveType.Double)
4✔
832

833
                c_block = self.builder.add_block()
4✔
834
                t_src, src_sub = self._add_read(c_block, left)
4✔
835
                t_dst = self.builder.add_access(c_block, left_cast)
4✔
836
                t_task = self.builder.add_tasklet(
4✔
837
                    c_block, TaskletCode.assign, ["_in"], ["_out"]
838
                )
839
                self.builder.add_memlet(c_block, t_src, "void", t_task, "_in", src_sub)
4✔
840
                self.builder.add_memlet(c_block, t_task, "_out", t_dst, "void", "")
4✔
841

842
                real_left = left_cast
4✔
843

844
            if right_is_int:
4✔
845
                right_cast = f"_tmp_{self._get_unique_id()}"
4✔
846
                self.builder.add_container(
4✔
847
                    right_cast, Scalar(PrimitiveType.Double), False
848
                )
849
                self.symbol_table[right_cast] = Scalar(PrimitiveType.Double)
4✔
850

851
                c_block = self.builder.add_block()
4✔
852
                t_src, src_sub = self._add_read(c_block, right)
4✔
853
                t_dst = self.builder.add_access(c_block, right_cast)
4✔
854
                t_task = self.builder.add_tasklet(
4✔
855
                    c_block, TaskletCode.assign, ["_in"], ["_out"]
856
                )
857
                self.builder.add_memlet(c_block, t_src, "void", t_task, "_in", src_sub)
4✔
858
                self.builder.add_memlet(c_block, t_task, "_out", t_dst, "void", "")
4✔
859

860
                real_right = right_cast
4✔
861

862
        # Special cases
863
        if op == "**":
4✔
864
            block = self.builder.add_block()
4✔
865
            t_left, left_sub = self._add_read(block, real_left)
4✔
866
            t_right, right_sub = self._add_read(block, real_right)
4✔
867
            t_out = self.builder.add_access(block, tmp_name)
4✔
868

869
            t_task = self.builder.add_cmath(block, CMathFunction.pow)
4✔
870
            self.builder.add_memlet(block, t_left, "void", t_task, "_in1", left_sub)
4✔
871
            self.builder.add_memlet(block, t_right, "void", t_task, "_in2", right_sub)
4✔
872
            self.builder.add_memlet(block, t_task, "_out", t_out, "void", "")
4✔
873

874
            return tmp_name
4✔
875
        elif op == "%":
4✔
876
            block = self.builder.add_block()
4✔
877
            t_left, left_sub = self._add_read(block, real_left)
4✔
878
            t_right, right_sub = self._add_read(block, real_right)
4✔
879
            t_out = self.builder.add_access(block, tmp_name)
4✔
880

881
            if dtype.primitive_type == PrimitiveType.Int64:
4✔
882
                # Implement ((a % b) + b) % b to match Python's modulo behavior
883

884
                # 1. rem1 = a % b
885
                t_rem1 = self.builder.add_tasklet(
4✔
886
                    block, TaskletCode.int_srem, ["_in1", "_in2"], ["_out"]
887
                )
888
                self.builder.add_memlet(block, t_left, "void", t_rem1, "_in1", left_sub)
4✔
889
                self.builder.add_memlet(
4✔
890
                    block, t_right, "void", t_rem1, "_in2", right_sub
891
                )
892

893
                rem1_name = f"_tmp_{self._get_unique_id()}"
4✔
894
                self.builder.add_container(rem1_name, dtype, False)
4✔
895
                t_rem1_out = self.builder.add_access(block, rem1_name)
4✔
896
                self.builder.add_memlet(block, t_rem1, "_out", t_rem1_out, "void", "")
4✔
897

898
                # 2. add = rem1 + b
899
                t_add = self.builder.add_tasklet(
4✔
900
                    block, TaskletCode.int_add, ["_in1", "_in2"], ["_out"]
901
                )
902
                self.builder.add_memlet(block, t_rem1_out, "void", t_add, "_in1", "")
4✔
903
                self.builder.add_memlet(
4✔
904
                    block, t_right, "void", t_add, "_in2", right_sub
905
                )
906

907
                add_name = f"_tmp_{self._get_unique_id()}"
4✔
908
                self.builder.add_container(add_name, dtype, False)
4✔
909
                t_add_out = self.builder.add_access(block, add_name)
4✔
910
                self.builder.add_memlet(block, t_add, "_out", t_add_out, "void", "")
4✔
911

912
                # 3. res = add % b
913
                t_rem2 = self.builder.add_tasklet(
4✔
914
                    block, TaskletCode.int_srem, ["_in1", "_in2"], ["_out"]
915
                )
916
                self.builder.add_memlet(block, t_add_out, "void", t_rem2, "_in1", "")
4✔
917
                self.builder.add_memlet(
4✔
918
                    block, t_right, "void", t_rem2, "_in2", right_sub
919
                )
920
                self.builder.add_memlet(block, t_rem2, "_out", t_out, "void", "")
4✔
921

922
                return tmp_name
4✔
923
            else:
924
                # Python's floored modulo: a % b = a - floor(a / b) * b
925
                # This differs from fmod which uses trunc instead of floor
926
                # Implement as: fmod(fmod(a, b) + b, b) to handle negative values
927

928
                # 1. rem1 = fmod(a, b)
929
                t_rem1 = self.builder.add_tasklet(
4✔
930
                    block, TaskletCode.fp_rem, ["_in1", "_in2"], ["_out"]
931
                )
932
                self.builder.add_memlet(block, t_left, "void", t_rem1, "_in1", left_sub)
4✔
933
                self.builder.add_memlet(
4✔
934
                    block, t_right, "void", t_rem1, "_in2", right_sub
935
                )
936

937
                rem1_name = f"_tmp_{self._get_unique_id()}"
4✔
938
                self.builder.add_container(rem1_name, dtype, False)
4✔
939
                t_rem1_out = self.builder.add_access(block, rem1_name)
4✔
940
                self.builder.add_memlet(block, t_rem1, "_out", t_rem1_out, "void", "")
4✔
941

942
                # 2. add = rem1 + b
943
                t_add = self.builder.add_tasklet(
4✔
944
                    block, TaskletCode.fp_add, ["_in1", "_in2"], ["_out"]
945
                )
946
                self.builder.add_memlet(block, t_rem1_out, "void", t_add, "_in1", "")
4✔
947
                self.builder.add_memlet(
4✔
948
                    block, t_right, "void", t_add, "_in2", right_sub
949
                )
950

951
                add_name = f"_tmp_{self._get_unique_id()}"
4✔
952
                self.builder.add_container(add_name, dtype, False)
4✔
953
                t_add_out = self.builder.add_access(block, add_name)
4✔
954
                self.builder.add_memlet(block, t_add, "_out", t_add_out, "void", "")
4✔
955

956
                # 3. res = fmod(add, b)
957
                t_rem2 = self.builder.add_tasklet(
4✔
958
                    block, TaskletCode.fp_rem, ["_in1", "_in2"], ["_out"]
959
                )
960
                self.builder.add_memlet(block, t_add_out, "void", t_rem2, "_in1", "")
4✔
961
                self.builder.add_memlet(
4✔
962
                    block, t_right, "void", t_rem2, "_in2", right_sub
963
                )
964
                self.builder.add_memlet(block, t_rem2, "_out", t_out, "void", "")
4✔
965

966
                return tmp_name
4✔
967

968
        tasklet_code = None
4✔
969
        if dtype.primitive_type == PrimitiveType.Int64:
4✔
970
            if op == "+":
4✔
971
                tasklet_code = TaskletCode.int_add
4✔
972
            elif op == "-":
4✔
973
                tasklet_code = TaskletCode.int_sub
4✔
974
            elif op == "*":
4✔
975
                tasklet_code = TaskletCode.int_mul
4✔
976
            elif op == "/":
4✔
977
                tasklet_code = TaskletCode.int_sdiv
×
978
            elif op == "//":
4✔
979
                tasklet_code = TaskletCode.int_sdiv
4✔
980
            elif op == "|":
4✔
981
                tasklet_code = TaskletCode.int_or
4✔
982
            elif op == "^":
4✔
983
                tasklet_code = TaskletCode.int_xor
4✔
984
        else:
985
            if op == "+":
4✔
986
                tasklet_code = TaskletCode.fp_add
4✔
987
            elif op == "-":
4✔
988
                tasklet_code = TaskletCode.fp_sub
4✔
989
            elif op == "*":
4✔
990
                tasklet_code = TaskletCode.fp_mul
4✔
991
            elif op == "/":
4✔
992
                tasklet_code = TaskletCode.fp_div
4✔
993
            elif op == "//":
×
994
                tasklet_code = TaskletCode.fp_div
×
995
            else:
996
                raise NotImplementedError(f"Operation {op} not supported for floats")
×
997

998
        block = self.builder.add_block()
4✔
999
        t_left, left_sub = self._add_read(block, real_left)
4✔
1000
        t_right, right_sub = self._add_read(block, real_right)
4✔
1001
        t_out = self.builder.add_access(block, tmp_name)
4✔
1002

1003
        t_task = self.builder.add_tasklet(
4✔
1004
            block, tasklet_code, ["_in1", "_in2"], ["_out"]
1005
        )
1006

1007
        self.builder.add_memlet(block, t_left, "void", t_task, "_in1", left_sub)
4✔
1008
        self.builder.add_memlet(block, t_right, "void", t_task, "_in2", right_sub)
4✔
1009
        self.builder.add_memlet(block, t_task, "_out", t_out, "void", "")
4✔
1010

1011
        return tmp_name
4✔
1012

1013
    def _add_assign_constant(self, target_name, value_str, dtype):
4✔
1014
        block = self.builder.add_block()
4✔
1015
        t_const = self.builder.add_constant(block, value_str, dtype)
4✔
1016
        t_dst = self.builder.add_access(block, target_name)
4✔
1017
        t_task = self.builder.add_tasklet(block, TaskletCode.assign, ["_in"], ["_out"])
4✔
1018
        self.builder.add_memlet(block, t_const, "void", t_task, "_in", "")
4✔
1019
        self.builder.add_memlet(block, t_task, "_out", t_dst, "void", "")
4✔
1020

1021
    def visit_BoolOp(self, node):
4✔
1022
        op = self.visit(node.op)
4✔
1023
        values = [f"({self.visit(v)} != 0)" for v in node.values]
4✔
1024
        expr_str = f"{f' {op} '.join(values)}"
4✔
1025

1026
        tmp_name = f"_tmp_{self._get_unique_id()}"
4✔
1027
        dtype = Scalar(PrimitiveType.Bool)
4✔
1028
        self.builder.add_container(tmp_name, dtype, False)
4✔
1029

1030
        # Use control flow to assign boolean value
1031
        self.builder.begin_if(expr_str)
4✔
1032
        self._add_assign_constant(tmp_name, "true", dtype)
4✔
1033
        self.builder.begin_else()
4✔
1034
        self._add_assign_constant(tmp_name, "false", dtype)
4✔
1035
        self.builder.end_if()
4✔
1036

1037
        self.symbol_table[tmp_name] = dtype
4✔
1038
        return tmp_name
4✔
1039

1040
    def visit_Compare(self, node):
4✔
1041
        left = self.visit(node.left)
4✔
1042
        if len(node.ops) > 1:
4✔
1043
            raise NotImplementedError("Chained comparisons not supported yet")
×
1044

1045
        op = self.visit(node.ops[0])
4✔
1046
        right = self.visit(node.comparators[0])
4✔
1047

1048
        # Check if this is an array comparison
1049
        left_is_array = left in self.array_info
4✔
1050
        right_is_array = right in self.array_info
4✔
1051

1052
        if left_is_array or right_is_array:
4✔
1053
            # Handle array comparison - return boolean array
1054
            return self._handle_array_compare(
4✔
1055
                left, op, right, left_is_array, right_is_array
1056
            )
1057

1058
        # Scalar comparison
1059
        expr_str = f"{left} {op} {right}"
4✔
1060

1061
        tmp_name = f"_tmp_{self._get_unique_id()}"
4✔
1062
        dtype = Scalar(PrimitiveType.Bool)
4✔
1063
        self.builder.add_container(tmp_name, dtype, False)
4✔
1064

1065
        # Use control flow to assign boolean value
1066
        self.builder.begin_if(expr_str)
4✔
1067
        self.builder.add_transition(tmp_name, "true")
4✔
1068
        self.builder.begin_else()
4✔
1069
        self.builder.add_transition(tmp_name, "false")
4✔
1070
        self.builder.end_if()
4✔
1071

1072
        self.symbol_table[tmp_name] = dtype
4✔
1073
        return tmp_name
4✔
1074

1075
    def visit_UnaryOp(self, node):
4✔
1076
        if (
4✔
1077
            isinstance(node.op, ast.USub)
1078
            and isinstance(node.operand, ast.Constant)
1079
            and isinstance(node.operand.value, (int, float))
1080
        ):
1081
            return f"-{node.operand.value}"
4✔
1082

1083
        op = self.visit(node.op)
4✔
1084
        operand = self.visit(node.operand)
4✔
1085

1086
        # Check if operand is an array - handle as array operation
1087
        if operand in self.array_info and op == "-":
4✔
1088
            return self._handle_array_negate(operand)
4✔
1089

1090
        tmp_name = f"_tmp_{self._get_unique_id()}"
4✔
1091
        dtype = Scalar(PrimitiveType.Double)
4✔
1092
        if operand in self.symbol_table:
4✔
1093
            dtype = self.symbol_table[operand]
4✔
1094
            # If it's a pointer (array), get the element type
1095
            if isinstance(dtype, Pointer) and dtype.has_pointee_type():
4✔
1096
                dtype = dtype.pointee_type
×
1097
        elif self._is_int(operand):
×
1098
            dtype = Scalar(PrimitiveType.Int64)
×
1099
        elif isinstance(node.op, ast.Not):
×
1100
            dtype = Scalar(PrimitiveType.Bool)
×
1101

1102
        self.builder.add_container(tmp_name, dtype, False)
4✔
1103
        self.symbol_table[tmp_name] = dtype
4✔
1104

1105
        block = self.builder.add_block()
4✔
1106
        t_src, src_sub = self._add_read(block, operand)
4✔
1107
        t_dst = self.builder.add_access(block, tmp_name)
4✔
1108

1109
        if isinstance(node.op, ast.Not):
4✔
1110
            t_const = self.builder.add_constant(
4✔
1111
                block, "true", Scalar(PrimitiveType.Bool)
1112
            )
1113
            t_task = self.builder.add_tasklet(
4✔
1114
                block, TaskletCode.int_xor, ["_in1", "_in2"], ["_out"]
1115
            )
1116
            self.builder.add_memlet(block, t_src, "void", t_task, "_in1", src_sub)
4✔
1117
            self.builder.add_memlet(block, t_const, "void", t_task, "_in2", "")
4✔
1118
            self.builder.add_memlet(block, t_task, "_out", t_dst, "void", "")
4✔
1119

1120
        elif op == "-":
4✔
1121
            if dtype.primitive_type == PrimitiveType.Int64:
4✔
1122
                t_const = self.builder.add_constant(block, "0", dtype)
4✔
1123
                t_task = self.builder.add_tasklet(
4✔
1124
                    block, TaskletCode.int_sub, ["_in1", "_in2"], ["_out"]
1125
                )
1126
                self.builder.add_memlet(block, t_const, "void", t_task, "_in1", "")
4✔
1127
                self.builder.add_memlet(block, t_src, "void", t_task, "_in2", src_sub)
4✔
1128
                self.builder.add_memlet(block, t_task, "_out", t_dst, "void", "")
4✔
1129
            else:
1130
                t_task = self.builder.add_tasklet(
4✔
1131
                    block, TaskletCode.fp_neg, ["_in"], ["_out"]
1132
                )
1133
                self.builder.add_memlet(block, t_src, "void", t_task, "_in", src_sub)
4✔
1134
                self.builder.add_memlet(block, t_task, "_out", t_dst, "void", "")
4✔
1135
        else:
1136
            t_task = self.builder.add_tasklet(
×
1137
                block, TaskletCode.assign, ["_in"], ["_out"]
1138
            )
1139
            self.builder.add_memlet(block, t_src, "void", t_task, "_in", src_sub)
×
1140
            self.builder.add_memlet(block, t_task, "_out", t_dst, "void", "")
×
1141

1142
        return tmp_name
4✔
1143

1144
    def _handle_array_negate(self, operand):
4✔
1145
        """Handle negation of an array operand (-arr)."""
1146
        shape = self.array_info[operand]["shapes"]
4✔
1147
        dtype = self._get_dtype(operand)
4✔
1148

1149
        # Create output array
1150
        tmp_name = self._create_array_temp(shape, dtype)
4✔
1151

1152
        # Use elementwise binary op: 0 - arr
1153
        # First create a zero constant
1154
        zero_name = f"_tmp_{self._get_unique_id()}"
4✔
1155
        self.builder.add_container(zero_name, dtype, False)
4✔
1156
        self.symbol_table[zero_name] = dtype
4✔
1157

1158
        zero_block = self.builder.add_block()
4✔
1159
        t_const = self.builder.add_constant(
4✔
1160
            zero_block,
1161
            "0.0" if dtype.primitive_type == PrimitiveType.Double else "0",
1162
            dtype,
1163
        )
1164
        t_zero = self.builder.add_access(zero_block, zero_name)
4✔
1165
        t_assign = self.builder.add_tasklet(
4✔
1166
            zero_block, TaskletCode.assign, ["_in"], ["_out"]
1167
        )
1168
        self.builder.add_memlet(zero_block, t_const, "void", t_assign, "_in", "")
4✔
1169
        self.builder.add_memlet(zero_block, t_assign, "_out", t_zero, "void", "")
4✔
1170

1171
        # Now subtract: tmp = 0 - operand (broadcast scalar subtraction)
1172
        self.builder.add_elementwise_op("sub", zero_name, operand, tmp_name, shape)
4✔
1173

1174
        return tmp_name
4✔
1175

1176
    def _handle_array_compare(self, left, op, right, left_is_array, right_is_array):
4✔
1177
        """Handle elementwise comparison of arrays, returning a boolean array.
1178

1179
        Supports: arr > 0, arr < scalar, arr1 > arr2, etc.
1180
        """
1181
        # Determine shape from the array operand
1182
        if left_is_array:
4✔
1183
            shape = self.array_info[left]["shapes"]
4✔
1184
            arr_name = left
4✔
1185
        else:
1186
            shape = self.array_info[right]["shapes"]
×
1187
            arr_name = right
×
1188

1189
        # Determine if we need integer or floating point comparison
1190
        # based on the array element type
1191
        use_int_cmp = False
4✔
1192
        arr_dtype = self._get_dtype(arr_name)
4✔
1193
        if arr_dtype.primitive_type in (PrimitiveType.Int32, PrimitiveType.Int64):
4✔
1194
            use_int_cmp = True
×
1195

1196
        # Create output boolean array
1197
        dtype = Scalar(PrimitiveType.Bool)
4✔
1198
        tmp_name = self._create_array_temp(shape, dtype)
4✔
1199

1200
        # Map comparison operators to tasklet codes
1201
        if use_int_cmp:
4✔
1202
            cmp_ops = {
×
1203
                ">": TaskletCode.int_sgt,
1204
                ">=": TaskletCode.int_sge,
1205
                "<": TaskletCode.int_slt,
1206
                "<=": TaskletCode.int_sle,
1207
                "==": TaskletCode.int_eq,
1208
                "!=": TaskletCode.int_ne,
1209
            }
1210
        else:
1211
            # Floating point ordered comparisons
1212
            cmp_ops = {
4✔
1213
                ">": TaskletCode.fp_ogt,
1214
                ">=": TaskletCode.fp_oge,
1215
                "<": TaskletCode.fp_olt,
1216
                "<=": TaskletCode.fp_ole,
1217
                "==": TaskletCode.fp_oeq,
1218
                "!=": TaskletCode.fp_one,
1219
            }
1220

1221
        if op not in cmp_ops:
4✔
1222
            raise NotImplementedError(
×
1223
                f"Comparison operator {op} not supported for arrays"
1224
            )
1225

1226
        tasklet_code = cmp_ops[op]
4✔
1227

1228
        # For scalar operand, we may need to convert integer to float
1229
        # Create a float constant if needed
1230
        scalar_name = None
4✔
1231
        if not left_is_array:
4✔
1232
            scalar_name = left
×
1233
        elif not right_is_array:
4✔
1234
            scalar_name = right
4✔
1235

1236
        if scalar_name is not None and not use_int_cmp:
4✔
1237
            # Check if scalar is an integer literal and convert to float
1238
            if self._is_int(scalar_name):
4✔
1239
                # Create a float constant
1240
                float_name = f"_tmp_{self._get_unique_id()}"
4✔
1241
                self.builder.add_container(
4✔
1242
                    float_name, Scalar(PrimitiveType.Double), False
1243
                )
1244
                self.symbol_table[float_name] = Scalar(PrimitiveType.Double)
4✔
1245

1246
                block_conv = self.builder.add_block()
4✔
1247
                t_const = self.builder.add_constant(
4✔
1248
                    block_conv, f"{scalar_name}.0", Scalar(PrimitiveType.Double)
1249
                )
1250
                t_float = self.builder.add_access(block_conv, float_name)
4✔
1251
                t_assign = self.builder.add_tasklet(
4✔
1252
                    block_conv, TaskletCode.assign, ["_in"], ["_out"]
1253
                )
1254
                self.builder.add_memlet(
4✔
1255
                    block_conv, t_const, "void", t_assign, "_in", ""
1256
                )
1257
                self.builder.add_memlet(
4✔
1258
                    block_conv, t_assign, "_out", t_float, "void", ""
1259
                )
1260

1261
                # Replace the scalar name with the converted float
1262
                if not left_is_array:
4✔
1263
                    left = float_name
×
1264
                else:
1265
                    right = float_name
4✔
1266

1267
        # Generate nested loops
1268
        loop_vars = []
4✔
1269
        for i, dim in enumerate(shape):
4✔
1270
            loop_var = f"_cmp_i{i}_{self._get_unique_id()}"
4✔
1271
            if not self.builder.exists(loop_var):
4✔
1272
                self.builder.add_container(loop_var, Scalar(PrimitiveType.Int64), False)
4✔
1273
                self.symbol_table[loop_var] = Scalar(PrimitiveType.Int64)
4✔
1274
            loop_vars.append(loop_var)
4✔
1275
            self.builder.begin_for(loop_var, "0", str(dim), "1")
4✔
1276

1277
        # Compute linear index for array access
1278
        linear_idx = self._compute_linear_index(loop_vars, shape, tmp_name, len(shape))
4✔
1279

1280
        # Create comparison block
1281
        block = self.builder.add_block()
4✔
1282

1283
        # Read left operand
1284
        if left_is_array:
4✔
1285
            t_left = self.builder.add_access(block, left)
4✔
1286
            left_sub = linear_idx
4✔
1287
        else:
1288
            t_left, left_sub = self._add_read(block, left)
×
1289

1290
        # Read right operand
1291
        if right_is_array:
4✔
1292
            t_right = self.builder.add_access(block, right)
×
1293
            right_sub = linear_idx
×
1294
        else:
1295
            t_right, right_sub = self._add_read(block, right)
4✔
1296

1297
        # Output access
1298
        t_out = self.builder.add_access(block, tmp_name)
4✔
1299

1300
        # Create tasklet for comparison
1301
        t_task = self.builder.add_tasklet(
4✔
1302
            block, tasklet_code, ["_in1", "_in2"], ["_out"]
1303
        )
1304

1305
        # Connect memlets
1306
        self.builder.add_memlet(block, t_left, "void", t_task, "_in1", left_sub)
4✔
1307
        self.builder.add_memlet(block, t_right, "void", t_task, "_in2", right_sub)
4✔
1308
        self.builder.add_memlet(block, t_task, "_out", t_out, "void", linear_idx)
4✔
1309

1310
        # Close loops
1311
        for _ in loop_vars:
4✔
1312
            self.builder.end_for()
4✔
1313

1314
        return tmp_name
4✔
1315

1316
    def _parse_array_arg(self, node, simple_visitor):
4✔
1317
        if isinstance(node, ast.Name):
×
1318
            if node.id in self.array_info:
×
1319
                return node.id, [], self.array_info[node.id]["shapes"]
×
1320
        elif isinstance(node, ast.Subscript):
×
1321
            if isinstance(node.value, ast.Name) and node.value.id in self.array_info:
×
1322
                name = node.value.id
×
1323
                ndim = self.array_info[name]["ndim"]
×
1324

1325
                indices = []
×
1326
                if isinstance(node.slice, ast.Tuple):
×
1327
                    indices = list(node.slice.elts)
×
1328
                else:
1329
                    indices = [node.slice]
×
1330

1331
                while len(indices) < ndim:
×
1332
                    indices.append(ast.Slice(lower=None, upper=None, step=None))
×
1333

1334
                start_indices = []
×
1335
                slice_shape = []
×
1336

1337
                for i, idx in enumerate(indices):
×
1338
                    if isinstance(idx, ast.Slice):
×
1339
                        start = "0"
×
1340
                        if idx.lower:
×
1341
                            start = simple_visitor.visit(idx.lower)
×
1342
                        start_indices.append(start)
×
1343

1344
                        shapes = self.array_info[name]["shapes"]
×
1345
                        dim_size = (
×
1346
                            shapes[i] if i < len(shapes) else f"_{name}_shape_{i}"
1347
                        )
1348
                        stop = dim_size
×
1349
                        if idx.upper:
×
1350
                            stop = simple_visitor.visit(idx.upper)
×
1351

1352
                        size = f"({stop} - {start})"
×
1353
                        slice_shape.append(size)
×
1354
                    else:
1355
                        val = simple_visitor.visit(idx)
×
1356
                        start_indices.append(val)
×
1357

1358
                shapes = self.array_info[name]["shapes"]
×
1359
                linear_index = ""
×
1360
                for i in range(ndim):
×
1361
                    term = start_indices[i]
×
1362
                    for j in range(i + 1, ndim):
×
1363
                        shape_val = shapes[j] if j < len(shapes) else None
×
1364
                        shape_sym = (
×
1365
                            shape_val if shape_val is not None else f"_{name}_shape_{j}"
1366
                        )
1367
                        term = f"({term} * {shape_sym})"
×
1368

1369
                    if i == 0:
×
1370
                        linear_index = term
×
1371
                    else:
1372
                        linear_index = f"({linear_index} + {term})"
×
1373

1374
                return name, [linear_index], slice_shape
×
1375

1376
        return None, None, None
×
1377

1378
    def visit_Attribute(self, node):
4✔
1379
        if node.attr == "shape":
4✔
1380
            if isinstance(node.value, ast.Name) and node.value.id in self.array_info:
4✔
1381
                return f"_shape_proxy_{node.value.id}"
4✔
1382

1383
        if isinstance(node.value, ast.Name) and node.value.id == "math":
4✔
1384
            val = ""
4✔
1385
            if node.attr == "pi":
4✔
1386
                val = "M_PI"
4✔
1387
            elif node.attr == "e":
4✔
1388
                val = "M_E"
4✔
1389

1390
            if val:
4✔
1391
                tmp_name = f"_tmp_{self._get_unique_id()}"
4✔
1392
                dtype = Scalar(PrimitiveType.Double)
4✔
1393
                self.builder.add_container(tmp_name, dtype, False)
4✔
1394
                self.symbol_table[tmp_name] = dtype
4✔
1395
                self._add_assign_constant(tmp_name, val, dtype)
4✔
1396
                return tmp_name
4✔
1397

1398
        # Handle class member access (e.g., obj.x, obj.y)
1399
        if isinstance(node.value, ast.Name):
4✔
1400
            obj_name = node.value.id
4✔
1401
            attr_name = node.attr
4✔
1402

1403
            # Check if the object is a class instance (has a Structure type)
1404
            if obj_name in self.symbol_table:
4✔
1405
                obj_type = self.symbol_table[obj_name]
4✔
1406
                if isinstance(obj_type, Pointer) and obj_type.has_pointee_type():
4✔
1407
                    pointee_type = obj_type.pointee_type
4✔
1408
                    if isinstance(pointee_type, Structure):
4✔
1409
                        struct_name = pointee_type.name
4✔
1410

1411
                        # Look up member index and type from structure info
1412
                        if (
4✔
1413
                            struct_name in self.structure_member_info
1414
                            and attr_name in self.structure_member_info[struct_name]
1415
                        ):
1416
                            member_index, member_type = self.structure_member_info[
4✔
1417
                                struct_name
1418
                            ][attr_name]
1419
                        else:
1420
                            # This should not happen if structure was registered properly
1421
                            raise RuntimeError(
×
1422
                                f"Member '{attr_name}' not found in structure '{struct_name}'. "
1423
                                f"Available members: {list(self.structure_member_info.get(struct_name, {}).keys())}"
1424
                            )
1425

1426
                        # Generate a tasklet to access the member
1427
                        tmp_name = f"_tmp_{self._get_unique_id()}"
4✔
1428

1429
                        self.builder.add_container(tmp_name, member_type, False)
4✔
1430
                        self.symbol_table[tmp_name] = member_type
4✔
1431

1432
                        # Create a tasklet that reads the member
1433
                        block = self.builder.add_block()
4✔
1434
                        obj_access = self.builder.add_access(block, obj_name)
4✔
1435
                        tmp_access = self.builder.add_access(block, tmp_name)
4✔
1436

1437
                        # Use tasklet to pass through the value
1438
                        # The actual member selection is done via the memlet subset
1439
                        tasklet = self.builder.add_tasklet(
4✔
1440
                            block, TaskletCode.assign, ["_in"], ["_out"]
1441
                        )
1442

1443
                        # Use member index in the subset to select the correct member
1444
                        subset = "0," + str(member_index)
4✔
1445
                        self.builder.add_memlet(
4✔
1446
                            block, obj_access, "", tasklet, "_in", subset
1447
                        )
1448
                        self.builder.add_memlet(block, tasklet, "_out", tmp_access, "")
4✔
1449

1450
                        return tmp_name
4✔
1451

1452
        raise NotImplementedError(f"Attribute access {node.attr} not supported")
×
1453

1454
    def _handle_expression_slicing(self, node, value_str, indices_nodes, shapes, ndim):
4✔
1455
        """Handle slicing in expressions (e.g., arr[1:, :, k+1]).
1456

1457
        Creates a temporary array, generates loops to copy sliced data,
1458
        and returns the temporary array name.
1459
        """
1460
        if not self.builder:
4✔
1461
            raise ValueError("Builder required for expression slicing")
×
1462

1463
        # Determine element type from source array
1464
        dtype = Scalar(PrimitiveType.Double)
4✔
1465
        if value_str in self.symbol_table:
4✔
1466
            t = self.symbol_table[value_str]
4✔
1467
            if isinstance(t, Pointer) and t.has_pointee_type():
4✔
1468
                dtype = t.pointee_type
4✔
1469

1470
        # Analyze each dimension: is it a slice or an index?
1471
        # For slices, compute the resulting shape dimension
1472
        # For indices, that dimension is collapsed
1473
        result_shapes = []  # Shape of the resulting array (for SDFG)
4✔
1474
        result_shapes_runtime = []  # Shape expressions for runtime evaluation
4✔
1475
        slice_info = []  # List of (dim_idx, start_str, stop_str, step_str) for slices
4✔
1476
        index_info = []  # List of (dim_idx, index_str) for point indices
4✔
1477

1478
        for i, idx in enumerate(indices_nodes):
4✔
1479
            shape_val = shapes[i] if i < len(shapes) else f"_{value_str}_shape_{i}"
4✔
1480

1481
            if isinstance(idx, ast.Slice):
4✔
1482
                # Parse slice bounds - check for indirect access patterns
1483
                start_str = "0"
4✔
1484
                start_str_runtime = "0"  # For runtime shape evaluation
4✔
1485
                if idx.lower is not None:
4✔
1486
                    # Check if lower bound contains indirect array access
1487
                    if self._contains_indirect_access(idx.lower):
4✔
1488
                        start_str, start_str_runtime = (
4✔
1489
                            self._materialize_indirect_access(
1490
                                idx.lower, return_original_expr=True
1491
                            )
1492
                        )
1493
                    else:
1494
                        start_str = self.visit(idx.lower)
4✔
1495
                        start_str_runtime = start_str
4✔
1496
                    # Handle negative indices
1497
                    if isinstance(start_str, str) and (
4✔
1498
                        start_str.startswith("-") or start_str.startswith("(-")
1499
                    ):
1500
                        start_str = f"({shape_val} + {start_str})"
×
1501
                        start_str_runtime = f"({shape_val} + {start_str_runtime})"
×
1502

1503
                stop_str = str(shape_val)
4✔
1504
                stop_str_runtime = str(shape_val)
4✔
1505
                if idx.upper is not None:
4✔
1506
                    # Check if upper bound contains indirect array access
1507
                    if self._contains_indirect_access(idx.upper):
4✔
1508
                        stop_str, stop_str_runtime = self._materialize_indirect_access(
4✔
1509
                            idx.upper, return_original_expr=True
1510
                        )
1511
                    else:
1512
                        stop_str = self.visit(idx.upper)
4✔
1513
                        stop_str_runtime = stop_str
4✔
1514
                    # Handle negative indices
1515
                    if isinstance(stop_str, str) and (
4✔
1516
                        stop_str.startswith("-") or stop_str.startswith("(-")
1517
                    ):
1518
                        stop_str = f"({shape_val} + {stop_str})"
4✔
1519
                        stop_str_runtime = f"({shape_val} + {stop_str_runtime})"
4✔
1520

1521
                step_str = "1"
4✔
1522
                if idx.step is not None:
4✔
1523
                    step_str = self.visit(idx.step)
×
1524

1525
                # Compute the size of this dimension in the result
1526
                dim_size = f"({stop_str} - {start_str})"
4✔
1527
                dim_size_runtime = f"({stop_str_runtime} - {start_str_runtime})"
4✔
1528
                result_shapes.append(dim_size)
4✔
1529
                result_shapes_runtime.append(dim_size_runtime)
4✔
1530
                slice_info.append((i, start_str, stop_str, step_str))
4✔
1531
            else:
1532
                # Point index - dimension is collapsed
1533
                # Check for indirect array access in the index
1534
                if self._contains_indirect_access(idx):
4✔
1535
                    index_str = self._materialize_indirect_access(idx)
×
1536
                else:
1537
                    index_str = self.visit(idx)
4✔
1538
                # Handle negative indices
1539
                if isinstance(index_str, str) and (
4✔
1540
                    index_str.startswith("-") or index_str.startswith("(-")
1541
                ):
1542
                    index_str = f"({shape_val} + {index_str})"
×
1543
                index_info.append((i, index_str))
4✔
1544

1545
        # Create temporary array for the result
1546
        tmp_name = self._get_temp_name("_slice_tmp_")
4✔
1547
        result_ndim = len(result_shapes)
4✔
1548

1549
        if result_ndim == 0:
4✔
1550
            # All dimensions indexed - result is a scalar
1551
            self.builder.add_container(tmp_name, dtype, False)
×
1552
            self.symbol_table[tmp_name] = dtype
×
1553
        else:
1554
            # Result is an array - use _create_array_temp to handle allocation
1555
            # Calculate size for malloc - use SDFG symbolic shapes
1556
            size_str = "1"
4✔
1557
            for dim in result_shapes:
4✔
1558
                size_str = f"({size_str} * {dim})"
4✔
1559

1560
            element_size = self.builder.get_sizeof(dtype)
4✔
1561
            total_size = f"({size_str} * {element_size})"
4✔
1562

1563
            # Create pointer
1564
            ptr_type = Pointer(dtype)
4✔
1565
            self.builder.add_container(tmp_name, ptr_type, False)
4✔
1566
            self.symbol_table[tmp_name] = ptr_type
4✔
1567
            # Store both SDFG shapes (for compilation) and runtime shapes (for evaluation)
1568
            # The "shapes" field uses SDFG symbolic variables for malloc sizing
1569
            # The "shapes_runtime" field uses original expressions for Python runtime evaluation
1570
            self.array_info[tmp_name] = {
4✔
1571
                "ndim": result_ndim,
1572
                "shapes": result_shapes,  # Uses materialized variables for SDFG
1573
                "shapes_runtime": result_shapes_runtime,  # Uses original expressions for runtime
1574
            }
1575

1576
            # Malloc for the temporary array
1577
            debug_info = DebugInfo()
4✔
1578
            block_alloc = self.builder.add_block(debug_info)
4✔
1579
            t_malloc = self.builder.add_malloc(block_alloc, total_size)
4✔
1580
            t_ptr = self.builder.add_access(block_alloc, tmp_name, debug_info)
4✔
1581
            self.builder.add_memlet(
4✔
1582
                block_alloc, t_malloc, "_ret", t_ptr, "void", "", ptr_type, debug_info
1583
            )
1584

1585
        # Generate loops to copy the sliced data
1586
        loop_vars = []
4✔
1587
        debug_info = DebugInfo()
4✔
1588

1589
        for dim_idx, (orig_dim, start_str, stop_str, step_str) in enumerate(slice_info):
4✔
1590
            loop_var = f"_slice_loop_{dim_idx}_{self._get_unique_id()}"
4✔
1591
            loop_vars.append((loop_var, orig_dim, start_str, step_str))
4✔
1592

1593
            if not self.builder.exists(loop_var):
4✔
1594
                self.builder.add_container(loop_var, Scalar(PrimitiveType.Int64), False)
4✔
1595
                self.symbol_table[loop_var] = Scalar(PrimitiveType.Int64)
4✔
1596

1597
            # Loop from 0 to (stop - start)
1598
            count_str = f"({stop_str} - {start_str})"
4✔
1599
            self.builder.begin_for(loop_var, "0", count_str, "1", debug_info)
4✔
1600

1601
        # Build source and destination indices
1602
        src_indices = [""] * ndim
4✔
1603
        dst_indices = []
4✔
1604

1605
        # Fill in point indices for source
1606
        for orig_dim, index_str in index_info:
4✔
1607
            src_indices[orig_dim] = index_str
4✔
1608

1609
        # Fill in slice indices for source and build destination indices
1610
        for loop_var, orig_dim, start_str, step_str in loop_vars:
4✔
1611
            if step_str == "1":
4✔
1612
                src_indices[orig_dim] = f"({start_str} + {loop_var})"
4✔
1613
            else:
1614
                src_indices[orig_dim] = f"({start_str} + {loop_var} * {step_str})"
×
1615
            dst_indices.append(loop_var)
4✔
1616

1617
        # Compute linear indices
1618
        src_linear = self._compute_linear_index(src_indices, shapes, value_str, ndim)
4✔
1619
        if result_ndim > 0:
4✔
1620
            dst_linear = self._compute_linear_index(
4✔
1621
                dst_indices, result_shapes, tmp_name, result_ndim
1622
            )
1623
        else:
1624
            dst_linear = "0"
×
1625

1626
        # Create the copy block
1627
        block = self.builder.add_block(debug_info)
4✔
1628
        t_src = self.builder.add_access(block, value_str, debug_info)
4✔
1629
        t_dst = self.builder.add_access(block, tmp_name, debug_info)
4✔
1630
        t_task = self.builder.add_tasklet(
4✔
1631
            block, TaskletCode.assign, ["_in"], ["_out"], debug_info
1632
        )
1633

1634
        self.builder.add_memlet(
4✔
1635
            block, t_src, "void", t_task, "_in", src_linear, None, debug_info
1636
        )
1637
        self.builder.add_memlet(
4✔
1638
            block, t_task, "_out", t_dst, "void", dst_linear, None, debug_info
1639
        )
1640

1641
        # Close all loops
1642
        for _ in loop_vars:
4✔
1643
            self.builder.end_for()
4✔
1644

1645
        return tmp_name
4✔
1646

1647
    def _compute_linear_index(self, indices, shapes, array_name, ndim):
4✔
1648
        """Compute linear index from multi-dimensional indices."""
1649
        if ndim == 0:
4✔
1650
            return "0"
×
1651

1652
        linear_index = ""
4✔
1653
        for i in range(ndim):
4✔
1654
            term = str(indices[i])
4✔
1655
            for j in range(i + 1, ndim):
4✔
1656
                shape_val = shapes[j] if j < len(shapes) else f"_{array_name}_shape_{j}"
4✔
1657
                term = f"(({term}) * {shape_val})"
4✔
1658

1659
            if i == 0:
4✔
1660
                linear_index = term
4✔
1661
            else:
1662
                linear_index = f"({linear_index} + {term})"
4✔
1663

1664
        return linear_index
4✔
1665

1666
    def _is_array_index(self, node):
4✔
1667
        """Check if a node represents an array that could be used as an index (gather).
1668

1669
        Returns True if the node is a Name referring to an array in array_info.
1670
        """
1671
        if isinstance(node, ast.Name):
4✔
1672
            return node.id in self.array_info
4✔
1673
        return False
4✔
1674

1675
    def _handle_gather(self, value_str, index_node, debug_info=None):
4✔
1676
        """Handle gather operation: x[indices] where indices is an array.
1677

1678
        Creates a temporary array and generates a loop to gather elements
1679
        from the source array using the index array.
1680

1681
        This is the canonical SDFG pattern for gather operations:
1682
        - Create a loop over the index array
1683
        - Load the index value using a tasklet+memlets
1684
        - Use that index in the memlet subset for the source array
1685
        """
1686
        if debug_info is None:
4✔
1687
            debug_info = DebugInfo()
4✔
1688

1689
        # Get the index array name
1690
        if isinstance(index_node, ast.Name):
4✔
1691
            idx_array_name = index_node.id
4✔
1692
        else:
1693
            # Visit the index to get its name (handles slices like cols)
1694
            idx_array_name = self.visit(index_node)
×
1695

1696
        if idx_array_name not in self.array_info:
4✔
1697
            raise ValueError(f"Gather index must be an array, got {idx_array_name}")
×
1698

1699
        # Get shapes
1700
        idx_shapes = self.array_info[idx_array_name].get("shapes", [])
4✔
1701
        src_ndim = self.array_info[value_str]["ndim"]
4✔
1702
        idx_ndim = self.array_info[idx_array_name]["ndim"]
4✔
1703

1704
        if idx_ndim != 1:
4✔
1705
            raise NotImplementedError("Only 1D index arrays supported for gather")
×
1706

1707
        # Result array has same shape as index array
1708
        result_shape = idx_shapes[0] if idx_shapes else f"_{idx_array_name}_shape_0"
4✔
1709

1710
        # Determine element type from source array
1711
        dtype = Scalar(PrimitiveType.Double)
4✔
1712
        if value_str in self.symbol_table:
4✔
1713
            t = self.symbol_table[value_str]
4✔
1714
            if isinstance(t, Pointer) and t.has_pointee_type():
4✔
1715
                dtype = t.pointee_type
4✔
1716

1717
        # Determine index type from index array
1718
        idx_dtype = Scalar(PrimitiveType.Int64)
4✔
1719
        if idx_array_name in self.symbol_table:
4✔
1720
            t = self.symbol_table[idx_array_name]
4✔
1721
            if isinstance(t, Pointer) and t.has_pointee_type():
4✔
1722
                idx_dtype = t.pointee_type
4✔
1723

1724
        # Create result array
1725
        tmp_name = self._get_temp_name("_gather_")
4✔
1726

1727
        # Calculate size for malloc
1728
        element_size = self.builder.get_sizeof(dtype)
4✔
1729
        total_size = f"({result_shape} * {element_size})"
4✔
1730

1731
        # Create pointer for result
1732
        ptr_type = Pointer(dtype)
4✔
1733
        self.builder.add_container(tmp_name, ptr_type, False)
4✔
1734
        self.symbol_table[tmp_name] = ptr_type
4✔
1735
        self.array_info[tmp_name] = {"ndim": 1, "shapes": [result_shape]}
4✔
1736

1737
        # Malloc for the result array
1738
        block_alloc = self.builder.add_block(debug_info)
4✔
1739
        t_malloc = self.builder.add_malloc(block_alloc, total_size)
4✔
1740
        t_ptr = self.builder.add_access(block_alloc, tmp_name, debug_info)
4✔
1741
        self.builder.add_memlet(
4✔
1742
            block_alloc, t_malloc, "_ret", t_ptr, "void", "", ptr_type, debug_info
1743
        )
1744

1745
        # Create loop variable
1746
        loop_var = f"_gather_i_{self._get_unique_id()}"
4✔
1747
        self.builder.add_container(loop_var, Scalar(PrimitiveType.Int64), False)
4✔
1748
        self.symbol_table[loop_var] = Scalar(PrimitiveType.Int64)
4✔
1749

1750
        # Create variable to hold the loaded index
1751
        idx_var = f"_gather_idx_{self._get_unique_id()}"
4✔
1752
        self.builder.add_container(idx_var, idx_dtype, False)
4✔
1753
        self.symbol_table[idx_var] = idx_dtype
4✔
1754

1755
        # Begin loop
1756
        self.builder.begin_for(loop_var, "0", str(result_shape), "1", debug_info)
4✔
1757

1758
        # Block 1: Load the index from index array using tasklet+memlets
1759
        block_load_idx = self.builder.add_block(debug_info)
4✔
1760
        idx_arr_access = self.builder.add_access(
4✔
1761
            block_load_idx, idx_array_name, debug_info
1762
        )
1763
        idx_var_access = self.builder.add_access(block_load_idx, idx_var, debug_info)
4✔
1764
        tasklet_load = self.builder.add_tasklet(
4✔
1765
            block_load_idx, TaskletCode.assign, ["_in"], ["_out"], debug_info
1766
        )
1767
        self.builder.add_memlet(
4✔
1768
            block_load_idx,
1769
            idx_arr_access,
1770
            "void",
1771
            tasklet_load,
1772
            "_in",
1773
            loop_var,
1774
            None,
1775
            debug_info,
1776
        )
1777
        self.builder.add_memlet(
4✔
1778
            block_load_idx,
1779
            tasklet_load,
1780
            "_out",
1781
            idx_var_access,
1782
            "void",
1783
            "",
1784
            None,
1785
            debug_info,
1786
        )
1787

1788
        # Block 2: Use the loaded index to gather from source array
1789
        block_gather = self.builder.add_block(debug_info)
4✔
1790
        src_access = self.builder.add_access(block_gather, value_str, debug_info)
4✔
1791
        dst_access = self.builder.add_access(block_gather, tmp_name, debug_info)
4✔
1792
        tasklet_gather = self.builder.add_tasklet(
4✔
1793
            block_gather, TaskletCode.assign, ["_in"], ["_out"], debug_info
1794
        )
1795

1796
        # Use the symbolic variable name (idx_var) in the memlet subset - this is key!
1797
        self.builder.add_memlet(
4✔
1798
            block_gather,
1799
            src_access,
1800
            "void",
1801
            tasklet_gather,
1802
            "_in",
1803
            idx_var,
1804
            None,
1805
            debug_info,
1806
        )
1807
        self.builder.add_memlet(
4✔
1808
            block_gather,
1809
            tasklet_gather,
1810
            "_out",
1811
            dst_access,
1812
            "void",
1813
            loop_var,
1814
            None,
1815
            debug_info,
1816
        )
1817

1818
        # End loop
1819
        self.builder.end_for()
4✔
1820

1821
        return tmp_name
4✔
1822

1823
    def visit_Subscript(self, node):
4✔
1824
        value_str = self.visit(node.value)
4✔
1825

1826
        if value_str.startswith("_shape_proxy_"):
4✔
1827
            array_name = value_str[len("_shape_proxy_") :]
4✔
1828
            if isinstance(node.slice, ast.Constant):
4✔
1829
                idx = node.slice.value
4✔
1830
            elif isinstance(node.slice, ast.Index):
×
1831
                idx = node.slice.value.value
×
1832
            else:
1833
                try:
×
1834
                    idx = int(self.visit(node.slice))
×
1835
                except:
×
1836
                    raise NotImplementedError(
×
1837
                        "Dynamic shape indexing not fully supported yet"
1838
                    )
1839

1840
            if (
4✔
1841
                array_name in self.array_info
1842
                and "shapes" in self.array_info[array_name]
1843
            ):
1844
                return self.array_info[array_name]["shapes"][idx]
4✔
1845

1846
            return f"_{array_name}_shape_{idx}"
×
1847

1848
        if value_str in self.array_info:
4✔
1849
            ndim = self.array_info[value_str]["ndim"]
4✔
1850
            shapes = self.array_info[value_str].get("shapes", [])
4✔
1851

1852
            indices = []
4✔
1853
            if isinstance(node.slice, ast.Tuple):
4✔
1854
                indices_nodes = node.slice.elts
4✔
1855
            else:
1856
                indices_nodes = [node.slice]
4✔
1857

1858
            # Check if all indices are full slices (e.g., path[:] or path[:, :])
1859
            # In this case, return just the array name since it's the full array
1860
            all_full_slices = True
4✔
1861
            for idx in indices_nodes:
4✔
1862
                if isinstance(idx, ast.Slice):
4✔
1863
                    # A full slice has no lower, upper bounds or only None
1864
                    if idx.lower is not None or idx.upper is not None:
4✔
1865
                        all_full_slices = False
4✔
1866
                        break
4✔
1867
                else:
1868
                    all_full_slices = False
4✔
1869
                    break
4✔
1870

1871
            # path[:] on an nD array returns the full array
1872
            # So if we have a single full slice, it covers all dimensions
1873
            if all_full_slices:
4✔
1874
                # This is path[:] or path[:,:] - return the array name
1875
                return value_str
4✔
1876

1877
            # Check if there are any slices in the indices
1878
            has_slices = any(isinstance(idx, ast.Slice) for idx in indices_nodes)
4✔
1879
            if has_slices:
4✔
1880
                # Handle mixed slicing (e.g., arr[1:, :, k] or arr[:-1, :, k+1])
1881
                return self._handle_expression_slicing(
4✔
1882
                    node, value_str, indices_nodes, shapes, ndim
1883
                )
1884

1885
            # Check for gather operation: x[indices_array] where indices_array is an array
1886
            # This happens when we have a 1D source array and a 1D index array
1887
            if len(indices_nodes) == 1 and self._is_array_index(indices_nodes[0]):
4✔
1888
                if self.builder:
4✔
1889
                    return self._handle_gather(value_str, indices_nodes[0])
4✔
1890

1891
            if isinstance(node.slice, ast.Tuple):
4✔
1892
                indices = [self.visit(elt) for elt in node.slice.elts]
4✔
1893
            else:
1894
                indices = [self.visit(node.slice)]
4✔
1895

1896
            if len(indices) != ndim:
4✔
1897
                raise ValueError(
×
1898
                    f"Array {value_str} has {ndim} dimensions, but accessed with {len(indices)} indices"
1899
                )
1900

1901
            # Normalize negative indices
1902
            normalized_indices = []
4✔
1903
            for i, idx_str in enumerate(indices):
4✔
1904
                shape_val = shapes[i] if i < len(shapes) else f"_{value_str}_shape_{i}"
4✔
1905
                # Check if index is negative (starts with "-" or "(-")
1906
                if isinstance(idx_str, str) and (
4✔
1907
                    idx_str.startswith("-") or idx_str.startswith("(-")
1908
                ):
1909
                    # Normalize: size + negative_index
1910
                    normalized_indices.append(f"({shape_val} + {idx_str})")
×
1911
                else:
1912
                    normalized_indices.append(idx_str)
4✔
1913

1914
            linear_index = ""
4✔
1915
            for i in range(ndim):
4✔
1916
                term = normalized_indices[i]
4✔
1917
                for j in range(i + 1, ndim):
4✔
1918
                    shape_val = shapes[j] if j < len(shapes) else None
4✔
1919
                    shape_sym = (
4✔
1920
                        shape_val
1921
                        if shape_val is not None
1922
                        else f"_{value_str}_shape_{j}"
1923
                    )
1924
                    term = f"(({term}) * {shape_sym})"
4✔
1925

1926
                if i == 0:
4✔
1927
                    linear_index = term
4✔
1928
                else:
1929
                    linear_index = f"({linear_index} + {term})"
4✔
1930

1931
            access_str = f"{value_str}({linear_index})"
4✔
1932

1933
            if self.builder and isinstance(node.ctx, ast.Load):
4✔
1934
                dtype = Scalar(PrimitiveType.Double)
4✔
1935
                if value_str in self.symbol_table:
4✔
1936
                    t = self.symbol_table[value_str]
4✔
1937
                    if type(t).__name__ == "Array" and hasattr(t, "element_type"):
4✔
1938
                        et = t.element_type
×
1939
                        if callable(et):
×
1940
                            et = et()
×
1941
                        dtype = et
×
1942
                    elif type(t).__name__ == "Pointer" and hasattr(t, "pointee_type"):
4✔
1943
                        et = t.pointee_type
4✔
1944
                        if callable(et):
4✔
1945
                            et = et()
×
1946
                        dtype = et
4✔
1947

1948
                tmp_name = f"_tmp_{self._get_unique_id()}"
4✔
1949
                self.builder.add_container(tmp_name, dtype, False)
4✔
1950

1951
                block = self.builder.add_block()
4✔
1952
                t_src = self.builder.add_access(block, value_str)
4✔
1953
                t_dst = self.builder.add_access(block, tmp_name)
4✔
1954
                t_task = self.builder.add_tasklet(
4✔
1955
                    block, TaskletCode.assign, ["_in"], ["_out"]
1956
                )
1957

1958
                self.builder.add_memlet(
4✔
1959
                    block, t_src, "void", t_task, "_in", linear_index
1960
                )
1961
                self.builder.add_memlet(block, t_task, "_out", t_dst, "void", "")
4✔
1962

1963
                self.symbol_table[tmp_name] = dtype
4✔
1964
                return tmp_name
4✔
1965

1966
            return access_str
4✔
1967

1968
        slice_val = self.visit(node.slice)
×
1969
        access_str = f"{value_str}({slice_val})"
×
1970

1971
        if (
×
1972
            self.builder
1973
            and isinstance(node.ctx, ast.Load)
1974
            and value_str in self.array_info
1975
        ):
1976
            tmp_name = f"_tmp_{self._get_unique_id()}"
×
1977
            self.builder.add_container(tmp_name, Scalar(PrimitiveType.Double), False)
×
1978
            self.builder.add_assignment(tmp_name, access_str)
×
1979
            self.symbol_table[tmp_name] = Scalar(PrimitiveType.Double)
×
1980
            return tmp_name
×
1981

1982
        return access_str
×
1983

1984
    def visit_Add(self, node):
4✔
1985
        return "+"
4✔
1986

1987
    def visit_Sub(self, node):
4✔
1988
        return "-"
4✔
1989

1990
    def visit_Mult(self, node):
4✔
1991
        return "*"
4✔
1992

1993
    def visit_Div(self, node):
4✔
1994
        return "/"
4✔
1995

1996
    def visit_FloorDiv(self, node):
4✔
1997
        return "//"
4✔
1998

1999
    def visit_Mod(self, node):
4✔
2000
        return "%"
4✔
2001

2002
    def visit_Pow(self, node):
4✔
2003
        return "**"
4✔
2004

2005
    def visit_Eq(self, node):
4✔
2006
        return "=="
×
2007

2008
    def visit_NotEq(self, node):
4✔
2009
        return "!="
×
2010

2011
    def visit_Lt(self, node):
4✔
2012
        return "<"
4✔
2013

2014
    def visit_LtE(self, node):
4✔
2015
        return "<="
×
2016

2017
    def visit_Gt(self, node):
4✔
2018
        return ">"
4✔
2019

2020
    def visit_GtE(self, node):
4✔
2021
        return ">="
×
2022

2023
    def visit_And(self, node):
4✔
2024
        return "&"
4✔
2025

2026
    def visit_Or(self, node):
4✔
2027
        return "|"
4✔
2028

2029
    def visit_BitAnd(self, node):
4✔
2030
        return "&"
×
2031

2032
    def visit_BitOr(self, node):
4✔
2033
        return "|"
4✔
2034

2035
    def visit_BitXor(self, node):
4✔
2036
        return "^"
4✔
2037

2038
    def visit_Not(self, node):
4✔
2039
        return "!"
4✔
2040

2041
    def visit_USub(self, node):
4✔
2042
        return "-"
4✔
2043

2044
    def visit_UAdd(self, node):
4✔
2045
        return "+"
×
2046

2047
    def visit_Invert(self, node):
4✔
2048
        return "~"
×
2049

2050
    def _get_dtype(self, name):
4✔
2051
        if name in self.symbol_table:
4✔
2052
            t = self.symbol_table[name]
4✔
2053
            if isinstance(t, Scalar):
4✔
2054
                return t
4✔
2055

2056
            if hasattr(t, "pointee_type"):
4✔
2057
                et = t.pointee_type
4✔
2058
                if callable(et):
4✔
2059
                    et = et()
×
2060
                if isinstance(et, Scalar):
4✔
2061
                    return et
4✔
2062

2063
            if hasattr(t, "element_type"):
×
2064
                et = t.element_type
×
2065
                if callable(et):
×
2066
                    et = et()
×
2067
                if isinstance(et, Scalar):
×
2068
                    return et
×
2069

2070
        if self._is_int(name):
4✔
2071
            return Scalar(PrimitiveType.Int64)
4✔
2072

2073
        return Scalar(PrimitiveType.Double)
4✔
2074

2075
    def _promote_dtypes(self, dtype_left, dtype_right):
4✔
2076
        """Promote two dtypes following NumPy rules: float > int, wider > narrower."""
2077
        # Priority order: Double > Float > Int64 > Int32
2078
        priority = {
4✔
2079
            PrimitiveType.Double: 4,
2080
            PrimitiveType.Float: 3,
2081
            PrimitiveType.Int64: 2,
2082
            PrimitiveType.Int32: 1,
2083
        }
2084
        left_prio = priority.get(dtype_left.primitive_type, 0)
4✔
2085
        right_prio = priority.get(dtype_right.primitive_type, 0)
4✔
2086
        if left_prio >= right_prio:
4✔
2087
            return dtype_left
4✔
2088
        else:
2089
            return dtype_right
4✔
2090

2091
    def _create_array_temp(
4✔
2092
        self, shape, dtype, zero_init=False, ones_init=False, shapes_runtime=None
2093
    ):
2094
        tmp_name = f"_tmp_{self._get_unique_id()}"
4✔
2095

2096
        # Handle 0-dimensional arrays as scalars
2097
        if not shape or (len(shape) == 0):
4✔
2098
            # 0-D array is just a scalar
2099
            self.builder.add_container(tmp_name, dtype, False)
4✔
2100
            self.symbol_table[tmp_name] = dtype
4✔
2101
            self.array_info[tmp_name] = {"ndim": 0, "shapes": []}
4✔
2102

2103
            if zero_init:
4✔
2104
                self.builder.add_assignment(
×
2105
                    tmp_name,
2106
                    "0.0" if dtype.primitive_type == PrimitiveType.Double else "0",
2107
                )
2108
            elif ones_init:
4✔
2109
                self.builder.add_assignment(
×
2110
                    tmp_name,
2111
                    "1.0" if dtype.primitive_type == PrimitiveType.Double else "1",
2112
                )
2113

2114
            return tmp_name
4✔
2115

2116
        # Calculate size
2117
        size_str = "1"
4✔
2118
        for dim in shape:
4✔
2119
            size_str = f"({size_str} * {dim})"
4✔
2120

2121
        element_size = self.builder.get_sizeof(dtype)
4✔
2122
        total_size = f"({size_str} * {element_size})"
4✔
2123

2124
        # Create pointer
2125
        ptr_type = Pointer(dtype)
4✔
2126
        self.builder.add_container(tmp_name, ptr_type, False)
4✔
2127
        self.symbol_table[tmp_name] = ptr_type
4✔
2128
        array_info_entry = {"ndim": len(shape), "shapes": shape}
4✔
2129
        if shapes_runtime is not None:
4✔
2130
            array_info_entry["shapes_runtime"] = shapes_runtime
4✔
2131
        self.array_info[tmp_name] = array_info_entry
4✔
2132

2133
        # Malloc
2134
        block1 = self.builder.add_block()
4✔
2135
        t_malloc = self.builder.add_malloc(block1, total_size)
4✔
2136
        t_ptr1 = self.builder.add_access(block1, tmp_name)
4✔
2137
        self.builder.add_memlet(block1, t_malloc, "_ret", t_ptr1, "void", "", ptr_type)
4✔
2138

2139
        if zero_init:
4✔
2140
            block2 = self.builder.add_block()
4✔
2141
            t_memset = self.builder.add_memset(block2, "0", total_size)
4✔
2142
            t_ptr2 = self.builder.add_access(block2, tmp_name)
4✔
2143
            self.builder.add_memlet(
4✔
2144
                block2, t_memset, "_ptr", t_ptr2, "void", "", ptr_type
2145
            )
2146
        elif ones_init:
4✔
2147
            # Initialize array with ones using a loop
2148
            loop_var = f"_i_{self._get_unique_id()}"
4✔
2149
            if not self.builder.exists(loop_var):
4✔
2150
                self.builder.add_container(loop_var, Scalar(PrimitiveType.Int64), False)
4✔
2151
                self.symbol_table[loop_var] = Scalar(PrimitiveType.Int64)
4✔
2152

2153
            self.builder.begin_for(loop_var, "0", size_str, "1")
4✔
2154

2155
            # Determine the value to set based on dtype
2156
            val = "1.0"
4✔
2157
            if dtype.primitive_type in [
4✔
2158
                PrimitiveType.Int64,
2159
                PrimitiveType.Int32,
2160
                PrimitiveType.Int8,
2161
                PrimitiveType.Int16,
2162
                PrimitiveType.UInt64,
2163
                PrimitiveType.UInt32,
2164
                PrimitiveType.UInt8,
2165
                PrimitiveType.UInt16,
2166
            ]:
2167
                val = "1"
4✔
2168

2169
            block_assign = self.builder.add_block()
4✔
2170
            t_const = self.builder.add_constant(block_assign, val, dtype)
4✔
2171
            t_arr = self.builder.add_access(block_assign, tmp_name)
4✔
2172

2173
            t_task = self.builder.add_tasklet(
4✔
2174
                block_assign, TaskletCode.assign, ["_in"], ["_out"]
2175
            )
2176
            self.builder.add_memlet(
4✔
2177
                block_assign, t_const, "void", t_task, "_in", "", dtype
2178
            )
2179
            self.builder.add_memlet(
4✔
2180
                block_assign, t_task, "_out", t_arr, "void", loop_var
2181
            )
2182

2183
            self.builder.end_for()
4✔
2184

2185
        return tmp_name
4✔
2186

2187
    def _handle_array_unary_op(self, op_type, operand):
4✔
2188
        # Determine output shape
2189
        shape = []
4✔
2190
        if operand in self.array_info:
4✔
2191
            shape = self.array_info[operand]["shapes"]
4✔
2192

2193
        # Determine dtype
2194
        dtype = self._get_dtype(operand)
4✔
2195

2196
        # For 0-D arrays (scalars), use an intrinsic (CMathNode) instead of library node
2197
        if not shape or len(shape) == 0:
4✔
2198
            tmp_name = self._create_array_temp(shape, dtype)
4✔
2199

2200
            # Map op_type to C function names
2201
            func_map = {
4✔
2202
                "sqrt": CMathFunction.sqrt,
2203
                "abs": CMathFunction.fabs,
2204
                "absolute": CMathFunction.fabs,
2205
                "exp": CMathFunction.exp,
2206
                "tanh": CMathFunction.tanh,
2207
            }
2208

2209
            block = self.builder.add_block()
4✔
2210
            t_src = self.builder.add_access(block, operand)
4✔
2211
            t_dst = self.builder.add_access(block, tmp_name)
4✔
2212
            t_task = self.builder.add_cmath(block, func_map[op_type])
4✔
2213

2214
            # CMathNode uses _in1, _in2, etc for inputs and _out for output
2215
            self.builder.add_memlet(block, t_src, "void", t_task, "_in1", "", dtype)
4✔
2216
            self.builder.add_memlet(block, t_task, "_out", t_dst, "void", "", dtype)
4✔
2217

2218
            return tmp_name
4✔
2219

2220
        tmp_name = self._create_array_temp(shape, dtype)
4✔
2221

2222
        # Add operation
2223
        self.builder.add_elementwise_unary_op(op_type, operand, tmp_name, shape)
4✔
2224

2225
        return tmp_name
4✔
2226

2227
    def _handle_array_binary_op(self, op_type, left, right):
4✔
2228
        # Determine output shape (handle broadcasting by picking the larger shape)
2229
        left_shape = []
4✔
2230
        right_shape = []
4✔
2231
        if left in self.array_info:
4✔
2232
            left_shape = self.array_info[left]["shapes"]
4✔
2233
        if right in self.array_info:
4✔
2234
            right_shape = self.array_info[right]["shapes"]
4✔
2235
        # Pick the shape with more dimensions for broadcasting
2236
        shape = left_shape if len(left_shape) >= len(right_shape) else right_shape
4✔
2237

2238
        # Determine dtype with promotion (float > int, wider > narrower)
2239
        dtype_left = self._get_dtype(left)
4✔
2240
        dtype_right = self._get_dtype(right)
4✔
2241

2242
        # Promote dtypes: Double > Float > Int64 > Int32
2243
        dtype = self._promote_dtypes(dtype_left, dtype_right)
4✔
2244

2245
        # Cast scalar operands to the promoted dtype if needed
2246
        real_left = left
4✔
2247
        real_right = right
4✔
2248

2249
        # Helper to check if operand is a scalar (not an array)
2250
        left_is_scalar = left not in self.array_info
4✔
2251
        right_is_scalar = right not in self.array_info
4✔
2252

2253
        # Cast left operand if needed (scalar int to float)
2254
        if left_is_scalar and dtype_left.primitive_type != dtype.primitive_type:
4✔
2255
            left_cast = f"_tmp_{self._get_unique_id()}"
4✔
2256
            self.builder.add_container(left_cast, dtype, False)
4✔
2257
            self.symbol_table[left_cast] = dtype
4✔
2258

2259
            c_block = self.builder.add_block()
4✔
2260
            t_src, src_sub = self._add_read(c_block, left)
4✔
2261
            t_dst = self.builder.add_access(c_block, left_cast)
4✔
2262
            t_task = self.builder.add_tasklet(
4✔
2263
                c_block, TaskletCode.assign, ["_in"], ["_out"]
2264
            )
2265
            self.builder.add_memlet(c_block, t_src, "void", t_task, "_in", src_sub)
4✔
2266
            self.builder.add_memlet(c_block, t_task, "_out", t_dst, "void", "")
4✔
2267

2268
            real_left = left_cast
4✔
2269

2270
        # Cast right operand if needed (scalar int to float)
2271
        if right_is_scalar and dtype_right.primitive_type != dtype.primitive_type:
4✔
2272
            right_cast = f"_tmp_{self._get_unique_id()}"
4✔
2273
            self.builder.add_container(right_cast, dtype, False)
4✔
2274
            self.symbol_table[right_cast] = dtype
4✔
2275

2276
            c_block = self.builder.add_block()
4✔
2277
            t_src, src_sub = self._add_read(c_block, right)
4✔
2278
            t_dst = self.builder.add_access(c_block, right_cast)
4✔
2279
            t_task = self.builder.add_tasklet(
4✔
2280
                c_block, TaskletCode.assign, ["_in"], ["_out"]
2281
            )
2282
            self.builder.add_memlet(c_block, t_src, "void", t_task, "_in", src_sub)
4✔
2283
            self.builder.add_memlet(c_block, t_task, "_out", t_dst, "void", "")
4✔
2284

2285
            real_right = right_cast
4✔
2286

2287
        tmp_name = self._create_array_temp(shape, dtype)
4✔
2288

2289
        # Add operation with promoted dtype for implicit casting
2290
        self.builder.add_elementwise_op(op_type, real_left, real_right, tmp_name, shape)
4✔
2291

2292
        return tmp_name
4✔
2293

2294
    def _shape_to_runtime_expr(self, shape_node):
4✔
2295
        """Convert a shape expression AST node to a runtime-evaluable string.
2296

2297
        This converts the AST to a string expression that can be evaluated
2298
        at runtime using only input arrays and shape symbols (_s0, _s1, etc.).
2299
        It does NOT visit the node (which would create SDFG variables).
2300
        """
2301
        if isinstance(shape_node, ast.Constant):
4✔
2302
            return str(shape_node.value)
4✔
2303
        elif isinstance(shape_node, ast.Name):
4✔
2304
            return shape_node.id
4✔
2305
        elif isinstance(shape_node, ast.BinOp):
4✔
2306
            left = self._shape_to_runtime_expr(shape_node.left)
4✔
2307
            right = self._shape_to_runtime_expr(shape_node.right)
4✔
2308
            op = self.visit(shape_node.op)
4✔
2309
            return f"({left} {op} {right})"
4✔
2310
        elif isinstance(shape_node, ast.UnaryOp):
4✔
2311
            operand = self._shape_to_runtime_expr(shape_node.operand)
×
2312
            if isinstance(shape_node.op, ast.USub):
×
2313
                return f"(-{operand})"
×
2314
            elif isinstance(shape_node.op, ast.UAdd):
×
2315
                return operand
×
2316
            else:
2317
                # Fall back to visit for other unary ops
2318
                return self.visit(shape_node)
×
2319
        elif isinstance(shape_node, ast.Subscript):
4✔
2320
            # Handle arr.shape[0] -> arr.shape[0] for runtime eval
2321
            # or _shape_proxy_arr[0] -> _s<idx>
2322
            val = shape_node.value
4✔
2323
            if isinstance(val, ast.Attribute) and val.attr == "shape":
4✔
2324
                # arr.shape[0] -> use the shape symbol
2325
                if isinstance(val.value, ast.Name):
4✔
2326
                    arr_name = val.value.id
4✔
2327
                    if isinstance(shape_node.slice, ast.Constant):
4✔
2328
                        idx = shape_node.slice.value
4✔
2329
                        # Get the shape symbol for this array dimension
2330
                        if arr_name in self.array_info:
4✔
2331
                            shapes = self.array_info[arr_name].get("shapes", [])
4✔
2332
                            if idx < len(shapes):
4✔
2333
                                return shapes[idx]
4✔
2334
                        return f"{arr_name}.shape[{idx}]"
×
2335
            # Fall back to visit
2336
            return self.visit(shape_node)
×
2337
        elif isinstance(shape_node, ast.Tuple):
×
2338
            return [self._shape_to_runtime_expr(elt) for elt in shape_node.elts]
×
2339
        elif isinstance(shape_node, ast.List):
×
2340
            return [self._shape_to_runtime_expr(elt) for elt in shape_node.elts]
×
2341
        else:
2342
            # Fall back to visit for complex expressions
2343
            return self.visit(shape_node)
×
2344

2345
    def _handle_numpy_alloc(self, node, func_name):
4✔
2346
        # Parse shape
2347
        shape_arg = node.args[0]
4✔
2348
        dims = []
4✔
2349
        dims_runtime = []  # Runtime-evaluable shape expressions
4✔
2350
        if isinstance(shape_arg, ast.Tuple):
4✔
2351
            dims = [self.visit(elt) for elt in shape_arg.elts]
4✔
2352
            dims_runtime = [self._shape_to_runtime_expr(elt) for elt in shape_arg.elts]
4✔
2353
        elif isinstance(shape_arg, ast.List):
4✔
2354
            dims = [self.visit(elt) for elt in shape_arg.elts]
×
2355
            dims_runtime = [self._shape_to_runtime_expr(elt) for elt in shape_arg.elts]
×
2356
        else:
2357
            val = self.visit(shape_arg)
4✔
2358
            runtime_val = self._shape_to_runtime_expr(shape_arg)
4✔
2359
            if val.startswith("_shape_proxy_"):
4✔
2360
                array_name = val[len("_shape_proxy_") :]
×
2361
                if array_name in self.array_info:
×
2362
                    dims = self.array_info[array_name]["shapes"]
×
2363
                    dims_runtime = self.array_info[array_name].get(
×
2364
                        "shapes_runtime", dims
2365
                    )
2366
                else:
2367
                    dims = [val]
×
2368
                    dims_runtime = [runtime_val]
×
2369
            else:
2370
                dims = [val]
4✔
2371
                dims_runtime = [runtime_val]
4✔
2372

2373
        # Parse dtype
2374
        dtype_arg = None
4✔
2375
        if len(node.args) > 1:
4✔
2376
            dtype_arg = node.args[1]
×
2377

2378
        for kw in node.keywords:
4✔
2379
            if kw.arg == "dtype":
4✔
2380
                dtype_arg = kw.value
4✔
2381
                break
4✔
2382

2383
        element_type = self._map_numpy_dtype(dtype_arg)
4✔
2384

2385
        return self._create_array_temp(
4✔
2386
            dims,
2387
            element_type,
2388
            zero_init=(func_name == "zeros"),
2389
            ones_init=(func_name == "ones"),
2390
            shapes_runtime=dims_runtime,
2391
        )
2392

2393
    def _handle_numpy_empty_like(self, node, func_name):
4✔
2394
        prototype_arg = node.args[0]
4✔
2395
        prototype_name = self.visit(prototype_arg)
4✔
2396

2397
        # Parse shape from prototype
2398
        dims = []
4✔
2399
        if prototype_name in self.array_info:
4✔
2400
            dims = self.array_info[prototype_name]["shapes"]
4✔
2401

2402
        # Parse dtype
2403
        dtype_arg = None
4✔
2404
        if len(node.args) > 1:
4✔
2405
            dtype_arg = node.args[1]
×
2406

2407
        for kw in node.keywords:
4✔
2408
            if kw.arg == "dtype":
4✔
2409
                dtype_arg = kw.value
4✔
2410
                break
4✔
2411

2412
        element_type = None
4✔
2413
        if dtype_arg:
4✔
2414
            element_type = self._map_numpy_dtype(dtype_arg)
4✔
2415
        else:
2416
            if prototype_name in self.symbol_table:
4✔
2417
                sym_type = self.symbol_table[prototype_name]
4✔
2418
                if isinstance(sym_type, Pointer) and sym_type.has_pointee_type():
4✔
2419
                    element_type = sym_type.pointee_type
4✔
2420

2421
        if element_type is None:
4✔
2422
            element_type = Scalar(PrimitiveType.Double)
×
2423

2424
        return self._create_array_temp(
4✔
2425
            dims,
2426
            element_type,
2427
            zero_init=False,
2428
            ones_init=False,
2429
        )
2430

2431
    def _handle_numpy_zeros_like(self, node, func_name):
4✔
2432
        prototype_arg = node.args[0]
4✔
2433
        prototype_name = self.visit(prototype_arg)
4✔
2434

2435
        # Parse shape from prototype
2436
        dims = []
4✔
2437
        if prototype_name in self.array_info:
4✔
2438
            dims = self.array_info[prototype_name]["shapes"]
4✔
2439

2440
        # Parse dtype
2441
        dtype_arg = None
4✔
2442
        if len(node.args) > 1:
4✔
2443
            dtype_arg = node.args[1]
×
2444

2445
        for kw in node.keywords:
4✔
2446
            if kw.arg == "dtype":
4✔
2447
                dtype_arg = kw.value
4✔
2448
                break
4✔
2449

2450
        element_type = None
4✔
2451
        if dtype_arg:
4✔
2452
            element_type = self._map_numpy_dtype(dtype_arg)
4✔
2453
        else:
2454
            if prototype_name in self.symbol_table:
4✔
2455
                sym_type = self.symbol_table[prototype_name]
4✔
2456
                if isinstance(sym_type, Pointer) and sym_type.has_pointee_type():
4✔
2457
                    element_type = sym_type.pointee_type
4✔
2458

2459
        if element_type is None:
4✔
2460
            element_type = Scalar(PrimitiveType.Double)
×
2461

2462
        return self._create_array_temp(
4✔
2463
            dims,
2464
            element_type,
2465
            zero_init=True,
2466
            ones_init=False,
2467
        )
2468

2469
    def _handle_numpy_eye(self, node, func_name):
4✔
2470
        # Parse N
2471
        N_arg = node.args[0]
4✔
2472
        N_str = self.visit(N_arg)
4✔
2473

2474
        # Parse M
2475
        M_str = N_str
4✔
2476
        if len(node.args) > 1:
4✔
2477
            M_str = self.visit(node.args[1])
×
2478

2479
        # Parse k
2480
        k_str = "0"
4✔
2481
        if len(node.args) > 2:
4✔
2482
            k_str = self.visit(node.args[2])
×
2483

2484
        # Check keywords for M, k, dtype
2485
        dtype_arg = None
4✔
2486
        for kw in node.keywords:
4✔
2487
            if kw.arg == "M":
4✔
2488
                M_str = self.visit(kw.value)
4✔
2489
                if M_str == "None":
4✔
2490
                    M_str = N_str
4✔
2491
            elif kw.arg == "k":
4✔
2492
                k_str = self.visit(kw.value)
4✔
2493
            elif kw.arg == "dtype":
4✔
2494
                dtype_arg = kw.value
4✔
2495

2496
        element_type = self._map_numpy_dtype(dtype_arg)
4✔
2497

2498
        ptr_name = self._create_array_temp([N_str, M_str], element_type, zero_init=True)
4✔
2499

2500
        # Loop to set diagonal
2501
        loop_var = f"_i_{self._get_unique_id()}"
4✔
2502
        if not self.builder.exists(loop_var):
4✔
2503
            self.builder.add_container(loop_var, Scalar(PrimitiveType.Int64), False)
4✔
2504
            self.symbol_table[loop_var] = Scalar(PrimitiveType.Int64)
4✔
2505

2506
        self.builder.begin_for(loop_var, "0", N_str, "1")
4✔
2507

2508
        # Condition: 0 <= i + k < M
2509
        cond = f"(({loop_var} + {k_str}) >= 0) & (({loop_var} + {k_str}) < {M_str})"
4✔
2510
        self.builder.begin_if(cond)
4✔
2511

2512
        # Assignment: A[i, i+k] = 1
2513
        val = "1.0"
4✔
2514
        if element_type.primitive_type in [
4✔
2515
            PrimitiveType.Int64,
2516
            PrimitiveType.Int32,
2517
            PrimitiveType.Int8,
2518
            PrimitiveType.Int16,
2519
            PrimitiveType.UInt64,
2520
            PrimitiveType.UInt32,
2521
            PrimitiveType.UInt8,
2522
            PrimitiveType.UInt16,
2523
        ]:
2524
            val = "1"
×
2525

2526
        block_assign = self.builder.add_block()
4✔
2527
        t_const = self.builder.add_constant(block_assign, val, element_type)
4✔
2528
        t_arr = self.builder.add_access(block_assign, ptr_name)
4✔
2529
        flat_index = f"(({loop_var}) * ({M_str}) + ({loop_var}) + ({k_str}))"
4✔
2530
        subset = flat_index
4✔
2531

2532
        t_task = self.builder.add_tasklet(
4✔
2533
            block_assign, TaskletCode.assign, ["_in"], ["_out"]
2534
        )
2535
        self.builder.add_memlet(
4✔
2536
            block_assign, t_const, "void", t_task, "_in", "", element_type
2537
        )
2538
        self.builder.add_memlet(block_assign, t_task, "_out", t_arr, "void", subset)
4✔
2539

2540
        self.builder.end_if()
4✔
2541
        self.builder.end_for()
4✔
2542

2543
        return ptr_name
4✔
2544

2545
    def _handle_numpy_binary_op(self, node, func_name):
4✔
2546
        args = [self.visit(arg) for arg in node.args]
4✔
2547
        if len(args) != 2:
4✔
2548
            raise NotImplementedError(
×
2549
                f"Numpy function {func_name} requires 2 arguments"
2550
            )
2551

2552
        op_map = {
4✔
2553
            "add": "add",
2554
            "subtract": "sub",
2555
            "multiply": "mul",
2556
            "divide": "div",
2557
            "power": "pow",
2558
            "minimum": "min",
2559
            "maximum": "max",
2560
        }
2561
        return self._handle_array_binary_op(op_map[func_name], args[0], args[1])
4✔
2562

2563
    def _handle_numpy_where(self, node, func_name):
4✔
2564
        """Handle np.where(condition, x, y) - elementwise ternary selection.
2565

2566
        Returns an array where elements are taken from x where condition is True,
2567
        and from y where condition is False.
2568
        """
2569
        if len(node.args) != 3:
4✔
2570
            raise NotImplementedError("np.where requires 3 arguments (condition, x, y)")
×
2571

2572
        # Visit all arguments
2573
        cond_name = self.visit(node.args[0])
4✔
2574
        x_name = self.visit(node.args[1])
4✔
2575
        y_name = self.visit(node.args[2])
4✔
2576

2577
        # Determine output shape from the array arguments
2578
        # Priority: condition > y > x (since x might be scalar 0)
2579
        shape = []
4✔
2580
        dtype = Scalar(PrimitiveType.Double)
4✔
2581

2582
        # Check condition shape
2583
        if cond_name in self.array_info:
4✔
2584
            shape = self.array_info[cond_name]["shapes"]
4✔
2585

2586
        # If condition is scalar, check y
2587
        if not shape and y_name in self.array_info:
4✔
2588
            shape = self.array_info[y_name]["shapes"]
×
2589

2590
        # If y is scalar, check x
2591
        if not shape and x_name in self.array_info:
4✔
2592
            shape = self.array_info[x_name]["shapes"]
×
2593

2594
        if not shape:
4✔
2595
            raise NotImplementedError("np.where requires at least one array argument")
×
2596

2597
        # Determine dtype from y (since x might be scalar 0)
2598
        if y_name in self.symbol_table:
4✔
2599
            y_type = self.symbol_table[y_name]
4✔
2600
            if isinstance(y_type, Pointer) and y_type.has_pointee_type():
4✔
2601
                dtype = y_type.pointee_type
4✔
2602
            elif isinstance(y_type, Scalar):
×
2603
                dtype = y_type
×
2604

2605
        # Create output array
2606
        tmp_name = self._create_array_temp(shape, dtype)
4✔
2607

2608
        # Generate nested loops for the shape
2609
        loop_vars = []
4✔
2610
        for i, dim in enumerate(shape):
4✔
2611
            loop_var = f"_where_i{i}_{self._get_unique_id()}"
4✔
2612
            if not self.builder.exists(loop_var):
4✔
2613
                self.builder.add_container(loop_var, Scalar(PrimitiveType.Int64), False)
4✔
2614
                self.symbol_table[loop_var] = Scalar(PrimitiveType.Int64)
4✔
2615
            loop_vars.append(loop_var)
4✔
2616
            self.builder.begin_for(loop_var, "0", str(dim), "1")
4✔
2617

2618
        # Compute linear index
2619
        linear_idx = self._compute_linear_index(loop_vars, shape, tmp_name, len(shape))
4✔
2620

2621
        # Read condition value
2622
        cond_tmp = f"_where_cond_{self._get_unique_id()}"
4✔
2623
        self.builder.add_container(cond_tmp, Scalar(PrimitiveType.Bool), False)
4✔
2624
        self.symbol_table[cond_tmp] = Scalar(PrimitiveType.Bool)
4✔
2625

2626
        block_cond = self.builder.add_block()
4✔
2627
        if cond_name in self.array_info:
4✔
2628
            t_cond_arr = self.builder.add_access(block_cond, cond_name)
4✔
2629
            t_cond_out = self.builder.add_access(block_cond, cond_tmp)
4✔
2630
            t_cond_task = self.builder.add_tasklet(
4✔
2631
                block_cond, TaskletCode.assign, ["_in"], ["_out"]
2632
            )
2633
            self.builder.add_memlet(
4✔
2634
                block_cond, t_cond_arr, "void", t_cond_task, "_in", linear_idx
2635
            )
2636
            self.builder.add_memlet(
4✔
2637
                block_cond, t_cond_task, "_out", t_cond_out, "void", ""
2638
            )
2639
        else:
2640
            # Scalar condition - just use it directly
2641
            t_cond_src, cond_sub = self._add_read(block_cond, cond_name)
×
2642
            t_cond_out = self.builder.add_access(block_cond, cond_tmp)
×
2643
            t_cond_task = self.builder.add_tasklet(
×
2644
                block_cond, TaskletCode.assign, ["_in"], ["_out"]
2645
            )
2646
            self.builder.add_memlet(
×
2647
                block_cond, t_cond_src, "void", t_cond_task, "_in", cond_sub
2648
            )
2649
            self.builder.add_memlet(
×
2650
                block_cond, t_cond_task, "_out", t_cond_out, "void", ""
2651
            )
2652

2653
        # If-else based on condition
2654
        self.builder.begin_if(f"{cond_tmp} == true")
4✔
2655

2656
        # True branch: assign x
2657
        block_true = self.builder.add_block()
4✔
2658
        t_out_true = self.builder.add_access(block_true, tmp_name)
4✔
2659
        if x_name in self.array_info:
4✔
2660
            # x is an array
2661
            t_x = self.builder.add_access(block_true, x_name)
4✔
2662
            t_task_true = self.builder.add_tasklet(
4✔
2663
                block_true, TaskletCode.assign, ["_in"], ["_out"]
2664
            )
2665
            self.builder.add_memlet(
4✔
2666
                block_true, t_x, "void", t_task_true, "_in", linear_idx
2667
            )
2668
        else:
2669
            # x is a scalar
2670
            t_x, x_sub = self._add_read(block_true, x_name)
4✔
2671
            t_task_true = self.builder.add_tasklet(
4✔
2672
                block_true, TaskletCode.assign, ["_in"], ["_out"]
2673
            )
2674
            self.builder.add_memlet(block_true, t_x, "void", t_task_true, "_in", x_sub)
4✔
2675
        self.builder.add_memlet(
4✔
2676
            block_true, t_task_true, "_out", t_out_true, "void", linear_idx
2677
        )
2678

2679
        self.builder.begin_else()
4✔
2680

2681
        # False branch: assign y
2682
        block_false = self.builder.add_block()
4✔
2683
        t_out_false = self.builder.add_access(block_false, tmp_name)
4✔
2684
        if y_name in self.array_info:
4✔
2685
            # y is an array
2686
            t_y = self.builder.add_access(block_false, y_name)
4✔
2687
            t_task_false = self.builder.add_tasklet(
4✔
2688
                block_false, TaskletCode.assign, ["_in"], ["_out"]
2689
            )
2690
            self.builder.add_memlet(
4✔
2691
                block_false, t_y, "void", t_task_false, "_in", linear_idx
2692
            )
2693
        else:
2694
            # y is a scalar
2695
            t_y, y_sub = self._add_read(block_false, y_name)
4✔
2696
            t_task_false = self.builder.add_tasklet(
4✔
2697
                block_false, TaskletCode.assign, ["_in"], ["_out"]
2698
            )
2699
            self.builder.add_memlet(
4✔
2700
                block_false, t_y, "void", t_task_false, "_in", y_sub
2701
            )
2702
        self.builder.add_memlet(
4✔
2703
            block_false, t_task_false, "_out", t_out_false, "void", linear_idx
2704
        )
2705

2706
        self.builder.end_if()
4✔
2707

2708
        # Close all loops
2709
        for _ in loop_vars:
4✔
2710
            self.builder.end_for()
4✔
2711

2712
        return tmp_name
4✔
2713

2714
    def _handle_numpy_clip(self, node, func_name):
4✔
2715
        """Handle np.clip(a, a_min, a_max) - elementwise clipping.
2716

2717
        Clips array values to be within [a_min, a_max].
2718
        Implemented as: min(max(a, a_min), a_max)
2719

2720
        This uses the existing min/max elementwise operations.
2721
        """
2722
        if len(node.args) != 3:
4✔
NEW
2723
            raise NotImplementedError("np.clip requires 3 arguments (a, a_min, a_max)")
×
2724

2725
        # Visit the array argument
2726
        arr_name = self.visit(node.args[0])
4✔
2727
        # Visit the bound arguments (scalars or arrays)
2728
        a_min = self.visit(node.args[1])
4✔
2729
        a_max = self.visit(node.args[2])
4✔
2730

2731
        # First: tmp1 = max(arr, a_min) - ensures values are at least a_min
2732
        tmp1 = self._handle_array_binary_op("max", arr_name, a_min)
4✔
2733

2734
        # Second: result = min(tmp1, a_max) - ensures values are at most a_max
2735
        result = self._handle_array_binary_op("min", tmp1, a_max)
4✔
2736

2737
        return result
4✔
2738

2739
    def _handle_numpy_matmul_op(self, left_node, right_node):
4✔
2740
        return self._handle_matmul_helper(left_node, right_node)
4✔
2741

2742
    def _handle_numpy_matmul(self, node, func_name):
4✔
2743
        if len(node.args) != 2:
4✔
2744
            raise NotImplementedError("matmul/dot requires 2 arguments")
×
2745
        return self._handle_matmul_helper(node.args[0], node.args[1])
4✔
2746

2747
    def _handle_numpy_outer(self, node, func_name):
4✔
2748
        if len(node.args) != 2:
4✔
2749
            raise NotImplementedError("outer requires 2 arguments")
×
2750

2751
        arg0 = node.args[0]
4✔
2752
        arg1 = node.args[1]
4✔
2753

2754
        if not self.la_handler:
4✔
2755
            raise RuntimeError("LinearAlgebraHandler not initialized")
×
2756

2757
        res_a = self.la_handler.parse_arg(arg0)
4✔
2758
        res_b = self.la_handler.parse_arg(arg1)
4✔
2759

2760
        # Resolve standard names if parse_arg failed (likely complex expression)
2761
        if not res_a[0]:
4✔
2762
            left_name = self.visit(arg0)
×
2763
            arg0 = ast.Name(id=left_name)
×
2764
            res_a = self.la_handler.parse_arg(arg0)
×
2765

2766
        if not res_b[0]:
4✔
2767
            right_name = self.visit(arg1)
×
2768
            arg1 = ast.Name(id=right_name)
×
2769
            res_b = self.la_handler.parse_arg(arg1)
×
2770

2771
        name_a, subset_a, shape_a, indices_a = res_a
4✔
2772
        name_b, subset_b, shape_b, indices_b = res_b
4✔
2773

2774
        if not name_a or not name_b:
4✔
2775
            raise NotImplementedError("Could not resolve outer operands")
×
2776

2777
        def get_flattened_size_expr(name, indices, shapes):
4✔
2778
            # Simplified: if slice, we use parse_arg's returned `shapes` (which are dim sizes of the slice)
2779
            # And multiply them.
2780
            size_expr = "1"
4✔
2781
            for s in shapes:
4✔
2782
                if size_expr == "1":
4✔
2783
                    size_expr = str(s)
4✔
2784
                else:
2785
                    size_expr = f"({size_expr} * {str(s)})"
×
2786
            return size_expr
4✔
2787

2788
        m_expr = get_flattened_size_expr(name_a, indices_a, shape_a)
4✔
2789
        n_expr = get_flattened_size_expr(name_b, indices_b, shape_b)
4✔
2790

2791
        # Create temporary container
2792
        # Since outer usually promotes types or uses standard types, we default to double for now.
2793
        dtype = Scalar(PrimitiveType.Double)
4✔
2794

2795
        # Use helper to create array temp which handles symbol table and array info
2796
        tmp_name = self._create_array_temp([m_expr, n_expr], dtype)
4✔
2797

2798
        new_call_node = ast.Call(
4✔
2799
            func=node.func, args=[arg0, arg1], keywords=node.keywords
2800
        )
2801

2802
        self.la_handler.handle_outer(tmp_name, new_call_node)
4✔
2803

2804
        return tmp_name
4✔
2805

2806
    def _handle_ufunc_outer(self, node, ufunc_name):
4✔
2807
        """Handle np.add.outer, np.subtract.outer, np.multiply.outer, etc.
2808

2809
        These compute the outer operation for the given ufunc:
2810
        - np.add.outer(a, b) -> a[:, np.newaxis] + b (outer sum)
2811
        - np.subtract.outer(a, b) -> a[:, np.newaxis] - b (outer difference)
2812
        - np.multiply.outer(a, b) -> a[:, np.newaxis] * b (same as np.outer)
2813
        """
2814
        if len(node.args) != 2:
4✔
2815
            raise NotImplementedError(f"{ufunc_name}.outer requires 2 arguments")
×
2816

2817
        # For np.multiply.outer, use the existing GEMM-based outer handler
2818
        if ufunc_name == "multiply":
4✔
2819
            return self._handle_numpy_outer(node, "outer")
4✔
2820

2821
        # Map ufunc names to operation names and tasklet opcodes
2822
        op_map = {
4✔
2823
            "add": ("add", TaskletCode.fp_add, TaskletCode.int_add),
2824
            "subtract": ("sub", TaskletCode.fp_sub, TaskletCode.int_sub),
2825
            "divide": ("div", TaskletCode.fp_div, TaskletCode.int_sdiv),
2826
            "minimum": ("min", CMathFunction.fmin, TaskletCode.int_smin),
2827
            "maximum": ("max", CMathFunction.fmax, TaskletCode.int_smax),
2828
        }
2829

2830
        if ufunc_name not in op_map:
4✔
2831
            raise NotImplementedError(f"{ufunc_name}.outer not supported")
×
2832

2833
        op_name, fp_opcode, int_opcode = op_map[ufunc_name]
4✔
2834

2835
        # Use la_handler.parse_arg to properly handle sliced arrays
2836
        if not self.la_handler:
4✔
2837
            raise RuntimeError("LinearAlgebraHandler not initialized")
×
2838

2839
        arg0 = node.args[0]
4✔
2840
        arg1 = node.args[1]
4✔
2841

2842
        res_a = self.la_handler.parse_arg(arg0)
4✔
2843
        res_b = self.la_handler.parse_arg(arg1)
4✔
2844

2845
        # If parse_arg fails for complex expressions, try visiting and re-parsing
2846
        if not res_a[0]:
4✔
2847
            left_name = self.visit(arg0)
×
2848
            arg0 = ast.Name(id=left_name)
×
2849
            res_a = self.la_handler.parse_arg(arg0)
×
2850

2851
        if not res_b[0]:
4✔
2852
            right_name = self.visit(arg1)
×
2853
            arg1 = ast.Name(id=right_name)
×
2854
            res_b = self.la_handler.parse_arg(arg1)
×
2855

2856
        name_a, subset_a, shape_a, indices_a = res_a
4✔
2857
        name_b, subset_b, shape_b, indices_b = res_b
4✔
2858

2859
        if not name_a or not name_b:
4✔
2860
            raise NotImplementedError("Could not resolve ufunc outer operands")
×
2861

2862
        # Compute flattened sizes - outer treats inputs as 1D
2863
        def get_flattened_size_expr(shapes):
4✔
2864
            if not shapes:
4✔
2865
                return "1"
×
2866
            size_expr = str(shapes[0])
4✔
2867
            for s in shapes[1:]:
4✔
2868
                size_expr = f"({size_expr} * {str(s)})"
×
2869
            return size_expr
4✔
2870

2871
        m_expr = get_flattened_size_expr(shape_a)
4✔
2872
        n_expr = get_flattened_size_expr(shape_b)
4✔
2873

2874
        # Determine output dtype - infer from inputs or default to double
2875
        dtype_left = self._get_dtype(name_a)
4✔
2876
        dtype_right = self._get_dtype(name_b)
4✔
2877
        dtype = self._promote_dtypes(dtype_left, dtype_right)
4✔
2878

2879
        # Determine if we're working with integers
2880
        is_int = dtype.primitive_type in [
4✔
2881
            PrimitiveType.Int64,
2882
            PrimitiveType.Int32,
2883
            PrimitiveType.Int8,
2884
            PrimitiveType.Int16,
2885
            PrimitiveType.UInt64,
2886
            PrimitiveType.UInt32,
2887
            PrimitiveType.UInt8,
2888
            PrimitiveType.UInt16,
2889
        ]
2890

2891
        # Create output array with shape (M, N)
2892
        tmp_name = self._create_array_temp([m_expr, n_expr], dtype)
4✔
2893

2894
        # Generate unique loop variable names
2895
        i_var = self._get_temp_name("_outer_i_")
4✔
2896
        j_var = self._get_temp_name("_outer_j_")
4✔
2897

2898
        # Ensure loop variables exist
2899
        if not self.builder.exists(i_var):
4✔
2900
            self.builder.add_container(i_var, Scalar(PrimitiveType.Int64), False)
4✔
2901
            self.symbol_table[i_var] = Scalar(PrimitiveType.Int64)
4✔
2902
        if not self.builder.exists(j_var):
4✔
2903
            self.builder.add_container(j_var, Scalar(PrimitiveType.Int64), False)
4✔
2904
            self.symbol_table[j_var] = Scalar(PrimitiveType.Int64)
4✔
2905

2906
        # Helper function to compute the linear index for a sliced array access
2907
        def compute_linear_index(name, subset, indices, loop_var):
4✔
2908
            """
2909
            Compute linear index for accessing element loop_var of a sliced array.
2910

2911
            For array A with shape (N, M):
2912
            - A[:, k] (column k): linear_index = loop_var * M + k
2913
            - A[k, :] (row k): linear_index = k * M + loop_var
2914
            - A[:] (1D array): linear_index = loop_var
2915

2916
            The indices list contains AST nodes showing which dims are sliced vs fixed.
2917
            subset contains start indices for each dimension.
2918
            """
2919
            if not indices:
4✔
2920
                # Simple 1D array, no slicing
2921
                return loop_var
4✔
2922

2923
            info = self.array_info.get(name, {})
4✔
2924
            shapes = info.get("shapes", [])
4✔
2925
            ndim = info.get("ndim", len(shapes))
4✔
2926

2927
            if ndim == 0:
4✔
2928
                return loop_var
×
2929

2930
            # Compute strides (row-major order)
2931
            strides = []
4✔
2932
            current_stride = "1"
4✔
2933
            for i in range(ndim - 1, -1, -1):
4✔
2934
                strides.insert(0, current_stride)
4✔
2935
                if i > 0:
4✔
2936
                    dim_size = shapes[i] if i < len(shapes) else f"_{name}_shape_{i}"
4✔
2937
                    if current_stride == "1":
4✔
2938
                        current_stride = str(dim_size)
4✔
2939
                    else:
2940
                        current_stride = f"({current_stride} * {dim_size})"
×
2941

2942
            # Build linear index from subset and indices info
2943
            terms = []
4✔
2944
            loop_var_used = False
4✔
2945

2946
            for i, idx in enumerate(indices):
4✔
2947
                stride = strides[i] if i < len(strides) else "1"
4✔
2948
                start = subset[i] if i < len(subset) else "0"
4✔
2949

2950
                if isinstance(idx, ast.Slice):
4✔
2951
                    # This dimension is sliced - use loop_var
2952
                    if stride == "1":
4✔
2953
                        term = f"({start} + {loop_var})"
4✔
2954
                    else:
2955
                        term = f"(({start} + {loop_var}) * {stride})"
4✔
2956
                    loop_var_used = True
4✔
2957
                else:
2958
                    # This dimension has a fixed index
2959
                    if stride == "1":
4✔
2960
                        term = start
4✔
2961
                    else:
2962
                        term = f"({start} * {stride})"
4✔
2963

2964
                terms.append(term)
4✔
2965

2966
            # Sum all terms
2967
            if not terms:
4✔
2968
                return loop_var
×
2969

2970
            result = terms[0]
4✔
2971
            for t in terms[1:]:
4✔
2972
                result = f"({result} + {t})"
4✔
2973

2974
            return result
4✔
2975

2976
        # Create nested for loops: for i in range(M): for j in range(N): C[i,j] = A[i] op B[j]
2977
        self.builder.begin_for(i_var, "0", m_expr, "1")
4✔
2978
        self.builder.begin_for(j_var, "0", n_expr, "1")
4✔
2979

2980
        # Create the assignment block: C[i, j] = A[i] op B[j]
2981
        block = self.builder.add_block()
4✔
2982

2983
        # Add access nodes
2984
        t_a = self.builder.add_access(block, name_a)
4✔
2985
        t_b = self.builder.add_access(block, name_b)
4✔
2986
        t_c = self.builder.add_access(block, tmp_name)
4✔
2987

2988
        # Determine tasklet type based on operation
2989
        if ufunc_name in ["minimum", "maximum"]:
4✔
2990
            # Use intrinsic for min/max
2991
            if is_int:
4✔
2992
                t_task = self.builder.add_tasklet(
4✔
2993
                    block, int_opcode, ["_in1", "_in2"], ["_out"]
2994
                )
2995
            else:
2996
                t_task = self.builder.add_cmath(block, fp_opcode)
4✔
2997
        else:
2998
            # Use regular tasklet for arithmetic ops
2999
            tasklet_code = int_opcode if is_int else fp_opcode
4✔
3000
            t_task = self.builder.add_tasklet(
4✔
3001
                block, tasklet_code, ["_in1", "_in2"], ["_out"]
3002
            )
3003

3004
        # Compute the linear index for A[i]
3005
        a_index = compute_linear_index(name_a, subset_a, indices_a, i_var)
4✔
3006

3007
        # Compute the linear index for B[j]
3008
        b_index = compute_linear_index(name_b, subset_b, indices_b, j_var)
4✔
3009

3010
        # Connect A[i + offset_a] -> tasklet
3011
        self.builder.add_memlet(block, t_a, "void", t_task, "_in1", a_index)
4✔
3012

3013
        # Connect B[j + offset_b] -> tasklet
3014
        self.builder.add_memlet(block, t_b, "void", t_task, "_in2", b_index)
4✔
3015

3016
        # Connect tasklet -> C[i * N + j] (linear index for 2D output)
3017
        flat_index = f"(({i_var}) * ({n_expr}) + ({j_var}))"
4✔
3018
        self.builder.add_memlet(block, t_task, "_out", t_c, "void", flat_index)
4✔
3019

3020
        self.builder.end_for()  # end j loop
4✔
3021
        self.builder.end_for()  # end i loop
4✔
3022

3023
        return tmp_name
4✔
3024

3025
    def _op_symbol(self, op_name):
4✔
3026
        """Convert operation name to symbol."""
3027
        symbols = {
×
3028
            "add": "+",
3029
            "sub": "-",
3030
            "mul": "*",
3031
            "div": "/",
3032
            "min": "min",  # Will need special handling
3033
            "max": "max",  # Will need special handling
3034
        }
3035
        return symbols.get(op_name, op_name)
×
3036

3037
    def _handle_matmul_helper(self, left_node, right_node):
4✔
3038
        if not self.la_handler:
4✔
3039
            raise RuntimeError("LinearAlgebraHandler not initialized")
×
3040

3041
        res_a = self.la_handler.parse_arg(left_node)
4✔
3042
        res_b = self.la_handler.parse_arg(right_node)
4✔
3043

3044
        if not res_a[0]:
4✔
3045
            left_name = self.visit(left_node)
×
3046
            left_node = ast.Name(id=left_name)
×
3047
            res_a = self.la_handler.parse_arg(left_node)
×
3048

3049
        if not res_b[0]:
4✔
3050
            right_name = self.visit(right_node)
4✔
3051
            right_node = ast.Name(id=right_name)
4✔
3052
            res_b = self.la_handler.parse_arg(right_node)
4✔
3053

3054
        name_a, subset_a, shape_a, indices_a = res_a
4✔
3055
        name_b, subset_b, shape_b, indices_b = res_b
4✔
3056

3057
        if not name_a or not name_b:
4✔
3058
            raise NotImplementedError("Could not resolve matmul operands")
×
3059

3060
        real_shape_a = shape_a
4✔
3061
        real_shape_b = shape_b
4✔
3062

3063
        ndim_a = len(real_shape_a)
4✔
3064
        ndim_b = len(real_shape_b)
4✔
3065

3066
        output_shape = []
4✔
3067
        is_scalar = False
4✔
3068

3069
        if ndim_a == 1 and ndim_b == 1:
4✔
3070
            is_scalar = True
4✔
3071
            output_shape = []
4✔
3072
        elif ndim_a == 2 and ndim_b == 2:
4✔
3073
            output_shape = [real_shape_a[0], real_shape_b[1]]
4✔
3074
        elif ndim_a == 2 and ndim_b == 1:
4✔
3075
            output_shape = [real_shape_a[0]]
4✔
3076
        elif ndim_a == 1 and ndim_b == 2:
4✔
3077
            output_shape = [real_shape_b[1]]
×
3078
        elif ndim_a > 2 or ndim_b > 2:
4✔
3079
            if ndim_a == ndim_b:
4✔
3080
                output_shape = list(real_shape_a[:-2]) + [
4✔
3081
                    real_shape_a[-2],
3082
                    real_shape_b[-1],
3083
                ]
3084
            else:
3085
                raise NotImplementedError(
×
3086
                    "Broadcasting with different ranks not fully supported yet"
3087
                )
3088
        else:
3089
            raise NotImplementedError(
×
3090
                f"Matmul with ranks {ndim_a} and {ndim_b} not supported"
3091
            )
3092

3093
        dtype = Scalar(PrimitiveType.Double)
4✔
3094

3095
        if is_scalar:
4✔
3096
            tmp_name = f"_tmp_{self._get_unique_id()}"
4✔
3097
            self.builder.add_container(tmp_name, dtype, False)
4✔
3098
            self.symbol_table[tmp_name] = dtype
4✔
3099
        else:
3100
            tmp_name = self._create_array_temp(output_shape, dtype)
4✔
3101

3102
        if ndim_a > 2 or ndim_b > 2:
4✔
3103
            # Generate loops for broadcasting
3104
            batch_dims = ndim_a - 2
4✔
3105
            loop_vars = []
4✔
3106

3107
            for i in range(batch_dims):
4✔
3108
                loop_var = f"_i{self._get_unique_id()}"
4✔
3109
                self.builder.add_container(loop_var, Scalar(PrimitiveType.Int64), False)
4✔
3110
                loop_vars.append(loop_var)
4✔
3111
                dim_size = real_shape_a[i]
4✔
3112
                self.builder.begin_for(loop_var, "0", str(dim_size), "1")
4✔
3113

3114
            def make_slice(name, indices):
4✔
3115
                elts = []
4✔
3116
                for idx in indices:
4✔
3117
                    if idx == ":":
4✔
3118
                        elts.append(ast.Slice())
4✔
3119
                    else:
3120
                        elts.append(ast.Name(id=idx))
4✔
3121

3122
                return ast.Subscript(
4✔
3123
                    value=ast.Name(id=name), slice=ast.Tuple(elts=elts), ctx=ast.Load()
3124
                )
3125

3126
            indices = loop_vars + [":", ":"]
4✔
3127
            slice_a = make_slice(name_a, indices)
4✔
3128
            slice_b = make_slice(name_b, indices)
4✔
3129
            slice_c = make_slice(tmp_name, indices)
4✔
3130

3131
            self.la_handler.handle_gemm(
4✔
3132
                slice_c, ast.BinOp(left=slice_a, op=ast.MatMult(), right=slice_b)
3133
            )
3134

3135
            for _ in range(batch_dims):
4✔
3136
                self.builder.end_for()
4✔
3137
        else:
3138
            if is_scalar:
4✔
3139
                self.la_handler.handle_dot(
4✔
3140
                    tmp_name,
3141
                    ast.BinOp(left=left_node, op=ast.MatMult(), right=right_node),
3142
                )
3143
            else:
3144
                self.la_handler.handle_gemm(
4✔
3145
                    tmp_name,
3146
                    ast.BinOp(left=left_node, op=ast.MatMult(), right=right_node),
3147
                )
3148

3149
        return tmp_name
4✔
3150

3151
    def _handle_numpy_unary_op(self, node, func_name):
4✔
3152
        args = [self.visit(arg) for arg in node.args]
4✔
3153
        if len(args) != 1:
4✔
3154
            raise NotImplementedError(f"Numpy function {func_name} requires 1 argument")
×
3155

3156
        op_name = func_name
4✔
3157
        if op_name == "absolute":
4✔
3158
            op_name = "abs"
×
3159

3160
        return self._handle_array_unary_op(op_name, args[0])
4✔
3161

3162
    def _handle_numpy_reduce(self, node, func_name):
4✔
3163
        args = node.args
4✔
3164
        keywords = {kw.arg: kw.value for kw in node.keywords}
4✔
3165

3166
        array_node = args[0]
4✔
3167
        array_name = self.visit(array_node)
4✔
3168

3169
        if array_name not in self.array_info:
4✔
3170
            raise ValueError(f"Reduction input must be an array, got {array_name}")
×
3171

3172
        input_shape = self.array_info[array_name]["shapes"]
4✔
3173
        ndim = len(input_shape)
4✔
3174

3175
        axis = None
4✔
3176
        if len(args) > 1:
4✔
3177
            axis = args[1]
×
3178
        elif "axis" in keywords:
4✔
3179
            axis = keywords["axis"]
4✔
3180

3181
        keepdims = False
4✔
3182
        if "keepdims" in keywords:
4✔
3183
            keepdims_node = keywords["keepdims"]
4✔
3184
            if isinstance(keepdims_node, ast.Constant):
4✔
3185
                keepdims = bool(keepdims_node.value)
4✔
3186

3187
        axes = []
4✔
3188
        if axis is None:
4✔
3189
            axes = list(range(ndim))
4✔
3190
        elif isinstance(axis, ast.Constant):  # Single axis
4✔
3191
            val = axis.value
4✔
3192
            if val < 0:
4✔
3193
                val += ndim
×
3194
            axes = [val]
4✔
3195
        elif isinstance(axis, ast.Tuple):  # Multiple axes
×
3196
            for elt in axis.elts:
×
3197
                if isinstance(elt, ast.Constant):
×
3198
                    val = elt.value
×
3199
                    if val < 0:
×
3200
                        val += ndim
×
3201
                    axes.append(val)
×
3202
        elif (
×
3203
            isinstance(axis, ast.UnaryOp)
3204
            and isinstance(axis.op, ast.USub)
3205
            and isinstance(axis.operand, ast.Constant)
3206
        ):
3207
            val = -axis.operand.value
×
3208
            if val < 0:
×
3209
                val += ndim
×
3210
            axes = [val]
×
3211
        else:
3212
            # Try to evaluate simple expression
3213
            try:
×
3214
                val = int(self.visit(axis))
×
3215
                if val < 0:
×
3216
                    val += ndim
×
3217
                axes = [val]
×
3218
            except:
×
3219
                raise NotImplementedError("Dynamic axis not supported")
×
3220

3221
        # Calculate output shape
3222
        output_shape = []
4✔
3223
        for i in range(ndim):
4✔
3224
            if i in axes:
4✔
3225
                if keepdims:
4✔
3226
                    output_shape.append("1")
4✔
3227
            else:
3228
                output_shape.append(input_shape[i])
4✔
3229

3230
        dtype = self._get_dtype(array_name)
4✔
3231

3232
        if not output_shape:
4✔
3233
            tmp_name = f"_tmp_{self._get_unique_id()}"
4✔
3234
            self.builder.add_container(tmp_name, dtype, False)
4✔
3235
            self.symbol_table[tmp_name] = dtype
4✔
3236
            self.array_info[tmp_name] = {"ndim": 0, "shapes": []}
4✔
3237
        else:
3238
            tmp_name = self._create_array_temp(output_shape, dtype)
4✔
3239

3240
        self.builder.add_reduce_op(
4✔
3241
            func_name, array_name, tmp_name, input_shape, axes, keepdims
3242
        )
3243

3244
        return tmp_name
4✔
3245

3246
    def _handle_numpy_astype(self, node, array_name):
4✔
3247
        """Handle numpy array.astype(dtype) method calls."""
3248
        if len(node.args) < 1:
4✔
3249
            raise ValueError("astype requires at least one argument (dtype)")
×
3250

3251
        dtype_arg = node.args[0]
4✔
3252
        target_dtype = self._map_numpy_dtype(dtype_arg)
4✔
3253

3254
        # Get input array shape
3255
        if array_name not in self.array_info:
4✔
3256
            raise ValueError(f"Array {array_name} not found in array_info")
×
3257

3258
        input_shape = self.array_info[array_name]["shapes"]
4✔
3259

3260
        # Create output array with target dtype
3261
        tmp_name = self._create_array_temp(input_shape, target_dtype)
4✔
3262

3263
        # Add cast operation
3264
        self.builder.add_cast_op(
4✔
3265
            array_name, tmp_name, input_shape, target_dtype.primitive_type
3266
        )
3267

3268
        return tmp_name
4✔
3269

3270
    def _handle_numpy_copy(self, node, array_name):
4✔
3271
        """Handle numpy array.copy() method calls using memcpy."""
3272
        if array_name not in self.array_info:
4✔
3273
            raise ValueError(f"Array {array_name} not found in array_info")
×
3274

3275
        input_shape = self.array_info[array_name]["shapes"]
4✔
3276

3277
        # Get element type from array
3278
        element_type = Scalar(PrimitiveType.Double)  # Default
4✔
3279
        if array_name in self.symbol_table:
4✔
3280
            sym_type = self.symbol_table[array_name]
4✔
3281
            if isinstance(sym_type, Pointer) and sym_type.has_pointee_type():
4✔
3282
                element_type = sym_type.pointee_type
4✔
3283

3284
        # Create output array with same dtype
3285
        tmp_name = self._create_array_temp(input_shape, element_type)
4✔
3286

3287
        # Calculate total number of bytes to copy
3288
        # count = total_elements * sizeof(element_type)
3289
        total_elements = " * ".join([f"({s})" for s in input_shape])
4✔
3290
        element_size = self.builder.get_sizeof(element_type)
4✔
3291
        count_expr = f"({total_elements}) * ({element_size})"
4✔
3292

3293
        # Get pointer type for memlets
3294
        ptr_type = Pointer(element_type)
4✔
3295

3296
        # Add memcpy operation
3297
        block = self.builder.add_block()
4✔
3298
        t_src = self.builder.add_access(block, array_name)
4✔
3299
        t_dst = self.builder.add_access(block, tmp_name)
4✔
3300
        t_memcpy = self.builder.add_memcpy(block, count_expr)
4✔
3301

3302
        # Connect source and destination
3303
        self.builder.add_memlet(block, t_src, "void", t_memcpy, "_src", "", ptr_type)
4✔
3304
        self.builder.add_memlet(block, t_memcpy, "_dst", t_dst, "void", "", ptr_type)
4✔
3305

3306
        return tmp_name
4✔
3307

3308
    def _handle_scipy_softmax(self, node, func_name):
4✔
3309
        args = node.args
4✔
3310
        keywords = {kw.arg: kw.value for kw in node.keywords}
4✔
3311

3312
        array_node = args[0]
4✔
3313
        array_name = self.visit(array_node)
4✔
3314

3315
        if array_name not in self.array_info:
4✔
3316
            raise ValueError(f"Softmax input must be an array, got {array_name}")
×
3317

3318
        input_shape = self.array_info[array_name]["shapes"]
4✔
3319
        ndim = len(input_shape)
4✔
3320

3321
        axis = None
4✔
3322
        if len(args) > 1:
4✔
3323
            axis = args[1]
×
3324
        elif "axis" in keywords:
4✔
3325
            axis = keywords["axis"]
4✔
3326

3327
        axes = []
4✔
3328
        if axis is None:
4✔
3329
            axes = list(range(ndim))
4✔
3330
        elif isinstance(axis, ast.Constant):  # Single axis
4✔
3331
            val = axis.value
4✔
3332
            if val < 0:
4✔
3333
                val += ndim
×
3334
            axes = [val]
4✔
3335
        elif isinstance(axis, ast.Tuple):  # Multiple axes
×
3336
            for elt in axis.elts:
×
3337
                if isinstance(elt, ast.Constant):
×
3338
                    val = elt.value
×
3339
                    if val < 0:
×
3340
                        val += ndim
×
3341
                    axes.append(val)
×
3342
        elif (
×
3343
            isinstance(axis, ast.UnaryOp)
3344
            and isinstance(axis.op, ast.USub)
3345
            and isinstance(axis.operand, ast.Constant)
3346
        ):
3347
            val = -axis.operand.value
×
3348
            if val < 0:
×
3349
                val += ndim
×
3350
            axes = [val]
×
3351
        else:
3352
            # Try to evaluate simple expression
3353
            try:
×
3354
                val = int(self.visit(axis))
×
3355
                if val < 0:
×
3356
                    val += ndim
×
3357
                axes = [val]
×
3358
            except:
×
3359
                raise NotImplementedError("Dynamic axis not supported")
×
3360

3361
        # Create output array
3362
        # Assume double for now, or infer from input
3363
        dtype = Scalar(PrimitiveType.Double)  # TODO: infer
4✔
3364

3365
        tmp_name = self._create_array_temp(input_shape, dtype)
4✔
3366

3367
        self.builder.add_reduce_op(
4✔
3368
            func_name, array_name, tmp_name, input_shape, axes, False
3369
        )
3370

3371
        return tmp_name
4✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc