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

daisytuner / docc / 22168646772

18 Feb 2026 06:09PM UTC coverage: 64.742%. First build
22168646772

push

github

web-flow
Merge pull request #526 from daisytuner/native-ndarray

Python - Native Tensor Support: Update operations to use tensor type

2783 of 4104 new or added lines in 42 files covered. (67.81%)

23724 of 36644 relevant lines covered (64.74%)

368.07 hits per line

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

67.83
/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

30

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

46
        # Lookup tables for variables
47
        self.tensor_table = tensor_table
4✔
48
        self.container_table = container_table
4✔
49

50
        # Debug info
51
        self.filename = filename
4✔
52
        self.function_name = function_name
4✔
53

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

70
        # Initialize handlers - they receive 'self' to access expression visitor methods
71
        self.numpy_visitor = NumPyHandler(self)
4✔
72
        self.math_handler = MathHandler(self)
4✔
73
        self.python_handler = PythonHandler(self)
4✔
74
        self.scipy_handler = SciPyHandler(self)
4✔
75

76
    def visit_Constant(self, node):
4✔
77
        if isinstance(node.value, bool):
4✔
78
            return "true" if node.value else "false"
4✔
79
        return str(node.value)
4✔
80

81
    def visit_Name(self, node):
4✔
82
        name = node.id
4✔
83
        if name not in self.container_table and self.globals_dict is not None:
4✔
84
            if name in self.globals_dict:
4✔
85
                val = self.globals_dict[name]
4✔
86
                if isinstance(val, (int, float)):
4✔
87
                    return str(val)
4✔
88
        return name
4✔
89

90
    def visit_Add(self, node):
4✔
91
        return "+"
4✔
92

93
    def visit_Sub(self, node):
4✔
94
        return "-"
4✔
95

96
    def visit_Mult(self, node):
4✔
97
        return "*"
4✔
98

99
    def visit_Div(self, node):
4✔
100
        return "/"
4✔
101

102
    def visit_FloorDiv(self, node):
4✔
103
        return "//"
4✔
104

105
    def visit_Mod(self, node):
4✔
106
        return "%"
4✔
107

108
    def visit_Pow(self, node):
4✔
109
        return "**"
4✔
110

111
    def visit_Eq(self, node):
4✔
112
        return "=="
4✔
113

114
    def visit_NotEq(self, node):
4✔
NEW
115
        return "!="
×
116

117
    def visit_Lt(self, node):
4✔
118
        return "<"
4✔
119

120
    def visit_LtE(self, node):
4✔
NEW
121
        return "<="
×
122

123
    def visit_Gt(self, node):
4✔
124
        return ">"
4✔
125

126
    def visit_GtE(self, node):
4✔
127
        return ">="
4✔
128

129
    def visit_And(self, node):
4✔
130
        return "&"
4✔
131

132
    def visit_Or(self, node):
4✔
133
        return "|"
4✔
134

135
    def visit_BitAnd(self, node):
4✔
136
        return "&"
4✔
137

138
    def visit_BitOr(self, node):
4✔
139
        return "|"
4✔
140

141
    def visit_BitXor(self, node):
4✔
142
        return "^"
4✔
143

144
    def visit_LShift(self, node):
4✔
NEW
145
        return "<<"
×
146

147
    def visit_RShift(self, node):
4✔
NEW
148
        return ">>"
×
149

150
    def visit_Not(self, node):
4✔
151
        return "!"
4✔
152

153
    def visit_USub(self, node):
4✔
154
        return "-"
4✔
155

156
    def visit_UAdd(self, node):
4✔
NEW
157
        return "+"
×
158

159
    def visit_Invert(self, node):
4✔
160
        return "~"
4✔
161

162
    def visit_BoolOp(self, node):
4✔
163
        op = self.visit(node.op)
4✔
164
        values = [f"({self.visit(v)} != 0)" for v in node.values]
4✔
165
        expr_str = f"{f' {op} '.join(values)}"
4✔
166

167
        tmp_name = self.builder.find_new_name()
4✔
168
        dtype = Scalar(PrimitiveType.Bool)
4✔
169
        self.builder.add_container(tmp_name, dtype, False)
4✔
170

171
        self.builder.begin_if(expr_str)
4✔
172
        self._add_assign_constant(tmp_name, "true", dtype)
4✔
173
        self.builder.begin_else()
4✔
174
        self._add_assign_constant(tmp_name, "false", dtype)
4✔
175
        self.builder.end_if()
4✔
176

177
        self.container_table[tmp_name] = dtype
4✔
178
        return tmp_name
4✔
179

180
    def visit_UnaryOp(self, node):
4✔
181
        if (
4✔
182
            isinstance(node.op, ast.USub)
183
            and isinstance(node.operand, ast.Constant)
184
            and isinstance(node.operand.value, (int, float))
185
        ):
186
            return f"-{node.operand.value}"
4✔
187

188
        op = self.visit(node.op)
4✔
189
        operand = self.visit(node.operand)
4✔
190

191
        if operand in self.tensor_table and op == "-":
4✔
192
            return self.numpy_visitor.handle_array_negate(operand)
4✔
193

194
        assert operand in self.container_table, f"Undefined variable: {operand}"
4✔
195
        tmp_name = self.builder.find_new_name()
4✔
196
        dtype = self.container_table[operand]
4✔
197
        self.builder.add_container(tmp_name, dtype, False)
4✔
198
        self.container_table[tmp_name] = dtype
4✔
199

200
        block = self.builder.add_block()
4✔
201
        t_src, src_sub = self._add_read(block, operand)
4✔
202
        t_dst = self.builder.add_access(block, tmp_name)
4✔
203

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

246
        return tmp_name
4✔
247

248
    def visit_BinOp(self, node):
4✔
249
        if isinstance(node.op, ast.MatMult):
4✔
250
            return self.numpy_visitor.handle_numpy_matmul_op(node.left, node.right)
4✔
251

252
        left = self.visit(node.left)
4✔
253
        op = self.visit(node.op)
4✔
254
        right = self.visit(node.right)
4✔
255

256
        left_is_array = left in self.tensor_table
4✔
257
        right_is_array = right in self.tensor_table
4✔
258

259
        if left_is_array or right_is_array:
4✔
260
            op_map = {"+": "add", "-": "sub", "*": "mul", "/": "div", "**": "pow"}
4✔
261
            if op in op_map:
4✔
262
                return self.numpy_visitor.handle_array_binary_op(
4✔
263
                    op_map[op], left, right
264
                )
265
            else:
NEW
266
                raise NotImplementedError(f"Array operation {op} not supported")
×
267

268
        tmp_name = self.builder.find_new_name()
4✔
269

270
        left_is_int = self._is_int(left)
4✔
271
        right_is_int = self._is_int(right)
4✔
272
        dtype = Scalar(PrimitiveType.Double)
4✔
273
        if left_is_int and right_is_int and op not in ["/", "**"]:
4✔
274
            dtype = Scalar(PrimitiveType.Int64)
4✔
275

276
        if not self.builder.exists(tmp_name):
4✔
277
            self.builder.add_container(tmp_name, dtype, False)
4✔
278
            self.container_table[tmp_name] = dtype
4✔
279

280
        real_left = left
4✔
281
        real_right = right
4✔
282
        if dtype.primitive_type == PrimitiveType.Double:
4✔
283
            if left_is_int:
4✔
284
                left_cast = self.builder.find_new_name()
4✔
285
                self.builder.add_container(
4✔
286
                    left_cast, Scalar(PrimitiveType.Double), False
287
                )
288
                self.container_table[left_cast] = Scalar(PrimitiveType.Double)
4✔
289

290
                c_block = self.builder.add_block()
4✔
291
                t_src, src_sub = self._add_read(c_block, left)
4✔
292
                t_dst = self.builder.add_access(c_block, left_cast)
4✔
293
                t_task = self.builder.add_tasklet(
4✔
294
                    c_block, TaskletCode.assign, ["_in"], ["_out"]
295
                )
296
                self.builder.add_memlet(c_block, t_src, "void", t_task, "_in", src_sub)
4✔
297
                self.builder.add_memlet(c_block, t_task, "_out", t_dst, "void", "")
4✔
298

299
                real_left = left_cast
4✔
300

301
            if right_is_int:
4✔
302
                right_cast = self.builder.find_new_name()
4✔
303
                self.builder.add_container(
4✔
304
                    right_cast, Scalar(PrimitiveType.Double), False
305
                )
306
                self.container_table[right_cast] = Scalar(PrimitiveType.Double)
4✔
307

308
                c_block = self.builder.add_block()
4✔
309
                t_src, src_sub = self._add_read(c_block, right)
4✔
310
                t_dst = self.builder.add_access(c_block, right_cast)
4✔
311
                t_task = self.builder.add_tasklet(
4✔
312
                    c_block, TaskletCode.assign, ["_in"], ["_out"]
313
                )
314
                self.builder.add_memlet(c_block, t_src, "void", t_task, "_in", src_sub)
4✔
315
                self.builder.add_memlet(c_block, t_task, "_out", t_dst, "void", "")
4✔
316

317
                real_right = right_cast
4✔
318

319
        if op == "**":
4✔
320
            block = self.builder.add_block()
4✔
321
            t_left, left_sub = self._add_read(block, real_left)
4✔
322
            t_right, right_sub = self._add_read(block, real_right)
4✔
323
            t_out = self.builder.add_access(block, tmp_name)
4✔
324

325
            t_task = self.builder.add_cmath(block, CMathFunction.pow)
4✔
326
            self.builder.add_memlet(block, t_left, "void", t_task, "_in1", left_sub)
4✔
327
            self.builder.add_memlet(block, t_right, "void", t_task, "_in2", right_sub)
4✔
328
            self.builder.add_memlet(block, t_task, "_out", t_out, "void", "")
4✔
329

330
            return tmp_name
4✔
331
        elif op == "%":
4✔
332
            block = self.builder.add_block()
4✔
333
            t_left, left_sub = self._add_read(block, real_left)
4✔
334
            t_right, right_sub = self._add_read(block, real_right)
4✔
335
            t_out = self.builder.add_access(block, tmp_name)
4✔
336

337
            if dtype.primitive_type == PrimitiveType.Int64:
4✔
338
                t_rem1 = self.builder.add_tasklet(
4✔
339
                    block, TaskletCode.int_srem, ["_in1", "_in2"], ["_out"]
340
                )
341
                self.builder.add_memlet(block, t_left, "void", t_rem1, "_in1", left_sub)
4✔
342
                self.builder.add_memlet(
4✔
343
                    block, t_right, "void", t_rem1, "_in2", right_sub
344
                )
345

346
                rem1_name = self.builder.find_new_name()
4✔
347
                self.builder.add_container(rem1_name, dtype, False)
4✔
348
                t_rem1_out = self.builder.add_access(block, rem1_name)
4✔
349
                self.builder.add_memlet(block, t_rem1, "_out", t_rem1_out, "void", "")
4✔
350

351
                t_add = self.builder.add_tasklet(
4✔
352
                    block, TaskletCode.int_add, ["_in1", "_in2"], ["_out"]
353
                )
354
                self.builder.add_memlet(block, t_rem1_out, "void", t_add, "_in1", "")
4✔
355
                self.builder.add_memlet(
4✔
356
                    block, t_right, "void", t_add, "_in2", right_sub
357
                )
358

359
                add_name = self.builder.find_new_name()
4✔
360
                self.builder.add_container(add_name, dtype, False)
4✔
361
                t_add_out = self.builder.add_access(block, add_name)
4✔
362
                self.builder.add_memlet(block, t_add, "_out", t_add_out, "void", "")
4✔
363

364
                t_rem2 = self.builder.add_tasklet(
4✔
365
                    block, TaskletCode.int_srem, ["_in1", "_in2"], ["_out"]
366
                )
367
                self.builder.add_memlet(block, t_add_out, "void", t_rem2, "_in1", "")
4✔
368
                self.builder.add_memlet(
4✔
369
                    block, t_right, "void", t_rem2, "_in2", right_sub
370
                )
371
                self.builder.add_memlet(block, t_rem2, "_out", t_out, "void", "")
4✔
372

373
                return tmp_name
4✔
374
            else:
375
                t_rem1 = self.builder.add_tasklet(
4✔
376
                    block, TaskletCode.fp_rem, ["_in1", "_in2"], ["_out"]
377
                )
378
                self.builder.add_memlet(block, t_left, "void", t_rem1, "_in1", left_sub)
4✔
379
                self.builder.add_memlet(
4✔
380
                    block, t_right, "void", t_rem1, "_in2", right_sub
381
                )
382

383
                rem1_name = self.builder.find_new_name()
4✔
384
                self.builder.add_container(rem1_name, dtype, False)
4✔
385
                t_rem1_out = self.builder.add_access(block, rem1_name)
4✔
386
                self.builder.add_memlet(block, t_rem1, "_out", t_rem1_out, "void", "")
4✔
387

388
                t_add = self.builder.add_tasklet(
4✔
389
                    block, TaskletCode.fp_add, ["_in1", "_in2"], ["_out"]
390
                )
391
                self.builder.add_memlet(block, t_rem1_out, "void", t_add, "_in1", "")
4✔
392
                self.builder.add_memlet(
4✔
393
                    block, t_right, "void", t_add, "_in2", right_sub
394
                )
395

396
                add_name = self.builder.find_new_name()
4✔
397
                self.builder.add_container(add_name, dtype, False)
4✔
398
                t_add_out = self.builder.add_access(block, add_name)
4✔
399
                self.builder.add_memlet(block, t_add, "_out", t_add_out, "void", "")
4✔
400

401
                t_rem2 = self.builder.add_tasklet(
4✔
402
                    block, TaskletCode.fp_rem, ["_in1", "_in2"], ["_out"]
403
                )
404
                self.builder.add_memlet(block, t_add_out, "void", t_rem2, "_in1", "")
4✔
405
                self.builder.add_memlet(
4✔
406
                    block, t_right, "void", t_rem2, "_in2", right_sub
407
                )
408
                self.builder.add_memlet(block, t_rem2, "_out", t_out, "void", "")
4✔
409

410
                return tmp_name
4✔
411

412
        tasklet_code = None
4✔
413
        if dtype.primitive_type == PrimitiveType.Int64:
4✔
414
            if op == "+":
4✔
415
                tasklet_code = TaskletCode.int_add
4✔
416
            elif op == "-":
4✔
417
                tasklet_code = TaskletCode.int_sub
4✔
418
            elif op == "*":
4✔
419
                tasklet_code = TaskletCode.int_mul
4✔
420
            elif op == "/":
4✔
NEW
421
                tasklet_code = TaskletCode.int_sdiv
×
422
            elif op == "//":
4✔
423
                tasklet_code = TaskletCode.int_sdiv
4✔
424
            elif op == "&":
4✔
425
                tasklet_code = TaskletCode.int_and
4✔
426
            elif op == "|":
4✔
427
                tasklet_code = TaskletCode.int_or
4✔
428
            elif op == "^":
4✔
429
                tasklet_code = TaskletCode.int_xor
4✔
NEW
430
            elif op == "<<":
×
NEW
431
                tasklet_code = TaskletCode.int_shl
×
NEW
432
            elif op == ">>":
×
NEW
433
                tasklet_code = TaskletCode.int_lshr
×
434
        else:
435
            if op == "+":
4✔
436
                tasklet_code = TaskletCode.fp_add
4✔
437
            elif op == "-":
4✔
438
                tasklet_code = TaskletCode.fp_sub
4✔
439
            elif op == "*":
4✔
440
                tasklet_code = TaskletCode.fp_mul
4✔
441
            elif op == "/":
4✔
442
                tasklet_code = TaskletCode.fp_div
4✔
NEW
443
            elif op == "//":
×
NEW
444
                tasklet_code = TaskletCode.fp_div
×
445
            else:
NEW
446
                raise NotImplementedError(f"Operation {op} not supported for floats")
×
447

448
        block = self.builder.add_block()
4✔
449
        t_left, left_sub = self._add_read(block, real_left)
4✔
450
        t_right, right_sub = self._add_read(block, real_right)
4✔
451
        t_out = self.builder.add_access(block, tmp_name)
4✔
452

453
        t_task = self.builder.add_tasklet(
4✔
454
            block, tasklet_code, ["_in1", "_in2"], ["_out"]
455
        )
456

457
        # For indexed array accesses like "arr(i,j)", we need to pass the Tensor type
458
        # to ensure correct type inference during validation
459
        left_type = self._get_memlet_type_for_access(real_left, left_sub)
4✔
460
        right_type = self._get_memlet_type_for_access(real_right, right_sub)
4✔
461

462
        self.builder.add_memlet(
4✔
463
            block, t_left, "void", t_task, "_in1", left_sub, left_type
464
        )
465
        self.builder.add_memlet(
4✔
466
            block, t_right, "void", t_task, "_in2", right_sub, right_type
467
        )
468
        self.builder.add_memlet(block, t_task, "_out", t_out, "void", "")
4✔
469

470
        return tmp_name
4✔
471

472
    def visit_Attribute(self, node):
4✔
473
        if node.attr == "shape":
4✔
474
            if isinstance(node.value, ast.Name) and node.value.id in self.tensor_table:
4✔
475
                return f"_shape_proxy_{node.value.id}"
4✔
476

477
        if node.attr == "T":
4✔
478
            value_name = None
4✔
479
            if isinstance(node.value, ast.Name):
4✔
480
                value_name = node.value.id
4✔
NEW
481
            elif isinstance(node.value, ast.Subscript):
×
NEW
482
                value_name = self.visit(node.value)
×
483

484
            if value_name and value_name in self.tensor_table:
4✔
485
                return self.numpy_visitor.handle_transpose_expr(node)
4✔
486

487
        if isinstance(node.value, ast.Name) and node.value.id == "math":
4✔
488
            val = ""
4✔
489
            if node.attr == "pi":
4✔
490
                val = "M_PI"
4✔
491
            elif node.attr == "e":
4✔
492
                val = "M_E"
4✔
493

494
            if val:
4✔
495
                tmp_name = self.builder.find_new_name()
4✔
496
                dtype = Scalar(PrimitiveType.Double)
4✔
497
                self.builder.add_container(tmp_name, dtype, False)
4✔
498
                self.container_table[tmp_name] = dtype
4✔
499
                self._add_assign_constant(tmp_name, val, dtype)
4✔
500
                return tmp_name
4✔
501

502
        if isinstance(node.value, ast.Name):
4✔
503
            obj_name = node.value.id
4✔
504
            attr_name = node.attr
4✔
505

506
            if obj_name in self.container_table:
4✔
507
                obj_type = self.container_table[obj_name]
4✔
508
                if isinstance(obj_type, Pointer) and obj_type.has_pointee_type():
4✔
509
                    pointee_type = obj_type.pointee_type
4✔
510
                    if isinstance(pointee_type, Structure):
4✔
511
                        struct_name = pointee_type.name
4✔
512

513
                        if (
4✔
514
                            struct_name in self.structure_member_info
515
                            and attr_name in self.structure_member_info[struct_name]
516
                        ):
517
                            member_index, member_type = self.structure_member_info[
4✔
518
                                struct_name
519
                            ][attr_name]
520
                        else:
NEW
521
                            raise RuntimeError(
×
522
                                f"Member '{attr_name}' not found in structure '{struct_name}'. "
523
                                f"Available members: {list(self.structure_member_info.get(struct_name, {}).keys())}"
524
                            )
525

526
                        tmp_name = self.builder.find_new_name()
4✔
527

528
                        self.builder.add_container(tmp_name, member_type, False)
4✔
529
                        self.container_table[tmp_name] = member_type
4✔
530

531
                        block = self.builder.add_block()
4✔
532
                        obj_access = self.builder.add_access(block, obj_name)
4✔
533
                        tmp_access = self.builder.add_access(block, tmp_name)
4✔
534

535
                        tasklet = self.builder.add_tasklet(
4✔
536
                            block, TaskletCode.assign, ["_in"], ["_out"]
537
                        )
538

539
                        subset = "0," + str(member_index)
4✔
540
                        self.builder.add_memlet(
4✔
541
                            block, obj_access, "", tasklet, "_in", subset
542
                        )
543
                        self.builder.add_memlet(block, tasklet, "_out", tmp_access, "")
4✔
544

545
                        return tmp_name
4✔
546

NEW
547
        raise NotImplementedError(f"Attribute access {node.attr} not supported")
×
548

549
    def visit_Compare(self, node):
4✔
550
        left = self.visit(node.left)
4✔
551
        if len(node.ops) > 1:
4✔
NEW
552
            raise NotImplementedError("Chained comparisons not supported yet")
×
553

554
        op = self.visit(node.ops[0])
4✔
555
        right = self.visit(node.comparators[0])
4✔
556

557
        left_is_array = left in self.tensor_table
4✔
558
        right_is_array = right in self.tensor_table
4✔
559

560
        if left_is_array or right_is_array:
4✔
561
            return self.numpy_visitor.handle_array_compare(
4✔
562
                left, op, right, left_is_array, right_is_array
563
            )
564

565
        expr_str = f"{left} {op} {right}"
4✔
566

567
        tmp_name = self.builder.find_new_name()
4✔
568
        dtype = Scalar(PrimitiveType.Bool)
4✔
569
        self.builder.add_container(tmp_name, dtype, False)
4✔
570

571
        self.builder.begin_if(expr_str)
4✔
572
        self.builder.add_transition(tmp_name, "true")
4✔
573
        self.builder.begin_else()
4✔
574
        self.builder.add_transition(tmp_name, "false")
4✔
575
        self.builder.end_if()
4✔
576

577
        self.container_table[tmp_name] = dtype
4✔
578
        return tmp_name
4✔
579

580
    def visit_Subscript(self, node):
4✔
581
        value_str = self.visit(node.value)
4✔
582

583
        if value_str.startswith("_shape_proxy_"):
4✔
584
            array_name = value_str[len("_shape_proxy_") :]
4✔
585
            if isinstance(node.slice, ast.Constant):
4✔
586
                idx = node.slice.value
4✔
NEW
587
            elif isinstance(node.slice, ast.Index):
×
NEW
588
                idx = node.slice.value.value
×
589
            else:
NEW
590
                try:
×
NEW
591
                    idx = int(self.visit(node.slice))
×
NEW
592
                except:
×
NEW
593
                    raise NotImplementedError(
×
594
                        "Dynamic shape indexing not fully supported yet"
595
                    )
596

597
            if array_name in self.tensor_table:
4✔
598
                return self.tensor_table[array_name].shape[idx]
4✔
599

NEW
600
            return f"_{array_name}_shape_{idx}"
×
601

602
        if value_str in self.tensor_table:
4✔
603
            tensor = self.tensor_table[value_str]
4✔
604
            ndim = len(tensor.shape)
4✔
605
            shapes = tensor.shape
4✔
606

607
            if isinstance(node.slice, ast.Tuple):
4✔
608
                indices_nodes = node.slice.elts
4✔
609
            else:
610
                indices_nodes = [node.slice]
4✔
611

612
            all_full_slices = True
4✔
613
            for idx in indices_nodes:
4✔
614
                if isinstance(idx, ast.Slice):
4✔
615
                    if idx.lower is not None or idx.upper is not None:
4✔
616
                        all_full_slices = False
4✔
617
                        break
4✔
618
                    # Also check for non-trivial step (step != None and step != 1)
619
                    if idx.step is not None:
4✔
620
                        # Check if step is a constant 1; if not, it's not a full slice
621
                        if isinstance(idx.step, ast.Constant) and idx.step.value == 1:
4✔
NEW
622
                            pass  # step=1 is equivalent to no step
×
623
                        else:
624
                            all_full_slices = False
4✔
625
                            break
4✔
626
                else:
627
                    all_full_slices = False
4✔
628
                    break
4✔
629

630
            if all_full_slices:
4✔
631
                return value_str
4✔
632

633
            has_slices = any(isinstance(idx, ast.Slice) for idx in indices_nodes)
4✔
634
            if has_slices:
4✔
635
                return self._handle_expression_slicing(
4✔
636
                    node, value_str, indices_nodes, shapes, ndim
637
                )
638

639
            if len(indices_nodes) == 1 and self._is_array_index(indices_nodes[0]):
4✔
NEW
640
                if self.builder:
×
NEW
641
                    return self._handle_gather(value_str, indices_nodes[0])
×
642

643
            if isinstance(node.slice, ast.Tuple):
4✔
644
                indices = [self.visit(elt) for elt in node.slice.elts]
4✔
645
            else:
646
                indices = [self.visit(node.slice)]
4✔
647

648
            if len(indices) != ndim:
4✔
NEW
649
                raise ValueError(
×
650
                    f"Array {value_str} has {ndim} dimensions, but accessed with {len(indices)} indices"
651
                )
652

653
            normalized_indices = []
4✔
654
            for i, idx_str in enumerate(indices):
4✔
655
                shape_val = shapes[i]
4✔
656
                if isinstance(idx_str, str) and (
4✔
657
                    idx_str.startswith("-") or idx_str.startswith("(-")
658
                ):
NEW
659
                    normalized_indices.append(f"({shape_val} + {idx_str})")
×
660
                else:
661
                    normalized_indices.append(idx_str)
4✔
662

663
            subscript_str = ",".join(normalized_indices)
4✔
664
            access_str = f"{value_str}({subscript_str})"
4✔
665

666
            if isinstance(node.ctx, ast.Load):
4✔
667
                tmp_name = self.builder.find_new_name()
4✔
668
                self.builder.add_container(tmp_name, tensor.element_type, False)
4✔
669
                self.container_table[tmp_name] = tensor.element_type
4✔
670

671
                block = self.builder.add_block()
4✔
672
                t_src = self.builder.add_access(block, value_str)
4✔
673
                t_dst = self.builder.add_access(block, tmp_name)
4✔
674
                t_task = self.builder.add_tasklet(
4✔
675
                    block, TaskletCode.assign, ["_in"], ["_out"]
676
                )
677
                self.builder.add_memlet(
4✔
678
                    block, t_src, "void", t_task, "_in", subscript_str, tensor
679
                )
680
                self.builder.add_memlet(
4✔
681
                    block, t_task, "_out", t_dst, "void", "", tensor.element_type
682
                )
683

684
                return tmp_name
4✔
685

686
            return access_str
4✔
687

NEW
688
        slice_val = self.visit(node.slice)
×
NEW
689
        access_str = f"{value_str}({slice_val})"
×
NEW
690
        return access_str
×
691

692
    def visit_AugAssign(self, node):
4✔
693
        if isinstance(node.target, ast.Name) and node.target.id in self.tensor_table:
4✔
694
            # Convert to slice assignment: target[:] = target op value
695
            ndim = len(self.tensor_table[node.target.id].shape)
4✔
696

697
            slices = []
4✔
698
            for _ in range(ndim):
4✔
699
                slices.append(ast.Slice(lower=None, upper=None, step=None))
4✔
700

701
            if ndim == 1:
4✔
702
                slice_arg = slices[0]
×
703
            else:
704
                slice_arg = ast.Tuple(elts=slices, ctx=ast.Load())
4✔
705

706
            slice_node = ast.Subscript(
4✔
707
                value=node.target, slice=slice_arg, ctx=ast.Store()
708
            )
709

710
            new_node = ast.Assign(
4✔
711
                targets=[slice_node],
712
                value=ast.BinOp(left=node.target, op=node.op, right=node.value),
713
            )
714
            self.visit_Assign(new_node)
4✔
715
        else:
716
            new_node = ast.Assign(
4✔
717
                targets=[node.target],
718
                value=ast.BinOp(left=node.target, op=node.op, right=node.value),
719
            )
720
            self.visit_Assign(new_node)
4✔
721

722
    def visit_Assign(self, node):
4✔
723
        # Handle multiple targets: a = b = c or a, b = expr
724
        if len(node.targets) > 1:
4✔
725
            tmp_name = self.builder.find_new_name()
4✔
726
            # Assign value to temporary
727
            val_assign = ast.Assign(
4✔
728
                targets=[ast.Name(id=tmp_name, ctx=ast.Store())], value=node.value
729
            )
730
            ast.copy_location(val_assign, node)
4✔
731
            self.visit_Assign(val_assign)
4✔
732

733
            # Assign temporary to targets
734
            for target in node.targets:
4✔
735
                assign = ast.Assign(
4✔
736
                    targets=[target], value=ast.Name(id=tmp_name, ctx=ast.Load())
737
                )
738
                ast.copy_location(assign, node)
4✔
739
                self.visit_Assign(assign)
4✔
740
            return
4✔
741
        target = node.targets[0]
4✔
742

743
        # Handle tuple unpacking: I, J, K = expr1, expr2, expr3
744
        if isinstance(target, ast.Tuple):
4✔
745
            if isinstance(node.value, ast.Tuple):
4✔
746
                if len(target.elts) != len(node.value.elts):
4✔
747
                    raise ValueError("Tuple unpacking size mismatch")
×
748
                for tgt, val in zip(target.elts, node.value.elts):
4✔
749
                    assign = ast.Assign(targets=[tgt], value=val)
4✔
750
                    ast.copy_location(assign, node)
4✔
751
                    self.visit_Assign(assign)
4✔
752
                return
4✔
753
            else:
754
                raise NotImplementedError(
×
755
                    "Tuple unpacking from non-tuple values not supported"
756
                )
757

758
        # Special cases, where rhs is not just a simple expression but requires special handling
759
        if self.numpy_visitor.is_gemm(node.value):
4✔
760
            if self.numpy_visitor.handle_gemm(target, node.value):
4✔
761
                return
×
762
            if self.numpy_visitor.handle_dot(target, node.value):
4✔
763
                return
×
764
        if self.numpy_visitor.is_outer(node.value):
4✔
765
            if self.numpy_visitor.handle_outer(target, node.value):
4✔
766
                return
4✔
767
        if self.scipy_handler.is_correlate2d(node.value):
4✔
NEW
768
            if self.scipy_handler.handle_correlate2d(target, node.value):
×
769
                return
×
770
        if self.numpy_visitor.is_transpose(node.value):
4✔
771
            if self.numpy_visitor.handle_transpose(target, node.value):
4✔
772
                return
4✔
773

774
        # Handle subscript assignments: a[i] = val or a[i, j] = val
775
        if isinstance(target, ast.Subscript):
4✔
776
            debug_info = get_debug_info(node, self.filename, self.function_name)
4✔
777

778
            target_name = self.visit(target.value)
4✔
779
            indices = []
4✔
780
            if isinstance(target.slice, ast.Tuple):
4✔
781
                indices = target.slice.elts
4✔
782
            else:
783
                indices = [target.slice]
4✔
784

785
            # Handle slice assignment separately
786
            has_slice = False
4✔
787
            for idx in indices:
4✔
788
                if isinstance(idx, ast.Slice):
4✔
789
                    has_slice = True
4✔
790
                    break
4✔
791

792
            if has_slice:
4✔
793
                self._handle_slice_assignment(
4✔
794
                    target, node.value, target_name, indices, debug_info
795
                )
796
                return
4✔
797

798
            # Handle rhs and store in scalar tmp
799
            rhs_tmp = self.visit(node.value)
4✔
800

801
            block = self.builder.add_block(debug_info)
4✔
802
            t_task = self.builder.add_tasklet(
4✔
803
                block, TaskletCode.assign, ["_in"], ["_out"], debug_info
804
            )
805

806
            t_src, src_sub = self._add_read(block, rhs_tmp, debug_info)
4✔
807
            self.builder.add_memlet(
4✔
808
                block, t_src, "void", t_task, "_in", src_sub, None, debug_info
809
            )
810

811
            lhs_expr = self.visit(target)
4✔
812
            if "(" in lhs_expr and lhs_expr.endswith(")"):
4✔
813
                subset = lhs_expr[lhs_expr.find("(") + 1 : -1]
4✔
814
                tensor_dst = self.tensor_table[target_name]
4✔
815

816
                t_dst = self.builder.add_access(block, target_name, debug_info)
4✔
817
                self.builder.add_memlet(
4✔
818
                    block, t_task, "_out", t_dst, "void", subset, tensor_dst, debug_info
819
                )
820
            else:
NEW
821
                t_dst = self.builder.add_access(block, target_name, debug_info)
×
NEW
822
                self.builder.add_memlet(
×
823
                    block, t_task, "_out", t_dst, "void", "", None, debug_info
824
                )
825
            return
4✔
826

827
        # Fallback: lhs is a simple scalar assignments
828
        if not isinstance(target, ast.Name):
4✔
829
            raise NotImplementedError("Only assignment to variables supported")
×
830

831
        target_name = target.id
4✔
832
        rhs_tmp = self.visit(node.value)
4✔
833
        debug_info = get_debug_info(node, self.filename, self.function_name)
4✔
834

835
        if not self.builder.exists(target_name):
4✔
836
            if isinstance(node.value, ast.Constant):
4✔
837
                val = node.value.value
4✔
838
                if isinstance(val, int):
4✔
839
                    dtype = Scalar(PrimitiveType.Int64)
4✔
840
                elif isinstance(val, float):
×
841
                    dtype = Scalar(PrimitiveType.Double)
×
842
                elif isinstance(val, bool):
×
843
                    dtype = Scalar(PrimitiveType.Bool)
×
844
                else:
845
                    raise NotImplementedError(f"Cannot infer type for {val}")
×
846

847
                self.builder.add_container(target_name, dtype, False)
4✔
848
                self.container_table[target_name] = dtype
4✔
849
            else:
850
                self.builder.add_container(
4✔
851
                    target_name, self.container_table[rhs_tmp], False
852
                )
853
                self.container_table[target_name] = self.container_table[rhs_tmp]
4✔
854

855
        if rhs_tmp in self.tensor_table:
4✔
856
            self.tensor_table[target_name] = self.tensor_table[rhs_tmp]
4✔
857

858
        # Also copy shapes_runtime_info if available
859
        if rhs_tmp in self.shapes_runtime_info:
4✔
860
            self.shapes_runtime_info[target_name] = self.shapes_runtime_info[rhs_tmp]
4✔
861

862
        # Distinguish assignments: scalar -> tasklet, pointer -> reference_memlet
863
        src_type = self.container_table.get(rhs_tmp)
4✔
864
        dst_type = self.container_table[target_name]
4✔
865
        if src_type and isinstance(src_type, Pointer) and isinstance(dst_type, Pointer):
4✔
866
            block = self.builder.add_block(debug_info)
4✔
867
            t_src = self.builder.add_access(block, rhs_tmp, debug_info)
4✔
868
            t_dst = self.builder.add_access(block, target_name, debug_info)
4✔
869
            self.builder.add_reference_memlet(
4✔
870
                block, t_src, t_dst, "0", src_type, debug_info
871
            )
872
        elif (src_type and isinstance(src_type, Scalar)) or isinstance(
4✔
873
            dst_type, Scalar
874
        ):
875
            block = self.builder.add_block(debug_info)
4✔
876
            t_dst = self.builder.add_access(block, target_name, debug_info)
4✔
877
            t_task = self.builder.add_tasklet(
4✔
878
                block, TaskletCode.assign, ["_in"], ["_out"], debug_info
879
            )
880

881
            if src_type:
4✔
882
                t_src = self.builder.add_access(block, rhs_tmp, debug_info)
4✔
883
            else:
884
                t_src = self.builder.add_constant(block, rhs_tmp, dst_type, debug_info)
4✔
885

886
            self.builder.add_memlet(
4✔
887
                block, t_src, "void", t_task, "_in", "", None, debug_info
888
            )
889
            self.builder.add_memlet(
4✔
890
                block, t_task, "_out", t_dst, "void", "", None, debug_info
891
            )
892

893
    def visit_Expr(self, node):
4✔
NEW
894
        self.visit(node.value)
×
895

896
    def visit_If(self, node):
4✔
897
        cond = self.visit(node.test)
4✔
898
        debug_info = get_debug_info(node, self.filename, self.function_name)
4✔
899
        self.builder.begin_if(f"{cond} != false", debug_info)
4✔
900

901
        for stmt in node.body:
4✔
902
            self.visit(stmt)
4✔
903

904
        if node.orelse:
4✔
905
            self.builder.begin_else(debug_info)
4✔
906
            for stmt in node.orelse:
4✔
907
                self.visit(stmt)
4✔
908

909
        self.builder.end_if()
4✔
910

911
    def visit_While(self, node):
4✔
912
        if node.orelse:
4✔
NEW
913
            raise NotImplementedError("while-else is not supported")
×
914

915
        debug_info = get_debug_info(node, self.filename, self.function_name)
4✔
916
        self.builder.begin_while(debug_info)
4✔
917

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

921
        # Create if-break pattern: if condition is false, break
922
        self.builder.begin_if(f"{cond} == false", debug_info)
4✔
923
        self.builder.add_break(debug_info)
4✔
924
        self.builder.end_if()
4✔
925

926
        for stmt in node.body:
4✔
927
            self.visit(stmt)
4✔
928

929
        self.builder.end_while()
4✔
930

931
    def visit_Break(self, node):
4✔
932
        debug_info = get_debug_info(node, self.filename, self.function_name)
4✔
933
        self.builder.add_break(debug_info)
4✔
934

935
    def visit_Continue(self, node):
4✔
936
        debug_info = get_debug_info(node, self.filename, self.function_name)
4✔
937
        self.builder.add_continue(debug_info)
4✔
938

939
    def visit_For(self, node):
4✔
940
        if node.orelse:
4✔
NEW
941
            raise NotImplementedError("while-else is not supported")
×
942
        if not isinstance(node.target, ast.Name):
4✔
NEW
943
            raise NotImplementedError("Only simple for loops supported")
×
944

945
        var = node.target.id
4✔
946
        debug_info = get_debug_info(node, self.filename, self.function_name)
4✔
947

948
        # Check if iterating over a range() call
949
        if (
4✔
950
            isinstance(node.iter, ast.Call)
951
            and isinstance(node.iter.func, ast.Name)
952
            and node.iter.func.id == "range"
953
        ):
954
            args = node.iter.args
4✔
955
            if len(args) == 1:
4✔
956
                start = "0"
4✔
957
                end = self.visit(args[0])
4✔
958
                step = "1"
4✔
959
            elif len(args) == 2:
4✔
960
                start = self.visit(args[0])
4✔
961
                end = self.visit(args[1])
4✔
962
                step = "1"
4✔
963
            elif len(args) == 3:
4✔
964
                start = self.visit(args[0])
4✔
965
                end = self.visit(args[1])
4✔
966

967
                # Special handling for step to avoid creating tasklets for constants
968
                step_node = args[2]
4✔
969
                if isinstance(step_node, ast.Constant):
4✔
970
                    step = str(step_node.value)
4✔
971
                elif (
4✔
972
                    isinstance(step_node, ast.UnaryOp)
973
                    and isinstance(step_node.op, ast.USub)
974
                    and isinstance(step_node.operand, ast.Constant)
975
                ):
976
                    step = f"-{step_node.operand.value}"
4✔
977
                else:
NEW
978
                    step = self.visit(step_node)
×
979
            else:
NEW
980
                raise ValueError("Invalid range arguments")
×
981

982
            if not self.builder.exists(var):
4✔
983
                self.builder.add_container(var, Scalar(PrimitiveType.Int64), False)
4✔
984
                self.container_table[var] = Scalar(PrimitiveType.Int64)
4✔
985

986
            self.builder.begin_for(var, start, end, step, debug_info)
4✔
987

988
            for stmt in node.body:
4✔
989
                self.visit(stmt)
4✔
990

991
            self.builder.end_for()
4✔
992
            return
4✔
993

994
        # Check if iterating over an ndarray (for x in array)
NEW
995
        if isinstance(node.iter, ast.Name):
×
NEW
996
            iter_name = node.iter.id
×
NEW
997
            if iter_name in self.tensor_table:
×
NEW
998
                arr_info = self.tensor_table[iter_name]
×
NEW
999
                if len(arr_info.shape) == 0:
×
NEW
1000
                    raise NotImplementedError("Cannot iterate over 0-dimensional array")
×
1001

1002
                # Get the size of the first dimension
NEW
1003
                arr_size = arr_info.shape[0]
×
1004

1005
                # Create a hidden index variable for the loop
NEW
1006
                idx_var = self.builder.find_new_name()
×
NEW
1007
                if not self.builder.exists(idx_var):
×
NEW
1008
                    self.builder.add_container(
×
1009
                        idx_var, Scalar(PrimitiveType.Int64), False
1010
                    )
NEW
1011
                    self.container_table[idx_var] = Scalar(PrimitiveType.Int64)
×
1012

1013
                # Determine the type of the loop variable (element type)
1014
                # For a 1D array, it's a scalar; for ND array, it's a view of N-1 dimensions
NEW
1015
                if len(arr_info.shape) == 1:
×
1016
                    # Element is a scalar - get the element type from the array's type
NEW
1017
                    arr_type = self.container_table.get(iter_name)
×
NEW
1018
                    if isinstance(arr_type, Pointer):
×
NEW
1019
                        elem_type = arr_type.pointee_type
×
1020
                    else:
NEW
1021
                        elem_type = Scalar(PrimitiveType.Double)  # Default fallback
×
1022

NEW
1023
                    if not self.builder.exists(var):
×
NEW
1024
                        self.builder.add_container(var, elem_type, False)
×
NEW
1025
                        self.container_table[var] = elem_type
×
1026
                else:
1027
                    # For multi-dimensional arrays, create a view/slice
1028
                    # The loop variable becomes a pointer to the sub-array
NEW
1029
                    inner_shapes = arr_info.shape[1:]
×
NEW
1030
                    inner_ndim = len(arr_info.shape) - 1
×
1031

NEW
1032
                    arr_type = self.container_table.get(iter_name)
×
NEW
1033
                    if isinstance(arr_type, Pointer):
×
NEW
1034
                        elem_type = arr_type  # Keep as pointer type for views
×
1035
                    else:
NEW
1036
                        elem_type = Pointer(Scalar(PrimitiveType.Double))
×
1037

NEW
1038
                    if not self.builder.exists(var):
×
NEW
1039
                        self.builder.add_container(var, elem_type, False)
×
NEW
1040
                        self.container_table[var] = elem_type
×
1041

1042
                    # Register the view in tensor_table
NEW
1043
                    self.tensor_table[var] = Tensor(
×
1044
                        element_type_from_sdfg_type(elem_type), inner_shapes
1045
                    )
1046

1047
                # Begin the for loop
NEW
1048
                self.builder.begin_for(idx_var, "0", str(arr_size), "1", debug_info)
×
1049

1050
                # Generate the assignment: var = array[idx_var]
1051
                # Create an AST node for the assignment and visit it
NEW
1052
                assign_node = ast.Assign(
×
1053
                    targets=[ast.Name(id=var, ctx=ast.Store())],
1054
                    value=ast.Subscript(
1055
                        value=ast.Name(id=iter_name, ctx=ast.Load()),
1056
                        slice=ast.Name(id=idx_var, ctx=ast.Load()),
1057
                        ctx=ast.Load(),
1058
                    ),
1059
                )
NEW
1060
                ast.copy_location(assign_node, node)
×
NEW
1061
                self.visit_Assign(assign_node)
×
1062

1063
                # Visit the loop body
NEW
1064
                for stmt in node.body:
×
NEW
1065
                    self.visit(stmt)
×
1066

NEW
1067
                self.builder.end_for()
×
NEW
1068
                return
×
1069

NEW
1070
        raise NotImplementedError(
×
1071
            f"Only range() loops and iteration over ndarrays supported, got: {ast.dump(node.iter)}"
1072
        )
1073

1074
    def visit_Return(self, node):
4✔
1075
        if node.value is None:
4✔
NEW
1076
            debug_info = get_debug_info(node, self.filename, self.function_name)
×
NEW
1077
            self.builder.add_return("", debug_info)
×
NEW
1078
            return
×
1079

1080
        if isinstance(node.value, ast.Tuple):
4✔
1081
            values = node.value.elts
4✔
1082
        else:
1083
            values = [node.value]
4✔
1084

1085
        parsed_values = [self.visit(v) for v in values]
4✔
1086
        debug_info = get_debug_info(node, self.filename, self.function_name)
4✔
1087

1088
        if self.infer_return_type:
4✔
1089
            for i, res in enumerate(parsed_values):
4✔
1090
                ret_name = f"_docc_ret_{i}"
4✔
1091
                if not self.builder.exists(ret_name):
4✔
1092
                    dtype = Scalar(PrimitiveType.Double)
4✔
1093
                    if res in self.container_table:
4✔
1094
                        dtype = self.container_table[res]
4✔
NEW
1095
                    elif isinstance(values[i], ast.Constant):
×
NEW
1096
                        val = values[i].value
×
NEW
1097
                        if isinstance(val, int):
×
NEW
1098
                            dtype = Scalar(PrimitiveType.Int64)
×
NEW
1099
                        elif isinstance(val, float):
×
NEW
1100
                            dtype = Scalar(PrimitiveType.Double)
×
NEW
1101
                        elif isinstance(val, bool):
×
NEW
1102
                            dtype = Scalar(PrimitiveType.Bool)
×
1103

1104
                    # Wrap Scalar in Pointer. Keep Arrays/Pointers as is.
1105
                    arg_type = dtype
4✔
1106
                    if isinstance(dtype, Scalar):
4✔
1107
                        arg_type = Pointer(dtype)
4✔
1108

1109
                    self.builder.add_container(ret_name, arg_type, is_argument=True)
4✔
1110
                    self.container_table[ret_name] = arg_type
4✔
1111

1112
                    if res in self.tensor_table:
4✔
1113
                        self.tensor_table[ret_name] = self.tensor_table[res]
4✔
1114

1115
            self.infer_return_type = False
4✔
1116

1117
        for i, res in enumerate(parsed_values):
4✔
1118
            ret_name = f"_docc_ret_{i}"
4✔
1119
            typ = self.container_table.get(ret_name)
4✔
1120

1121
            is_array_return = False
4✔
1122
            if res in self.tensor_table:
4✔
1123
                # Only treat as array return if it has dimensions
1124
                # 0-d arrays (scalars) should be handled by scalar assignment
1125
                if len(self.tensor_table[res].shape) > 0:
4✔
1126
                    is_array_return = True
4✔
1127
            elif res in self.container_table:
4✔
1128
                if isinstance(self.container_table[res], Pointer):
4✔
NEW
1129
                    is_array_return = True
×
1130

1131
            # Simple Scalar Assignment
1132
            if not is_array_return:
4✔
1133
                block = self.builder.add_block(debug_info)
4✔
1134
                t_dst = self.builder.add_access(block, ret_name, debug_info)
4✔
1135

1136
                t_src, src_sub = self._add_read(block, res, debug_info)
4✔
1137

1138
                t_task = self.builder.add_tasklet(
4✔
1139
                    block, TaskletCode.assign, ["_in"], ["_out"], debug_info
1140
                )
1141
                self.builder.add_memlet(
4✔
1142
                    block, t_src, "void", t_task, "_in", src_sub, None, debug_info
1143
                )
1144
                self.builder.add_memlet(
4✔
1145
                    block, t_task, "_out", t_dst, "void", "0", None, debug_info
1146
                )
1147

1148
            # Array Assignment (Copy)
1149
            else:
1150
                # Record shape for metadata
1151
                if res in self.tensor_table:
4✔
1152
                    # Prefer runtime shapes if available (for indirect access patterns)
1153
                    # Fall back to regular shapes otherwise
1154
                    res_info = self.tensor_table[res]
4✔
1155
                    if res in self.shapes_runtime_info:
4✔
1156
                        shape = self.shapes_runtime_info[res]
4✔
1157
                    else:
1158
                        shape = res_info.shape
4✔
1159
                    # Convert to string expressions
1160
                    self.captured_return_shapes[ret_name] = [str(s) for s in shape]
4✔
1161

1162
                    # Return arrays are always contiguous - compute fresh strides
1163
                    contiguous_strides = self.numpy_visitor._compute_strides(shape, "C")
4✔
1164
                    self.captured_return_strides[ret_name] = [
4✔
1165
                        str(s) for s in contiguous_strides
1166
                    ]
1167

1168
                    # Always overwrite tensor_table for return arrays with contiguous strides
1169
                    # (source tensor may have non-standard strides from views/flip)
1170
                    self.tensor_table[ret_name] = Tensor(
4✔
1171
                        res_info.element_type, shape, contiguous_strides
1172
                    )
1173

1174
                # Copy Logic using visit_Assign
1175
                ndim = 1
4✔
1176
                if ret_name in self.tensor_table:
4✔
1177
                    ndim = len(self.tensor_table[ret_name].shape)
4✔
1178

1179
                slice_node = ast.Slice(lower=None, upper=None, step=None)
4✔
1180
                if ndim > 1:
4✔
1181
                    target_slice = ast.Tuple(elts=[slice_node] * ndim, ctx=ast.Load())
4✔
1182
                else:
1183
                    target_slice = slice_node
4✔
1184

1185
                target_sub = ast.Subscript(
4✔
1186
                    value=ast.Name(id=ret_name, ctx=ast.Load()),
1187
                    slice=target_slice,
1188
                    ctx=ast.Store(),
1189
                )
1190

1191
                # Value node reconstruction
1192
                if isinstance(values[i], ast.Name):
4✔
1193
                    val_node = values[i]
4✔
1194
                else:
1195
                    val_node = ast.Name(id=res, ctx=ast.Load())
4✔
1196

1197
                assign_node = ast.Assign(targets=[target_sub], value=val_node)
4✔
1198
                self.visit_Assign(assign_node)
4✔
1199

1200
        # Add control flow return to exit the function/path
1201
        self.builder.add_return("", debug_info)
4✔
1202

1203
    def visit_Call(self, node):
4✔
1204
        func_name = ""
4✔
1205
        module_name = ""
4✔
1206
        submodule_name = ""
4✔
1207
        if isinstance(node.func, ast.Attribute):
4✔
1208
            if isinstance(node.func.value, ast.Name):
4✔
1209
                if node.func.value.id == "math":
4✔
1210
                    module_name = "math"
4✔
1211
                    func_name = node.func.attr
4✔
1212
                elif node.func.value.id in ["numpy", "np"]:
4✔
1213
                    module_name = "numpy"
4✔
1214
                    func_name = node.func.attr
4✔
1215
                else:
1216
                    array_name = node.func.value.id
4✔
1217
                    method_name = node.func.attr
4✔
1218
                    if array_name in self.tensor_table and method_name == "astype":
4✔
1219
                        return self.numpy_visitor.handle_numpy_astype(node, array_name)
4✔
1220
                    elif array_name in self.tensor_table and method_name == "copy":
4✔
1221
                        return self.numpy_visitor.handle_numpy_copy(node, array_name)
4✔
1222
            elif isinstance(node.func.value, ast.Attribute):
4✔
1223
                if (
4✔
1224
                    isinstance(node.func.value.value, ast.Name)
1225
                    and node.func.value.value.id == "scipy"
1226
                ):
1227
                    module_name = "scipy"
4✔
1228
                    submodule_name = node.func.value.attr
4✔
1229
                    func_name = node.func.attr
4✔
1230
                elif (
4✔
1231
                    isinstance(node.func.value.value, ast.Name)
1232
                    and node.func.value.value.id in ["numpy", "np"]
1233
                    and node.func.attr == "outer"
1234
                ):
1235
                    ufunc_name = node.func.value.attr
4✔
1236
                    return self.numpy_visitor.handle_ufunc_outer(node, ufunc_name)
4✔
1237

1238
        elif isinstance(node.func, ast.Name):
4✔
1239
            func_name = node.func.id
4✔
1240

1241
        if module_name == "numpy":
4✔
1242
            if self.numpy_visitor.has_handler(func_name):
4✔
1243
                return self.numpy_visitor.handle_numpy_call(node, func_name)
4✔
1244

1245
        if module_name == "math":
4✔
1246
            if self.math_handler.has_handler(func_name):
4✔
1247
                return self.math_handler.handle_math_call(node, func_name)
4✔
1248

1249
        if module_name == "scipy":
4✔
1250
            if self.scipy_handler.has_handler(submodule_name, func_name):
4✔
1251
                return self.scipy_handler.handle_scipy_call(
4✔
1252
                    node, submodule_name, func_name
1253
                )
1254

1255
        if self.python_handler.has_handler(func_name):
4✔
1256
            return self.python_handler.handle_python_call(node, func_name)
4✔
1257

1258
        if func_name in self.globals_dict:
4✔
1259
            obj = self.globals_dict[func_name]
4✔
1260
            if inspect.isfunction(obj):
4✔
1261
                return self._handle_inline_call(node, obj)
4✔
1262

NEW
1263
        raise NotImplementedError(f"Function call {func_name} not supported")
×
1264

1265
    def _handle_inline_call(self, node, func_obj):
4✔
1266
        try:
4✔
1267
            source_lines, start_line = inspect.getsourcelines(func_obj)
4✔
1268
            source = textwrap.dedent("".join(source_lines))
4✔
1269
            tree = ast.parse(source)
4✔
1270
            func_def = tree.body[0]
4✔
NEW
1271
        except Exception as e:
×
NEW
1272
            raise NotImplementedError(
×
1273
                f"Could not parse function {func_obj.__name__}: {e}"
1274
            )
1275

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

1278
        if len(arg_vars) != len(func_def.args.args):
4✔
NEW
1279
            raise NotImplementedError(
×
1280
                f"Argument count mismatch for {func_obj.__name__}"
1281
            )
1282

1283
        suffix = f"_{func_obj.__name__}_{self._get_unique_id()}"
4✔
1284
        res_name = f"_res{suffix}"
4✔
1285

1286
        # Combine globals with closure variables of the inlined function
1287
        combined_globals = dict(self.globals_dict)
4✔
1288
        closure_constants = {}  # name -> value for numeric closure vars
4✔
1289
        if func_obj.__closure__ is not None and func_obj.__code__.co_freevars:
4✔
1290
            for name, cell in zip(func_obj.__code__.co_freevars, func_obj.__closure__):
4✔
1291
                val = cell.cell_contents
4✔
1292
                combined_globals[name] = val
4✔
1293
                # Track numeric constants for injection
1294
                if isinstance(val, (int, float)) and not isinstance(val, bool):
4✔
1295
                    closure_constants[name] = val
4✔
1296

1297
        class VariableRenamer(ast.NodeTransformer):
4✔
1298
            BUILTINS = {
4✔
1299
                "range",
1300
                "len",
1301
                "int",
1302
                "float",
1303
                "bool",
1304
                "str",
1305
                "list",
1306
                "dict",
1307
                "tuple",
1308
                "set",
1309
                "print",
1310
                "abs",
1311
                "min",
1312
                "max",
1313
                "sum",
1314
                "enumerate",
1315
                "zip",
1316
                "map",
1317
                "filter",
1318
                "sorted",
1319
                "reversed",
1320
                "True",
1321
                "False",
1322
                "None",
1323
            }
1324

1325
            def __init__(self, suffix, globals_dict):
4✔
1326
                self.suffix = suffix
4✔
1327
                self.globals_dict = globals_dict
4✔
1328

1329
            def visit_Name(self, node):
4✔
1330
                if node.id in self.globals_dict or node.id in self.BUILTINS:
4✔
1331
                    return node
4✔
1332
                return ast.Name(id=f"{node.id}{self.suffix}", ctx=node.ctx)
4✔
1333

1334
            def visit_Return(self, node):
4✔
1335
                if node.value:
4✔
1336
                    val = self.visit(node.value)
4✔
1337
                    return ast.Assign(
4✔
1338
                        targets=[ast.Name(id=res_name, ctx=ast.Store())],
1339
                        value=val,
1340
                    )
NEW
1341
                return node
×
1342

1343
        renamer = VariableRenamer(suffix, combined_globals)
4✔
1344
        new_body = [renamer.visit(stmt) for stmt in func_def.body]
4✔
1345

1346
        param_assignments = []
4✔
1347

1348
        # Inject closure constants as assignments
1349
        for name, val in closure_constants.items():
4✔
1350
            if isinstance(val, int):
4✔
1351
                self.container_table[name] = Scalar(PrimitiveType.Int64)
4✔
1352
                self.builder.add_container(name, Scalar(PrimitiveType.Int64), False)
4✔
1353
                val_node = ast.Constant(value=val)
4✔
1354
            else:
NEW
1355
                self.container_table[name] = Scalar(PrimitiveType.Double)
×
NEW
1356
                self.builder.add_container(name, Scalar(PrimitiveType.Double), False)
×
NEW
1357
                val_node = ast.Constant(value=val)
×
1358
            assign = ast.Assign(
4✔
1359
                targets=[ast.Name(id=name, ctx=ast.Store())], value=val_node
1360
            )
1361
            param_assignments.append(assign)
4✔
1362

1363
        for arg_def, arg_val in zip(func_def.args.args, arg_vars):
4✔
1364
            param_name = f"{arg_def.arg}{suffix}"
4✔
1365

1366
            if arg_val in self.container_table:
4✔
1367
                self.container_table[param_name] = self.container_table[arg_val]
4✔
1368
                self.builder.add_container(
4✔
1369
                    param_name, self.container_table[arg_val], False
1370
                )
1371
                val_node = ast.Name(id=arg_val, ctx=ast.Load())
4✔
NEW
1372
            elif self._is_int(arg_val):
×
NEW
1373
                self.container_table[param_name] = Scalar(PrimitiveType.Int64)
×
NEW
1374
                self.builder.add_container(
×
1375
                    param_name, Scalar(PrimitiveType.Int64), False
1376
                )
NEW
1377
                val_node = ast.Constant(value=int(arg_val))
×
1378
            else:
NEW
1379
                try:
×
NEW
1380
                    val = float(arg_val)
×
NEW
1381
                    self.container_table[param_name] = Scalar(PrimitiveType.Double)
×
NEW
1382
                    self.builder.add_container(
×
1383
                        param_name, Scalar(PrimitiveType.Double), False
1384
                    )
NEW
1385
                    val_node = ast.Constant(value=val)
×
NEW
1386
                except ValueError:
×
NEW
1387
                    val_node = ast.Name(id=arg_val, ctx=ast.Load())
×
1388

1389
            assign = ast.Assign(
4✔
1390
                targets=[ast.Name(id=param_name, ctx=ast.Store())], value=val_node
1391
            )
1392
            param_assignments.append(assign)
4✔
1393

1394
        final_body = param_assignments + new_body
4✔
1395

1396
        # Create a new parser instance for the inlined function
1397
        parser = ASTParser(
4✔
1398
            self.builder,
1399
            self.tensor_table,
1400
            self.container_table,
1401
            globals_dict=combined_globals,
1402
            unique_counter_ref=self._unique_counter_ref,
1403
        )
1404

1405
        for stmt in final_body:
4✔
1406
            parser.visit(stmt)
4✔
1407

1408
        return res_name
4✔
1409

1410
    def _add_assign_constant(self, target_name, value_str, dtype):
4✔
1411
        block = self.builder.add_block()
4✔
1412
        t_const = self.builder.add_constant(block, value_str, dtype)
4✔
1413
        t_dst = self.builder.add_access(block, target_name)
4✔
1414
        t_task = self.builder.add_tasklet(block, TaskletCode.assign, ["_in"], ["_out"])
4✔
1415
        self.builder.add_memlet(block, t_const, "void", t_task, "_in", "")
4✔
1416
        self.builder.add_memlet(block, t_task, "_out", t_dst, "void", "")
4✔
1417

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

1421
        Uses a zero-copy view when possible (positive step, no indirect access).
1422
        Falls back to copy-based approach for complex cases.
1423
        """
1424
        if not self.builder:
4✔
NEW
1425
            raise ValueError("Builder required for expression slicing")
×
1426

1427
        # Try view-based approach first (zero-copy)
1428
        if self._can_use_slice_view(indices_nodes):
4✔
1429
            return self._create_slice_view(value_str, indices_nodes, shapes, ndim)
4✔
1430

1431
        # Fall back to copy-based approach for complex cases
NEW
1432
        return self._handle_expression_slicing_copy(
×
1433
            node, value_str, indices_nodes, shapes, ndim
1434
        )
1435

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

1439
        Views can be used when:
1440
        - All steps are non-zero constants (positive or negative)
1441
        - No indirect array access in slice parameters
1442

1443
        Returns True if a view can be used, False if a copy is required.
1444
        """
1445
        for idx in indices_nodes:
4✔
1446
            if isinstance(idx, ast.Slice):
4✔
1447
                # Check for zero step (invalid)
1448
                if idx.step is not None:
4✔
1449
                    if isinstance(idx.step, ast.Constant):
4✔
1450
                        if idx.step.value == 0:
4✔
NEW
1451
                            return False  # Zero step is invalid
×
1452
                    elif isinstance(idx.step, ast.UnaryOp) and isinstance(
4✔
1453
                        idx.step.op, ast.USub
1454
                    ):
1455
                        # Negative step like -2 is OK
1456
                        pass
4✔
NEW
1457
                    elif self._contains_indirect_access(idx.step):
×
NEW
1458
                        return False  # Dynamic step requires copy
×
1459

1460
                # Check for indirect access in slice bounds
1461
                if idx.lower is not None and self._contains_indirect_access(idx.lower):
4✔
NEW
1462
                    return False
×
1463
                if idx.upper is not None and self._contains_indirect_access(idx.upper):
4✔
NEW
1464
                    return False
×
1465
            else:
1466
                # Fixed index: check for indirect access
1467
                if self._contains_indirect_access(idx):
4✔
NEW
1468
                    return False
×
1469
        return True
4✔
1470

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

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

1477
        For positive step A[start:stop:step, ...] on dimension i:
1478
        - new_shape[i] = ceil((stop - start) / step)
1479
        - new_stride[i] = old_stride[i] * step
1480
        - offset contribution = start * old_stride[i]
1481

1482
        For negative step A[start:stop:step, ...] (e.g., ::-1):
1483
        - Default start = shape - 1 (last element)
1484
        - Default stop = -1 (before first element)
1485
        - new_shape[i] = ceil((start - stop) / abs(step))
1486
        - new_stride[i] = old_stride[i] * step (negative)
1487
        - offset contribution = start * old_stride[i] (points to last element)
1488

1489
        For a fixed index A[k, ...] on dimension i (dimension reduction):
1490
        - offset contribution = k * old_stride[i]
1491
        - dimension is removed from output
1492
        """
1493
        in_tensor = self.tensor_table[value_str]
4✔
1494
        in_shape = in_tensor.shape
4✔
1495
        dtype = in_tensor.element_type
4✔
1496

1497
        # Get input strides (compute if not available)
1498
        in_strides = (
4✔
1499
            in_tensor.strides
1500
            if hasattr(in_tensor, "strides") and in_tensor.strides
1501
            else None
1502
        )
1503
        if in_strides is None:
4✔
NEW
1504
            in_strides = self.numpy_visitor._compute_strides(in_shape, "C")
×
1505

1506
        # Get base offset from input tensor
1507
        in_offset = getattr(in_tensor, "offset", "0") or "0"
4✔
1508

1509
        # Build output shape, strides, and compute offset
1510
        out_shape = []
4✔
1511
        out_strides = []
4✔
1512
        offset_terms = []
4✔
1513
        if in_offset != "0":
4✔
1514
            offset_terms.append(str(in_offset))
4✔
1515

1516
        for i, idx in enumerate(indices_nodes):
4✔
1517
            shape_val = shapes[i] if i < len(shapes) else f"_{value_str}_shape_{i}"
4✔
1518
            stride_val = in_strides[i] if i < len(in_strides) else "1"
4✔
1519

1520
            if isinstance(idx, ast.Slice):
4✔
1521
                # Determine step value and sign
1522
                step_str = "1"
4✔
1523
                step_is_negative = False
4✔
1524
                step_value = 1
4✔
1525

1526
                if idx.step is not None:
4✔
1527
                    if isinstance(idx.step, ast.Constant):
4✔
1528
                        step_value = idx.step.value
4✔
1529
                        step_str = str(step_value)
4✔
1530
                        step_is_negative = step_value < 0
4✔
1531
                    elif isinstance(idx.step, ast.UnaryOp) and isinstance(
4✔
1532
                        idx.step.op, ast.USub
1533
                    ):
1534
                        # Handle -N syntax
1535
                        if isinstance(idx.step.operand, ast.Constant):
4✔
1536
                            step_value = -idx.step.operand.value
4✔
1537
                            step_str = str(step_value)
4✔
1538
                            step_is_negative = True
4✔
1539
                        else:
NEW
1540
                            step_str = self.visit(idx.step)
×
1541
                    else:
NEW
1542
                        step_str = self.visit(idx.step)
×
1543

1544
                if step_is_negative:
4✔
1545
                    # Negative step: iterate from end to start
1546
                    # Default start = shape - 1, default stop = -1 (before 0)
1547
                    if idx.lower is not None:
4✔
NEW
1548
                        start_str = self.visit(idx.lower)
×
NEW
1549
                        if isinstance(start_str, str) and (
×
1550
                            start_str.startswith("-") or start_str.startswith("(-")
1551
                        ):
NEW
1552
                            start_str = f"({shape_val} + {start_str})"
×
1553
                    else:
1554
                        start_str = f"({shape_val} - 1)"
4✔
1555

1556
                    if idx.upper is not None:
4✔
NEW
1557
                        stop_str = self.visit(idx.upper)
×
NEW
1558
                        if isinstance(stop_str, str) and (
×
1559
                            stop_str.startswith("-") or stop_str.startswith("(-")
1560
                        ):
NEW
1561
                            stop_str = f"({shape_val} + {stop_str})"
×
1562
                    else:
1563
                        stop_str = "-1"
4✔
1564

1565
                    # Shape for negative step: ceil((start - stop) / abs(step))
1566
                    abs_step = abs(step_value)
4✔
1567
                    if abs_step == 1:
4✔
1568
                        dim_size = f"({start_str} - {stop_str})"
4✔
1569
                    else:
1570
                        dim_size = f"(({start_str} - {stop_str} + {abs_step} - 1) / {abs_step})"
4✔
1571
                    out_shape.append(dim_size)
4✔
1572

1573
                    # Stride for negative step: old_stride * step (negative)
1574
                    out_strides.append(f"({stride_val} * {step_str})")
4✔
1575

1576
                    # Offset: start * old_stride (points to first element to access)
1577
                    offset_terms.append(f"({start_str} * {stride_val})")
4✔
1578
                else:
1579
                    # Positive step (original logic)
1580
                    start_str = "0"
4✔
1581
                    if idx.lower is not None:
4✔
1582
                        start_str = self.visit(idx.lower)
4✔
1583
                        if isinstance(start_str, str) and (
4✔
1584
                            start_str.startswith("-") or start_str.startswith("(-")
1585
                        ):
NEW
1586
                            start_str = f"({shape_val} + {start_str})"
×
1587

1588
                    stop_str = str(shape_val)
4✔
1589
                    if idx.upper is not None:
4✔
1590
                        stop_str = self.visit(idx.upper)
4✔
1591
                        if isinstance(stop_str, str) and (
4✔
1592
                            stop_str.startswith("-") or stop_str.startswith("(-")
1593
                        ):
1594
                            stop_str = f"({shape_val} + {stop_str})"
4✔
1595

1596
                    # Compute new shape: ceil((stop - start) / step)
1597
                    if step_str == "1":
4✔
1598
                        dim_size = f"({stop_str} - {start_str})"
4✔
1599
                    else:
1600
                        dim_size = f"idiv({stop_str} - {start_str} + {step_str} - 1, {step_str})"
4✔
1601
                    out_shape.append(dim_size)
4✔
1602

1603
                    # Compute new stride: old_stride * step
1604
                    if step_str == "1":
4✔
1605
                        out_strides.append(stride_val)
4✔
1606
                    else:
1607
                        out_strides.append(f"({stride_val} * {step_str})")
4✔
1608

1609
                    # Add offset contribution: start * stride
1610
                    if start_str != "0":
4✔
1611
                        offset_terms.append(f"({start_str} * {stride_val})")
4✔
1612
            else:
1613
                # Fixed index: dimension is removed, just add offset
1614
                index_str = self.visit(idx)
4✔
1615
                if isinstance(index_str, str) and (
4✔
1616
                    index_str.startswith("-") or index_str.startswith("(-")
1617
                ):
1618
                    index_str = f"({shape_val} + {index_str})"
4✔
1619
                offset_terms.append(f"({index_str} * {stride_val})")
4✔
1620

1621
        # Combine offset terms
1622
        if not offset_terms:
4✔
1623
            out_offset = "0"
4✔
1624
        elif len(offset_terms) == 1:
4✔
1625
            out_offset = offset_terms[0]
4✔
1626
        else:
1627
            out_offset = " + ".join(offset_terms)
4✔
1628

1629
        # Create new pointer container
1630
        tmp_name = self.builder.find_new_name("_slice_view_")
4✔
1631
        ptr_type = Pointer(dtype)
4✔
1632
        self.builder.add_container(tmp_name, ptr_type, False)
4✔
1633
        self.container_table[tmp_name] = ptr_type
4✔
1634

1635
        # Create output tensor with new shape, strides, and offset
1636
        # Offset is stored in the Tensor (like Tensor.flip() does)
1637
        # Reference memlet just creates the pointer alias with "0" offset
1638
        if out_shape:
4✔
1639
            out_tensor = Tensor(dtype, out_shape, out_strides, out_offset)
4✔
1640
            self.tensor_table[tmp_name] = out_tensor
4✔
1641
        else:
1642
            # Scalar result (all indices were fixed)
NEW
1643
            self.builder.add_container(tmp_name, dtype, False)
×
NEW
1644
            self.container_table[tmp_name] = dtype
×
1645

1646
        # Create reference memlet (offset is handled by tensor's offset property)
1647
        debug_info = DebugInfo()
4✔
1648
        block = self.builder.add_block(debug_info)
4✔
1649
        t_src = self.builder.add_access(block, value_str, debug_info)
4✔
1650
        t_dst = self.builder.add_access(block, tmp_name, debug_info)
4✔
1651
        self.builder.add_reference_memlet(block, t_src, t_dst, "0", ptr_type)
4✔
1652

1653
        return tmp_name
4✔
1654

1655
    def _handle_expression_slicing_copy(
4✔
1656
        self, node, value_str, indices_nodes, shapes, ndim
1657
    ):
1658
        """Copy-based slicing for cases that cannot use views.
1659

1660
        This allocates a new array and copies elements using nested loops.
1661
        Used for negative steps or indirect access patterns.
1662
        """
NEW
1663
        dtype = Scalar(PrimitiveType.Double)
×
NEW
1664
        if value_str in self.container_table:
×
NEW
1665
            t = self.container_table[value_str]
×
NEW
1666
            if isinstance(t, Pointer) and t.has_pointee_type():
×
NEW
1667
                dtype = t.pointee_type
×
1668

NEW
1669
        result_shapes = []
×
NEW
1670
        result_shapes_runtime = []
×
NEW
1671
        slice_info = []
×
NEW
1672
        index_info = []
×
1673

NEW
1674
        for i, idx in enumerate(indices_nodes):
×
NEW
1675
            shape_val = shapes[i] if i < len(shapes) else f"_{value_str}_shape_{i}"
×
1676

NEW
1677
            if isinstance(idx, ast.Slice):
×
NEW
1678
                start_str = "0"
×
NEW
1679
                start_str_runtime = "0"
×
NEW
1680
                if idx.lower is not None:
×
NEW
1681
                    if self._contains_indirect_access(idx.lower):
×
NEW
1682
                        start_str, start_str_runtime = (
×
1683
                            self._materialize_indirect_access(
1684
                                idx.lower, return_original_expr=True
1685
                            )
1686
                        )
1687
                    else:
NEW
1688
                        start_str = self.visit(idx.lower)
×
NEW
1689
                        start_str_runtime = start_str
×
NEW
1690
                    if isinstance(start_str, str) and (
×
1691
                        start_str.startswith("-") or start_str.startswith("(-")
1692
                    ):
NEW
1693
                        start_str = f"({shape_val} + {start_str})"
×
NEW
1694
                        start_str_runtime = f"({shape_val} + {start_str_runtime})"
×
1695

NEW
1696
                stop_str = str(shape_val)
×
NEW
1697
                stop_str_runtime = str(shape_val)
×
NEW
1698
                if idx.upper is not None:
×
NEW
1699
                    if self._contains_indirect_access(idx.upper):
×
NEW
1700
                        stop_str, stop_str_runtime = self._materialize_indirect_access(
×
1701
                            idx.upper, return_original_expr=True
1702
                        )
1703
                    else:
NEW
1704
                        stop_str = self.visit(idx.upper)
×
NEW
1705
                        stop_str_runtime = stop_str
×
NEW
1706
                    if isinstance(stop_str, str) and (
×
1707
                        stop_str.startswith("-") or stop_str.startswith("(-")
1708
                    ):
NEW
1709
                        stop_str = f"({shape_val} + {stop_str})"
×
NEW
1710
                        stop_str_runtime = f"({shape_val} + {stop_str_runtime})"
×
1711

NEW
1712
                step_str = "1"
×
NEW
1713
                if idx.step is not None:
×
NEW
1714
                    step_str = self.visit(idx.step)
×
1715

1716
                # Compute dimension size accounting for step: ceil((stop - start) / step)
1717
                # For symbolic expressions, use integer ceiling formula: idiv(n + d - 1, d)
NEW
1718
                if step_str == "1":
×
NEW
1719
                    dim_size = f"({stop_str} - {start_str})"
×
NEW
1720
                    dim_size_runtime = f"({stop_str_runtime} - {start_str_runtime})"
×
1721
                else:
NEW
1722
                    dim_size = (
×
1723
                        f"idiv({stop_str} - {start_str} + {step_str} - 1, {step_str})"
1724
                    )
NEW
1725
                    dim_size_runtime = f"idiv({stop_str_runtime} - {start_str_runtime} + {step_str} - 1, {step_str})"
×
NEW
1726
                result_shapes.append(dim_size)
×
NEW
1727
                result_shapes_runtime.append(dim_size_runtime)
×
NEW
1728
                slice_info.append((i, start_str, stop_str, step_str))
×
1729
            else:
NEW
1730
                if self._contains_indirect_access(idx):
×
NEW
1731
                    index_str = self._materialize_indirect_access(idx)
×
1732
                else:
NEW
1733
                    index_str = self.visit(idx)
×
NEW
1734
                if isinstance(index_str, str) and (
×
1735
                    index_str.startswith("-") or index_str.startswith("(-")
1736
                ):
NEW
1737
                    index_str = f"({shape_val} + {index_str})"
×
NEW
1738
                index_info.append((i, index_str))
×
1739

NEW
1740
        tmp_name = self.builder.find_new_name("_slice_tmp_")
×
NEW
1741
        result_ndim = len(result_shapes)
×
1742

NEW
1743
        if result_ndim == 0:
×
NEW
1744
            self.builder.add_container(tmp_name, dtype, False)
×
NEW
1745
            self.container_table[tmp_name] = dtype
×
1746
        else:
NEW
1747
            size_str = "1"
×
NEW
1748
            for dim in result_shapes:
×
NEW
1749
                size_str = f"({size_str} * {dim})"
×
1750

NEW
1751
            element_size = self.builder.get_sizeof(dtype)
×
NEW
1752
            total_size = f"({size_str} * {element_size})"
×
1753

NEW
1754
            ptr_type = Pointer(dtype)
×
NEW
1755
            self.builder.add_container(tmp_name, ptr_type, False)
×
NEW
1756
            self.container_table[tmp_name] = ptr_type
×
NEW
1757
            tensor_info = Tensor(dtype, result_shapes)
×
NEW
1758
            self.shapes_runtime_info[tmp_name] = (
×
1759
                result_shapes_runtime  # Store runtime shapes separately
1760
            )
NEW
1761
            self.tensor_table[tmp_name] = tensor_info
×
1762

NEW
1763
            debug_info = DebugInfo()
×
NEW
1764
            block_alloc = self.builder.add_block(debug_info)
×
NEW
1765
            t_malloc = self.builder.add_malloc(block_alloc, total_size)
×
NEW
1766
            t_ptr = self.builder.add_access(block_alloc, tmp_name, debug_info)
×
1767
            self.builder.add_memlet(
×
1768
                block_alloc, t_malloc, "_ret", t_ptr, "void", "", ptr_type, debug_info
1769
            )
1770

NEW
1771
        loop_vars = []
×
NEW
1772
        debug_info = DebugInfo()
×
1773

NEW
1774
        for dim_idx, (orig_dim, start_str, stop_str, step_str) in enumerate(slice_info):
×
NEW
1775
            loop_var = self.builder.find_new_name(f"_slice_loop_{dim_idx}_")
×
NEW
1776
            loop_vars.append((loop_var, orig_dim, start_str, step_str))
×
1777

NEW
1778
            if not self.builder.exists(loop_var):
×
NEW
1779
                self.builder.add_container(loop_var, Scalar(PrimitiveType.Int64), False)
×
NEW
1780
                self.container_table[loop_var] = Scalar(PrimitiveType.Int64)
×
1781

1782
            # Account for step in loop count: ceil((stop - start) / step)
NEW
1783
            if step_str == "1":
×
NEW
1784
                count_str = f"({stop_str} - {start_str})"
×
1785
            else:
NEW
1786
                count_str = (
×
1787
                    f"idiv({stop_str} - {start_str} + {step_str} - 1, {step_str})"
1788
                )
NEW
1789
            self.builder.begin_for(loop_var, "0", count_str, "1", debug_info)
×
1790

NEW
1791
        src_indices = [""] * ndim
×
NEW
1792
        dst_indices = []
×
1793

NEW
1794
        for orig_dim, index_str in index_info:
×
NEW
1795
            src_indices[orig_dim] = index_str
×
1796

NEW
1797
        for loop_var, orig_dim, start_str, step_str in loop_vars:
×
NEW
1798
            if step_str == "1":
×
NEW
1799
                src_indices[orig_dim] = f"({start_str} + {loop_var})"
×
1800
            else:
NEW
1801
                src_indices[orig_dim] = f"({start_str} + {loop_var} * {step_str})"
×
NEW
1802
            dst_indices.append(loop_var)
×
1803

NEW
1804
        src_linear = self._compute_linear_index(src_indices, shapes, value_str, ndim)
×
NEW
1805
        if result_ndim > 0:
×
NEW
1806
            dst_linear = self._compute_linear_index(
×
1807
                dst_indices, result_shapes, tmp_name, result_ndim
1808
            )
1809
        else:
NEW
1810
            dst_linear = "0"
×
1811

NEW
1812
        block = self.builder.add_block(debug_info)
×
NEW
1813
        t_src = self.builder.add_access(block, value_str, debug_info)
×
NEW
1814
        t_dst = self.builder.add_access(block, tmp_name, debug_info)
×
NEW
1815
        t_task = self.builder.add_tasklet(
×
1816
            block, TaskletCode.assign, ["_in"], ["_out"], debug_info
1817
        )
1818

NEW
1819
        self.builder.add_memlet(
×
1820
            block, t_src, "void", t_task, "_in", src_linear, None, debug_info
1821
        )
NEW
1822
        self.builder.add_memlet(
×
1823
            block, t_task, "_out", t_dst, "void", dst_linear, None, debug_info
1824
        )
1825

NEW
1826
        for _ in loop_vars:
×
NEW
1827
            self.builder.end_for()
×
1828

NEW
1829
        return tmp_name
×
1830

1831
    def _compute_linear_index(self, indices, shapes, array_name, ndim):
4✔
1832
        """Compute linear index from multi-dimensional indices."""
NEW
1833
        if ndim == 0:
×
NEW
1834
            return "0"
×
1835

NEW
1836
        linear_index = ""
×
NEW
1837
        for i in range(ndim):
×
NEW
1838
            term = str(indices[i])
×
NEW
1839
            for j in range(i + 1, ndim):
×
NEW
1840
                shape_val = shapes[j] if j < len(shapes) else f"_{array_name}_shape_{j}"
×
NEW
1841
                term = f"(({term}) * {shape_val})"
×
1842

NEW
1843
            if i == 0:
×
NEW
1844
                linear_index = term
×
1845
            else:
NEW
1846
                linear_index = f"({linear_index} + {term})"
×
1847

NEW
1848
        return linear_index
×
1849

1850
    def _is_array_index(self, node):
4✔
1851
        """Check if a node represents an array that could be used as an index (gather)."""
1852
        if isinstance(node, ast.Name):
4✔
1853
            return node.id in self.tensor_table
4✔
1854
        return False
4✔
1855

1856
    def _handle_gather(self, value_str, index_node, debug_info=None):
4✔
1857
        """Handle gather operation: x[indices] where indices is an array."""
NEW
1858
        if debug_info is None:
×
NEW
1859
            debug_info = DebugInfo()
×
1860

NEW
1861
        if isinstance(index_node, ast.Name):
×
NEW
1862
            idx_array_name = index_node.id
×
1863
        else:
NEW
1864
            idx_array_name = self.visit(index_node)
×
1865

NEW
1866
        if idx_array_name not in self.tensor_table:
×
NEW
1867
            raise ValueError(f"Gather index must be an array, got {idx_array_name}")
×
1868

NEW
1869
        idx_shapes = self.tensor_table[idx_array_name].shape
×
NEW
1870
        idx_ndim = len(idx_shapes)
×
1871

NEW
1872
        if idx_ndim != 1:
×
NEW
1873
            raise NotImplementedError("Only 1D index arrays supported for gather")
×
1874

NEW
1875
        result_shape = idx_shapes[0] if idx_shapes else f"_{idx_array_name}_shape_0"
×
1876

1877
        # For runtime evaluation, prefer shapes_runtime_info if available
1878
        # This ensures we use expressions that can be evaluated at runtime
NEW
1879
        if idx_array_name in self.shapes_runtime_info:
×
NEW
1880
            runtime_shapes = self.shapes_runtime_info[idx_array_name]
×
NEW
1881
            result_shape_runtime = runtime_shapes[0] if runtime_shapes else result_shape
×
1882
        else:
NEW
1883
            result_shape_runtime = result_shape
×
1884

NEW
1885
        dtype = Scalar(PrimitiveType.Double)
×
NEW
1886
        if value_str in self.container_table:
×
NEW
1887
            t = self.container_table[value_str]
×
NEW
1888
            if isinstance(t, Pointer) and t.has_pointee_type():
×
NEW
1889
                dtype = t.pointee_type
×
1890

NEW
1891
        idx_dtype = Scalar(PrimitiveType.Int64)
×
NEW
1892
        if idx_array_name in self.container_table:
×
NEW
1893
            t = self.container_table[idx_array_name]
×
NEW
1894
            if isinstance(t, Pointer) and t.has_pointee_type():
×
NEW
1895
                idx_dtype = t.pointee_type
×
1896

NEW
1897
        tmp_name = self.builder.find_new_name("_gather_")
×
1898

NEW
1899
        element_size = self.builder.get_sizeof(dtype)
×
NEW
1900
        total_size = f"({result_shape} * {element_size})"
×
1901

NEW
1902
        ptr_type = Pointer(dtype)
×
NEW
1903
        self.builder.add_container(tmp_name, ptr_type, False)
×
NEW
1904
        self.container_table[tmp_name] = ptr_type
×
NEW
1905
        self.tensor_table[tmp_name] = Tensor(dtype, [result_shape])
×
1906
        # Store runtime evaluable shape for this gather result
NEW
1907
        self.shapes_runtime_info[tmp_name] = [result_shape_runtime]
×
1908

NEW
1909
        block_alloc = self.builder.add_block(debug_info)
×
NEW
1910
        t_malloc = self.builder.add_malloc(block_alloc, total_size)
×
NEW
1911
        t_ptr = self.builder.add_access(block_alloc, tmp_name, debug_info)
×
NEW
1912
        self.builder.add_memlet(
×
1913
            block_alloc, t_malloc, "_ret", t_ptr, "void", "", ptr_type, debug_info
1914
        )
1915

NEW
1916
        loop_var = self.builder.find_new_name("_gather_i_")
×
NEW
1917
        self.builder.add_container(loop_var, Scalar(PrimitiveType.Int64), False)
×
NEW
1918
        self.container_table[loop_var] = Scalar(PrimitiveType.Int64)
×
1919

NEW
1920
        idx_var = self.builder.find_new_name("_gather_idx_")
×
NEW
1921
        self.builder.add_container(idx_var, idx_dtype, False)
×
NEW
1922
        self.container_table[idx_var] = idx_dtype
×
1923

NEW
1924
        self.builder.begin_for(loop_var, "0", str(result_shape), "1", debug_info)
×
1925

NEW
1926
        block_load_idx = self.builder.add_block(debug_info)
×
NEW
1927
        idx_arr_access = self.builder.add_access(
×
1928
            block_load_idx, idx_array_name, debug_info
1929
        )
NEW
1930
        idx_var_access = self.builder.add_access(block_load_idx, idx_var, debug_info)
×
NEW
1931
        tasklet_load = self.builder.add_tasklet(
×
1932
            block_load_idx, TaskletCode.assign, ["_in"], ["_out"], debug_info
1933
        )
NEW
1934
        self.builder.add_memlet(
×
1935
            block_load_idx,
1936
            idx_arr_access,
1937
            "void",
1938
            tasklet_load,
1939
            "_in",
1940
            loop_var,
1941
            None,
1942
            debug_info,
1943
        )
NEW
1944
        self.builder.add_memlet(
×
1945
            block_load_idx,
1946
            tasklet_load,
1947
            "_out",
1948
            idx_var_access,
1949
            "void",
1950
            "",
1951
            None,
1952
            debug_info,
1953
        )
1954

NEW
1955
        block_gather = self.builder.add_block(debug_info)
×
NEW
1956
        src_access = self.builder.add_access(block_gather, value_str, debug_info)
×
NEW
1957
        dst_access = self.builder.add_access(block_gather, tmp_name, debug_info)
×
NEW
1958
        tasklet_gather = self.builder.add_tasklet(
×
1959
            block_gather, TaskletCode.assign, ["_in"], ["_out"], debug_info
1960
        )
1961

NEW
1962
        self.builder.add_memlet(
×
1963
            block_gather,
1964
            src_access,
1965
            "void",
1966
            tasklet_gather,
1967
            "_in",
1968
            idx_var,
1969
            None,
1970
            debug_info,
1971
        )
NEW
1972
        self.builder.add_memlet(
×
1973
            block_gather,
1974
            tasklet_gather,
1975
            "_out",
1976
            dst_access,
1977
            "void",
1978
            loop_var,
1979
            None,
1980
            debug_info,
1981
        )
1982

NEW
1983
        self.builder.end_for()
×
1984

NEW
1985
        return tmp_name
×
1986

1987
    def _get_max_array_ndim_in_expr(self, node):
4✔
1988
        """Get the maximum array dimensionality in an expression."""
1989
        max_ndim = 0
4✔
1990

1991
        class NdimVisitor(ast.NodeVisitor):
4✔
1992
            def __init__(self, tensor_table):
4✔
1993
                self.tensor_table = tensor_table
4✔
1994
                self.max_ndim = 0
4✔
1995

1996
            def visit_Name(self, node):
4✔
1997
                if node.id in self.tensor_table:
4✔
1998
                    ndim = len(self.tensor_table[node.id].shape)
4✔
1999
                    self.max_ndim = max(self.max_ndim, ndim)
4✔
2000
                return self.generic_visit(node)
4✔
2001

2002
        visitor = NdimVisitor(self.tensor_table)
4✔
2003
        visitor.visit(node)
4✔
2004
        return visitor.max_ndim
4✔
2005

2006
    def _handle_broadcast_slice_assignment(
4✔
2007
        self,
2008
        target,
2009
        materialized_rhs,
2010
        target_name,
2011
        indices,
2012
        target_ndim,
2013
        value_ndim,
2014
        debug_info,
2015
    ):
2016
        """Handle slice assignment with broadcasting (e.g., 2D[:,:] = 1D[:]).
2017

2018
        materialized_rhs is the already-evaluated RHS array name (not AST node).
2019
        """
NEW
2020
        broadcast_dims = target_ndim - value_ndim
×
NEW
2021
        shapes = self.tensor_table[target_name].shape
×
NEW
2022
        rhs_tensor = self.tensor_table.get(materialized_rhs)
×
NEW
2023
        rhs_shapes = rhs_tensor.shape if rhs_tensor else []
×
2024

2025
        # Create outer loops for broadcast dimensions
2026
        outer_loop_vars = []
×
2027
        for i in range(broadcast_dims):
×
NEW
2028
            loop_var = self.builder.find_new_name(f"_bcast_iter_{i}_")
×
2029
            outer_loop_vars.append(loop_var)
×
2030

2031
            if not self.builder.exists(loop_var):
×
2032
                self.builder.add_container(loop_var, Scalar(PrimitiveType.Int64), False)
×
NEW
2033
                self.container_table[loop_var] = Scalar(PrimitiveType.Int64)
×
2034

2035
            dim_size = shapes[i] if i < len(shapes) else f"_{target_name}_shape_{i}"
×
NEW
2036
            self.builder.begin_for(loop_var, "0", str(dim_size), "1", debug_info)
×
2037

2038
        # Create inner loops for value dimensions
NEW
2039
        inner_loop_vars = []
×
NEW
2040
        for i in range(value_ndim):
×
NEW
2041
            loop_var = self.builder.find_new_name(f"_inner_iter_{i}_")
×
NEW
2042
            inner_loop_vars.append(loop_var)
×
2043

NEW
2044
            if not self.builder.exists(loop_var):
×
NEW
2045
                self.builder.add_container(loop_var, Scalar(PrimitiveType.Int64), False)
×
NEW
2046
                self.container_table[loop_var] = Scalar(PrimitiveType.Int64)
×
2047

2048
            # Use RHS shape for inner dimension bounds
2049
            dim_size = (
×
2050
                rhs_shapes[i] if i < len(rhs_shapes) else shapes[broadcast_dims + i]
2051
            )
NEW
2052
            self.builder.begin_for(loop_var, "0", str(dim_size), "1", debug_info)
×
2053

2054
        # Create assignment block: target[outer_vars, inner_vars] = rhs[inner_vars]
2055
        block = self.builder.add_block(debug_info)
×
NEW
2056
        t_src = self.builder.add_access(block, materialized_rhs, debug_info)
×
NEW
2057
        t_dst = self.builder.add_access(block, target_name, debug_info)
×
NEW
2058
        t_task = self.builder.add_tasklet(
×
2059
            block, TaskletCode.assign, ["_in"], ["_out"], debug_info
2060
        )
2061

2062
        # Source index: just inner loop vars
NEW
2063
        src_index = ",".join(inner_loop_vars) if inner_loop_vars else "0"
×
2064

2065
        # Target index: outer_vars + inner_vars combined
NEW
2066
        all_target_vars = outer_loop_vars + inner_loop_vars
×
NEW
2067
        target_index = ",".join(all_target_vars) if all_target_vars else "0"
×
2068

NEW
2069
        self.builder.add_memlet(
×
2070
            block, t_src, "void", t_task, "_in", src_index, rhs_tensor, debug_info
2071
        )
2072

NEW
2073
        tensor_dst = self.tensor_table[target_name]
×
NEW
2074
        self.builder.add_memlet(
×
2075
            block, t_task, "_out", t_dst, "void", target_index, tensor_dst, debug_info
2076
        )
2077

2078
        # Close all loops (inner first, then outer)
NEW
2079
        for _ in inner_loop_vars:
×
NEW
2080
            self.builder.end_for()
×
2081
        for _ in outer_loop_vars:
×
2082
            self.builder.end_for()
×
2083

2084
    def _handle_slice_assignment(
4✔
2085
        self, target, value, target_name, indices, debug_info=None
2086
    ):
2087
        if debug_info is None:
4✔
2088
            debug_info = DebugInfo()
×
2089

2090
        # Add missing dimensions
2091
        tensor_info = self.tensor_table[target_name]
4✔
2092
        ndim = len(tensor_info.shape)
4✔
2093
        if len(indices) < ndim:
4✔
2094
            indices = list(indices)
4✔
2095
            for _ in range(ndim - len(indices)):
4✔
2096
                indices.append(ast.Slice(lower=None, upper=None, step=None))
4✔
2097

2098
        # Handle ufunc outer case separately to preserve slice shape info
2099
        has_outer, ufunc_name, outer_node = contains_ufunc_outer(value)
4✔
2100
        if has_outer:
4✔
2101
            self._handle_ufunc_outer_slice_assignment(
4✔
2102
                target, value, target_name, indices, debug_info
2103
            )
2104
            return
4✔
2105

2106
        # Count slice dimensions to determine effective target dimensionality
2107
        target_slice_ndim = sum(1 for idx in indices if isinstance(idx, ast.Slice))
4✔
2108
        value_max_ndim = self._get_max_array_ndim_in_expr(value)
4✔
2109

2110
        # ALWAYS evaluate RHS first (NumPy semantics) - before any loops
2111
        materialized_rhs = self.visit(value)
4✔
2112

2113
        if (
4✔
2114
            target_slice_ndim > 0
2115
            and value_max_ndim > 0
2116
            and target_slice_ndim > value_max_ndim
2117
        ):
2118
            # Broadcasting case: use row-by-row approach with reference memlets
2119
            self._handle_broadcast_slice_assignment(
×
2120
                target,
2121
                materialized_rhs,
2122
                target_name,
2123
                indices,
2124
                target_slice_ndim,
2125
                value_max_ndim,
2126
                debug_info,
2127
            )
2128
            return
×
2129

2130
        loop_vars = []
4✔
2131
        new_target_indices = []
4✔
2132

2133
        for i, idx in enumerate(indices):
4✔
2134
            if isinstance(idx, ast.Slice):
4✔
2135
                loop_var = self.builder.find_new_name(f"_slice_iter_{len(loop_vars)}_")
4✔
2136
                loop_vars.append(loop_var)
4✔
2137

2138
                if not self.builder.exists(loop_var):
4✔
2139
                    self.builder.add_container(
4✔
2140
                        loop_var, Scalar(PrimitiveType.Int64), False
2141
                    )
2142
                    self.container_table[loop_var] = Scalar(PrimitiveType.Int64)
4✔
2143

2144
                start_str = "0"
4✔
2145
                if idx.lower:
4✔
2146
                    start_str = self.visit(idx.lower)
4✔
2147
                    if start_str.startswith("-"):
4✔
2148
                        dim_size = (
×
2149
                            str(tensor_info.shape[i])
2150
                            if i < len(tensor_info.shape)
2151
                            else f"_{target_name}_shape_{i}"
2152
                        )
2153
                        start_str = f"({dim_size} {start_str})"
×
2154

2155
                stop_str = ""
4✔
2156
                if idx.upper and not (
4✔
2157
                    isinstance(idx.upper, ast.Constant) and idx.upper.value is None
2158
                ):
2159
                    stop_str = self.visit(idx.upper)
4✔
2160
                    if stop_str.startswith("-") or stop_str.startswith("(-"):
4✔
2161
                        dim_size = (
×
2162
                            str(tensor_info.shape[i])
2163
                            if i < len(tensor_info.shape)
2164
                            else f"_{target_name}_shape_{i}"
2165
                        )
2166
                        stop_str = f"({dim_size} {stop_str})"
×
2167
                else:
2168
                    stop_str = (
4✔
2169
                        str(tensor_info.shape[i])
2170
                        if i < len(tensor_info.shape)
2171
                        else f"_{target_name}_shape_{i}"
2172
                    )
2173

2174
                step_str = "1"
4✔
2175
                if idx.step:
4✔
NEW
2176
                    step_str = self.visit(idx.step)
×
2177

2178
                count_str = f"({stop_str} - {start_str})"
4✔
2179

2180
                self.builder.begin_for(loop_var, "0", count_str, "1", debug_info)
4✔
2181
                self.container_table[loop_var] = Scalar(PrimitiveType.Int64)
4✔
2182
                new_target_indices.append(
4✔
2183
                    ast.Name(
2184
                        id=f"{start_str} + {loop_var} * {step_str}", ctx=ast.Load()
2185
                    )
2186
                )
2187
            else:
2188
                dim_size = (
4✔
2189
                    tensor_info.shape[i]
2190
                    if i < len(tensor_info.shape)
2191
                    else f"_{target_name}_shape_{i}"
2192
                )
2193
                normalized_idx = normalize_negative_index(idx, dim_size)
4✔
2194
                # intermediate computations are placed outside the loops
2195
                idx_str = self.visit(normalized_idx)
4✔
2196
                new_target_indices.append(ast.Name(id=idx_str, ctx=ast.Load()))
4✔
2197

2198
        rewriter = SliceRewriter(loop_vars, self.tensor_table, self)
4✔
2199
        new_value = rewriter.visit(copy.deepcopy(value))
4✔
2200

2201
        new_target = copy.deepcopy(target)
4✔
2202
        if len(new_target_indices) == 1:
4✔
2203
            new_target.slice = new_target_indices[0]
4✔
2204
        else:
2205
            new_target.slice = ast.Tuple(elts=new_target_indices, ctx=ast.Load())
4✔
2206

2207
        rhs_memlet_type = None
4✔
2208
        rhs_indexed_subset = ""
4✔
2209
        if materialized_rhs in self.tensor_table:
4✔
2210
            rhs_tensor = self.tensor_table[materialized_rhs]
4✔
2211
            rhs_ndim = len(rhs_tensor.shape)
4✔
2212
            if rhs_ndim > 0 and rhs_ndim == len(loop_vars):
4✔
2213
                # RHS is an array matching the slice dimensions - index it with loop vars
2214
                rhs_indexed_subset = ",".join(loop_vars)
4✔
2215
                rhs_memlet_type = rhs_tensor
4✔
2216

2217
        block = self.builder.add_block(debug_info)
4✔
2218
        t_task = self.builder.add_tasklet(
4✔
2219
            block, TaskletCode.assign, ["_in"], ["_out"], debug_info
2220
        )
2221

2222
        t_src, src_sub = self._add_read(block, materialized_rhs, debug_info)
4✔
2223
        # Use indexed subset if RHS is an array that needs indexing
2224
        actual_src_sub = rhs_indexed_subset if rhs_indexed_subset else src_sub
4✔
2225
        self.builder.add_memlet(
4✔
2226
            block,
2227
            t_src,
2228
            "void",
2229
            t_task,
2230
            "_in",
2231
            actual_src_sub,
2232
            rhs_memlet_type,
2233
            debug_info,
2234
        )
2235

2236
        lhs_expr = self.visit(new_target)
4✔
2237
        if "(" in lhs_expr and lhs_expr.endswith(")"):
4✔
2238
            subset = lhs_expr[lhs_expr.find("(") + 1 : -1]
4✔
2239
            tensor_dst = self.tensor_table[target_name]
4✔
2240

2241
            t_dst = self.builder.add_access(block, target_name, debug_info)
4✔
2242
            self.builder.add_memlet(
4✔
2243
                block, t_task, "_out", t_dst, "void", subset, tensor_dst, debug_info
2244
            )
2245
        else:
NEW
2246
            t_dst = self.builder.add_access(block, target_name, debug_info)
×
NEW
2247
            self.builder.add_memlet(
×
2248
                block, t_task, "_out", t_dst, "void", "", None, debug_info
2249
            )
2250

2251
        for _ in loop_vars:
4✔
2252
            self.builder.end_for()
4✔
2253

2254
    def _handle_ufunc_outer_slice_assignment(
4✔
2255
        self, target, value, target_name, indices, debug_info=None
2256
    ):
2257
        """Handle slice assignment where RHS contains a ufunc outer operation.
2258

2259
        Example: path[:] = np.minimum(path[:], np.add.outer(path[:, k], path[k, :]))
2260

2261
        The strategy is:
2262
        1. Evaluate the entire RHS expression, which will create a temporary array
2263
           containing the result of the ufunc outer (potentially wrapped in other ops)
2264
        2. Copy the temporary result to the target slice
2265

2266
        This avoids the loop transformation that would destroy slice shape info.
2267
        """
2268
        if debug_info is None:
4✔
2269
            from docc.sdfg import DebugInfo
×
2270

2271
            debug_info = DebugInfo()
×
2272

2273
        # Evaluate the full RHS expression
2274
        # This will:
2275
        # - Create temp arrays for ufunc outer results
2276
        # - Apply any wrapping operations (np.minimum, etc.)
2277
        # - Return the name of the final result array
2278
        result_name = self.visit(value)
4✔
2279

2280
        # Now we need to copy result to target slice
2281
        # Count slice dimensions to determine if we need loops
2282
        target_slice_ndim = sum(1 for idx in indices if isinstance(idx, ast.Slice))
4✔
2283

2284
        if target_slice_ndim == 0:
4✔
2285
            # No slices on target - just simple assignment
NEW
2286
            target_str = self.visit(target)
×
2287
            block = self.builder.add_block(debug_info)
×
NEW
2288
            t_src, src_sub = self._add_read(block, result_name, debug_info)
×
2289
            t_dst = self.builder.add_access(block, target_str, debug_info)
×
2290
            t_task = self.builder.add_tasklet(
×
2291
                block, TaskletCode.assign, ["_in"], ["_out"], debug_info
2292
            )
2293
            self.builder.add_memlet(
×
2294
                block, t_src, "void", t_task, "_in", src_sub, None, debug_info
2295
            )
2296
            self.builder.add_memlet(
×
2297
                block, t_task, "_out", t_dst, "void", "", None, debug_info
2298
            )
2299
            return
×
2300

2301
        # We have slices on the target - need to create loops for copying
2302
        # Get target array info
2303
        target_shapes = self.tensor_table[target_name].shape
4✔
2304

2305
        loop_vars = []
4✔
2306
        new_target_indices = []
4✔
2307

2308
        for i, idx in enumerate(indices):
4✔
2309
            if isinstance(idx, ast.Slice):
4✔
2310
                loop_var = self.builder.find_new_name(f"_copy_iter_{len(loop_vars)}_")
4✔
2311
                loop_vars.append(loop_var)
4✔
2312

2313
                if not self.builder.exists(loop_var):
4✔
2314
                    self.builder.add_container(
4✔
2315
                        loop_var, Scalar(PrimitiveType.Int64), False
2316
                    )
2317
                    self.container_table[loop_var] = Scalar(PrimitiveType.Int64)
4✔
2318

2319
                start_str = "0"
4✔
2320
                if idx.lower:
4✔
NEW
2321
                    start_str = self.visit(idx.lower)
×
2322

2323
                stop_str = ""
4✔
2324
                if idx.upper and not (
4✔
2325
                    isinstance(idx.upper, ast.Constant) and idx.upper.value is None
2326
                ):
NEW
2327
                    stop_str = self.visit(idx.upper)
×
2328
                else:
2329
                    stop_str = (
4✔
2330
                        target_shapes[i]
2331
                        if i < len(target_shapes)
2332
                        else f"_{target_name}_shape_{i}"
2333
                    )
2334

2335
                step_str = "1"
4✔
2336
                if idx.step:
4✔
NEW
2337
                    step_str = self.visit(idx.step)
×
2338

2339
                count_str = f"({stop_str} - {start_str})"
4✔
2340

2341
                self.builder.begin_for(loop_var, "0", count_str, "1", debug_info)
4✔
2342
                self.container_table[loop_var] = Scalar(PrimitiveType.Int64)
4✔
2343

2344
                new_target_indices.append(
4✔
2345
                    ast.Name(
2346
                        id=f"{start_str} + {loop_var} * {step_str}", ctx=ast.Load()
2347
                    )
2348
                )
2349
            else:
2350
                # Handle non-slice indices - need to normalize negative indices
2351
                dim_size = (
×
2352
                    target_shapes[i]
2353
                    if i < len(target_shapes)
2354
                    else f"_{target_name}_shape_{i}"
2355
                )
2356
                normalized_idx = normalize_negative_index(idx, dim_size)
×
2357
                # Visit the index NOW before any loops are opened to ensure
2358
                # intermediate computations are placed outside the loops
NEW
2359
                idx_str = self.visit(normalized_idx)
×
NEW
2360
                new_target_indices.append(ast.Name(id=idx_str, ctx=ast.Load()))
×
2361

2362
        # Create assignment block: target[i,j,...] = result[i,j,...]
2363
        block = self.builder.add_block(debug_info)
4✔
2364

2365
        # Access nodes
2366
        t_src = self.builder.add_access(block, result_name, debug_info)
4✔
2367
        t_dst = self.builder.add_access(block, target_name, debug_info)
4✔
2368
        t_task = self.builder.add_tasklet(
4✔
2369
            block, TaskletCode.assign, ["_in"], ["_out"], debug_info
2370
        )
2371

2372
        # Source index - just use loop vars for flat array from ufunc outer
2373
        # The ufunc outer result is a flat array of size M*N
2374
        if len(loop_vars) == 2:
4✔
2375
            # 2D case: result is indexed as i * N + j
2376
            # Get the second dimension size from target shapes
2377
            n_dim = (
4✔
2378
                target_shapes[1]
2379
                if len(target_shapes) > 1
2380
                else f"_{target_name}_shape_1"
2381
            )
2382
            src_index = f"(({loop_vars[0]}) * ({n_dim}) + ({loop_vars[1]}))"
4✔
2383
        elif len(loop_vars) == 1:
×
2384
            src_index = loop_vars[0]
×
2385
        else:
2386
            # General case - compute linear index
2387
            src_terms = []
×
2388
            stride = "1"
×
2389
            for i in range(len(loop_vars) - 1, -1, -1):
×
2390
                if stride == "1":
×
2391
                    src_terms.insert(0, loop_vars[i])
×
2392
                else:
2393
                    src_terms.insert(0, f"({loop_vars[i]} * {stride})")
×
2394
                if i > 0:
×
2395
                    dim_size = (
×
2396
                        target_shapes[i]
2397
                        if i < len(target_shapes)
2398
                        else f"_{target_name}_shape_{i}"
2399
                    )
2400
                    stride = (
×
2401
                        f"({stride} * {dim_size})" if stride != "1" else str(dim_size)
2402
                    )
2403
            src_index = " + ".join(src_terms) if src_terms else "0"
×
2404

2405
        # Target index - compute linear index (row-major order)
2406
        # For 2D array with shape (M, N): linear_index = i * N + j
2407
        target_index_parts = []
4✔
2408
        for idx in new_target_indices:
4✔
2409
            if isinstance(idx, ast.Name):
4✔
2410
                target_index_parts.append(idx.id)
4✔
2411
            else:
NEW
2412
                target_index_parts.append(self.visit(idx))
×
2413

2414
        # Convert to linear index
2415
        if len(target_index_parts) == 2:
4✔
2416
            # 2D case
2417
            n_dim = (
4✔
2418
                target_shapes[1]
2419
                if len(target_shapes) > 1
2420
                else f"_{target_name}_shape_1"
2421
            )
2422
            target_index = (
4✔
2423
                f"(({target_index_parts[0]}) * ({n_dim}) + ({target_index_parts[1]}))"
2424
            )
2425
        elif len(target_index_parts) == 1:
×
2426
            target_index = target_index_parts[0]
×
2427
        else:
2428
            # General case - compute linear index with strides
2429
            stride = "1"
×
2430
            target_index = "0"
×
2431
            for i in range(len(target_index_parts) - 1, -1, -1):
×
2432
                idx_part = target_index_parts[i]
×
2433
                if stride == "1":
×
2434
                    term = idx_part
×
2435
                else:
2436
                    term = f"(({idx_part}) * ({stride}))"
×
2437

2438
                if target_index == "0":
×
2439
                    target_index = term
×
2440
                else:
2441
                    target_index = f"({term} + {target_index})"
×
2442

2443
                if i > 0:
×
2444
                    dim_size = (
×
2445
                        target_shapes[i]
2446
                        if i < len(target_shapes)
2447
                        else f"_{target_name}_shape_{i}"
2448
                    )
2449
                    stride = (
×
2450
                        f"({stride} * {dim_size})" if stride != "1" else str(dim_size)
2451
                    )
2452

2453
        # Connect memlets
2454
        self.builder.add_memlet(
4✔
2455
            block, t_src, "void", t_task, "_in", src_index, None, debug_info
2456
        )
2457
        self.builder.add_memlet(
4✔
2458
            block, t_task, "_out", t_dst, "void", target_index, None, debug_info
2459
        )
2460

2461
        # End loops
2462
        for _ in loop_vars:
4✔
2463
            self.builder.end_for()
4✔
2464

2465
    def _contains_indirect_access(self, node):
4✔
2466
        """Check if an AST node contains any indirect array access."""
2467
        if isinstance(node, ast.Subscript):
4✔
NEW
2468
            if isinstance(node.value, ast.Name):
×
NEW
2469
                arr_name = node.value.id
×
NEW
2470
                if arr_name in self.tensor_table:
×
NEW
2471
                    return True
×
2472
        elif isinstance(node, ast.BinOp):
4✔
2473
            return self._contains_indirect_access(
4✔
2474
                node.left
2475
            ) or self._contains_indirect_access(node.right)
2476
        elif isinstance(node, ast.UnaryOp):
4✔
2477
            return self._contains_indirect_access(node.operand)
4✔
2478
        return False
4✔
2479

2480
    def _materialize_indirect_access(
4✔
2481
        self, node, debug_info=None, return_original_expr=False
2482
    ):
2483
        """Materialize an array access into a scalar variable using tasklet+memlets."""
NEW
2484
        if not self.builder:
×
NEW
2485
            expr = self.visit(node)
×
NEW
2486
            return (expr, expr) if return_original_expr else expr
×
2487

NEW
2488
        if debug_info is None:
×
NEW
2489
            debug_info = DebugInfo()
×
2490

NEW
2491
        if not isinstance(node, ast.Subscript):
×
NEW
2492
            expr = self.visit(node)
×
NEW
2493
            return (expr, expr) if return_original_expr else expr
×
2494

NEW
2495
        if not isinstance(node.value, ast.Name):
×
NEW
2496
            expr = self.visit(node)
×
NEW
2497
            return (expr, expr) if return_original_expr else expr
×
2498

NEW
2499
        arr_name = node.value.id
×
NEW
2500
        if arr_name not in self.tensor_table:
×
NEW
2501
            expr = self.visit(node)
×
NEW
2502
            return (expr, expr) if return_original_expr else expr
×
2503

NEW
2504
        dtype = Scalar(PrimitiveType.Int64)
×
NEW
2505
        if arr_name in self.container_table:
×
NEW
2506
            t = self.container_table[arr_name]
×
NEW
2507
            if isinstance(t, Pointer) and t.has_pointee_type():
×
NEW
2508
                dtype = t.pointee_type
×
2509

NEW
2510
        tmp_name = self.builder.find_new_name("_idx_")
×
NEW
2511
        self.builder.add_container(tmp_name, dtype, False)
×
NEW
2512
        self.container_table[tmp_name] = dtype
×
2513

NEW
2514
        ndim = len(self.tensor_table[arr_name].shape)
×
NEW
2515
        shapes = self.tensor_table[arr_name].shape
×
2516

NEW
2517
        if isinstance(node.slice, ast.Tuple):
×
NEW
2518
            indices = [self.visit(elt) for elt in node.slice.elts]
×
2519
        else:
NEW
2520
            indices = [self.visit(node.slice)]
×
2521

NEW
2522
        materialized_indices = []
×
NEW
2523
        for idx_str in indices:
×
NEW
2524
            if "(" in idx_str and idx_str.endswith(")"):
×
NEW
2525
                materialized_indices.append(idx_str)
×
2526
            else:
NEW
2527
                materialized_indices.append(idx_str)
×
2528

NEW
2529
        linear_index = self._compute_linear_index(
×
2530
            materialized_indices, shapes, arr_name, ndim
2531
        )
2532

NEW
2533
        block = self.builder.add_block(debug_info)
×
NEW
2534
        t_src = self.builder.add_access(block, arr_name, debug_info)
×
NEW
2535
        t_dst = self.builder.add_access(block, tmp_name, debug_info)
×
NEW
2536
        t_task = self.builder.add_tasklet(
×
2537
            block, TaskletCode.assign, ["_in"], ["_out"], debug_info
2538
        )
2539

NEW
2540
        self.builder.add_memlet(
×
2541
            block, t_src, "void", t_task, "_in", linear_index, None, debug_info
2542
        )
NEW
2543
        self.builder.add_memlet(
×
2544
            block, t_task, "_out", t_dst, "void", "", None, debug_info
2545
        )
2546

NEW
2547
        if return_original_expr:
×
NEW
2548
            original_expr = f"{arr_name}({linear_index})"
×
NEW
2549
            return (tmp_name, original_expr)
×
2550

NEW
2551
        return tmp_name
×
2552

2553
    def _get_unique_id(self):
4✔
2554
        self._unique_counter_ref[0] += 1
4✔
2555
        return self._unique_counter_ref[0]
4✔
2556

2557
    def _get_memlet_type_for_access(self, expr_str, subset):
4✔
2558
        """Get the Tensor type for an indexed array access expression.
2559

2560
        When accessing an array like "arr(i,j)" with a multi-dimensional subset,
2561
        we need to pass the Tensor type to add_memlet for correct type inference.
2562
        If the expression is a simple scalar variable or constant, returns None.
2563
        """
2564
        if not subset:
4✔
2565
            return None
4✔
2566

2567
        # Check if expr_str is an indexed array access like "arr(i,j)"
2568
        if "(" in expr_str and expr_str.endswith(")"):
4✔
2569
            name = expr_str.split("(")[0]
4✔
2570
            if name in self.tensor_table:
4✔
2571
                return self.tensor_table[name]
4✔
2572

2573
        # Check if expr_str is a simple array name with a non-empty subset from _add_read
NEW
2574
        if expr_str in self.tensor_table:
×
NEW
2575
            return self.tensor_table[expr_str]
×
2576

NEW
2577
        return None
×
2578

2579
    def _element_type(self, name):
4✔
2580
        if name in self.container_table:
4✔
2581
            return element_type_from_sdfg_type(self.container_table[name])
4✔
2582
        else:  # Constant
2583
            if self._is_int(name):
4✔
2584
                return Scalar(PrimitiveType.Int64)
4✔
2585
            else:
2586
                return Scalar(PrimitiveType.Double)
4✔
2587

2588
    def _is_int(self, operand):
4✔
2589
        try:
4✔
2590
            if operand.lstrip("-").isdigit():
4✔
2591
                return True
4✔
NEW
2592
        except ValueError:
×
NEW
2593
            pass
×
2594

2595
        name = operand
4✔
2596
        if "(" in operand and operand.endswith(")"):
4✔
2597
            name = operand.split("(")[0]
4✔
2598

2599
        if name in self.container_table:
4✔
2600
            t = self.container_table[name]
4✔
2601

2602
            def is_int_ptype(pt):
4✔
2603
                return pt in [
4✔
2604
                    PrimitiveType.Int64,
2605
                    PrimitiveType.Int32,
2606
                    PrimitiveType.Int8,
2607
                    PrimitiveType.Int16,
2608
                    PrimitiveType.UInt64,
2609
                    PrimitiveType.UInt32,
2610
                    PrimitiveType.UInt8,
2611
                    PrimitiveType.UInt16,
2612
                ]
2613

2614
            if isinstance(t, Scalar):
4✔
2615
                return is_int_ptype(t.primitive_type)
4✔
2616

2617
            if type(t).__name__ == "Array" and hasattr(t, "element_type"):
4✔
NEW
2618
                et = t.element_type
×
NEW
2619
                if callable(et):
×
NEW
2620
                    et = et()
×
NEW
2621
                if isinstance(et, Scalar):
×
NEW
2622
                    return is_int_ptype(et.primitive_type)
×
2623

2624
            if type(t).__name__ == "Pointer":
4✔
2625
                if hasattr(t, "pointee_type"):
4✔
2626
                    et = t.pointee_type
4✔
2627
                    if callable(et):
4✔
NEW
2628
                        et = et()
×
2629
                    if isinstance(et, Scalar):
4✔
2630
                        return is_int_ptype(et.primitive_type)
4✔
NEW
2631
                if hasattr(t, "element_type"):
×
NEW
2632
                    et = t.element_type
×
NEW
2633
                    if callable(et):
×
NEW
2634
                        et = et()
×
NEW
2635
                    if isinstance(et, Scalar):
×
NEW
2636
                        return is_int_ptype(et.primitive_type)
×
2637

2638
        return False
4✔
2639

2640
    def _add_read(self, block, expr_str, debug_info=None):
4✔
2641
        try:
4✔
2642
            if (block, expr_str) in self._access_cache:
4✔
NEW
2643
                return self._access_cache[(block, expr_str)]
×
NEW
2644
        except TypeError:
×
NEW
2645
            pass
×
2646

2647
        if debug_info is None:
4✔
2648
            debug_info = DebugInfo()
4✔
2649

2650
        if "(" in expr_str and expr_str.endswith(")"):
4✔
2651
            name = expr_str.split("(")[0]
4✔
2652
            subset = expr_str[expr_str.find("(") + 1 : -1]
4✔
2653
            access = self.builder.add_access(block, name, debug_info)
4✔
2654
            try:
4✔
2655
                self._access_cache[(block, expr_str)] = (access, subset)
4✔
NEW
2656
            except TypeError:
×
NEW
2657
                pass
×
2658
            return access, subset
4✔
2659

2660
        if self.builder.exists(expr_str):
4✔
2661
            access = self.builder.add_access(block, expr_str, debug_info)
4✔
2662
            subset = ""
4✔
2663
            if expr_str in self.container_table:
4✔
2664
                sym_type = self.container_table[expr_str]
4✔
2665
                if isinstance(sym_type, Pointer):
4✔
2666
                    if expr_str in self.tensor_table:
4✔
2667
                        ndim = len(self.tensor_table[expr_str].shape)
4✔
2668
                        if ndim == 0:
4✔
NEW
2669
                            subset = "0"
×
2670
                    else:
NEW
2671
                        subset = "0"
×
2672
            try:
4✔
2673
                self._access_cache[(block, expr_str)] = (access, subset)
4✔
NEW
2674
            except TypeError:
×
NEW
2675
                pass
×
2676
            return access, subset
4✔
2677

2678
        dtype = Scalar(PrimitiveType.Double)
4✔
2679
        if self._is_int(expr_str):
4✔
2680
            dtype = Scalar(PrimitiveType.Int64)
4✔
2681
        elif expr_str == "true" or expr_str == "false":
4✔
NEW
2682
            dtype = Scalar(PrimitiveType.Bool)
×
2683

2684
        const_node = self.builder.add_constant(block, expr_str, dtype, debug_info)
4✔
2685
        try:
4✔
2686
            self._access_cache[(block, expr_str)] = (const_node, "")
4✔
NEW
2687
        except TypeError:
×
NEW
2688
            pass
×
2689
        return const_node, ""
4✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc