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

daisytuner / docc / 22941264241

11 Mar 2026 07:16AM UTC coverage: 63.677% (-0.9%) from 64.621%
22941264241

Pull #569

github

web-flow
Merge fb0bb9692 into af8bb4c54
Pull Request #569: HipTarget

191 of 803 new or added lines in 15 files covered. (23.79%)

595 existing lines in 6 files now uncovered.

24699 of 38788 relevant lines covered (63.68%)

370.82 hits per line

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

67.89
/python/docc/python/ast_parser.py
1
import ast
4✔
2
import copy
4✔
3
import inspect
4✔
4
import textwrap
4✔
5
from docc.sdfg import (
4✔
6
    Scalar,
7
    PrimitiveType,
8
    Pointer,
9
    TaskletCode,
10
    DebugInfo,
11
    Structure,
12
    CMathFunction,
13
    Tensor,
14
)
15
from docc.python.ast_utils import (
4✔
16
    SliceRewriter,
17
    get_debug_info,
18
    contains_ufunc_outer,
19
    normalize_negative_index,
20
)
21
from docc.python.types import (
4✔
22
    sdfg_type_from_type,
23
    element_type_from_sdfg_type,
24
)
25
from docc.python.functions.numpy import NumPyHandler
4✔
26
from docc.python.functions.math import MathHandler
4✔
27
from docc.python.functions.python import PythonHandler
4✔
28
from docc.python.functions.scipy import SciPyHandler
4✔
29
from docc.python.memory import ManagedMemoryHandler
4✔
30

31

32
class ASTParser(ast.NodeVisitor):
4✔
33
    def __init__(
4✔
34
        self,
35
        builder,
36
        tensor_table,
37
        container_table,
38
        filename="",
39
        function_name="",
40
        infer_return_type=False,
41
        globals_dict=None,
42
        unique_counter_ref=None,
43
        structure_member_info=None,
44
        memory_handler=None,
45
    ):
46
        self.builder = builder
4✔
47

48
        # Lookup tables for variables
49
        self.tensor_table = tensor_table
4✔
50
        self.container_table = container_table
4✔
51

52
        # Debug info
53
        self.filename = filename
4✔
54
        self.function_name = function_name
4✔
55

56
        # Context
57
        self.infer_return_type = infer_return_type
4✔
58
        self.globals_dict = globals_dict if globals_dict is not None else {}
4✔
59
        self._unique_counter_ref = (
4✔
60
            unique_counter_ref if unique_counter_ref is not None else [0]
61
        )
62
        self._access_cache = {}
4✔
63
        self.structure_member_info = (
4✔
64
            structure_member_info if structure_member_info is not None else {}
65
        )
66
        self.captured_return_shapes = {}  # Map param name to shape string list
4✔
67
        self.captured_return_strides = {}  # Map param name to stride string list
4✔
68
        self.shapes_runtime_info = (
4✔
69
            {}
70
        )  # Map array name to runtime shapes (separate from Tensor)
71

72
        # Memory manager for hoisted allocations (shared with inline parsers)
73
        self.memory_handler = (
4✔
74
            memory_handler
75
            if memory_handler is not None
76
            else ManagedMemoryHandler(builder)
77
        )
78

79
        # Initialize handlers - they receive 'self' to access expression visitor methods
80
        self.numpy_visitor = NumPyHandler(self)
4✔
81
        self.math_handler = MathHandler(self)
4✔
82
        self.python_handler = PythonHandler(self)
4✔
83
        self.scipy_handler = SciPyHandler(self)
4✔
84

85
    def visit_Constant(self, node):
4✔
86
        if isinstance(node.value, bool):
4✔
87
            return "true" if node.value else "false"
4✔
88
        return str(node.value)
4✔
89

90
    def visit_Name(self, node):
4✔
91
        name = node.id
4✔
92
        if name not in self.container_table and self.globals_dict is not None:
4✔
93
            if name in self.globals_dict:
4✔
94
                val = self.globals_dict[name]
4✔
95
                if isinstance(val, (int, float)):
4✔
96
                    return str(val)
4✔
97
        return name
4✔
98

99
    def visit_Add(self, node):
4✔
100
        return "+"
4✔
101

102
    def visit_Sub(self, node):
4✔
103
        return "-"
4✔
104

105
    def visit_Mult(self, node):
4✔
106
        return "*"
4✔
107

108
    def visit_Div(self, node):
4✔
109
        return "/"
4✔
110

111
    def visit_FloorDiv(self, node):
4✔
112
        return "//"
4✔
113

114
    def visit_Mod(self, node):
4✔
115
        return "%"
4✔
116

117
    def visit_Pow(self, node):
4✔
118
        return "**"
4✔
119

120
    def visit_Eq(self, node):
4✔
121
        return "=="
4✔
122

123
    def visit_NotEq(self, node):
4✔
124
        return "!="
×
125

126
    def visit_Lt(self, node):
4✔
127
        return "<"
4✔
128

129
    def visit_LtE(self, node):
4✔
130
        return "<="
×
131

132
    def visit_Gt(self, node):
4✔
133
        return ">"
4✔
134

135
    def visit_GtE(self, node):
4✔
136
        return ">="
4✔
137

138
    def visit_And(self, node):
4✔
139
        return "&"
4✔
140

141
    def visit_Or(self, node):
4✔
142
        return "|"
4✔
143

144
    def visit_BitAnd(self, node):
4✔
145
        return "&"
4✔
146

147
    def visit_BitOr(self, node):
4✔
148
        return "|"
4✔
149

150
    def visit_BitXor(self, node):
4✔
151
        return "^"
4✔
152

153
    def visit_LShift(self, node):
4✔
154
        return "<<"
×
155

156
    def visit_RShift(self, node):
4✔
157
        return ">>"
×
158

159
    def visit_Not(self, node):
4✔
160
        return "!"
4✔
161

162
    def visit_USub(self, node):
4✔
163
        return "-"
4✔
164

165
    def visit_UAdd(self, node):
4✔
166
        return "+"
×
167

168
    def visit_Invert(self, node):
4✔
169
        return "~"
4✔
170

171
    def visit_BoolOp(self, node):
4✔
172
        op = self.visit(node.op)
4✔
173
        values = [f"({self.visit(v)} != 0)" for v in node.values]
4✔
174
        expr_str = f"{f' {op} '.join(values)}"
4✔
175

176
        tmp_name = self.builder.find_new_name()
4✔
177
        dtype = Scalar(PrimitiveType.Bool)
4✔
178
        self.builder.add_container(tmp_name, dtype, False)
4✔
179

180
        self.builder.begin_if(expr_str)
4✔
181
        self._add_assign_constant(tmp_name, "true", dtype)
4✔
182
        self.builder.begin_else()
4✔
183
        self._add_assign_constant(tmp_name, "false", dtype)
4✔
184
        self.builder.end_if()
4✔
185

186
        self.container_table[tmp_name] = dtype
4✔
187
        return tmp_name
4✔
188

189
    def visit_UnaryOp(self, node):
4✔
190
        if (
4✔
191
            isinstance(node.op, ast.USub)
192
            and isinstance(node.operand, ast.Constant)
193
            and isinstance(node.operand.value, (int, float))
194
        ):
195
            return f"-{node.operand.value}"
4✔
196

197
        op = self.visit(node.op)
4✔
198
        operand = self.visit(node.operand)
4✔
199

200
        if operand in self.tensor_table and op == "-":
4✔
201
            return self.numpy_visitor.handle_array_negate(operand)
4✔
202

203
        assert operand in self.container_table, f"Undefined variable: {operand}"
4✔
204
        tmp_name = self.builder.find_new_name()
4✔
205
        dtype = self.container_table[operand]
4✔
206
        self.builder.add_container(tmp_name, dtype, False)
4✔
207
        self.container_table[tmp_name] = dtype
4✔
208

209
        block = self.builder.add_block()
4✔
210
        t_src, src_sub = self._add_read(block, operand)
4✔
211
        t_dst = self.builder.add_access(block, tmp_name)
4✔
212

213
        if isinstance(node.op, ast.Not):
4✔
214
            t_const = self.builder.add_constant(
4✔
215
                block, "true", Scalar(PrimitiveType.Bool)
216
            )
217
            t_task = self.builder.add_tasklet(
4✔
218
                block, TaskletCode.int_xor, ["_in1", "_in2"], ["_out"]
219
            )
220
            self.builder.add_memlet(block, t_src, "void", t_task, "_in1", src_sub)
4✔
221
            self.builder.add_memlet(block, t_const, "void", t_task, "_in2", "")
4✔
222
            self.builder.add_memlet(block, t_task, "_out", t_dst, "void", "")
4✔
223
        elif op == "-":
4✔
224
            if dtype.primitive_type == PrimitiveType.Int64:
4✔
225
                t_const = self.builder.add_constant(block, "0", dtype)
4✔
226
                t_task = self.builder.add_tasklet(
4✔
227
                    block, TaskletCode.int_sub, ["_in1", "_in2"], ["_out"]
228
                )
229
                self.builder.add_memlet(block, t_const, "void", t_task, "_in1", "")
4✔
230
                self.builder.add_memlet(block, t_src, "void", t_task, "_in2", src_sub)
4✔
231
                self.builder.add_memlet(block, t_task, "_out", t_dst, "void", "")
4✔
232
            else:
233
                t_task = self.builder.add_tasklet(
4✔
234
                    block, TaskletCode.fp_neg, ["_in"], ["_out"]
235
                )
236
                self.builder.add_memlet(block, t_src, "void", t_task, "_in", src_sub)
4✔
237
                self.builder.add_memlet(block, t_task, "_out", t_dst, "void", "")
4✔
238
        elif op == "~":
4✔
239
            t_const = self.builder.add_constant(
4✔
240
                block, "-1", Scalar(PrimitiveType.Int64)
241
            )
242
            t_task = self.builder.add_tasklet(
4✔
243
                block, TaskletCode.int_xor, ["_in1", "_in2"], ["_out"]
244
            )
245
            self.builder.add_memlet(block, t_src, "void", t_task, "_in1", src_sub)
4✔
246
            self.builder.add_memlet(block, t_const, "void", t_task, "_in2", "")
4✔
247
            self.builder.add_memlet(block, t_task, "_out", t_dst, "void", "")
4✔
248
        else:
249
            t_task = self.builder.add_tasklet(
×
250
                block, TaskletCode.assign, ["_in"], ["_out"]
251
            )
252
            self.builder.add_memlet(block, t_src, "void", t_task, "_in", src_sub)
×
253
            self.builder.add_memlet(block, t_task, "_out", t_dst, "void", "")
×
254

255
        return tmp_name
4✔
256

257
    def visit_BinOp(self, node):
4✔
258
        if isinstance(node.op, ast.MatMult):
4✔
259
            return self.numpy_visitor.handle_numpy_matmul_op(node.left, node.right)
4✔
260

261
        left = self.visit(node.left)
4✔
262
        op = self.visit(node.op)
4✔
263
        right = self.visit(node.right)
4✔
264

265
        left_is_array = left in self.tensor_table
4✔
266
        right_is_array = right in self.tensor_table
4✔
267

268
        if left_is_array or right_is_array:
4✔
269
            op_map = {"+": "add", "-": "sub", "*": "mul", "/": "div", "**": "pow"}
4✔
270
            if op in op_map:
4✔
271
                return self.numpy_visitor.handle_array_binary_op(
4✔
272
                    op_map[op], left, right
273
                )
274
            else:
275
                raise NotImplementedError(f"Array operation {op} not supported")
×
276

277
        tmp_name = self.builder.find_new_name()
4✔
278

279
        left_is_int = self._is_int(left)
4✔
280
        right_is_int = self._is_int(right)
4✔
281
        dtype = Scalar(PrimitiveType.Double)
4✔
282
        if left_is_int and right_is_int and op not in ["/", "**"]:
4✔
283
            dtype = Scalar(PrimitiveType.Int64)
4✔
284

285
        if not self.builder.exists(tmp_name):
4✔
286
            self.builder.add_container(tmp_name, dtype, False)
4✔
287
            self.container_table[tmp_name] = dtype
4✔
288

289
        real_left = left
4✔
290
        real_right = right
4✔
291
        if dtype.primitive_type == PrimitiveType.Double:
4✔
292
            if left_is_int:
4✔
293
                left_cast = self.builder.find_new_name()
4✔
294
                self.builder.add_container(
4✔
295
                    left_cast, Scalar(PrimitiveType.Double), False
296
                )
297
                self.container_table[left_cast] = Scalar(PrimitiveType.Double)
4✔
298

299
                c_block = self.builder.add_block()
4✔
300
                t_src, src_sub = self._add_read(c_block, left)
4✔
301
                t_dst = self.builder.add_access(c_block, left_cast)
4✔
302
                t_task = self.builder.add_tasklet(
4✔
303
                    c_block, TaskletCode.assign, ["_in"], ["_out"]
304
                )
305
                self.builder.add_memlet(c_block, t_src, "void", t_task, "_in", src_sub)
4✔
306
                self.builder.add_memlet(c_block, t_task, "_out", t_dst, "void", "")
4✔
307

308
                real_left = left_cast
4✔
309

310
            if right_is_int:
4✔
311
                right_cast = self.builder.find_new_name()
4✔
312
                self.builder.add_container(
4✔
313
                    right_cast, Scalar(PrimitiveType.Double), False
314
                )
315
                self.container_table[right_cast] = Scalar(PrimitiveType.Double)
4✔
316

317
                c_block = self.builder.add_block()
4✔
318
                t_src, src_sub = self._add_read(c_block, right)
4✔
319
                t_dst = self.builder.add_access(c_block, right_cast)
4✔
320
                t_task = self.builder.add_tasklet(
4✔
321
                    c_block, TaskletCode.assign, ["_in"], ["_out"]
322
                )
323
                self.builder.add_memlet(c_block, t_src, "void", t_task, "_in", src_sub)
4✔
324
                self.builder.add_memlet(c_block, t_task, "_out", t_dst, "void", "")
4✔
325

326
                real_right = right_cast
4✔
327

328
        if op == "**":
4✔
329
            block = self.builder.add_block()
4✔
330
            t_left, left_sub = self._add_read(block, real_left)
4✔
331
            t_right, right_sub = self._add_read(block, real_right)
4✔
332
            t_out = self.builder.add_access(block, tmp_name)
4✔
333

334
            t_task = self.builder.add_cmath(
4✔
335
                block, CMathFunction.pow, dtype.primitive_type
336
            )
337
            self.builder.add_memlet(block, t_left, "void", t_task, "_in1", left_sub)
4✔
338
            self.builder.add_memlet(block, t_right, "void", t_task, "_in2", right_sub)
4✔
339
            self.builder.add_memlet(block, t_task, "_out", t_out, "void", "")
4✔
340

341
            return tmp_name
4✔
342
        elif op == "%":
4✔
343
            block = self.builder.add_block()
4✔
344
            t_left, left_sub = self._add_read(block, real_left)
4✔
345
            t_right, right_sub = self._add_read(block, real_right)
4✔
346
            t_out = self.builder.add_access(block, tmp_name)
4✔
347

348
            if dtype.primitive_type == PrimitiveType.Int64:
4✔
349
                t_rem1 = self.builder.add_tasklet(
4✔
350
                    block, TaskletCode.int_srem, ["_in1", "_in2"], ["_out"]
351
                )
352
                self.builder.add_memlet(block, t_left, "void", t_rem1, "_in1", left_sub)
4✔
353
                self.builder.add_memlet(
4✔
354
                    block, t_right, "void", t_rem1, "_in2", right_sub
355
                )
356

357
                rem1_name = self.builder.find_new_name()
4✔
358
                self.builder.add_container(rem1_name, dtype, False)
4✔
359
                t_rem1_out = self.builder.add_access(block, rem1_name)
4✔
360
                self.builder.add_memlet(block, t_rem1, "_out", t_rem1_out, "void", "")
4✔
361

362
                t_add = self.builder.add_tasklet(
4✔
363
                    block, TaskletCode.int_add, ["_in1", "_in2"], ["_out"]
364
                )
365
                self.builder.add_memlet(block, t_rem1_out, "void", t_add, "_in1", "")
4✔
366
                self.builder.add_memlet(
4✔
367
                    block, t_right, "void", t_add, "_in2", right_sub
368
                )
369

370
                add_name = self.builder.find_new_name()
4✔
371
                self.builder.add_container(add_name, dtype, False)
4✔
372
                t_add_out = self.builder.add_access(block, add_name)
4✔
373
                self.builder.add_memlet(block, t_add, "_out", t_add_out, "void", "")
4✔
374

375
                t_rem2 = self.builder.add_tasklet(
4✔
376
                    block, TaskletCode.int_srem, ["_in1", "_in2"], ["_out"]
377
                )
378
                self.builder.add_memlet(block, t_add_out, "void", t_rem2, "_in1", "")
4✔
379
                self.builder.add_memlet(
4✔
380
                    block, t_right, "void", t_rem2, "_in2", right_sub
381
                )
382
                self.builder.add_memlet(block, t_rem2, "_out", t_out, "void", "")
4✔
383

384
                return tmp_name
4✔
385
            else:
386
                t_rem1 = self.builder.add_tasklet(
4✔
387
                    block, TaskletCode.fp_rem, ["_in1", "_in2"], ["_out"]
388
                )
389
                self.builder.add_memlet(block, t_left, "void", t_rem1, "_in1", left_sub)
4✔
390
                self.builder.add_memlet(
4✔
391
                    block, t_right, "void", t_rem1, "_in2", right_sub
392
                )
393

394
                rem1_name = self.builder.find_new_name()
4✔
395
                self.builder.add_container(rem1_name, dtype, False)
4✔
396
                t_rem1_out = self.builder.add_access(block, rem1_name)
4✔
397
                self.builder.add_memlet(block, t_rem1, "_out", t_rem1_out, "void", "")
4✔
398

399
                t_add = self.builder.add_tasklet(
4✔
400
                    block, TaskletCode.fp_add, ["_in1", "_in2"], ["_out"]
401
                )
402
                self.builder.add_memlet(block, t_rem1_out, "void", t_add, "_in1", "")
4✔
403
                self.builder.add_memlet(
4✔
404
                    block, t_right, "void", t_add, "_in2", right_sub
405
                )
406

407
                add_name = self.builder.find_new_name()
4✔
408
                self.builder.add_container(add_name, dtype, False)
4✔
409
                t_add_out = self.builder.add_access(block, add_name)
4✔
410
                self.builder.add_memlet(block, t_add, "_out", t_add_out, "void", "")
4✔
411

412
                t_rem2 = self.builder.add_tasklet(
4✔
413
                    block, TaskletCode.fp_rem, ["_in1", "_in2"], ["_out"]
414
                )
415
                self.builder.add_memlet(block, t_add_out, "void", t_rem2, "_in1", "")
4✔
416
                self.builder.add_memlet(
4✔
417
                    block, t_right, "void", t_rem2, "_in2", right_sub
418
                )
419
                self.builder.add_memlet(block, t_rem2, "_out", t_out, "void", "")
4✔
420

421
                return tmp_name
4✔
422

423
        tasklet_code = None
4✔
424
        if dtype.primitive_type == PrimitiveType.Int64:
4✔
425
            if op == "+":
4✔
426
                tasklet_code = TaskletCode.int_add
4✔
427
            elif op == "-":
4✔
428
                tasklet_code = TaskletCode.int_sub
4✔
429
            elif op == "*":
4✔
430
                tasklet_code = TaskletCode.int_mul
4✔
431
            elif op == "/":
4✔
UNCOV
432
                tasklet_code = TaskletCode.int_sdiv
×
433
            elif op == "//":
4✔
434
                tasklet_code = TaskletCode.int_sdiv
4✔
435
            elif op == "&":
4✔
436
                tasklet_code = TaskletCode.int_and
4✔
437
            elif op == "|":
4✔
438
                tasklet_code = TaskletCode.int_or
4✔
439
            elif op == "^":
4✔
440
                tasklet_code = TaskletCode.int_xor
4✔
441
            elif op == "<<":
×
442
                tasklet_code = TaskletCode.int_shl
×
UNCOV
443
            elif op == ">>":
×
UNCOV
444
                tasklet_code = TaskletCode.int_lshr
×
445
        else:
446
            if op == "+":
4✔
447
                tasklet_code = TaskletCode.fp_add
4✔
448
            elif op == "-":
4✔
449
                tasklet_code = TaskletCode.fp_sub
4✔
450
            elif op == "*":
4✔
451
                tasklet_code = TaskletCode.fp_mul
4✔
452
            elif op == "/":
4✔
453
                tasklet_code = TaskletCode.fp_div
4✔
UNCOV
454
            elif op == "//":
×
455
                tasklet_code = TaskletCode.fp_div
×
456
            else:
UNCOV
457
                raise NotImplementedError(f"Operation {op} not supported for floats")
×
458

459
        block = self.builder.add_block()
4✔
460
        t_left, left_sub = self._add_read(block, real_left)
4✔
461
        t_right, right_sub = self._add_read(block, real_right)
4✔
462
        t_out = self.builder.add_access(block, tmp_name)
4✔
463

464
        t_task = self.builder.add_tasklet(
4✔
465
            block, tasklet_code, ["_in1", "_in2"], ["_out"]
466
        )
467

468
        # For indexed array accesses like "arr(i,j)", we need to pass the Tensor type
469
        # to ensure correct type inference during validation
470
        left_type = self._get_memlet_type_for_access(real_left, left_sub)
4✔
471
        right_type = self._get_memlet_type_for_access(real_right, right_sub)
4✔
472

473
        self.builder.add_memlet(
4✔
474
            block, t_left, "void", t_task, "_in1", left_sub, left_type
475
        )
476
        self.builder.add_memlet(
4✔
477
            block, t_right, "void", t_task, "_in2", right_sub, right_type
478
        )
479
        self.builder.add_memlet(block, t_task, "_out", t_out, "void", "")
4✔
480

481
        return tmp_name
4✔
482

483
    def visit_Attribute(self, node):
4✔
484
        if node.attr == "shape":
4✔
485
            if isinstance(node.value, ast.Name) and node.value.id in self.tensor_table:
4✔
486
                return f"_shape_proxy_{node.value.id}"
4✔
487

488
        if node.attr == "T":
4✔
489
            value_name = None
4✔
490
            if isinstance(node.value, ast.Name):
4✔
491
                value_name = node.value.id
4✔
UNCOV
492
            elif isinstance(node.value, ast.Subscript):
×
UNCOV
493
                value_name = self.visit(node.value)
×
494

495
            if value_name and value_name in self.tensor_table:
4✔
496
                return self.numpy_visitor.handle_transpose_expr(node)
4✔
497

498
        if isinstance(node.value, ast.Name) and node.value.id == "math":
4✔
499
            val = ""
4✔
500
            if node.attr == "pi":
4✔
501
                val = "M_PI"
4✔
502
            elif node.attr == "e":
4✔
503
                val = "M_E"
4✔
504

505
            if val:
4✔
506
                tmp_name = self.builder.find_new_name()
4✔
507
                dtype = Scalar(PrimitiveType.Double)
4✔
508
                self.builder.add_container(tmp_name, dtype, False)
4✔
509
                self.container_table[tmp_name] = dtype
4✔
510
                self._add_assign_constant(tmp_name, val, dtype)
4✔
511
                return tmp_name
4✔
512

513
        if isinstance(node.value, ast.Name):
4✔
514
            obj_name = node.value.id
4✔
515
            attr_name = node.attr
4✔
516

517
            if obj_name in self.container_table:
4✔
518
                obj_type = self.container_table[obj_name]
4✔
519
                if isinstance(obj_type, Pointer) and obj_type.has_pointee_type():
4✔
520
                    pointee_type = obj_type.pointee_type
4✔
521
                    if isinstance(pointee_type, Structure):
4✔
522
                        struct_name = pointee_type.name
4✔
523

524
                        if (
4✔
525
                            struct_name in self.structure_member_info
526
                            and attr_name in self.structure_member_info[struct_name]
527
                        ):
528
                            member_index, member_type = self.structure_member_info[
4✔
529
                                struct_name
530
                            ][attr_name]
531
                        else:
UNCOV
532
                            raise RuntimeError(
×
533
                                f"Member '{attr_name}' not found in structure '{struct_name}'. "
534
                                f"Available members: {list(self.structure_member_info.get(struct_name, {}).keys())}"
535
                            )
536

537
                        tmp_name = self.builder.find_new_name()
4✔
538

539
                        self.builder.add_container(tmp_name, member_type, False)
4✔
540
                        self.container_table[tmp_name] = member_type
4✔
541

542
                        block = self.builder.add_block()
4✔
543
                        obj_access = self.builder.add_access(block, obj_name)
4✔
544
                        tmp_access = self.builder.add_access(block, tmp_name)
4✔
545

546
                        tasklet = self.builder.add_tasklet(
4✔
547
                            block, TaskletCode.assign, ["_in"], ["_out"]
548
                        )
549

550
                        subset = "0," + str(member_index)
4✔
551
                        self.builder.add_memlet(
4✔
552
                            block, obj_access, "", tasklet, "_in", subset
553
                        )
554
                        self.builder.add_memlet(block, tasklet, "_out", tmp_access, "")
4✔
555

556
                        return tmp_name
4✔
557

UNCOV
558
        raise NotImplementedError(f"Attribute access {node.attr} not supported")
×
559

560
    def visit_Compare(self, node):
4✔
561
        left = self.visit(node.left)
4✔
562
        if len(node.ops) > 1:
4✔
UNCOV
563
            raise NotImplementedError("Chained comparisons not supported yet")
×
564

565
        op = self.visit(node.ops[0])
4✔
566
        right = self.visit(node.comparators[0])
4✔
567

568
        left_is_array = left in self.tensor_table
4✔
569
        right_is_array = right in self.tensor_table
4✔
570

571
        if left_is_array or right_is_array:
4✔
572
            return self.numpy_visitor.handle_array_compare(
4✔
573
                left, op, right, left_is_array, right_is_array
574
            )
575

576
        expr_str = f"{left} {op} {right}"
4✔
577

578
        tmp_name = self.builder.find_new_name()
4✔
579
        dtype = Scalar(PrimitiveType.Bool)
4✔
580
        self.builder.add_container(tmp_name, dtype, False)
4✔
581

582
        self.builder.begin_if(expr_str)
4✔
583
        self.builder.add_transition(tmp_name, "true")
4✔
584
        self.builder.begin_else()
4✔
585
        self.builder.add_transition(tmp_name, "false")
4✔
586
        self.builder.end_if()
4✔
587

588
        self.container_table[tmp_name] = dtype
4✔
589
        return tmp_name
4✔
590

591
    def visit_Subscript(self, node):
4✔
592
        value_str = self.visit(node.value)
4✔
593

594
        if value_str.startswith("_shape_proxy_"):
4✔
595
            array_name = value_str[len("_shape_proxy_") :]
4✔
596
            if isinstance(node.slice, ast.Constant):
4✔
597
                idx = node.slice.value
4✔
UNCOV
598
            elif isinstance(node.slice, ast.Index):
×
599
                idx = node.slice.value.value
×
600
            else:
601
                try:
×
602
                    idx = int(self.visit(node.slice))
×
UNCOV
603
                except:
×
UNCOV
604
                    raise NotImplementedError(
×
605
                        "Dynamic shape indexing not fully supported yet"
606
                    )
607

608
            if array_name in self.tensor_table:
4✔
609
                return self.tensor_table[array_name].shape[idx]
4✔
610

UNCOV
611
            return f"_{array_name}_shape_{idx}"
×
612

613
        if value_str in self.tensor_table:
4✔
614
            tensor = self.tensor_table[value_str]
4✔
615
            ndim = len(tensor.shape)
4✔
616
            shapes = tensor.shape
4✔
617

618
            if isinstance(node.slice, ast.Tuple):
4✔
619
                indices_nodes = node.slice.elts
4✔
620
            else:
621
                indices_nodes = [node.slice]
4✔
622

623
            all_full_slices = True
4✔
624
            for idx in indices_nodes:
4✔
625
                if isinstance(idx, ast.Slice):
4✔
626
                    if idx.lower is not None or idx.upper is not None:
4✔
627
                        all_full_slices = False
4✔
628
                        break
4✔
629
                    # Also check for non-trivial step (step != None and step != 1)
630
                    if idx.step is not None:
4✔
631
                        # Check if step is a constant 1; if not, it's not a full slice
632
                        if isinstance(idx.step, ast.Constant) and idx.step.value == 1:
4✔
UNCOV
633
                            pass  # step=1 is equivalent to no step
×
634
                        else:
635
                            all_full_slices = False
4✔
636
                            break
4✔
637
                else:
638
                    all_full_slices = False
4✔
639
                    break
4✔
640

641
            if all_full_slices:
4✔
642
                return value_str
4✔
643

644
            has_slices = any(isinstance(idx, ast.Slice) for idx in indices_nodes)
4✔
645
            if has_slices:
4✔
646
                return self._handle_expression_slicing(
4✔
647
                    node, value_str, indices_nodes, shapes, ndim
648
                )
649

650
            if len(indices_nodes) == 1 and self._is_array_index(indices_nodes[0]):
4✔
UNCOV
651
                if self.builder:
×
UNCOV
652
                    return self._handle_gather(value_str, indices_nodes[0])
×
653

654
            if isinstance(node.slice, ast.Tuple):
4✔
655
                indices = [self.visit(elt) for elt in node.slice.elts]
4✔
656
            else:
657
                indices = [self.visit(node.slice)]
4✔
658

659
            if len(indices) != ndim:
4✔
UNCOV
660
                raise ValueError(
×
661
                    f"Array {value_str} has {ndim} dimensions, but accessed with {len(indices)} indices"
662
                )
663

664
            normalized_indices = []
4✔
665
            for i, idx_str in enumerate(indices):
4✔
666
                shape_val = shapes[i]
4✔
667
                if isinstance(idx_str, str) and (
4✔
668
                    idx_str.startswith("-") or idx_str.startswith("(-")
669
                ):
UNCOV
670
                    normalized_indices.append(f"({shape_val} + {idx_str})")
×
671
                else:
672
                    normalized_indices.append(idx_str)
4✔
673

674
            subscript_str = ",".join(normalized_indices)
4✔
675
            access_str = f"{value_str}({subscript_str})"
4✔
676

677
            if isinstance(node.ctx, ast.Load):
4✔
678
                tmp_name = self.builder.find_new_name()
4✔
679
                self.builder.add_container(tmp_name, tensor.element_type, False)
4✔
680
                self.container_table[tmp_name] = tensor.element_type
4✔
681

682
                block = self.builder.add_block()
4✔
683
                t_src = self.builder.add_access(block, value_str)
4✔
684
                t_dst = self.builder.add_access(block, tmp_name)
4✔
685
                t_task = self.builder.add_tasklet(
4✔
686
                    block, TaskletCode.assign, ["_in"], ["_out"]
687
                )
688
                self.builder.add_memlet(
4✔
689
                    block, t_src, "void", t_task, "_in", subscript_str, tensor
690
                )
691
                self.builder.add_memlet(
4✔
692
                    block, t_task, "_out", t_dst, "void", "", tensor.element_type
693
                )
694

695
                return tmp_name
4✔
696

697
            return access_str
4✔
698

699
        slice_val = self.visit(node.slice)
×
UNCOV
700
        access_str = f"{value_str}({slice_val})"
×
UNCOV
701
        return access_str
×
702

703
    def visit_AugAssign(self, node):
4✔
704
        if isinstance(node.target, ast.Name) and node.target.id in self.tensor_table:
4✔
705
            # Convert to slice assignment: target[:] = target op value
706
            ndim = len(self.tensor_table[node.target.id].shape)
4✔
707

708
            slices = []
4✔
709
            for _ in range(ndim):
4✔
710
                slices.append(ast.Slice(lower=None, upper=None, step=None))
4✔
711

712
            if ndim == 1:
4✔
UNCOV
713
                slice_arg = slices[0]
×
714
            else:
715
                slice_arg = ast.Tuple(elts=slices, ctx=ast.Load())
4✔
716

717
            slice_node = ast.Subscript(
4✔
718
                value=node.target, slice=slice_arg, ctx=ast.Store()
719
            )
720

721
            new_node = ast.Assign(
4✔
722
                targets=[slice_node],
723
                value=ast.BinOp(left=node.target, op=node.op, right=node.value),
724
            )
725
            self.visit_Assign(new_node)
4✔
726
        else:
727
            new_node = ast.Assign(
4✔
728
                targets=[node.target],
729
                value=ast.BinOp(left=node.target, op=node.op, right=node.value),
730
            )
731
            self.visit_Assign(new_node)
4✔
732

733
    def visit_Assign(self, node):
4✔
734
        # Handle multiple targets: a = b = c or a, b = expr
735
        if len(node.targets) > 1:
4✔
736
            tmp_name = self.builder.find_new_name()
4✔
737
            # Assign value to temporary
738
            val_assign = ast.Assign(
4✔
739
                targets=[ast.Name(id=tmp_name, ctx=ast.Store())], value=node.value
740
            )
741
            ast.copy_location(val_assign, node)
4✔
742
            self.visit_Assign(val_assign)
4✔
743

744
            # Assign temporary to targets
745
            for target in node.targets:
4✔
746
                assign = ast.Assign(
4✔
747
                    targets=[target], value=ast.Name(id=tmp_name, ctx=ast.Load())
748
                )
749
                ast.copy_location(assign, node)
4✔
750
                self.visit_Assign(assign)
4✔
751
            return
4✔
752
        target = node.targets[0]
4✔
753

754
        # Handle tuple unpacking: I, J, K = expr1, expr2, expr3
755
        if isinstance(target, ast.Tuple):
4✔
756
            if isinstance(node.value, ast.Tuple):
4✔
757
                if len(target.elts) != len(node.value.elts):
4✔
UNCOV
758
                    raise ValueError("Tuple unpacking size mismatch")
×
759
                for tgt, val in zip(target.elts, node.value.elts):
4✔
760
                    assign = ast.Assign(targets=[tgt], value=val)
4✔
761
                    ast.copy_location(assign, node)
4✔
762
                    self.visit_Assign(assign)
4✔
763
                return
4✔
764
            else:
UNCOV
765
                raise NotImplementedError(
×
766
                    "Tuple unpacking from non-tuple values not supported"
767
                )
768

769
        # Special cases, where rhs is not just a simple expression but requires special handling
770
        if self.numpy_visitor.is_gemm(node.value):
4✔
771
            if self.numpy_visitor.handle_gemm(target, node.value):
4✔
772
                return
4✔
773
            if self.numpy_visitor.handle_dot(target, node.value):
4✔
UNCOV
774
                return
×
775
        if self.numpy_visitor.is_outer(node.value):
4✔
776
            if self.numpy_visitor.handle_outer(target, node.value):
4✔
777
                return
4✔
778
        if self.scipy_handler.is_correlate2d(node.value):
4✔
UNCOV
779
            if self.scipy_handler.handle_correlate2d(target, node.value):
×
UNCOV
780
                return
×
781
        if self.numpy_visitor.is_transpose(node.value):
4✔
782
            if self.numpy_visitor.handle_transpose(target, node.value):
4✔
783
                return
4✔
784

785
        # Handle subscript assignments: a[i] = val or a[i, j] = val
786
        if isinstance(target, ast.Subscript):
4✔
787
            debug_info = get_debug_info(node, self.filename, self.function_name)
4✔
788

789
            target_name = self.visit(target.value)
4✔
790
            indices = []
4✔
791
            if isinstance(target.slice, ast.Tuple):
4✔
792
                indices = target.slice.elts
4✔
793
            else:
794
                indices = [target.slice]
4✔
795

796
            # Handle slice assignment separately
797
            has_slice = False
4✔
798
            for idx in indices:
4✔
799
                if isinstance(idx, ast.Slice):
4✔
800
                    has_slice = True
4✔
801
                    break
4✔
802

803
            if has_slice:
4✔
804
                self._handle_slice_assignment(
4✔
805
                    target, node.value, target_name, indices, debug_info
806
                )
807
                return
4✔
808

809
            # Handle rhs and store in scalar tmp
810
            rhs_tmp = self.visit(node.value)
4✔
811

812
            block = self.builder.add_block(debug_info)
4✔
813
            t_task = self.builder.add_tasklet(
4✔
814
                block, TaskletCode.assign, ["_in"], ["_out"], debug_info
815
            )
816

817
            t_src, src_sub = self._add_read(block, rhs_tmp, debug_info)
4✔
818
            self.builder.add_memlet(
4✔
819
                block, t_src, "void", t_task, "_in", src_sub, None, debug_info
820
            )
821

822
            lhs_expr = self.visit(target)
4✔
823
            if "(" in lhs_expr and lhs_expr.endswith(")"):
4✔
824
                subset = lhs_expr[lhs_expr.find("(") + 1 : -1]
4✔
825
                tensor_dst = self.tensor_table[target_name]
4✔
826

827
                t_dst = self.builder.add_access(block, target_name, debug_info)
4✔
828
                self.builder.add_memlet(
4✔
829
                    block, t_task, "_out", t_dst, "void", subset, tensor_dst, debug_info
830
                )
831
            else:
UNCOV
832
                t_dst = self.builder.add_access(block, target_name, debug_info)
×
UNCOV
833
                self.builder.add_memlet(
×
834
                    block, t_task, "_out", t_dst, "void", "", None, debug_info
835
                )
836
            return
4✔
837

838
        # Fallback: lhs is a simple scalar assignments
839
        if not isinstance(target, ast.Name):
4✔
UNCOV
840
            raise NotImplementedError("Only assignment to variables supported")
×
841

842
        target_name = target.id
4✔
843
        rhs_tmp = self.visit(node.value)
4✔
844
        debug_info = get_debug_info(node, self.filename, self.function_name)
4✔
845

846
        if not self.builder.exists(target_name):
4✔
847
            if isinstance(node.value, ast.Constant):
4✔
848
                val = node.value.value
4✔
849
                if isinstance(val, int):
4✔
850
                    dtype = Scalar(PrimitiveType.Int64)
4✔
851
                elif isinstance(val, float):
×
852
                    dtype = Scalar(PrimitiveType.Double)
×
UNCOV
853
                elif isinstance(val, bool):
×
854
                    dtype = Scalar(PrimitiveType.Bool)
×
855
                else:
UNCOV
856
                    raise NotImplementedError(f"Cannot infer type for {val}")
×
857

858
                self.builder.add_container(target_name, dtype, False)
4✔
859
                self.container_table[target_name] = dtype
4✔
860
            else:
861
                self.builder.add_container(
4✔
862
                    target_name, self.container_table[rhs_tmp], False
863
                )
864
                self.container_table[target_name] = self.container_table[rhs_tmp]
4✔
865

866
        if rhs_tmp in self.tensor_table:
4✔
867
            self.tensor_table[target_name] = self.tensor_table[rhs_tmp]
4✔
868

869
        # Also copy shapes_runtime_info if available
870
        if rhs_tmp in self.shapes_runtime_info:
4✔
871
            self.shapes_runtime_info[target_name] = self.shapes_runtime_info[rhs_tmp]
4✔
872

873
        # Distinguish assignments: scalar -> tasklet, pointer -> reference_memlet
874
        src_type = self.container_table.get(rhs_tmp)
4✔
875
        dst_type = self.container_table[target_name]
4✔
876
        if src_type and isinstance(src_type, Pointer) and isinstance(dst_type, Pointer):
4✔
877
            block = self.builder.add_block(debug_info)
4✔
878
            t_src = self.builder.add_access(block, rhs_tmp, debug_info)
4✔
879
            t_dst = self.builder.add_access(block, target_name, debug_info)
4✔
880
            self.builder.add_reference_memlet(
4✔
881
                block, t_src, t_dst, "0", src_type, debug_info
882
            )
883
        elif (src_type and isinstance(src_type, Scalar)) or isinstance(
4✔
884
            dst_type, Scalar
885
        ):
886
            block = self.builder.add_block(debug_info)
4✔
887
            t_dst = self.builder.add_access(block, target_name, debug_info)
4✔
888
            t_task = self.builder.add_tasklet(
4✔
889
                block, TaskletCode.assign, ["_in"], ["_out"], debug_info
890
            )
891

892
            if src_type:
4✔
893
                t_src = self.builder.add_access(block, rhs_tmp, debug_info)
4✔
894
            else:
895
                t_src = self.builder.add_constant(block, rhs_tmp, dst_type, debug_info)
4✔
896

897
            self.builder.add_memlet(
4✔
898
                block, t_src, "void", t_task, "_in", "", None, debug_info
899
            )
900
            self.builder.add_memlet(
4✔
901
                block, t_task, "_out", t_dst, "void", "", None, debug_info
902
            )
903

904
    def visit_Expr(self, node):
4✔
UNCOV
905
        self.visit(node.value)
×
906

907
    def visit_If(self, node):
4✔
908
        cond = self.visit(node.test)
4✔
909
        debug_info = get_debug_info(node, self.filename, self.function_name)
4✔
910
        self.builder.begin_if(f"{cond} != false", debug_info)
4✔
911

912
        for stmt in node.body:
4✔
913
            self.visit(stmt)
4✔
914

915
        if node.orelse:
4✔
916
            self.builder.begin_else(debug_info)
4✔
917
            for stmt in node.orelse:
4✔
918
                self.visit(stmt)
4✔
919

920
        self.builder.end_if()
4✔
921

922
    def visit_While(self, node):
4✔
923
        if node.orelse:
4✔
UNCOV
924
            raise NotImplementedError("while-else is not supported")
×
925

926
        debug_info = get_debug_info(node, self.filename, self.function_name)
4✔
927
        self.builder.begin_while(debug_info)
4✔
928

929
        # Evaluate condition inside the loop so it's re-evaluated each iteration
930
        cond = self.visit(node.test)
4✔
931

932
        # Create if-break pattern: if condition is false, break
933
        self.builder.begin_if(f"{cond} == false", debug_info)
4✔
934
        self.builder.add_break(debug_info)
4✔
935
        self.builder.end_if()
4✔
936

937
        for stmt in node.body:
4✔
938
            self.visit(stmt)
4✔
939

940
        self.builder.end_while()
4✔
941

942
    def visit_Break(self, node):
4✔
943
        debug_info = get_debug_info(node, self.filename, self.function_name)
4✔
944
        self.builder.add_break(debug_info)
4✔
945

946
    def visit_Continue(self, node):
4✔
947
        debug_info = get_debug_info(node, self.filename, self.function_name)
4✔
948
        self.builder.add_continue(debug_info)
4✔
949

950
    def visit_For(self, node):
4✔
951
        if node.orelse:
4✔
952
            raise NotImplementedError("while-else is not supported")
×
953
        if not isinstance(node.target, ast.Name):
4✔
UNCOV
954
            raise NotImplementedError("Only simple for loops supported")
×
955

956
        var = node.target.id
4✔
957
        debug_info = get_debug_info(node, self.filename, self.function_name)
4✔
958

959
        # Check if iterating over a range() call
960
        if (
4✔
961
            isinstance(node.iter, ast.Call)
962
            and isinstance(node.iter.func, ast.Name)
963
            and node.iter.func.id == "range"
964
        ):
965
            args = node.iter.args
4✔
966
            if len(args) == 1:
4✔
967
                start = "0"
4✔
968
                end = self.visit(args[0])
4✔
969
                step = "1"
4✔
970
            elif len(args) == 2:
4✔
971
                start = self.visit(args[0])
4✔
972
                end = self.visit(args[1])
4✔
973
                step = "1"
4✔
974
            elif len(args) == 3:
4✔
975
                start = self.visit(args[0])
4✔
976
                end = self.visit(args[1])
4✔
977

978
                # Special handling for step to avoid creating tasklets for constants
979
                step_node = args[2]
4✔
980
                if isinstance(step_node, ast.Constant):
4✔
981
                    step = str(step_node.value)
4✔
982
                elif (
4✔
983
                    isinstance(step_node, ast.UnaryOp)
984
                    and isinstance(step_node.op, ast.USub)
985
                    and isinstance(step_node.operand, ast.Constant)
986
                ):
987
                    step = f"-{step_node.operand.value}"
4✔
988
                else:
989
                    step = self.visit(step_node)
×
990
            else:
UNCOV
991
                raise ValueError("Invalid range arguments")
×
992

993
            if not self.builder.exists(var):
4✔
994
                self.builder.add_container(var, Scalar(PrimitiveType.Int64), False)
4✔
995
                self.container_table[var] = Scalar(PrimitiveType.Int64)
4✔
996

997
            self.builder.begin_for(var, start, end, step, debug_info)
4✔
998

999
            for stmt in node.body:
4✔
1000
                self.visit(stmt)
4✔
1001

1002
            self.builder.end_for()
4✔
1003
            return
4✔
1004

1005
        # Check if iterating over an ndarray (for x in array)
1006
        if isinstance(node.iter, ast.Name):
×
1007
            iter_name = node.iter.id
×
1008
            if iter_name in self.tensor_table:
×
1009
                arr_info = self.tensor_table[iter_name]
×
UNCOV
1010
                if len(arr_info.shape) == 0:
×
UNCOV
1011
                    raise NotImplementedError("Cannot iterate over 0-dimensional array")
×
1012

1013
                # Get the size of the first dimension
UNCOV
1014
                arr_size = arr_info.shape[0]
×
1015

1016
                # Create a hidden index variable for the loop
1017
                idx_var = self.builder.find_new_name()
×
UNCOV
1018
                if not self.builder.exists(idx_var):
×
UNCOV
1019
                    self.builder.add_container(
×
1020
                        idx_var, Scalar(PrimitiveType.Int64), False
1021
                    )
UNCOV
1022
                    self.container_table[idx_var] = Scalar(PrimitiveType.Int64)
×
1023

1024
                # Determine the type of the loop variable (element type)
1025
                # For a 1D array, it's a scalar; for ND array, it's a view of N-1 dimensions
1026
                if len(arr_info.shape) == 1:
×
1027
                    # Element is a scalar - get the element type from the array's type
1028
                    arr_type = self.container_table.get(iter_name)
×
UNCOV
1029
                    if isinstance(arr_type, Pointer):
×
1030
                        elem_type = arr_type.pointee_type
×
1031
                    else:
1032
                        elem_type = Scalar(PrimitiveType.Double)  # Default fallback
×
1033

1034
                    if not self.builder.exists(var):
×
UNCOV
1035
                        self.builder.add_container(var, elem_type, False)
×
UNCOV
1036
                        self.container_table[var] = elem_type
×
1037
                else:
1038
                    # For multi-dimensional arrays, create a view/slice
1039
                    # The loop variable becomes a pointer to the sub-array
UNCOV
1040
                    inner_shapes = arr_info.shape[1:]
×
1041
                    inner_ndim = len(arr_info.shape) - 1
×
1042

1043
                    arr_type = self.container_table.get(iter_name)
×
UNCOV
1044
                    if isinstance(arr_type, Pointer):
×
1045
                        elem_type = arr_type  # Keep as pointer type for views
×
1046
                    else:
1047
                        elem_type = Pointer(Scalar(PrimitiveType.Double))
×
1048

1049
                    if not self.builder.exists(var):
×
UNCOV
1050
                        self.builder.add_container(var, elem_type, False)
×
UNCOV
1051
                        self.container_table[var] = elem_type
×
1052

1053
                    # Register the view in tensor_table
UNCOV
1054
                    self.tensor_table[var] = Tensor(
×
1055
                        element_type_from_sdfg_type(elem_type), inner_shapes
1056
                    )
1057

1058
                # Begin the for loop
UNCOV
1059
                self.builder.begin_for(idx_var, "0", str(arr_size), "1", debug_info)
×
1060

1061
                # Generate the assignment: var = array[idx_var]
1062
                # Create an AST node for the assignment and visit it
UNCOV
1063
                assign_node = ast.Assign(
×
1064
                    targets=[ast.Name(id=var, ctx=ast.Store())],
1065
                    value=ast.Subscript(
1066
                        value=ast.Name(id=iter_name, ctx=ast.Load()),
1067
                        slice=ast.Name(id=idx_var, ctx=ast.Load()),
1068
                        ctx=ast.Load(),
1069
                    ),
1070
                )
UNCOV
1071
                ast.copy_location(assign_node, node)
×
UNCOV
1072
                self.visit_Assign(assign_node)
×
1073

1074
                # Visit the loop body
UNCOV
1075
                for stmt in node.body:
×
1076
                    self.visit(stmt)
×
1077

UNCOV
1078
                self.builder.end_for()
×
1079
                return
×
1080

UNCOV
1081
        raise NotImplementedError(
×
1082
            f"Only range() loops and iteration over ndarrays supported, got: {ast.dump(node.iter)}"
1083
        )
1084

1085
    def visit_Return(self, node):
4✔
1086
        if node.value is None:
4✔
1087
            debug_info = get_debug_info(node, self.filename, self.function_name)
×
1088
            # Emit frees for all deferred allocations before returning
1089
            if self.memory_handler.has_allocations():
×
1090
                self.memory_handler.emit_frees()
×
UNCOV
1091
            self.builder.add_return("", debug_info)
×
UNCOV
1092
            return
×
1093

1094
        if isinstance(node.value, ast.Tuple):
4✔
1095
            values = node.value.elts
4✔
1096
        else:
1097
            values = [node.value]
4✔
1098

1099
        parsed_values = [self.visit(v) for v in values]
4✔
1100
        debug_info = get_debug_info(node, self.filename, self.function_name)
4✔
1101

1102
        if self.infer_return_type:
4✔
1103
            for i, res in enumerate(parsed_values):
4✔
1104
                ret_name = f"_docc_ret_{i}"
4✔
1105
                if not self.builder.exists(ret_name):
4✔
1106
                    dtype = Scalar(PrimitiveType.Double)
4✔
1107
                    if res in self.container_table:
4✔
1108
                        dtype = self.container_table[res]
4✔
1109
                    elif isinstance(values[i], ast.Constant):
×
1110
                        val = values[i].value
×
1111
                        if isinstance(val, int):
×
1112
                            dtype = Scalar(PrimitiveType.Int64)
×
1113
                        elif isinstance(val, float):
×
1114
                            dtype = Scalar(PrimitiveType.Double)
×
UNCOV
1115
                        elif isinstance(val, bool):
×
UNCOV
1116
                            dtype = Scalar(PrimitiveType.Bool)
×
1117

1118
                    # Wrap Scalar in Pointer. Keep Arrays/Pointers as is.
1119
                    arg_type = dtype
4✔
1120
                    if isinstance(dtype, Scalar):
4✔
1121
                        arg_type = Pointer(dtype)
4✔
1122

1123
                    self.builder.add_container(ret_name, arg_type, is_argument=True)
4✔
1124
                    self.container_table[ret_name] = arg_type
4✔
1125

1126
                    if res in self.tensor_table:
4✔
1127
                        self.tensor_table[ret_name] = self.tensor_table[res]
4✔
1128

1129
            self.infer_return_type = False
4✔
1130

1131
        for i, res in enumerate(parsed_values):
4✔
1132
            ret_name = f"_docc_ret_{i}"
4✔
1133
            typ = self.container_table.get(ret_name)
4✔
1134

1135
            is_array_return = False
4✔
1136
            if res in self.tensor_table:
4✔
1137
                # Only treat as array return if it has dimensions
1138
                # 0-d arrays (scalars) should be handled by scalar assignment
1139
                if len(self.tensor_table[res].shape) > 0:
4✔
1140
                    is_array_return = True
4✔
1141
            elif res in self.container_table:
4✔
1142
                if isinstance(self.container_table[res], Pointer):
4✔
UNCOV
1143
                    is_array_return = True
×
1144

1145
            # Simple Scalar Assignment
1146
            if not is_array_return:
4✔
1147
                block = self.builder.add_block(debug_info)
4✔
1148
                t_dst = self.builder.add_access(block, ret_name, debug_info)
4✔
1149

1150
                t_src, src_sub = self._add_read(block, res, debug_info)
4✔
1151

1152
                t_task = self.builder.add_tasklet(
4✔
1153
                    block, TaskletCode.assign, ["_in"], ["_out"], debug_info
1154
                )
1155
                self.builder.add_memlet(
4✔
1156
                    block, t_src, "void", t_task, "_in", src_sub, None, debug_info
1157
                )
1158
                self.builder.add_memlet(
4✔
1159
                    block, t_task, "_out", t_dst, "void", "0", None, debug_info
1160
                )
1161

1162
            # Array Assignment (Copy)
1163
            else:
1164
                # Record shape for metadata
1165
                if res in self.tensor_table:
4✔
1166
                    # Prefer runtime shapes if available (for indirect access patterns)
1167
                    # Fall back to regular shapes otherwise
1168
                    res_info = self.tensor_table[res]
4✔
1169
                    if res in self.shapes_runtime_info:
4✔
1170
                        shape = self.shapes_runtime_info[res]
4✔
1171
                    else:
1172
                        shape = res_info.shape
4✔
1173
                    # Convert to string expressions
1174
                    self.captured_return_shapes[ret_name] = [str(s) for s in shape]
4✔
1175

1176
                    # Return arrays are always contiguous - compute fresh strides
1177
                    contiguous_strides = self.numpy_visitor._compute_strides(shape, "C")
4✔
1178
                    self.captured_return_strides[ret_name] = [
4✔
1179
                        str(s) for s in contiguous_strides
1180
                    ]
1181

1182
                    # Always overwrite tensor_table for return arrays with contiguous strides
1183
                    # (source tensor may have non-standard strides from views/flip)
1184
                    self.tensor_table[ret_name] = Tensor(
4✔
1185
                        res_info.element_type, shape, contiguous_strides
1186
                    )
1187

1188
                # Copy Logic using visit_Assign
1189
                ndim = 1
4✔
1190
                if ret_name in self.tensor_table:
4✔
1191
                    ndim = len(self.tensor_table[ret_name].shape)
4✔
1192

1193
                slice_node = ast.Slice(lower=None, upper=None, step=None)
4✔
1194
                if ndim > 1:
4✔
1195
                    target_slice = ast.Tuple(elts=[slice_node] * ndim, ctx=ast.Load())
4✔
1196
                else:
1197
                    target_slice = slice_node
4✔
1198

1199
                target_sub = ast.Subscript(
4✔
1200
                    value=ast.Name(id=ret_name, ctx=ast.Load()),
1201
                    slice=target_slice,
1202
                    ctx=ast.Store(),
1203
                )
1204

1205
                # Value node reconstruction
1206
                if isinstance(values[i], ast.Name):
4✔
1207
                    val_node = values[i]
4✔
1208
                else:
1209
                    val_node = ast.Name(id=res, ctx=ast.Load())
4✔
1210

1211
                assign_node = ast.Assign(targets=[target_sub], value=val_node)
4✔
1212
                self.visit_Assign(assign_node)
4✔
1213

1214
        # Emit frees for all deferred allocations before returning
1215
        if self.memory_handler.has_allocations():
4✔
1216
            self.memory_handler.emit_frees()
4✔
1217

1218
        # Add control flow return to exit the function/path
1219
        self.builder.add_return("", debug_info)
4✔
1220

1221
    def visit_Call(self, node):
4✔
1222
        func_name = ""
4✔
1223
        module_name = ""
4✔
1224
        submodule_name = ""
4✔
1225
        if isinstance(node.func, ast.Attribute):
4✔
1226
            if isinstance(node.func.value, ast.Name):
4✔
1227
                if node.func.value.id == "math":
4✔
1228
                    module_name = "math"
4✔
1229
                    func_name = node.func.attr
4✔
1230
                elif node.func.value.id in ["numpy", "np"]:
4✔
1231
                    module_name = "numpy"
4✔
1232
                    func_name = node.func.attr
4✔
1233
                else:
1234
                    array_name = node.func.value.id
4✔
1235
                    method_name = node.func.attr
4✔
1236
                    if array_name in self.tensor_table and method_name == "astype":
4✔
1237
                        return self.numpy_visitor.handle_numpy_astype(node, array_name)
4✔
1238
                    elif array_name in self.tensor_table and method_name == "copy":
4✔
1239
                        return self.numpy_visitor.handle_numpy_copy(node, array_name)
4✔
1240
            elif isinstance(node.func.value, ast.Attribute):
4✔
1241
                if (
4✔
1242
                    isinstance(node.func.value.value, ast.Name)
1243
                    and node.func.value.value.id == "scipy"
1244
                ):
1245
                    module_name = "scipy"
4✔
1246
                    submodule_name = node.func.value.attr
4✔
1247
                    func_name = node.func.attr
4✔
1248
                elif (
4✔
1249
                    isinstance(node.func.value.value, ast.Name)
1250
                    and node.func.value.value.id in ["numpy", "np"]
1251
                    and node.func.attr == "outer"
1252
                ):
1253
                    ufunc_name = node.func.value.attr
4✔
1254
                    return self.numpy_visitor.handle_ufunc_outer(node, ufunc_name)
4✔
1255

1256
        elif isinstance(node.func, ast.Name):
4✔
1257
            func_name = node.func.id
4✔
1258

1259
        if module_name == "numpy":
4✔
1260
            if self.numpy_visitor.has_handler(func_name):
4✔
1261
                return self.numpy_visitor.handle_numpy_call(node, func_name)
4✔
1262

1263
        if module_name == "math":
4✔
1264
            if self.math_handler.has_handler(func_name):
4✔
1265
                return self.math_handler.handle_math_call(node, func_name)
4✔
1266

1267
        if module_name == "scipy":
4✔
1268
            if self.scipy_handler.has_handler(submodule_name, func_name):
4✔
1269
                return self.scipy_handler.handle_scipy_call(
4✔
1270
                    node, submodule_name, func_name
1271
                )
1272

1273
        if self.python_handler.has_handler(func_name):
4✔
1274
            return self.python_handler.handle_python_call(node, func_name)
4✔
1275

1276
        if func_name in self.globals_dict:
4✔
1277
            obj = self.globals_dict[func_name]
4✔
1278
            if inspect.isfunction(obj):
4✔
1279
                return self._handle_inline_call(node, obj)
4✔
1280

UNCOV
1281
        raise NotImplementedError(f"Function call {func_name} not supported")
×
1282

1283
    def _handle_inline_call(self, node, func_obj):
4✔
1284
        try:
4✔
1285
            source_lines, start_line = inspect.getsourcelines(func_obj)
4✔
1286
            source = textwrap.dedent("".join(source_lines))
4✔
1287
            tree = ast.parse(source)
4✔
1288
            func_def = tree.body[0]
4✔
UNCOV
1289
        except Exception as e:
×
UNCOV
1290
            raise NotImplementedError(
×
1291
                f"Could not parse function {func_obj.__name__}: {e}"
1292
            )
1293

1294
        arg_vars = [self.visit(arg) for arg in node.args]
4✔
1295

1296
        if len(arg_vars) != len(func_def.args.args):
4✔
UNCOV
1297
            raise NotImplementedError(
×
1298
                f"Argument count mismatch for {func_obj.__name__}"
1299
            )
1300

1301
        suffix = f"_{func_obj.__name__}_{self._get_unique_id()}"
4✔
1302
        res_name = f"_res{suffix}"
4✔
1303

1304
        # Combine globals with closure variables of the inlined function
1305
        combined_globals = dict(self.globals_dict)
4✔
1306
        closure_constants = {}  # name -> value for numeric closure vars
4✔
1307
        if func_obj.__closure__ is not None and func_obj.__code__.co_freevars:
4✔
1308
            for name, cell in zip(func_obj.__code__.co_freevars, func_obj.__closure__):
4✔
1309
                val = cell.cell_contents
4✔
1310
                combined_globals[name] = val
4✔
1311
                # Track numeric constants for injection
1312
                if isinstance(val, (int, float)) and not isinstance(val, bool):
4✔
1313
                    closure_constants[name] = val
4✔
1314

1315
        class VariableRenamer(ast.NodeTransformer):
4✔
1316
            BUILTINS = {
4✔
1317
                "range",
1318
                "len",
1319
                "int",
1320
                "float",
1321
                "bool",
1322
                "str",
1323
                "list",
1324
                "dict",
1325
                "tuple",
1326
                "set",
1327
                "print",
1328
                "abs",
1329
                "min",
1330
                "max",
1331
                "sum",
1332
                "enumerate",
1333
                "zip",
1334
                "map",
1335
                "filter",
1336
                "sorted",
1337
                "reversed",
1338
                "True",
1339
                "False",
1340
                "None",
1341
            }
1342

1343
            def __init__(self, suffix, globals_dict):
4✔
1344
                self.suffix = suffix
4✔
1345
                self.globals_dict = globals_dict
4✔
1346

1347
            def visit_Name(self, node):
4✔
1348
                if node.id in self.globals_dict or node.id in self.BUILTINS:
4✔
1349
                    return node
4✔
1350
                return ast.Name(id=f"{node.id}{self.suffix}", ctx=node.ctx)
4✔
1351

1352
            def visit_Return(self, node):
4✔
1353
                if node.value:
4✔
1354
                    val = self.visit(node.value)
4✔
1355
                    return ast.Assign(
4✔
1356
                        targets=[ast.Name(id=res_name, ctx=ast.Store())],
1357
                        value=val,
1358
                    )
UNCOV
1359
                return node
×
1360

1361
        renamer = VariableRenamer(suffix, combined_globals)
4✔
1362
        new_body = [renamer.visit(stmt) for stmt in func_def.body]
4✔
1363

1364
        param_assignments = []
4✔
1365

1366
        # Inject closure constants as assignments
1367
        for name, val in closure_constants.items():
4✔
1368
            if isinstance(val, int):
4✔
1369
                self.container_table[name] = Scalar(PrimitiveType.Int64)
4✔
1370
                self.builder.add_container(name, Scalar(PrimitiveType.Int64), False)
4✔
1371
                val_node = ast.Constant(value=val)
4✔
1372
            else:
1373
                self.container_table[name] = Scalar(PrimitiveType.Double)
×
UNCOV
1374
                self.builder.add_container(name, Scalar(PrimitiveType.Double), False)
×
UNCOV
1375
                val_node = ast.Constant(value=val)
×
1376
            assign = ast.Assign(
4✔
1377
                targets=[ast.Name(id=name, ctx=ast.Store())], value=val_node
1378
            )
1379
            param_assignments.append(assign)
4✔
1380

1381
        for arg_def, arg_val in zip(func_def.args.args, arg_vars):
4✔
1382
            param_name = f"{arg_def.arg}{suffix}"
4✔
1383

1384
            if arg_val in self.container_table:
4✔
1385
                self.container_table[param_name] = self.container_table[arg_val]
4✔
1386
                self.builder.add_container(
4✔
1387
                    param_name, self.container_table[arg_val], False
1388
                )
1389
                val_node = ast.Name(id=arg_val, ctx=ast.Load())
4✔
1390
            elif self._is_int(arg_val):
×
UNCOV
1391
                self.container_table[param_name] = Scalar(PrimitiveType.Int64)
×
UNCOV
1392
                self.builder.add_container(
×
1393
                    param_name, Scalar(PrimitiveType.Int64), False
1394
                )
1395
                val_node = ast.Constant(value=int(arg_val))
×
1396
            else:
1397
                try:
×
1398
                    val = float(arg_val)
×
UNCOV
1399
                    self.container_table[param_name] = Scalar(PrimitiveType.Double)
×
UNCOV
1400
                    self.builder.add_container(
×
1401
                        param_name, Scalar(PrimitiveType.Double), False
1402
                    )
1403
                    val_node = ast.Constant(value=val)
×
UNCOV
1404
                except ValueError:
×
UNCOV
1405
                    val_node = ast.Name(id=arg_val, ctx=ast.Load())
×
1406

1407
            assign = ast.Assign(
4✔
1408
                targets=[ast.Name(id=param_name, ctx=ast.Store())], value=val_node
1409
            )
1410
            param_assignments.append(assign)
4✔
1411

1412
        final_body = param_assignments + new_body
4✔
1413

1414
        # Create a new parser instance for the inlined function
1415
        # Share memory_handler so hoisted allocations go to main function entry
1416
        parser = ASTParser(
4✔
1417
            self.builder,
1418
            self.tensor_table,
1419
            self.container_table,
1420
            globals_dict=combined_globals,
1421
            unique_counter_ref=self._unique_counter_ref,
1422
            memory_handler=self.memory_handler,
1423
        )
1424

1425
        for stmt in final_body:
4✔
1426
            parser.visit(stmt)
4✔
1427

1428
        return res_name
4✔
1429

1430
    def _add_assign_constant(self, target_name, value_str, dtype):
4✔
1431
        block = self.builder.add_block()
4✔
1432
        t_const = self.builder.add_constant(block, value_str, dtype)
4✔
1433
        t_dst = self.builder.add_access(block, target_name)
4✔
1434
        t_task = self.builder.add_tasklet(block, TaskletCode.assign, ["_in"], ["_out"])
4✔
1435
        self.builder.add_memlet(block, t_const, "void", t_task, "_in", "")
4✔
1436
        self.builder.add_memlet(block, t_task, "_out", t_dst, "void", "")
4✔
1437

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

1441
        Uses a zero-copy view when possible (positive step, no indirect access).
1442
        Falls back to copy-based approach for complex cases.
1443
        """
1444
        if not self.builder:
4✔
UNCOV
1445
            raise ValueError("Builder required for expression slicing")
×
1446

1447
        # Try view-based approach first (zero-copy)
1448
        if self._can_use_slice_view(indices_nodes):
4✔
1449
            return self._create_slice_view(value_str, indices_nodes, shapes, ndim)
4✔
1450

1451
        # Fall back to copy-based approach for complex cases
UNCOV
1452
        return self._handle_expression_slicing_copy(
×
1453
            node, value_str, indices_nodes, shapes, ndim
1454
        )
1455

1456
    def _can_use_slice_view(self, indices_nodes):
4✔
1457
        """Check if slicing can be expressed as a zero-copy view.
1458

1459
        Views can be used when:
1460
        - All steps are non-zero constants (positive or negative)
1461
        - No indirect array access in slice parameters
1462

1463
        Returns True if a view can be used, False if a copy is required.
1464
        """
1465
        for idx in indices_nodes:
4✔
1466
            if isinstance(idx, ast.Slice):
4✔
1467
                # Check for zero step (invalid)
1468
                if idx.step is not None:
4✔
1469
                    if isinstance(idx.step, ast.Constant):
4✔
1470
                        if idx.step.value == 0:
4✔
UNCOV
1471
                            return False  # Zero step is invalid
×
1472
                    elif isinstance(idx.step, ast.UnaryOp) and isinstance(
4✔
1473
                        idx.step.op, ast.USub
1474
                    ):
1475
                        # Negative step like -2 is OK
1476
                        pass
4✔
UNCOV
1477
                    elif self._contains_indirect_access(idx.step):
×
UNCOV
1478
                        return False  # Dynamic step requires copy
×
1479

1480
                # Check for indirect access in slice bounds
1481
                if idx.lower is not None and self._contains_indirect_access(idx.lower):
4✔
1482
                    return False
×
1483
                if idx.upper is not None and self._contains_indirect_access(idx.upper):
4✔
UNCOV
1484
                    return False
×
1485
            else:
1486
                # Fixed index: check for indirect access
1487
                if self._contains_indirect_access(idx):
4✔
UNCOV
1488
                    return False
×
1489
        return True
4✔
1490

1491
    def _create_slice_view(self, value_str, indices_nodes, shapes, ndim):
4✔
1492
        """Create a zero-copy view for array slicing.
1493

1494
        This creates a new tensor that shares data with the source but has
1495
        adjusted shape, strides, and offset to represent the sliced region.
1496

1497
        For positive step A[start:stop:step, ...] on dimension i:
1498
        - new_shape[i] = ceil((stop - start) / step)
1499
        - new_stride[i] = old_stride[i] * step
1500
        - offset contribution = start * old_stride[i]
1501

1502
        For negative step A[start:stop:step, ...] (e.g., ::-1):
1503
        - Default start = shape - 1 (last element)
1504
        - Default stop = -1 (before first element)
1505
        - new_shape[i] = ceil((start - stop) / abs(step))
1506
        - new_stride[i] = old_stride[i] * step (negative)
1507
        - offset contribution = start * old_stride[i] (points to last element)
1508

1509
        For a fixed index A[k, ...] on dimension i (dimension reduction):
1510
        - offset contribution = k * old_stride[i]
1511
        - dimension is removed from output
1512
        """
1513
        in_tensor = self.tensor_table[value_str]
4✔
1514
        in_shape = in_tensor.shape
4✔
1515
        dtype = in_tensor.element_type
4✔
1516

1517
        # Get input strides (compute if not available)
1518
        in_strides = (
4✔
1519
            in_tensor.strides
1520
            if hasattr(in_tensor, "strides") and in_tensor.strides
1521
            else None
1522
        )
1523
        if in_strides is None:
4✔
UNCOV
1524
            in_strides = self.numpy_visitor._compute_strides(in_shape, "C")
×
1525

1526
        # Get base offset from input tensor
1527
        in_offset = getattr(in_tensor, "offset", "0") or "0"
4✔
1528

1529
        # Build output shape, strides, and compute offset
1530
        out_shape = []
4✔
1531
        out_strides = []
4✔
1532
        offset_terms = []
4✔
1533
        if in_offset != "0":
4✔
1534
            offset_terms.append(str(in_offset))
4✔
1535

1536
        for i, idx in enumerate(indices_nodes):
4✔
1537
            shape_val = shapes[i] if i < len(shapes) else f"_{value_str}_shape_{i}"
4✔
1538
            stride_val = in_strides[i] if i < len(in_strides) else "1"
4✔
1539

1540
            if isinstance(idx, ast.Slice):
4✔
1541
                # Determine step value and sign
1542
                step_str = "1"
4✔
1543
                step_is_negative = False
4✔
1544
                step_value = 1
4✔
1545

1546
                if idx.step is not None:
4✔
1547
                    if isinstance(idx.step, ast.Constant):
4✔
1548
                        step_value = idx.step.value
4✔
1549
                        step_str = str(step_value)
4✔
1550
                        step_is_negative = step_value < 0
4✔
1551
                    elif isinstance(idx.step, ast.UnaryOp) and isinstance(
4✔
1552
                        idx.step.op, ast.USub
1553
                    ):
1554
                        # Handle -N syntax
1555
                        if isinstance(idx.step.operand, ast.Constant):
4✔
1556
                            step_value = -idx.step.operand.value
4✔
1557
                            step_str = str(step_value)
4✔
1558
                            step_is_negative = True
4✔
1559
                        else:
1560
                            step_str = self.visit(idx.step)
×
1561
                    else:
UNCOV
1562
                        step_str = self.visit(idx.step)
×
1563

1564
                if step_is_negative:
4✔
1565
                    # Negative step: iterate from end to start
1566
                    # Default start = shape - 1, default stop = -1 (before 0)
1567
                    if idx.lower is not None:
4✔
UNCOV
1568
                        start_str = self.visit(idx.lower)
×
UNCOV
1569
                        if isinstance(start_str, str) and (
×
1570
                            start_str.startswith("-") or start_str.startswith("(-")
1571
                        ):
UNCOV
1572
                            start_str = f"({shape_val} + {start_str})"
×
1573
                    else:
1574
                        start_str = f"({shape_val} - 1)"
4✔
1575

1576
                    if idx.upper is not None:
4✔
UNCOV
1577
                        stop_str = self.visit(idx.upper)
×
UNCOV
1578
                        if isinstance(stop_str, str) and (
×
1579
                            stop_str.startswith("-") or stop_str.startswith("(-")
1580
                        ):
UNCOV
1581
                            stop_str = f"({shape_val} + {stop_str})"
×
1582
                    else:
1583
                        stop_str = "-1"
4✔
1584

1585
                    # Shape for negative step: ceil((start - stop) / abs(step))
1586
                    abs_step = abs(step_value)
4✔
1587
                    if abs_step == 1:
4✔
1588
                        dim_size = f"({start_str} - {stop_str})"
4✔
1589
                    else:
1590
                        dim_size = f"(({start_str} - {stop_str} + {abs_step} - 1) / {abs_step})"
4✔
1591
                    out_shape.append(dim_size)
4✔
1592

1593
                    # Stride for negative step: old_stride * step (negative)
1594
                    out_strides.append(f"({stride_val} * {step_str})")
4✔
1595

1596
                    # Offset: start * old_stride (points to first element to access)
1597
                    offset_terms.append(f"({start_str} * {stride_val})")
4✔
1598
                else:
1599
                    # Positive step (original logic)
1600
                    start_str = "0"
4✔
1601
                    if idx.lower is not None:
4✔
1602
                        start_str = self.visit(idx.lower)
4✔
1603
                        if isinstance(start_str, str) and (
4✔
1604
                            start_str.startswith("-") or start_str.startswith("(-")
1605
                        ):
UNCOV
1606
                            start_str = f"({shape_val} + {start_str})"
×
1607

1608
                    stop_str = str(shape_val)
4✔
1609
                    if idx.upper is not None:
4✔
1610
                        stop_str = self.visit(idx.upper)
4✔
1611
                        if isinstance(stop_str, str) and (
4✔
1612
                            stop_str.startswith("-") or stop_str.startswith("(-")
1613
                        ):
1614
                            stop_str = f"({shape_val} + {stop_str})"
4✔
1615

1616
                    # Compute new shape: ceil((stop - start) / step)
1617
                    if step_str == "1":
4✔
1618
                        dim_size = f"({stop_str} - {start_str})"
4✔
1619
                    else:
1620
                        dim_size = f"idiv({stop_str} - {start_str} + {step_str} - 1, {step_str})"
4✔
1621
                    out_shape.append(dim_size)
4✔
1622

1623
                    # Compute new stride: old_stride * step
1624
                    if step_str == "1":
4✔
1625
                        out_strides.append(stride_val)
4✔
1626
                    else:
1627
                        out_strides.append(f"({stride_val} * {step_str})")
4✔
1628

1629
                    # Add offset contribution: start * stride
1630
                    if start_str != "0":
4✔
1631
                        offset_terms.append(f"({start_str} * {stride_val})")
4✔
1632
            else:
1633
                # Fixed index: dimension is removed, just add offset
1634
                index_str = self.visit(idx)
4✔
1635
                if isinstance(index_str, str) and (
4✔
1636
                    index_str.startswith("-") or index_str.startswith("(-")
1637
                ):
1638
                    index_str = f"({shape_val} + {index_str})"
4✔
1639
                offset_terms.append(f"({index_str} * {stride_val})")
4✔
1640

1641
        # Combine offset terms
1642
        if not offset_terms:
4✔
1643
            out_offset = "0"
4✔
1644
        elif len(offset_terms) == 1:
4✔
1645
            out_offset = offset_terms[0]
4✔
1646
        else:
1647
            out_offset = " + ".join(offset_terms)
4✔
1648

1649
        # Create new pointer container
1650
        tmp_name = self.builder.find_new_name("_slice_view_")
4✔
1651
        ptr_type = Pointer(dtype)
4✔
1652
        self.builder.add_container(tmp_name, ptr_type, False)
4✔
1653
        self.container_table[tmp_name] = ptr_type
4✔
1654

1655
        # Create output tensor with new shape, strides, and offset
1656
        # Offset is stored in the Tensor (like Tensor.flip() does)
1657
        # Reference memlet just creates the pointer alias with "0" offset
1658
        if out_shape:
4✔
1659
            out_tensor = Tensor(dtype, out_shape, out_strides, out_offset)
4✔
1660
            self.tensor_table[tmp_name] = out_tensor
4✔
1661
        else:
1662
            # Scalar result (all indices were fixed)
UNCOV
1663
            self.builder.add_container(tmp_name, dtype, False)
×
UNCOV
1664
            self.container_table[tmp_name] = dtype
×
1665

1666
        # Create reference memlet (offset is handled by tensor's offset property)
1667
        debug_info = DebugInfo()
4✔
1668
        block = self.builder.add_block(debug_info)
4✔
1669
        t_src = self.builder.add_access(block, value_str, debug_info)
4✔
1670
        t_dst = self.builder.add_access(block, tmp_name, debug_info)
4✔
1671
        self.builder.add_reference_memlet(block, t_src, t_dst, "0", ptr_type)
4✔
1672

1673
        return tmp_name
4✔
1674

1675
    def _handle_expression_slicing_copy(
4✔
1676
        self, node, value_str, indices_nodes, shapes, ndim
1677
    ):
1678
        """Copy-based slicing for cases that cannot use views.
1679

1680
        This allocates a new array and copies elements using nested loops.
1681
        Used for negative steps or indirect access patterns.
1682
        """
1683
        dtype = Scalar(PrimitiveType.Double)
×
1684
        if value_str in self.container_table:
×
1685
            t = self.container_table[value_str]
×
UNCOV
1686
            if isinstance(t, Pointer) and t.has_pointee_type():
×
1687
                dtype = t.pointee_type
×
1688

1689
        result_shapes = []
×
1690
        result_shapes_runtime = []
×
UNCOV
1691
        slice_info = []
×
1692
        index_info = []
×
1693

UNCOV
1694
        for i, idx in enumerate(indices_nodes):
×
1695
            shape_val = shapes[i] if i < len(shapes) else f"_{value_str}_shape_{i}"
×
1696

1697
            if isinstance(idx, ast.Slice):
×
1698
                start_str = "0"
×
1699
                start_str_runtime = "0"
×
1700
                if idx.lower is not None:
×
UNCOV
1701
                    if self._contains_indirect_access(idx.lower):
×
UNCOV
1702
                        start_str, start_str_runtime = (
×
1703
                            self._materialize_indirect_access(
1704
                                idx.lower, return_original_expr=True
1705
                            )
1706
                        )
1707
                    else:
1708
                        start_str = self.visit(idx.lower)
×
UNCOV
1709
                        start_str_runtime = start_str
×
UNCOV
1710
                    if isinstance(start_str, str) and (
×
1711
                        start_str.startswith("-") or start_str.startswith("(-")
1712
                    ):
UNCOV
1713
                        start_str = f"({shape_val} + {start_str})"
×
1714
                        start_str_runtime = f"({shape_val} + {start_str_runtime})"
×
1715

1716
                stop_str = str(shape_val)
×
1717
                stop_str_runtime = str(shape_val)
×
1718
                if idx.upper is not None:
×
UNCOV
1719
                    if self._contains_indirect_access(idx.upper):
×
UNCOV
1720
                        stop_str, stop_str_runtime = self._materialize_indirect_access(
×
1721
                            idx.upper, return_original_expr=True
1722
                        )
1723
                    else:
1724
                        stop_str = self.visit(idx.upper)
×
UNCOV
1725
                        stop_str_runtime = stop_str
×
UNCOV
1726
                    if isinstance(stop_str, str) and (
×
1727
                        stop_str.startswith("-") or stop_str.startswith("(-")
1728
                    ):
UNCOV
1729
                        stop_str = f"({shape_val} + {stop_str})"
×
1730
                        stop_str_runtime = f"({shape_val} + {stop_str_runtime})"
×
1731

1732
                step_str = "1"
×
UNCOV
1733
                if idx.step is not None:
×
UNCOV
1734
                    step_str = self.visit(idx.step)
×
1735

1736
                # Compute dimension size accounting for step: ceil((stop - start) / step)
1737
                # For symbolic expressions, use integer ceiling formula: idiv(n + d - 1, d)
1738
                if step_str == "1":
×
UNCOV
1739
                    dim_size = f"({stop_str} - {start_str})"
×
1740
                    dim_size_runtime = f"({stop_str_runtime} - {start_str_runtime})"
×
1741
                else:
UNCOV
1742
                    dim_size = (
×
1743
                        f"idiv({stop_str} - {start_str} + {step_str} - 1, {step_str})"
1744
                    )
1745
                    dim_size_runtime = f"idiv({stop_str_runtime} - {start_str_runtime} + {step_str} - 1, {step_str})"
×
1746
                result_shapes.append(dim_size)
×
UNCOV
1747
                result_shapes_runtime.append(dim_size_runtime)
×
1748
                slice_info.append((i, start_str, stop_str, step_str))
×
1749
            else:
UNCOV
1750
                if self._contains_indirect_access(idx):
×
1751
                    index_str = self._materialize_indirect_access(idx)
×
1752
                else:
UNCOV
1753
                    index_str = self.visit(idx)
×
UNCOV
1754
                if isinstance(index_str, str) and (
×
1755
                    index_str.startswith("-") or index_str.startswith("(-")
1756
                ):
UNCOV
1757
                    index_str = f"({shape_val} + {index_str})"
×
1758
                index_info.append((i, index_str))
×
1759

UNCOV
1760
        tmp_name = self.builder.find_new_name("_slice_tmp_")
×
1761
        result_ndim = len(result_shapes)
×
1762

1763
        if result_ndim == 0:
×
UNCOV
1764
            self.builder.add_container(tmp_name, dtype, False)
×
1765
            self.container_table[tmp_name] = dtype
×
1766
        else:
1767
            size_str = "1"
×
UNCOV
1768
            for dim in result_shapes:
×
1769
                size_str = f"({size_str} * {dim})"
×
1770

UNCOV
1771
            element_size = self.builder.get_sizeof(dtype)
×
1772
            total_size = f"({size_str} * {element_size})"
×
1773

1774
            ptr_type = Pointer(dtype)
×
1775
            self.builder.add_container(tmp_name, ptr_type, False)
×
1776
            self.container_table[tmp_name] = ptr_type
×
UNCOV
1777
            tensor_info = Tensor(dtype, result_shapes)
×
UNCOV
1778
            self.shapes_runtime_info[tmp_name] = (
×
1779
                result_shapes_runtime  # Store runtime shapes separately
1780
            )
1781
            self.tensor_table[tmp_name] = tensor_info
×
1782

1783
            debug_info = DebugInfo()
×
1784
            block_alloc = self.builder.add_block(debug_info)
×
1785
            t_malloc = self.builder.add_malloc(block_alloc, total_size)
×
UNCOV
1786
            t_ptr = self.builder.add_access(block_alloc, tmp_name, debug_info)
×
UNCOV
1787
            self.builder.add_memlet(
×
1788
                block_alloc, t_malloc, "_ret", t_ptr, "void", "", ptr_type, debug_info
1789
            )
1790

UNCOV
1791
        loop_vars = []
×
1792
        debug_info = DebugInfo()
×
1793

1794
        for dim_idx, (orig_dim, start_str, stop_str, step_str) in enumerate(slice_info):
×
UNCOV
1795
            loop_var = self.builder.find_new_name(f"_slice_loop_{dim_idx}_")
×
1796
            loop_vars.append((loop_var, orig_dim, start_str, step_str))
×
1797

1798
            if not self.builder.exists(loop_var):
×
UNCOV
1799
                self.builder.add_container(loop_var, Scalar(PrimitiveType.Int64), False)
×
UNCOV
1800
                self.container_table[loop_var] = Scalar(PrimitiveType.Int64)
×
1801

1802
            # Account for step in loop count: ceil((stop - start) / step)
UNCOV
1803
            if step_str == "1":
×
1804
                count_str = f"({stop_str} - {start_str})"
×
1805
            else:
UNCOV
1806
                count_str = (
×
1807
                    f"idiv({stop_str} - {start_str} + {step_str} - 1, {step_str})"
1808
                )
1809
            self.builder.begin_for(loop_var, "0", count_str, "1", debug_info)
×
1810

UNCOV
1811
        src_indices = [""] * ndim
×
1812
        dst_indices = []
×
1813

UNCOV
1814
        for orig_dim, index_str in index_info:
×
1815
            src_indices[orig_dim] = index_str
×
1816

1817
        for loop_var, orig_dim, start_str, step_str in loop_vars:
×
UNCOV
1818
            if step_str == "1":
×
1819
                src_indices[orig_dim] = f"({start_str} + {loop_var})"
×
1820
            else:
UNCOV
1821
                src_indices[orig_dim] = f"({start_str} + {loop_var} * {step_str})"
×
1822
            dst_indices.append(loop_var)
×
1823

1824
        src_linear = self._compute_linear_index(src_indices, shapes, value_str, ndim)
×
UNCOV
1825
        if result_ndim > 0:
×
UNCOV
1826
            dst_linear = self._compute_linear_index(
×
1827
                dst_indices, result_shapes, tmp_name, result_ndim
1828
            )
1829
        else:
1830
            dst_linear = "0"
×
1831

1832
        block = self.builder.add_block(debug_info)
×
1833
        t_src = self.builder.add_access(block, value_str, debug_info)
×
UNCOV
1834
        t_dst = self.builder.add_access(block, tmp_name, debug_info)
×
UNCOV
1835
        t_task = self.builder.add_tasklet(
×
1836
            block, TaskletCode.assign, ["_in"], ["_out"], debug_info
1837
        )
1838

UNCOV
1839
        self.builder.add_memlet(
×
1840
            block, t_src, "void", t_task, "_in", src_linear, None, debug_info
1841
        )
UNCOV
1842
        self.builder.add_memlet(
×
1843
            block, t_task, "_out", t_dst, "void", dst_linear, None, debug_info
1844
        )
1845

UNCOV
1846
        for _ in loop_vars:
×
1847
            self.builder.end_for()
×
1848

UNCOV
1849
        return tmp_name
×
1850

1851
    def _compute_linear_index(self, indices, shapes, array_name, ndim):
4✔
1852
        """Compute linear index from multi-dimensional indices."""
UNCOV
1853
        if ndim == 0:
×
1854
            return "0"
×
1855

1856
        linear_index = ""
×
1857
        for i in range(ndim):
×
1858
            term = str(indices[i])
×
1859
            for j in range(i + 1, ndim):
×
UNCOV
1860
                shape_val = shapes[j] if j < len(shapes) else f"_{array_name}_shape_{j}"
×
1861
                term = f"(({term}) * {shape_val})"
×
1862

UNCOV
1863
            if i == 0:
×
1864
                linear_index = term
×
1865
            else:
1866
                linear_index = f"({linear_index} + {term})"
×
1867

UNCOV
1868
        return linear_index
×
1869

1870
    def _is_array_index(self, node):
4✔
1871
        """Check if a node represents an array that could be used as an index (gather)."""
1872
        if isinstance(node, ast.Name):
4✔
1873
            return node.id in self.tensor_table
4✔
1874
        return False
4✔
1875

1876
    def _handle_gather(self, value_str, index_node, debug_info=None):
4✔
1877
        """Handle gather operation: x[indices] where indices is an array."""
UNCOV
1878
        if debug_info is None:
×
1879
            debug_info = DebugInfo()
×
1880

UNCOV
1881
        if isinstance(index_node, ast.Name):
×
1882
            idx_array_name = index_node.id
×
1883
        else:
1884
            idx_array_name = self.visit(index_node)
×
1885

UNCOV
1886
        if idx_array_name not in self.tensor_table:
×
1887
            raise ValueError(f"Gather index must be an array, got {idx_array_name}")
×
1888

UNCOV
1889
        idx_shapes = self.tensor_table[idx_array_name].shape
×
1890
        idx_ndim = len(idx_shapes)
×
1891

UNCOV
1892
        if idx_ndim != 1:
×
1893
            raise NotImplementedError("Only 1D index arrays supported for gather")
×
1894

UNCOV
1895
        result_shape = idx_shapes[0] if idx_shapes else f"_{idx_array_name}_shape_0"
×
1896

1897
        # For runtime evaluation, prefer shapes_runtime_info if available
1898
        # This ensures we use expressions that can be evaluated at runtime
1899
        if idx_array_name in self.shapes_runtime_info:
×
UNCOV
1900
            runtime_shapes = self.shapes_runtime_info[idx_array_name]
×
1901
            result_shape_runtime = runtime_shapes[0] if runtime_shapes else result_shape
×
1902
        else:
1903
            result_shape_runtime = result_shape
×
1904

1905
        dtype = Scalar(PrimitiveType.Double)
×
1906
        if value_str in self.container_table:
×
1907
            t = self.container_table[value_str]
×
UNCOV
1908
            if isinstance(t, Pointer) and t.has_pointee_type():
×
1909
                dtype = t.pointee_type
×
1910

1911
        idx_dtype = Scalar(PrimitiveType.Int64)
×
1912
        if idx_array_name in self.container_table:
×
1913
            t = self.container_table[idx_array_name]
×
UNCOV
1914
            if isinstance(t, Pointer) and t.has_pointee_type():
×
1915
                idx_dtype = t.pointee_type
×
1916

1917
        tmp_name = self.builder.find_new_name("_gather_")
×
1918

UNCOV
1919
        element_size = self.builder.get_sizeof(dtype)
×
1920
        total_size = f"({result_shape} * {element_size})"
×
1921

1922
        ptr_type = Pointer(dtype)
×
1923
        self.builder.add_container(tmp_name, ptr_type, False)
×
UNCOV
1924
        self.container_table[tmp_name] = ptr_type
×
1925
        self.tensor_table[tmp_name] = Tensor(dtype, [result_shape])
×
1926
        # Store runtime evaluable shape for this gather result
1927
        self.shapes_runtime_info[tmp_name] = [result_shape_runtime]
×
1928

1929
        block_alloc = self.builder.add_block(debug_info)
×
1930
        t_malloc = self.builder.add_malloc(block_alloc, total_size)
×
UNCOV
1931
        t_ptr = self.builder.add_access(block_alloc, tmp_name, debug_info)
×
UNCOV
1932
        self.builder.add_memlet(
×
1933
            block_alloc, t_malloc, "_ret", t_ptr, "void", "", ptr_type, debug_info
1934
        )
1935

1936
        loop_var = self.builder.find_new_name("_gather_i_")
×
UNCOV
1937
        self.builder.add_container(loop_var, Scalar(PrimitiveType.Int64), False)
×
1938
        self.container_table[loop_var] = Scalar(PrimitiveType.Int64)
×
1939

1940
        idx_var = self.builder.find_new_name("_gather_idx_")
×
UNCOV
1941
        self.builder.add_container(idx_var, idx_dtype, False)
×
1942
        self.container_table[idx_var] = idx_dtype
×
1943

1944
        self.builder.begin_for(loop_var, "0", str(result_shape), "1", debug_info)
×
1945

UNCOV
1946
        block_load_idx = self.builder.add_block(debug_info)
×
UNCOV
1947
        idx_arr_access = self.builder.add_access(
×
1948
            block_load_idx, idx_array_name, debug_info
1949
        )
UNCOV
1950
        idx_var_access = self.builder.add_access(block_load_idx, idx_var, debug_info)
×
UNCOV
1951
        tasklet_load = self.builder.add_tasklet(
×
1952
            block_load_idx, TaskletCode.assign, ["_in"], ["_out"], debug_info
1953
        )
UNCOV
1954
        self.builder.add_memlet(
×
1955
            block_load_idx,
1956
            idx_arr_access,
1957
            "void",
1958
            tasklet_load,
1959
            "_in",
1960
            loop_var,
1961
            None,
1962
            debug_info,
1963
        )
UNCOV
1964
        self.builder.add_memlet(
×
1965
            block_load_idx,
1966
            tasklet_load,
1967
            "_out",
1968
            idx_var_access,
1969
            "void",
1970
            "",
1971
            None,
1972
            debug_info,
1973
        )
1974

1975
        block_gather = self.builder.add_block(debug_info)
×
1976
        src_access = self.builder.add_access(block_gather, value_str, debug_info)
×
UNCOV
1977
        dst_access = self.builder.add_access(block_gather, tmp_name, debug_info)
×
UNCOV
1978
        tasklet_gather = self.builder.add_tasklet(
×
1979
            block_gather, TaskletCode.assign, ["_in"], ["_out"], debug_info
1980
        )
1981

UNCOV
1982
        self.builder.add_memlet(
×
1983
            block_gather,
1984
            src_access,
1985
            "void",
1986
            tasklet_gather,
1987
            "_in",
1988
            idx_var,
1989
            None,
1990
            debug_info,
1991
        )
UNCOV
1992
        self.builder.add_memlet(
×
1993
            block_gather,
1994
            tasklet_gather,
1995
            "_out",
1996
            dst_access,
1997
            "void",
1998
            loop_var,
1999
            None,
2000
            debug_info,
2001
        )
2002

2003
        self.builder.end_for()
×
2004

UNCOV
2005
        return tmp_name
×
2006

2007
    def _get_max_array_ndim_in_expr(self, node):
4✔
2008
        """Get the maximum array dimensionality in an expression."""
2009
        max_ndim = 0
4✔
2010

2011
        class NdimVisitor(ast.NodeVisitor):
4✔
2012
            def __init__(self, tensor_table):
4✔
2013
                self.tensor_table = tensor_table
4✔
2014
                self.max_ndim = 0
4✔
2015

2016
            def visit_Name(self, node):
4✔
2017
                if node.id in self.tensor_table:
4✔
2018
                    ndim = len(self.tensor_table[node.id].shape)
4✔
2019
                    self.max_ndim = max(self.max_ndim, ndim)
4✔
2020
                return self.generic_visit(node)
4✔
2021

2022
        visitor = NdimVisitor(self.tensor_table)
4✔
2023
        visitor.visit(node)
4✔
2024
        return visitor.max_ndim
4✔
2025

2026
    def _handle_broadcast_slice_assignment(
4✔
2027
        self,
2028
        target,
2029
        materialized_rhs,
2030
        target_name,
2031
        indices,
2032
        target_ndim,
2033
        value_ndim,
2034
        debug_info,
2035
    ):
2036
        """Handle slice assignment with broadcasting (e.g., 2D[:,:] = 1D[:]).
2037

2038
        materialized_rhs is the already-evaluated RHS array name (not AST node).
2039
        """
2040
        broadcast_dims = target_ndim - value_ndim
×
2041
        shapes = self.tensor_table[target_name].shape
×
UNCOV
2042
        rhs_tensor = self.tensor_table.get(materialized_rhs)
×
UNCOV
2043
        rhs_shapes = rhs_tensor.shape if rhs_tensor else []
×
2044

2045
        # Create outer loops for broadcast dimensions
2046
        outer_loop_vars = []
×
2047
        for i in range(broadcast_dims):
×
UNCOV
2048
            loop_var = self.builder.find_new_name(f"_bcast_iter_{i}_")
×
2049
            outer_loop_vars.append(loop_var)
×
2050

2051
            if not self.builder.exists(loop_var):
×
UNCOV
2052
                self.builder.add_container(loop_var, Scalar(PrimitiveType.Int64), False)
×
2053
                self.container_table[loop_var] = Scalar(PrimitiveType.Int64)
×
2054

UNCOV
2055
            dim_size = shapes[i] if i < len(shapes) else f"_{target_name}_shape_{i}"
×
UNCOV
2056
            self.builder.begin_for(loop_var, "0", str(dim_size), "1", debug_info)
×
2057

2058
        # Create inner loops for value dimensions
2059
        inner_loop_vars = []
×
2060
        for i in range(value_ndim):
×
UNCOV
2061
            loop_var = self.builder.find_new_name(f"_inner_iter_{i}_")
×
2062
            inner_loop_vars.append(loop_var)
×
2063

2064
            if not self.builder.exists(loop_var):
×
UNCOV
2065
                self.builder.add_container(loop_var, Scalar(PrimitiveType.Int64), False)
×
UNCOV
2066
                self.container_table[loop_var] = Scalar(PrimitiveType.Int64)
×
2067

2068
            # Use RHS shape for inner dimension bounds
UNCOV
2069
            dim_size = (
×
2070
                rhs_shapes[i] if i < len(rhs_shapes) else shapes[broadcast_dims + i]
2071
            )
UNCOV
2072
            self.builder.begin_for(loop_var, "0", str(dim_size), "1", debug_info)
×
2073

2074
        # Create assignment block: target[outer_vars, inner_vars] = rhs[inner_vars]
2075
        block = self.builder.add_block(debug_info)
×
2076
        t_src = self.builder.add_access(block, materialized_rhs, debug_info)
×
UNCOV
2077
        t_dst = self.builder.add_access(block, target_name, debug_info)
×
UNCOV
2078
        t_task = self.builder.add_tasklet(
×
2079
            block, TaskletCode.assign, ["_in"], ["_out"], debug_info
2080
        )
2081

2082
        # Source index: just inner loop vars
UNCOV
2083
        src_index = ",".join(inner_loop_vars) if inner_loop_vars else "0"
×
2084

2085
        # Target index: outer_vars + inner_vars combined
UNCOV
2086
        all_target_vars = outer_loop_vars + inner_loop_vars
×
2087
        target_index = ",".join(all_target_vars) if all_target_vars else "0"
×
2088

UNCOV
2089
        self.builder.add_memlet(
×
2090
            block, t_src, "void", t_task, "_in", src_index, rhs_tensor, debug_info
2091
        )
2092

UNCOV
2093
        tensor_dst = self.tensor_table[target_name]
×
UNCOV
2094
        self.builder.add_memlet(
×
2095
            block, t_task, "_out", t_dst, "void", target_index, tensor_dst, debug_info
2096
        )
2097

2098
        # Close all loops (inner first, then outer)
2099
        for _ in inner_loop_vars:
×
2100
            self.builder.end_for()
×
UNCOV
2101
        for _ in outer_loop_vars:
×
UNCOV
2102
            self.builder.end_for()
×
2103

2104
    def _handle_slice_assignment(
4✔
2105
        self, target, value, target_name, indices, debug_info=None
2106
    ):
2107
        if debug_info is None:
4✔
UNCOV
2108
            debug_info = DebugInfo()
×
2109

2110
        # Add missing dimensions
2111
        tensor_info = self.tensor_table[target_name]
4✔
2112
        ndim = len(tensor_info.shape)
4✔
2113
        if len(indices) < ndim:
4✔
2114
            indices = list(indices)
4✔
2115
            for _ in range(ndim - len(indices)):
4✔
2116
                indices.append(ast.Slice(lower=None, upper=None, step=None))
4✔
2117

2118
        # Handle ufunc outer case separately to preserve slice shape info
2119
        has_outer, ufunc_name, outer_node = contains_ufunc_outer(value)
4✔
2120
        if has_outer:
4✔
2121
            self._handle_ufunc_outer_slice_assignment(
4✔
2122
                target, value, target_name, indices, debug_info
2123
            )
2124
            return
4✔
2125

2126
        # Count slice dimensions to determine effective target dimensionality
2127
        target_slice_ndim = sum(1 for idx in indices if isinstance(idx, ast.Slice))
4✔
2128
        value_max_ndim = self._get_max_array_ndim_in_expr(value)
4✔
2129

2130
        # ALWAYS evaluate RHS first (NumPy semantics) - before any loops
2131
        materialized_rhs = self.visit(value)
4✔
2132

2133
        if (
4✔
2134
            target_slice_ndim > 0
2135
            and value_max_ndim > 0
2136
            and target_slice_ndim > value_max_ndim
2137
        ):
2138
            # Broadcasting case: use row-by-row approach with reference memlets
UNCOV
2139
            self._handle_broadcast_slice_assignment(
×
2140
                target,
2141
                materialized_rhs,
2142
                target_name,
2143
                indices,
2144
                target_slice_ndim,
2145
                value_max_ndim,
2146
                debug_info,
2147
            )
UNCOV
2148
            return
×
2149

2150
        loop_vars = []
4✔
2151
        new_target_indices = []
4✔
2152

2153
        for i, idx in enumerate(indices):
4✔
2154
            if isinstance(idx, ast.Slice):
4✔
2155
                loop_var = self.builder.find_new_name(f"_slice_iter_{len(loop_vars)}_")
4✔
2156
                loop_vars.append(loop_var)
4✔
2157

2158
                if not self.builder.exists(loop_var):
4✔
2159
                    self.builder.add_container(
4✔
2160
                        loop_var, Scalar(PrimitiveType.Int64), False
2161
                    )
2162
                    self.container_table[loop_var] = Scalar(PrimitiveType.Int64)
4✔
2163

2164
                start_str = "0"
4✔
2165
                if idx.lower:
4✔
2166
                    start_str = self.visit(idx.lower)
4✔
2167
                    if start_str.startswith("-"):
4✔
UNCOV
2168
                        dim_size = (
×
2169
                            str(tensor_info.shape[i])
2170
                            if i < len(tensor_info.shape)
2171
                            else f"_{target_name}_shape_{i}"
2172
                        )
UNCOV
2173
                        start_str = f"({dim_size} {start_str})"
×
2174

2175
                stop_str = ""
4✔
2176
                if idx.upper and not (
4✔
2177
                    isinstance(idx.upper, ast.Constant) and idx.upper.value is None
2178
                ):
2179
                    stop_str = self.visit(idx.upper)
4✔
2180
                    if stop_str.startswith("-") or stop_str.startswith("(-"):
4✔
UNCOV
2181
                        dim_size = (
×
2182
                            str(tensor_info.shape[i])
2183
                            if i < len(tensor_info.shape)
2184
                            else f"_{target_name}_shape_{i}"
2185
                        )
UNCOV
2186
                        stop_str = f"({dim_size} {stop_str})"
×
2187
                else:
2188
                    stop_str = (
4✔
2189
                        str(tensor_info.shape[i])
2190
                        if i < len(tensor_info.shape)
2191
                        else f"_{target_name}_shape_{i}"
2192
                    )
2193

2194
                step_str = "1"
4✔
2195
                if idx.step:
4✔
UNCOV
2196
                    step_str = self.visit(idx.step)
×
2197

2198
                count_str = f"({stop_str} - {start_str})"
4✔
2199

2200
                self.builder.begin_for(loop_var, "0", count_str, "1", debug_info)
4✔
2201
                self.container_table[loop_var] = Scalar(PrimitiveType.Int64)
4✔
2202
                new_target_indices.append(
4✔
2203
                    ast.Name(
2204
                        id=f"{start_str} + {loop_var} * {step_str}", ctx=ast.Load()
2205
                    )
2206
                )
2207
            else:
2208
                dim_size = (
4✔
2209
                    tensor_info.shape[i]
2210
                    if i < len(tensor_info.shape)
2211
                    else f"_{target_name}_shape_{i}"
2212
                )
2213
                normalized_idx = normalize_negative_index(idx, dim_size)
4✔
2214
                # intermediate computations are placed outside the loops
2215
                idx_str = self.visit(normalized_idx)
4✔
2216
                new_target_indices.append(ast.Name(id=idx_str, ctx=ast.Load()))
4✔
2217

2218
        rewriter = SliceRewriter(loop_vars, self.tensor_table, self)
4✔
2219
        new_value = rewriter.visit(copy.deepcopy(value))
4✔
2220

2221
        new_target = copy.deepcopy(target)
4✔
2222
        if len(new_target_indices) == 1:
4✔
2223
            new_target.slice = new_target_indices[0]
4✔
2224
        else:
2225
            new_target.slice = ast.Tuple(elts=new_target_indices, ctx=ast.Load())
4✔
2226

2227
        rhs_memlet_type = None
4✔
2228
        rhs_indexed_subset = ""
4✔
2229
        if materialized_rhs in self.tensor_table:
4✔
2230
            rhs_tensor = self.tensor_table[materialized_rhs]
4✔
2231
            rhs_ndim = len(rhs_tensor.shape)
4✔
2232
            if rhs_ndim > 0 and rhs_ndim == len(loop_vars):
4✔
2233
                # RHS is an array matching the slice dimensions - index it with loop vars
2234
                rhs_indexed_subset = ",".join(loop_vars)
4✔
2235
                rhs_memlet_type = rhs_tensor
4✔
2236

2237
        block = self.builder.add_block(debug_info)
4✔
2238
        t_task = self.builder.add_tasklet(
4✔
2239
            block, TaskletCode.assign, ["_in"], ["_out"], debug_info
2240
        )
2241

2242
        t_src, src_sub = self._add_read(block, materialized_rhs, debug_info)
4✔
2243
        # Use indexed subset if RHS is an array that needs indexing
2244
        actual_src_sub = rhs_indexed_subset if rhs_indexed_subset else src_sub
4✔
2245
        self.builder.add_memlet(
4✔
2246
            block,
2247
            t_src,
2248
            "void",
2249
            t_task,
2250
            "_in",
2251
            actual_src_sub,
2252
            rhs_memlet_type,
2253
            debug_info,
2254
        )
2255

2256
        lhs_expr = self.visit(new_target)
4✔
2257
        if "(" in lhs_expr and lhs_expr.endswith(")"):
4✔
2258
            subset = lhs_expr[lhs_expr.find("(") + 1 : -1]
4✔
2259
            tensor_dst = self.tensor_table[target_name]
4✔
2260

2261
            t_dst = self.builder.add_access(block, target_name, debug_info)
4✔
2262
            self.builder.add_memlet(
4✔
2263
                block, t_task, "_out", t_dst, "void", subset, tensor_dst, debug_info
2264
            )
2265
        else:
UNCOV
2266
            t_dst = self.builder.add_access(block, target_name, debug_info)
×
UNCOV
2267
            self.builder.add_memlet(
×
2268
                block, t_task, "_out", t_dst, "void", "", None, debug_info
2269
            )
2270

2271
        for _ in loop_vars:
4✔
2272
            self.builder.end_for()
4✔
2273

2274
    def _handle_ufunc_outer_slice_assignment(
4✔
2275
        self, target, value, target_name, indices, debug_info=None
2276
    ):
2277
        """Handle slice assignment where RHS contains a ufunc outer operation.
2278

2279
        Example: path[:] = np.minimum(path[:], np.add.outer(path[:, k], path[k, :]))
2280

2281
        The strategy is:
2282
        1. Evaluate the entire RHS expression, which will create a temporary array
2283
           containing the result of the ufunc outer (potentially wrapped in other ops)
2284
        2. Copy the temporary result to the target slice
2285

2286
        This avoids the loop transformation that would destroy slice shape info.
2287
        """
2288
        if debug_info is None:
4✔
2289
            from docc.sdfg import DebugInfo
×
2290

UNCOV
2291
            debug_info = DebugInfo()
×
2292

2293
        # Evaluate the full RHS expression
2294
        # This will:
2295
        # - Create temp arrays for ufunc outer results
2296
        # - Apply any wrapping operations (np.minimum, etc.)
2297
        # - Return the name of the final result array
2298
        result_name = self.visit(value)
4✔
2299

2300
        # Now we need to copy result to target slice
2301
        # Count slice dimensions to determine if we need loops
2302
        target_slice_ndim = sum(1 for idx in indices if isinstance(idx, ast.Slice))
4✔
2303

2304
        if target_slice_ndim == 0:
4✔
2305
            # No slices on target - just simple assignment
2306
            target_str = self.visit(target)
×
2307
            block = self.builder.add_block(debug_info)
×
2308
            t_src, src_sub = self._add_read(block, result_name, debug_info)
×
UNCOV
2309
            t_dst = self.builder.add_access(block, target_str, debug_info)
×
UNCOV
2310
            t_task = self.builder.add_tasklet(
×
2311
                block, TaskletCode.assign, ["_in"], ["_out"], debug_info
2312
            )
UNCOV
2313
            self.builder.add_memlet(
×
2314
                block, t_src, "void", t_task, "_in", src_sub, None, debug_info
2315
            )
UNCOV
2316
            self.builder.add_memlet(
×
2317
                block, t_task, "_out", t_dst, "void", "", None, debug_info
2318
            )
UNCOV
2319
            return
×
2320

2321
        # We have slices on the target - need to create loops for copying
2322
        # Get target array info
2323
        target_shapes = self.tensor_table[target_name].shape
4✔
2324

2325
        loop_vars = []
4✔
2326
        new_target_indices = []
4✔
2327

2328
        for i, idx in enumerate(indices):
4✔
2329
            if isinstance(idx, ast.Slice):
4✔
2330
                loop_var = self.builder.find_new_name(f"_copy_iter_{len(loop_vars)}_")
4✔
2331
                loop_vars.append(loop_var)
4✔
2332

2333
                if not self.builder.exists(loop_var):
4✔
2334
                    self.builder.add_container(
4✔
2335
                        loop_var, Scalar(PrimitiveType.Int64), False
2336
                    )
2337
                    self.container_table[loop_var] = Scalar(PrimitiveType.Int64)
4✔
2338

2339
                start_str = "0"
4✔
2340
                if idx.lower:
4✔
UNCOV
2341
                    start_str = self.visit(idx.lower)
×
2342

2343
                stop_str = ""
4✔
2344
                if idx.upper and not (
4✔
2345
                    isinstance(idx.upper, ast.Constant) and idx.upper.value is None
2346
                ):
UNCOV
2347
                    stop_str = self.visit(idx.upper)
×
2348
                else:
2349
                    stop_str = (
4✔
2350
                        target_shapes[i]
2351
                        if i < len(target_shapes)
2352
                        else f"_{target_name}_shape_{i}"
2353
                    )
2354

2355
                step_str = "1"
4✔
2356
                if idx.step:
4✔
UNCOV
2357
                    step_str = self.visit(idx.step)
×
2358

2359
                count_str = f"({stop_str} - {start_str})"
4✔
2360

2361
                self.builder.begin_for(loop_var, "0", count_str, "1", debug_info)
4✔
2362
                self.container_table[loop_var] = Scalar(PrimitiveType.Int64)
4✔
2363

2364
                new_target_indices.append(
4✔
2365
                    ast.Name(
2366
                        id=f"{start_str} + {loop_var} * {step_str}", ctx=ast.Load()
2367
                    )
2368
                )
2369
            else:
2370
                # Handle non-slice indices - need to normalize negative indices
UNCOV
2371
                dim_size = (
×
2372
                    target_shapes[i]
2373
                    if i < len(target_shapes)
2374
                    else f"_{target_name}_shape_{i}"
2375
                )
UNCOV
2376
                normalized_idx = normalize_negative_index(idx, dim_size)
×
2377
                # Visit the index NOW before any loops are opened to ensure
2378
                # intermediate computations are placed outside the loops
UNCOV
2379
                idx_str = self.visit(normalized_idx)
×
UNCOV
2380
                new_target_indices.append(ast.Name(id=idx_str, ctx=ast.Load()))
×
2381

2382
        # Create assignment block: target[i,j,...] = result[i,j,...]
2383
        block = self.builder.add_block(debug_info)
4✔
2384

2385
        # Access nodes
2386
        t_src = self.builder.add_access(block, result_name, debug_info)
4✔
2387
        t_dst = self.builder.add_access(block, target_name, debug_info)
4✔
2388
        t_task = self.builder.add_tasklet(
4✔
2389
            block, TaskletCode.assign, ["_in"], ["_out"], debug_info
2390
        )
2391

2392
        # Source index - just use loop vars for flat array from ufunc outer
2393
        # The ufunc outer result is a flat array of size M*N
2394
        if len(loop_vars) == 2:
4✔
2395
            # 2D case: result is indexed as i * N + j
2396
            # Get the second dimension size from target shapes
2397
            n_dim = (
4✔
2398
                target_shapes[1]
2399
                if len(target_shapes) > 1
2400
                else f"_{target_name}_shape_1"
2401
            )
2402
            src_index = f"(({loop_vars[0]}) * ({n_dim}) + ({loop_vars[1]}))"
4✔
UNCOV
2403
        elif len(loop_vars) == 1:
×
UNCOV
2404
            src_index = loop_vars[0]
×
2405
        else:
2406
            # General case - compute linear index
2407
            src_terms = []
×
2408
            stride = "1"
×
2409
            for i in range(len(loop_vars) - 1, -1, -1):
×
UNCOV
2410
                if stride == "1":
×
2411
                    src_terms.insert(0, loop_vars[i])
×
2412
                else:
2413
                    src_terms.insert(0, f"({loop_vars[i]} * {stride})")
×
UNCOV
2414
                if i > 0:
×
UNCOV
2415
                    dim_size = (
×
2416
                        target_shapes[i]
2417
                        if i < len(target_shapes)
2418
                        else f"_{target_name}_shape_{i}"
2419
                    )
UNCOV
2420
                    stride = (
×
2421
                        f"({stride} * {dim_size})" if stride != "1" else str(dim_size)
2422
                    )
UNCOV
2423
            src_index = " + ".join(src_terms) if src_terms else "0"
×
2424

2425
        # Target index - compute linear index (row-major order)
2426
        # For 2D array with shape (M, N): linear_index = i * N + j
2427
        target_index_parts = []
4✔
2428
        for idx in new_target_indices:
4✔
2429
            if isinstance(idx, ast.Name):
4✔
2430
                target_index_parts.append(idx.id)
4✔
2431
            else:
UNCOV
2432
                target_index_parts.append(self.visit(idx))
×
2433

2434
        # Convert to linear index
2435
        if len(target_index_parts) == 2:
4✔
2436
            # 2D case
2437
            n_dim = (
4✔
2438
                target_shapes[1]
2439
                if len(target_shapes) > 1
2440
                else f"_{target_name}_shape_1"
2441
            )
2442
            target_index = (
4✔
2443
                f"(({target_index_parts[0]}) * ({n_dim}) + ({target_index_parts[1]}))"
2444
            )
UNCOV
2445
        elif len(target_index_parts) == 1:
×
UNCOV
2446
            target_index = target_index_parts[0]
×
2447
        else:
2448
            # General case - compute linear index with strides
2449
            stride = "1"
×
2450
            target_index = "0"
×
2451
            for i in range(len(target_index_parts) - 1, -1, -1):
×
2452
                idx_part = target_index_parts[i]
×
UNCOV
2453
                if stride == "1":
×
2454
                    term = idx_part
×
2455
                else:
2456
                    term = f"(({idx_part}) * ({stride}))"
×
2457

UNCOV
2458
                if target_index == "0":
×
2459
                    target_index = term
×
2460
                else:
2461
                    target_index = f"({term} + {target_index})"
×
2462

UNCOV
2463
                if i > 0:
×
UNCOV
2464
                    dim_size = (
×
2465
                        target_shapes[i]
2466
                        if i < len(target_shapes)
2467
                        else f"_{target_name}_shape_{i}"
2468
                    )
UNCOV
2469
                    stride = (
×
2470
                        f"({stride} * {dim_size})" if stride != "1" else str(dim_size)
2471
                    )
2472

2473
        # Connect memlets
2474
        self.builder.add_memlet(
4✔
2475
            block, t_src, "void", t_task, "_in", src_index, None, debug_info
2476
        )
2477
        self.builder.add_memlet(
4✔
2478
            block, t_task, "_out", t_dst, "void", target_index, None, debug_info
2479
        )
2480

2481
        # End loops
2482
        for _ in loop_vars:
4✔
2483
            self.builder.end_for()
4✔
2484

2485
    def _contains_indirect_access(self, node):
4✔
2486
        """Check if an AST node contains any indirect array access."""
2487
        if isinstance(node, ast.Subscript):
4✔
2488
            if isinstance(node.value, ast.Name):
×
2489
                arr_name = node.value.id
×
UNCOV
2490
                if arr_name in self.tensor_table:
×
UNCOV
2491
                    return True
×
2492
        elif isinstance(node, ast.BinOp):
4✔
2493
            return self._contains_indirect_access(
4✔
2494
                node.left
2495
            ) or self._contains_indirect_access(node.right)
2496
        elif isinstance(node, ast.UnaryOp):
4✔
2497
            return self._contains_indirect_access(node.operand)
4✔
2498
        return False
4✔
2499

2500
    def _materialize_indirect_access(
4✔
2501
        self, node, debug_info=None, return_original_expr=False
2502
    ):
2503
        """Materialize an array access into a scalar variable using tasklet+memlets."""
2504
        if not self.builder:
×
UNCOV
2505
            expr = self.visit(node)
×
2506
            return (expr, expr) if return_original_expr else expr
×
2507

UNCOV
2508
        if debug_info is None:
×
2509
            debug_info = DebugInfo()
×
2510

2511
        if not isinstance(node, ast.Subscript):
×
UNCOV
2512
            expr = self.visit(node)
×
2513
            return (expr, expr) if return_original_expr else expr
×
2514

2515
        if not isinstance(node.value, ast.Name):
×
UNCOV
2516
            expr = self.visit(node)
×
2517
            return (expr, expr) if return_original_expr else expr
×
2518

2519
        arr_name = node.value.id
×
2520
        if arr_name not in self.tensor_table:
×
UNCOV
2521
            expr = self.visit(node)
×
2522
            return (expr, expr) if return_original_expr else expr
×
2523

2524
        dtype = Scalar(PrimitiveType.Int64)
×
2525
        if arr_name in self.container_table:
×
2526
            t = self.container_table[arr_name]
×
UNCOV
2527
            if isinstance(t, Pointer) and t.has_pointee_type():
×
2528
                dtype = t.pointee_type
×
2529

2530
        tmp_name = self.builder.find_new_name("_idx_")
×
UNCOV
2531
        self.builder.add_container(tmp_name, dtype, False)
×
2532
        self.container_table[tmp_name] = dtype
×
2533

UNCOV
2534
        ndim = len(self.tensor_table[arr_name].shape)
×
2535
        shapes = self.tensor_table[arr_name].shape
×
2536

UNCOV
2537
        if isinstance(node.slice, ast.Tuple):
×
2538
            indices = [self.visit(elt) for elt in node.slice.elts]
×
2539
        else:
2540
            indices = [self.visit(node.slice)]
×
2541

2542
        materialized_indices = []
×
2543
        for idx_str in indices:
×
UNCOV
2544
            if "(" in idx_str and idx_str.endswith(")"):
×
2545
                materialized_indices.append(idx_str)
×
2546
            else:
2547
                materialized_indices.append(idx_str)
×
2548

UNCOV
2549
        linear_index = self._compute_linear_index(
×
2550
            materialized_indices, shapes, arr_name, ndim
2551
        )
2552

2553
        block = self.builder.add_block(debug_info)
×
2554
        t_src = self.builder.add_access(block, arr_name, debug_info)
×
UNCOV
2555
        t_dst = self.builder.add_access(block, tmp_name, debug_info)
×
UNCOV
2556
        t_task = self.builder.add_tasklet(
×
2557
            block, TaskletCode.assign, ["_in"], ["_out"], debug_info
2558
        )
2559

UNCOV
2560
        self.builder.add_memlet(
×
2561
            block, t_src, "void", t_task, "_in", linear_index, None, debug_info
2562
        )
UNCOV
2563
        self.builder.add_memlet(
×
2564
            block, t_task, "_out", t_dst, "void", "", None, debug_info
2565
        )
2566

2567
        if return_original_expr:
×
UNCOV
2568
            original_expr = f"{arr_name}({linear_index})"
×
2569
            return (tmp_name, original_expr)
×
2570

UNCOV
2571
        return tmp_name
×
2572

2573
    def _get_unique_id(self):
4✔
2574
        self._unique_counter_ref[0] += 1
4✔
2575
        return self._unique_counter_ref[0]
4✔
2576

2577
    def _get_memlet_type_for_access(self, expr_str, subset):
4✔
2578
        """Get the Tensor type for an indexed array access expression.
2579

2580
        When accessing an array like "arr(i,j)" with a multi-dimensional subset,
2581
        we need to pass the Tensor type to add_memlet for correct type inference.
2582
        If the expression is a simple scalar variable or constant, returns None.
2583
        """
2584
        if not subset:
4✔
2585
            return None
4✔
2586

2587
        # Check if expr_str is an indexed array access like "arr(i,j)"
2588
        if "(" in expr_str and expr_str.endswith(")"):
4✔
2589
            name = expr_str.split("(")[0]
4✔
2590
            if name in self.tensor_table:
4✔
2591
                return self.tensor_table[name]
4✔
2592

2593
        # Check if expr_str is a simple array name with a non-empty subset from _add_read
UNCOV
2594
        if expr_str in self.tensor_table:
×
2595
            return self.tensor_table[expr_str]
×
2596

UNCOV
2597
        return None
×
2598

2599
    def _element_type(self, name):
4✔
2600
        if name in self.container_table:
4✔
2601
            return element_type_from_sdfg_type(self.container_table[name])
4✔
2602
        else:  # Constant
2603
            if self._is_int(name):
4✔
2604
                return Scalar(PrimitiveType.Int64)
4✔
2605
            else:
2606
                return Scalar(PrimitiveType.Double)
4✔
2607

2608
    def _is_int(self, operand):
4✔
2609
        try:
4✔
2610
            if operand.lstrip("-").isdigit():
4✔
2611
                return True
4✔
UNCOV
2612
        except ValueError:
×
UNCOV
2613
            pass
×
2614

2615
        name = operand
4✔
2616
        if "(" in operand and operand.endswith(")"):
4✔
2617
            name = operand.split("(")[0]
4✔
2618

2619
        if name in self.container_table:
4✔
2620
            t = self.container_table[name]
4✔
2621

2622
            def is_int_ptype(pt):
4✔
2623
                return pt in [
4✔
2624
                    PrimitiveType.Int64,
2625
                    PrimitiveType.Int32,
2626
                    PrimitiveType.Int8,
2627
                    PrimitiveType.Int16,
2628
                    PrimitiveType.UInt64,
2629
                    PrimitiveType.UInt32,
2630
                    PrimitiveType.UInt8,
2631
                    PrimitiveType.UInt16,
2632
                ]
2633

2634
            if isinstance(t, Scalar):
4✔
2635
                return is_int_ptype(t.primitive_type)
4✔
2636

2637
            if type(t).__name__ == "Array" and hasattr(t, "element_type"):
4✔
2638
                et = t.element_type
×
2639
                if callable(et):
×
2640
                    et = et()
×
UNCOV
2641
                if isinstance(et, Scalar):
×
UNCOV
2642
                    return is_int_ptype(et.primitive_type)
×
2643

2644
            if type(t).__name__ == "Pointer":
4✔
2645
                if hasattr(t, "pointee_type"):
4✔
2646
                    et = t.pointee_type
4✔
2647
                    if callable(et):
4✔
UNCOV
2648
                        et = et()
×
2649
                    if isinstance(et, Scalar):
4✔
2650
                        return is_int_ptype(et.primitive_type)
4✔
2651
                if hasattr(t, "element_type"):
×
2652
                    et = t.element_type
×
2653
                    if callable(et):
×
2654
                        et = et()
×
UNCOV
2655
                    if isinstance(et, Scalar):
×
UNCOV
2656
                        return is_int_ptype(et.primitive_type)
×
2657

2658
        return False
4✔
2659

2660
    def _add_read(self, block, expr_str, debug_info=None):
4✔
2661
        try:
4✔
2662
            if (block, expr_str) in self._access_cache:
4✔
2663
                return self._access_cache[(block, expr_str)]
×
UNCOV
2664
        except TypeError:
×
UNCOV
2665
            pass
×
2666

2667
        if debug_info is None:
4✔
2668
            debug_info = DebugInfo()
4✔
2669

2670
        if "(" in expr_str and expr_str.endswith(")"):
4✔
2671
            name = expr_str.split("(")[0]
4✔
2672
            subset = expr_str[expr_str.find("(") + 1 : -1]
4✔
2673
            access = self.builder.add_access(block, name, debug_info)
4✔
2674
            try:
4✔
2675
                self._access_cache[(block, expr_str)] = (access, subset)
4✔
UNCOV
2676
            except TypeError:
×
UNCOV
2677
                pass
×
2678
            return access, subset
4✔
2679

2680
        if self.builder.exists(expr_str):
4✔
2681
            access = self.builder.add_access(block, expr_str, debug_info)
4✔
2682
            subset = ""
4✔
2683
            if expr_str in self.container_table:
4✔
2684
                sym_type = self.container_table[expr_str]
4✔
2685
                if isinstance(sym_type, Pointer):
4✔
2686
                    if expr_str in self.tensor_table:
4✔
2687
                        ndim = len(self.tensor_table[expr_str].shape)
4✔
2688
                        if ndim == 0:
4✔
2689
                            subset = "0"
×
2690
                    else:
UNCOV
2691
                        subset = "0"
×
2692
            try:
4✔
2693
                self._access_cache[(block, expr_str)] = (access, subset)
4✔
UNCOV
2694
            except TypeError:
×
UNCOV
2695
                pass
×
2696
            return access, subset
4✔
2697

2698
        dtype = Scalar(PrimitiveType.Double)
4✔
2699
        if self._is_int(expr_str):
4✔
2700
            dtype = Scalar(PrimitiveType.Int64)
4✔
2701
        elif expr_str == "true" or expr_str == "false":
4✔
UNCOV
2702
            dtype = Scalar(PrimitiveType.Bool)
×
2703

2704
        const_node = self.builder.add_constant(block, expr_str, dtype, debug_info)
4✔
2705
        try:
4✔
2706
            self._access_cache[(block, expr_str)] = (const_node, "")
4✔
UNCOV
2707
        except TypeError:
×
UNCOV
2708
            pass
×
2709
        return const_node, ""
4✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc