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

daisytuner / docc / 28690270008

04 Jul 2026 01:08AM UTC coverage: 62.47% (+0.3%) from 62.147%
28690270008

push

github

web-flow
Merge pull request #832 from daisytuner/python-gather-tests

activates numpy tests

88 of 98 new or added lines in 2 files covered. (89.8%)

25 existing lines in 2 files now uncovered.

39754 of 63637 relevant lines covered (62.47%)

977.78 hits per line

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

79.23
/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.memory import ManagedMemoryHandler
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
        memory_handler=None,
44
    ):
45
        self.builder = builder
4✔
46

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

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

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

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

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

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

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

97
    def visit_Add(self, node):
4✔
98
        return "+"
4✔
99

100
    def visit_Sub(self, node):
4✔
101
        return "-"
4✔
102

103
    def visit_Mult(self, node):
4✔
104
        return "*"
4✔
105

106
    def visit_Div(self, node):
4✔
107
        return "/"
4✔
108

109
    def visit_FloorDiv(self, node):
4✔
110
        return "//"
4✔
111

112
    def visit_Mod(self, node):
4✔
113
        return "%"
4✔
114

115
    def visit_Pow(self, node):
4✔
116
        return "**"
4✔
117

118
    def visit_Eq(self, node):
4✔
119
        return "=="
4✔
120

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

124
    def visit_Lt(self, node):
4✔
125
        return "<"
4✔
126

127
    def visit_LtE(self, node):
4✔
128
        return "<="
4✔
129

130
    def visit_Gt(self, node):
4✔
131
        return ">"
4✔
132

133
    def visit_GtE(self, node):
4✔
134
        return ">="
4✔
135

136
    def visit_And(self, node):
4✔
137
        return "&"
4✔
138

139
    def visit_Or(self, node):
4✔
140
        return "|"
4✔
141

142
    def visit_BitAnd(self, node):
4✔
143
        return "&"
4✔
144

145
    def visit_BitOr(self, node):
4✔
146
        return "|"
4✔
147

148
    def visit_BitXor(self, node):
4✔
149
        return "^"
4✔
150

151
    def visit_LShift(self, node):
4✔
152
        return "<<"
×
153

154
    def visit_RShift(self, node):
4✔
155
        return ">>"
×
156

157
    def visit_Not(self, node):
4✔
158
        return "!"
4✔
159

160
    def visit_USub(self, node):
4✔
161
        return "-"
4✔
162

163
    def visit_UAdd(self, node):
4✔
164
        return "+"
×
165

166
    def visit_Invert(self, node):
4✔
167
        return "~"
4✔
168

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

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

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

184
        self.container_table[tmp_name] = dtype
4✔
185
        return tmp_name
4✔
186

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

195
        op = self.visit(node.op)
4✔
196
        operand = self.visit(node.operand)
4✔
197

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

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

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

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

253
        return tmp_name
4✔
254

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

259
        left = self.visit(node.left)
4✔
260
        op = self.visit(node.op)
4✔
261
        right = self.visit(node.right)
4✔
262

263
        left_is_array = left in self.tensor_table
4✔
264
        right_is_array = right in self.tensor_table
4✔
265

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

275
        tmp_name = self.builder.find_new_name()
4✔
276

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

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

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

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

306
                real_left = left_cast
4✔
307

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

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

324
                real_right = right_cast
4✔
325

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

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

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

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

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

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

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

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

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

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

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

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

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

419
                return tmp_name
4✔
420

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

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

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

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

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

479
        return tmp_name
4✔
480

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

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

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

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

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

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

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

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

535
                        tmp_name = self.builder.find_new_name()
4✔
536

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

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

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

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

554
                        return tmp_name
4✔
555

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

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

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

566
        left_is_array = left in self.tensor_table
4✔
567
        right_is_array = right in self.tensor_table
4✔
568

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

574
        expr_str = f"{left} {op} {right}"
4✔
575

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

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

586
        self.container_table[tmp_name] = dtype
4✔
587
        return tmp_name
4✔
588

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

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

606
            if array_name in self.tensor_table:
4✔
607
                return self.tensor_table[array_name].shape[idx]
4✔
608

609
            return f"_{array_name}_shape_{idx}"
×
610

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

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

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

639
            if all_full_slices:
4✔
640
                return value_str
4✔
641

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

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

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

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

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

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

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

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

693
                return tmp_name
4✔
694

695
            return access_str
4✔
696

697
        slice_val = self.visit(node.slice)
×
698
        access_str = f"{value_str}({slice_val})"
×
699
        return access_str
×
700

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

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

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

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

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

731
    def visit_Assign(self, node):
4✔
732
        # Handle multiple targets: a = b = c or a, b = expr
733
        if len(node.targets) > 1:
4✔
734
            rhs_result = self.visit(node.value)
4✔
735
            if isinstance(rhs_result, str) and rhs_result in self.container_table:
4✔
736
                val_node = ast.Name(id=rhs_result, ctx=ast.Load())
4✔
737
                ast.copy_location(val_node, node)
4✔
738
            else:
739
                # Literals / expressions without a container: re-emit directly.
NEW
740
                val_node = node.value
×
741

742
            # Assign the evaluated value to each target
743
            for target in node.targets:
4✔
744
                assign = ast.Assign(targets=[target], value=val_node)
4✔
745
                ast.copy_location(assign, node)
4✔
746
                self.visit_Assign(assign)
4✔
747
            return
4✔
748
        target = node.targets[0]
4✔
749

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

765
        # Special cases, where rhs is not just a simple expression but requires special handling
766
        if self.numpy_visitor.is_gemm(node.value):
4✔
767
            if self.numpy_visitor.handle_gemm(target, node.value):
4✔
768
                return
4✔
769
            if self.numpy_visitor.handle_dot(target, node.value):
4✔
770
                return
×
771
        if self.numpy_visitor.is_outer(node.value):
4✔
772
            if self.numpy_visitor.handle_outer(target, node.value):
4✔
773
                return
4✔
774
        if self.numpy_visitor.is_transpose(node.value):
4✔
775
            if self.numpy_visitor.handle_transpose(target, node.value):
4✔
776
                return
4✔
777

778
        # Handle subscript assignments: a[i] = val or a[i, j] = val
779
        if isinstance(target, ast.Subscript):
4✔
780
            debug_info = get_debug_info(node, self.filename, self.function_name)
4✔
781

782
            target_name = self.visit(target.value)
4✔
783
            indices = []
4✔
784
            if isinstance(target.slice, ast.Tuple):
4✔
785
                indices = target.slice.elts
4✔
786
            else:
787
                indices = [target.slice]
4✔
788

789
            # Handle slice assignment separately
790
            has_slice = False
4✔
791
            for idx in indices:
4✔
792
                if isinstance(idx, ast.Slice):
4✔
793
                    has_slice = True
4✔
794
                    break
4✔
795

796
            if has_slice:
4✔
797
                self._handle_slice_assignment(
4✔
798
                    target, node.value, target_name, indices, debug_info
799
                )
800
                return
4✔
801

802
            # Handle boolean-mask assignment: target[mask] = value
803
            if (
4✔
804
                len(indices) == 1
805
                and target_name in self.tensor_table
806
                and self._is_boolean_mask_index(indices[0])
807
            ):
808
                self._handle_masked_assignment(
4✔
809
                    target, indices[0], node.value, target_name, node
810
                )
811
                return
4✔
812

813
            # Handle rhs and store in scalar tmp
814
            rhs_tmp = self.visit(node.value)
4✔
815

816
            # Evaluate the LHS (index) expression before creating the store
817
            # block/tasklet.
818
            lhs_expr = self.visit(target)
4✔
819

820
            block = self.builder.add_block(debug_info)
4✔
821
            t_task = self.builder.add_tasklet(
4✔
822
                block, TaskletCode.assign, ["_in"], ["_out"], debug_info
823
            )
824

825
            t_src, src_sub = self._add_read(block, rhs_tmp, debug_info)
4✔
826
            self.builder.add_memlet(
4✔
827
                block, t_src, "void", t_task, "_in", src_sub, None, debug_info
828
            )
829

830
            if "(" in lhs_expr and lhs_expr.endswith(")"):
4✔
831
                subset = lhs_expr[lhs_expr.find("(") + 1 : -1]
4✔
832
                tensor_dst = self.tensor_table[target_name]
4✔
833

834
                t_dst = self.builder.add_access(block, target_name, debug_info)
4✔
835
                self.builder.add_memlet(
4✔
836
                    block, t_task, "_out", t_dst, "void", subset, tensor_dst, debug_info
837
                )
838
            else:
839
                t_dst = self.builder.add_access(block, target_name, debug_info)
×
840
                self.builder.add_memlet(
×
841
                    block, t_task, "_out", t_dst, "void", "", None, debug_info
842
                )
843
            return
4✔
844

845
        # Fallback: lhs is a simple scalar assignments
846
        if not isinstance(target, ast.Name):
4✔
847
            raise NotImplementedError("Only assignment to variables supported")
×
848

849
        target_name = target.id
4✔
850
        rhs_tmp = self.visit(node.value)
4✔
851
        debug_info = get_debug_info(node, self.filename, self.function_name)
4✔
852

853
        if not self.builder.exists(target_name):
4✔
854
            if isinstance(node.value, ast.Constant):
4✔
855
                val = node.value.value
4✔
856
                if isinstance(val, int):
4✔
857
                    dtype = Scalar(PrimitiveType.Int64)
4✔
858
                elif isinstance(val, float):
4✔
859
                    dtype = Scalar(PrimitiveType.Double)
4✔
860
                elif isinstance(val, bool):
×
861
                    dtype = Scalar(PrimitiveType.Bool)
×
862
                else:
863
                    raise NotImplementedError(f"Cannot infer type for {val}")
×
864

865
                self.builder.add_container(target_name, dtype, False)
4✔
866
                self.container_table[target_name] = dtype
4✔
867
            else:
868
                self.builder.add_container(
4✔
869
                    target_name, self.container_table[rhs_tmp], False
870
                )
871
                self.container_table[target_name] = self.container_table[rhs_tmp]
4✔
872

873
        if rhs_tmp in self.tensor_table:
4✔
874
            self.tensor_table[target_name] = self.tensor_table[rhs_tmp]
4✔
875

876
        # Also copy shapes_runtime_info if available
877
        if rhs_tmp in self.shapes_runtime_info:
4✔
878
            self.shapes_runtime_info[target_name] = self.shapes_runtime_info[rhs_tmp]
4✔
879

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

899
            if src_type:
4✔
900
                t_src = self.builder.add_access(block, rhs_tmp, debug_info)
4✔
901
            else:
902
                t_src = self.builder.add_constant(block, rhs_tmp, dst_type, debug_info)
4✔
903

904
            self.builder.add_memlet(
4✔
905
                block, t_src, "void", t_task, "_in", "", None, debug_info
906
            )
907
            self.builder.add_memlet(
4✔
908
                block, t_task, "_out", t_dst, "void", "", None, debug_info
909
            )
910

911
    def visit_Expr(self, node):
4✔
912
        self.visit(node.value)
×
913

914
    def visit_If(self, node):
4✔
915
        cond = self.visit(node.test)
4✔
916
        debug_info = get_debug_info(node, self.filename, self.function_name)
4✔
917
        self.builder.begin_if(f"{cond} != false", debug_info)
4✔
918

919
        for stmt in node.body:
4✔
920
            self.visit(stmt)
4✔
921

922
        if node.orelse:
4✔
923
            self.builder.begin_else(debug_info)
4✔
924
            for stmt in node.orelse:
4✔
925
                self.visit(stmt)
4✔
926

927
        self.builder.end_if()
4✔
928

929
    def visit_While(self, node):
4✔
930
        if node.orelse:
4✔
931
            raise NotImplementedError("while-else is not supported")
×
932

933
        debug_info = get_debug_info(node, self.filename, self.function_name)
4✔
934
        self.builder.begin_while(debug_info)
4✔
935

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

939
        # Create if-break pattern: if condition is false, break
940
        self.builder.begin_if(f"{cond} == false", debug_info)
4✔
941
        self.builder.add_break(debug_info)
4✔
942
        self.builder.end_if()
4✔
943

944
        for stmt in node.body:
4✔
945
            self.visit(stmt)
4✔
946

947
        self.builder.end_while()
4✔
948

949
    def visit_Break(self, node):
4✔
950
        debug_info = get_debug_info(node, self.filename, self.function_name)
4✔
951
        self.builder.add_break(debug_info)
4✔
952

953
    def visit_Continue(self, node):
4✔
954
        debug_info = get_debug_info(node, self.filename, self.function_name)
4✔
955
        self.builder.add_continue(debug_info)
4✔
956

957
    def visit_For(self, node):
4✔
958
        if node.orelse:
4✔
959
            raise NotImplementedError("while-else is not supported")
×
960
        if not isinstance(node.target, ast.Name):
4✔
961
            raise NotImplementedError("Only simple for loops supported")
×
962

963
        var = node.target.id
4✔
964
        debug_info = get_debug_info(node, self.filename, self.function_name)
4✔
965

966
        # Check if iterating over a range() call
967
        if (
4✔
968
            isinstance(node.iter, ast.Call)
969
            and isinstance(node.iter.func, ast.Name)
970
            and node.iter.func.id == "range"
971
        ):
972
            args = node.iter.args
4✔
973
            if len(args) == 1:
4✔
974
                start = "0"
4✔
975
                end = self.visit(args[0])
4✔
976
                step = "1"
4✔
977
            elif len(args) == 2:
4✔
978
                start = self.visit(args[0])
4✔
979
                end = self.visit(args[1])
4✔
980
                step = "1"
4✔
981
            elif len(args) == 3:
4✔
982
                start = self.visit(args[0])
4✔
983
                end = self.visit(args[1])
4✔
984

985
                # Special handling for step to avoid creating tasklets for constants
986
                step_node = args[2]
4✔
987
                if isinstance(step_node, ast.Constant):
4✔
988
                    step = str(step_node.value)
4✔
989
                elif (
4✔
990
                    isinstance(step_node, ast.UnaryOp)
991
                    and isinstance(step_node.op, ast.USub)
992
                    and isinstance(step_node.operand, ast.Constant)
993
                ):
994
                    step = f"-{step_node.operand.value}"
4✔
995
                else:
996
                    step = self.visit(step_node)
×
997
            else:
998
                raise ValueError("Invalid range arguments")
×
999

1000
            if not self.builder.exists(var):
4✔
1001
                self.builder.add_container(var, Scalar(PrimitiveType.Int64), False)
4✔
1002
                self.container_table[var] = Scalar(PrimitiveType.Int64)
4✔
1003

1004
            self.builder.begin_for(var, start, end, step, debug_info)
4✔
1005

1006
            for stmt in node.body:
4✔
1007
                self.visit(stmt)
4✔
1008

1009
            self.builder.end_for()
4✔
1010
            return
4✔
1011

1012
        # Check if iterating over an ndarray (for x in array)
1013
        if isinstance(node.iter, ast.Name):
×
1014
            iter_name = node.iter.id
×
1015
            if iter_name in self.tensor_table:
×
1016
                arr_info = self.tensor_table[iter_name]
×
1017
                if len(arr_info.shape) == 0:
×
1018
                    raise NotImplementedError("Cannot iterate over 0-dimensional array")
×
1019

1020
                # Get the size of the first dimension
1021
                arr_size = arr_info.shape[0]
×
1022

1023
                # Create a hidden index variable for the loop
1024
                idx_var = self.builder.find_new_name()
×
1025
                if not self.builder.exists(idx_var):
×
1026
                    self.builder.add_container(
×
1027
                        idx_var, Scalar(PrimitiveType.Int64), False
1028
                    )
1029
                    self.container_table[idx_var] = Scalar(PrimitiveType.Int64)
×
1030

1031
                # Determine the type of the loop variable (element type)
1032
                # For a 1D array, it's a scalar; for ND array, it's a view of N-1 dimensions
1033
                if len(arr_info.shape) == 1:
×
1034
                    # Element is a scalar - get the element type from the array's type
1035
                    arr_type = self.container_table.get(iter_name)
×
1036
                    if isinstance(arr_type, Pointer):
×
1037
                        elem_type = arr_type.pointee_type
×
1038
                    else:
1039
                        elem_type = Scalar(PrimitiveType.Double)  # Default fallback
×
1040

1041
                    if not self.builder.exists(var):
×
1042
                        self.builder.add_container(var, elem_type, False)
×
1043
                        self.container_table[var] = elem_type
×
1044
                else:
1045
                    # For multi-dimensional arrays, create a view/slice
1046
                    # The loop variable becomes a pointer to the sub-array
1047
                    inner_shapes = arr_info.shape[1:]
×
1048
                    inner_ndim = len(arr_info.shape) - 1
×
1049

1050
                    arr_type = self.container_table.get(iter_name)
×
1051
                    if isinstance(arr_type, Pointer):
×
1052
                        elem_type = arr_type  # Keep as pointer type for views
×
1053
                    else:
1054
                        elem_type = Pointer(Scalar(PrimitiveType.Double))
×
1055

1056
                    if not self.builder.exists(var):
×
1057
                        self.builder.add_container(var, elem_type, False)
×
1058
                        self.container_table[var] = elem_type
×
1059

1060
                    # Register the view in tensor_table
1061
                    self.tensor_table[var] = Tensor(
×
1062
                        element_type_from_sdfg_type(elem_type), inner_shapes
1063
                    )
1064

1065
                # Begin the for loop
1066
                self.builder.begin_for(idx_var, "0", str(arr_size), "1", debug_info)
×
1067

1068
                # Generate the assignment: var = array[idx_var]
1069
                # Create an AST node for the assignment and visit it
1070
                assign_node = ast.Assign(
×
1071
                    targets=[ast.Name(id=var, ctx=ast.Store())],
1072
                    value=ast.Subscript(
1073
                        value=ast.Name(id=iter_name, ctx=ast.Load()),
1074
                        slice=ast.Name(id=idx_var, ctx=ast.Load()),
1075
                        ctx=ast.Load(),
1076
                    ),
1077
                )
1078
                ast.copy_location(assign_node, node)
×
1079
                self.visit_Assign(assign_node)
×
1080

1081
                # Visit the loop body
1082
                for stmt in node.body:
×
1083
                    self.visit(stmt)
×
1084

1085
                self.builder.end_for()
×
1086
                return
×
1087

1088
        raise NotImplementedError(
×
1089
            f"Only range() loops and iteration over ndarrays supported, got: {ast.dump(node.iter)}"
1090
        )
1091

1092
    def visit_Return(self, node):
4✔
1093
        if node.value is None:
4✔
1094
            debug_info = get_debug_info(node, self.filename, self.function_name)
×
1095
            # Emit frees for all deferred allocations before returning
1096
            if self.memory_handler.has_allocations():
×
1097
                self.memory_handler.emit_frees()
×
1098
            self.builder.add_return("", debug_info)
×
1099
            return
×
1100

1101
        if isinstance(node.value, ast.Tuple):
4✔
1102
            values = node.value.elts
4✔
1103
        else:
1104
            values = [node.value]
4✔
1105

1106
        parsed_values = [self.visit(v) for v in values]
4✔
1107
        debug_info = get_debug_info(node, self.filename, self.function_name)
4✔
1108

1109
        if self.infer_return_type:
4✔
1110
            for i, res in enumerate(parsed_values):
4✔
1111
                ret_name = f"_docc_ret_{i}"
4✔
1112
                if not self.builder.exists(ret_name):
4✔
1113
                    dtype = Scalar(PrimitiveType.Double)
4✔
1114
                    if res in self.container_table:
4✔
1115
                        dtype = self.container_table[res]
4✔
1116
                    elif isinstance(values[i], ast.Constant):
×
1117
                        val = values[i].value
×
1118
                        if isinstance(val, int):
×
1119
                            dtype = Scalar(PrimitiveType.Int64)
×
1120
                        elif isinstance(val, float):
×
1121
                            dtype = Scalar(PrimitiveType.Double)
×
1122
                        elif isinstance(val, bool):
×
1123
                            dtype = Scalar(PrimitiveType.Bool)
×
1124

1125
                    # Wrap Scalar in Pointer. Keep Arrays/Pointers as is.
1126
                    arg_type = dtype
4✔
1127
                    if isinstance(dtype, Scalar):
4✔
1128
                        arg_type = Pointer(dtype)
4✔
1129

1130
                    self.builder.add_container(ret_name, arg_type, is_argument=True)
4✔
1131
                    self.container_table[ret_name] = arg_type
4✔
1132

1133
                    if res in self.tensor_table:
4✔
1134
                        self.tensor_table[ret_name] = self.tensor_table[res]
4✔
1135

1136
            self.infer_return_type = False
4✔
1137

1138
        for i, res in enumerate(parsed_values):
4✔
1139
            ret_name = f"_docc_ret_{i}"
4✔
1140
            typ = self.container_table.get(ret_name)
4✔
1141

1142
            is_array_return = False
4✔
1143
            if res in self.tensor_table:
4✔
1144
                # Only treat as array return if it has dimensions
1145
                # 0-d arrays (scalars) should be handled by scalar assignment
1146
                if len(self.tensor_table[res].shape) > 0:
4✔
1147
                    is_array_return = True
4✔
1148
            elif res in self.container_table:
4✔
1149
                if isinstance(self.container_table[res], Pointer):
4✔
1150
                    is_array_return = True
×
1151

1152
            # Simple Scalar Assignment
1153
            if not is_array_return:
4✔
1154
                block = self.builder.add_block(debug_info)
4✔
1155
                t_dst = self.builder.add_access(block, ret_name, debug_info)
4✔
1156

1157
                t_src, src_sub = self._add_read(block, res, debug_info)
4✔
1158

1159
                t_task = self.builder.add_tasklet(
4✔
1160
                    block, TaskletCode.assign, ["_in"], ["_out"], debug_info
1161
                )
1162
                self.builder.add_memlet(
4✔
1163
                    block, t_src, "void", t_task, "_in", src_sub, None, debug_info
1164
                )
1165
                self.builder.add_memlet(
4✔
1166
                    block, t_task, "_out", t_dst, "void", "0", None, debug_info
1167
                )
1168

1169
            # Array Assignment (Copy)
1170
            else:
1171
                # Record shape for metadata
1172
                if res in self.tensor_table:
4✔
1173
                    # Prefer runtime shapes if available (for indirect access patterns)
1174
                    # Fall back to regular shapes otherwise
1175
                    res_info = self.tensor_table[res]
4✔
1176
                    if res in self.shapes_runtime_info:
4✔
1177
                        shape = self.shapes_runtime_info[res]
4✔
1178
                    else:
1179
                        shape = res_info.shape
4✔
1180
                    # Convert to string expressions
1181
                    self.captured_return_shapes[ret_name] = [str(s) for s in shape]
4✔
1182

1183
                    # Return arrays are always contiguous - compute fresh strides
1184
                    contiguous_strides = self.numpy_visitor._compute_strides(shape, "C")
4✔
1185
                    self.captured_return_strides[ret_name] = [
4✔
1186
                        str(s) for s in contiguous_strides
1187
                    ]
1188

1189
                    # Always overwrite tensor_table for return arrays with contiguous strides
1190
                    # (source tensor may have non-standard strides from views/flip)
1191
                    self.tensor_table[ret_name] = Tensor(
4✔
1192
                        res_info.element_type, shape, contiguous_strides
1193
                    )
1194

1195
                # Copy Logic using visit_Assign
1196
                ndim = 1
4✔
1197
                if ret_name in self.tensor_table:
4✔
1198
                    ndim = len(self.tensor_table[ret_name].shape)
4✔
1199

1200
                slice_node = ast.Slice(lower=None, upper=None, step=None)
4✔
1201
                if ndim > 1:
4✔
1202
                    target_slice = ast.Tuple(elts=[slice_node] * ndim, ctx=ast.Load())
4✔
1203
                else:
1204
                    target_slice = slice_node
4✔
1205

1206
                target_sub = ast.Subscript(
4✔
1207
                    value=ast.Name(id=ret_name, ctx=ast.Load()),
1208
                    slice=target_slice,
1209
                    ctx=ast.Store(),
1210
                )
1211

1212
                # Value node reconstruction
1213
                if isinstance(values[i], ast.Name):
4✔
1214
                    val_node = values[i]
4✔
1215
                else:
1216
                    val_node = ast.Name(id=res, ctx=ast.Load())
4✔
1217

1218
                assign_node = ast.Assign(targets=[target_sub], value=val_node)
4✔
1219
                self.visit_Assign(assign_node)
4✔
1220

1221
        # Emit frees for all deferred allocations before returning
1222
        if self.memory_handler.has_allocations():
4✔
1223
            self.memory_handler.emit_frees()
4✔
1224

1225
        # Add control flow return to exit the function/path
1226
        self.builder.add_return("", debug_info)
4✔
1227

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

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

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

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

1267
        if self.python_handler.has_handler(func_name):
4✔
1268
            return self.python_handler.handle_python_call(node, func_name)
4✔
1269

1270
        if func_name in self.globals_dict:
4✔
1271
            obj = self.globals_dict[func_name]
4✔
1272
            if inspect.isfunction(obj):
4✔
1273
                return self._handle_inline_call(node, obj)
4✔
1274

1275
        raise NotImplementedError(f"Function call {func_name} not supported")
×
1276

1277
    def _handle_inline_call(self, node, func_obj):
4✔
1278
        try:
4✔
1279
            source_lines, start_line = inspect.getsourcelines(func_obj)
4✔
1280
            source = textwrap.dedent("".join(source_lines))
4✔
1281
            tree = ast.parse(source)
4✔
1282
            func_def = tree.body[0]
4✔
1283
        except Exception as e:
×
1284
            raise NotImplementedError(
×
1285
                f"Could not parse function {func_obj.__name__}: {e}"
1286
            )
1287

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

1290
        if len(arg_vars) != len(func_def.args.args):
4✔
1291
            raise NotImplementedError(
×
1292
                f"Argument count mismatch for {func_obj.__name__}"
1293
            )
1294

1295
        suffix = f"_{func_obj.__name__}_{self._get_unique_id()}"
4✔
1296
        res_name = f"_res{suffix}"
4✔
1297

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

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

1337
            def __init__(self, suffix, globals_dict):
4✔
1338
                self.suffix = suffix
4✔
1339
                self.globals_dict = globals_dict
4✔
1340

1341
            def visit_Name(self, node):
4✔
1342
                if node.id in self.globals_dict or node.id in self.BUILTINS:
4✔
1343
                    return node
4✔
1344
                return ast.Name(id=f"{node.id}{self.suffix}", ctx=node.ctx)
4✔
1345

1346
            def visit_Return(self, node):
4✔
1347
                if node.value:
4✔
1348
                    val = self.visit(node.value)
4✔
1349
                    return ast.Assign(
4✔
1350
                        targets=[ast.Name(id=res_name, ctx=ast.Store())],
1351
                        value=val,
1352
                    )
1353
                return node
×
1354

1355
        renamer = VariableRenamer(suffix, combined_globals)
4✔
1356
        new_body = [renamer.visit(stmt) for stmt in func_def.body]
4✔
1357

1358
        param_assignments = []
4✔
1359

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

1375
        for arg_def, arg_val in zip(func_def.args.args, arg_vars):
4✔
1376
            param_name = f"{arg_def.arg}{suffix}"
4✔
1377

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

1401
            assign = ast.Assign(
4✔
1402
                targets=[ast.Name(id=param_name, ctx=ast.Store())], value=val_node
1403
            )
1404
            param_assignments.append(assign)
4✔
1405

1406
        final_body = param_assignments + new_body
4✔
1407

1408
        # Create a new parser instance for the inlined function
1409
        # Share memory_handler so hoisted allocations go to main function entry
1410
        parser = ASTParser(
4✔
1411
            self.builder,
1412
            self.tensor_table,
1413
            self.container_table,
1414
            globals_dict=combined_globals,
1415
            unique_counter_ref=self._unique_counter_ref,
1416
            memory_handler=self.memory_handler,
1417
        )
1418

1419
        for stmt in final_body:
4✔
1420
            parser.visit(stmt)
4✔
1421

1422
        return res_name
4✔
1423

1424
    def _add_assign_constant(self, target_name, value_str, dtype):
4✔
1425
        block = self.builder.add_block()
4✔
1426
        t_const = self.builder.add_constant(block, value_str, dtype)
4✔
1427
        t_dst = self.builder.add_access(block, target_name)
4✔
1428
        t_task = self.builder.add_tasklet(block, TaskletCode.assign, ["_in"], ["_out"])
4✔
1429
        self.builder.add_memlet(block, t_const, "void", t_task, "_in", "")
4✔
1430
        self.builder.add_memlet(block, t_task, "_out", t_dst, "void", "")
4✔
1431

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

1435
        Uses a zero-copy view when possible (positive step, no indirect access).
1436
        Falls back to copy-based approach for complex cases.
1437
        """
1438
        if not self.builder:
4✔
1439
            raise ValueError("Builder required for expression slicing")
×
1440

1441
        # Try view-based approach first (zero-copy)
1442
        if self._can_use_slice_view(indices_nodes):
4✔
1443
            return self._create_slice_view(value_str, indices_nodes, shapes, ndim)
4✔
1444

1445
        # Fall back to copy-based approach for complex cases
1446
        return self._handle_expression_slicing_copy(
4✔
1447
            node, value_str, indices_nodes, shapes, ndim
1448
        )
1449

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

1453
        Views can be used when:
1454
        - All steps are non-zero constants (positive or negative)
1455
        - No indirect array access in slice parameters
1456

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

1474
                # Check for indirect access in slice bounds
1475
                if idx.lower is not None and self._contains_indirect_access(idx.lower):
4✔
1476
                    return False
4✔
1477
                if idx.upper is not None and self._contains_indirect_access(idx.upper):
4✔
1478
                    return False
×
1479
            else:
1480
                # Fixed index: check for indirect access
1481
                if self._contains_indirect_access(idx):
4✔
1482
                    return False
×
1483
        return True
4✔
1484

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

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

1491
        For positive step A[start:stop:step, ...] on dimension i:
1492
        - new_shape[i] = ceil((stop - start) / step)
1493
        - new_stride[i] = old_stride[i] * step
1494
        - offset contribution = start * old_stride[i]
1495

1496
        For negative step A[start:stop:step, ...] (e.g., ::-1):
1497
        - Default start = shape - 1 (last element)
1498
        - Default stop = -1 (before first element)
1499
        - new_shape[i] = ceil((start - stop) / abs(step))
1500
        - new_stride[i] = old_stride[i] * step (negative)
1501
        - offset contribution = start * old_stride[i] (points to last element)
1502

1503
        For a fixed index A[k, ...] on dimension i (dimension reduction):
1504
        - offset contribution = k * old_stride[i]
1505
        - dimension is removed from output
1506
        """
1507
        in_tensor = self.tensor_table[value_str]
4✔
1508
        in_shape = in_tensor.shape
4✔
1509
        dtype = in_tensor.element_type
4✔
1510

1511
        # Get input strides (compute if not available)
1512
        in_strides = (
4✔
1513
            in_tensor.strides
1514
            if hasattr(in_tensor, "strides") and in_tensor.strides
1515
            else None
1516
        )
1517
        if in_strides is None:
4✔
1518
            in_strides = self.numpy_visitor._compute_strides(in_shape, "C")
×
1519

1520
        # Get base offset from input tensor
1521
        in_offset = getattr(in_tensor, "offset", "0") or "0"
4✔
1522

1523
        # Build output shape, strides, and compute offset
1524
        out_shape = []
4✔
1525
        out_strides = []
4✔
1526
        offset_terms = []
4✔
1527
        if in_offset != "0":
4✔
1528
            offset_terms.append(str(in_offset))
4✔
1529

1530
        for i, idx in enumerate(indices_nodes):
4✔
1531
            shape_val = shapes[i] if i < len(shapes) else f"_{value_str}_shape_{i}"
4✔
1532
            stride_val = in_strides[i] if i < len(in_strides) else "1"
4✔
1533

1534
            if isinstance(idx, ast.Slice):
4✔
1535
                # Determine step value and sign
1536
                step_str = "1"
4✔
1537
                step_is_negative = False
4✔
1538
                step_value = 1
4✔
1539

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

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

1570
                    if idx.upper is not None:
4✔
1571
                        stop_str = self.visit(idx.upper)
×
1572
                        if isinstance(stop_str, str) and (
×
1573
                            stop_str.startswith("-") or stop_str.startswith("(-")
1574
                        ):
1575
                            stop_str = f"({shape_val} + {stop_str})"
×
1576
                    else:
1577
                        stop_str = "-1"
4✔
1578

1579
                    # Shape for negative step: ceil((start - stop) / abs(step))
1580
                    abs_step = abs(step_value)
4✔
1581
                    if abs_step == 1:
4✔
1582
                        dim_size = f"({start_str} - {stop_str})"
4✔
1583
                    else:
1584
                        dim_size = f"(({start_str} - {stop_str} + {abs_step} - 1) / {abs_step})"
4✔
1585
                    out_shape.append(dim_size)
4✔
1586

1587
                    # Stride for negative step: old_stride * step (negative)
1588
                    out_strides.append(f"({stride_val} * {step_str})")
4✔
1589

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

1602
                    stop_str = str(shape_val)
4✔
1603
                    if idx.upper is not None:
4✔
1604
                        stop_str = self.visit(idx.upper)
4✔
1605
                        if isinstance(stop_str, str) and (
4✔
1606
                            stop_str.startswith("-") or stop_str.startswith("(-")
1607
                        ):
1608
                            stop_str = f"({shape_val} + {stop_str})"
4✔
1609

1610
                    # Compute new shape: ceil((stop - start) / step)
1611
                    if step_str == "1":
4✔
1612
                        dim_size = f"({stop_str} - {start_str})"
4✔
1613
                    else:
1614
                        dim_size = f"idiv({stop_str} - {start_str} + {step_str} - 1, {step_str})"
4✔
1615
                    out_shape.append(dim_size)
4✔
1616

1617
                    # Compute new stride: old_stride * step
1618
                    if step_str == "1":
4✔
1619
                        out_strides.append(stride_val)
4✔
1620
                    else:
1621
                        out_strides.append(f"({stride_val} * {step_str})")
4✔
1622

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

1635
        # Combine offset terms
1636
        if not offset_terms:
4✔
1637
            out_offset = "0"
4✔
1638
        elif len(offset_terms) == 1:
4✔
1639
            out_offset = offset_terms[0]
4✔
1640
        else:
1641
            out_offset = " + ".join(offset_terms)
4✔
1642

1643
        # Create new pointer container
1644
        tmp_name = self.builder.find_new_name("_slice_view_")
4✔
1645
        ptr_type = Pointer(dtype)
4✔
1646
        self.builder.add_container(tmp_name, ptr_type, False)
4✔
1647
        self.container_table[tmp_name] = ptr_type
4✔
1648

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

1660
        # Create reference memlet (offset is handled by tensor's offset property)
1661
        debug_info = DebugInfo()
4✔
1662
        block = self.builder.add_block(debug_info)
4✔
1663
        t_src = self.builder.add_access(block, value_str, debug_info)
4✔
1664
        t_dst = self.builder.add_access(block, tmp_name, debug_info)
4✔
1665
        self.builder.add_reference_memlet(block, t_src, t_dst, "0", ptr_type)
4✔
1666

1667
        return tmp_name
4✔
1668

1669
    def _handle_expression_slicing_copy(
4✔
1670
        self, node, value_str, indices_nodes, shapes, ndim
1671
    ):
1672
        """Copy-based slicing for cases that cannot use views.
1673

1674
        This allocates a new array and copies elements using nested loops.
1675
        Used for negative steps or indirect access patterns.
1676
        """
1677
        dtype = Scalar(PrimitiveType.Double)
4✔
1678
        if value_str in self.container_table:
4✔
1679
            t = self.container_table[value_str]
4✔
1680
            if isinstance(t, Pointer) and t.has_pointee_type():
4✔
1681
                dtype = t.pointee_type
4✔
1682

1683
        result_shapes = []
4✔
1684
        result_shapes_runtime = []
4✔
1685
        slice_info = []
4✔
1686
        index_info = []
4✔
1687

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

1691
            if isinstance(idx, ast.Slice):
4✔
1692
                start_str = "0"
4✔
1693
                start_str_runtime = "0"
4✔
1694
                if idx.lower is not None:
4✔
1695
                    if self._contains_indirect_access(idx.lower):
4✔
1696
                        start_str, start_str_runtime = (
4✔
1697
                            self._materialize_indirect_access(
1698
                                idx.lower, return_original_expr=True
1699
                            )
1700
                        )
1701
                    else:
1702
                        start_str = self.visit(idx.lower)
×
1703
                        start_str_runtime = start_str
×
1704
                    if isinstance(start_str, str) and (
4✔
1705
                        start_str.startswith("-") or start_str.startswith("(-")
1706
                    ):
1707
                        start_str = f"({shape_val} + {start_str})"
×
1708
                        start_str_runtime = f"({shape_val} + {start_str_runtime})"
×
1709

1710
                stop_str = str(shape_val)
4✔
1711
                stop_str_runtime = str(shape_val)
4✔
1712
                if idx.upper is not None:
4✔
1713
                    if self._contains_indirect_access(idx.upper):
4✔
1714
                        stop_str, stop_str_runtime = self._materialize_indirect_access(
4✔
1715
                            idx.upper, return_original_expr=True
1716
                        )
1717
                    else:
1718
                        stop_str = self.visit(idx.upper)
×
1719
                        stop_str_runtime = stop_str
×
1720
                    if isinstance(stop_str, str) and (
4✔
1721
                        stop_str.startswith("-") or stop_str.startswith("(-")
1722
                    ):
1723
                        stop_str = f"({shape_val} + {stop_str})"
×
1724
                        stop_str_runtime = f"({shape_val} + {stop_str_runtime})"
×
1725

1726
                step_str = "1"
4✔
1727
                if idx.step is not None:
4✔
1728
                    step_str = self.visit(idx.step)
×
1729

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

1754
        tmp_name = self.builder.find_new_name("_slice_tmp_")
4✔
1755
        result_ndim = len(result_shapes)
4✔
1756

1757
        if result_ndim == 0:
4✔
1758
            self.builder.add_container(tmp_name, dtype, False)
×
1759
            self.container_table[tmp_name] = dtype
×
1760
        else:
1761
            size_str = "1"
4✔
1762
            for dim in result_shapes:
4✔
1763
                size_str = f"({size_str} * {dim})"
4✔
1764

1765
            element_size = self.builder.get_sizeof(dtype)
4✔
1766
            total_size = f"({size_str} * {element_size})"
4✔
1767

1768
            ptr_type = Pointer(dtype)
4✔
1769
            self.builder.add_container(tmp_name, ptr_type, False)
4✔
1770
            self.container_table[tmp_name] = ptr_type
4✔
1771
            tensor_info = Tensor(dtype, result_shapes)
4✔
1772
            self.shapes_runtime_info[tmp_name] = (
4✔
1773
                result_shapes_runtime  # Store runtime shapes separately
1774
            )
1775
            self.tensor_table[tmp_name] = tensor_info
4✔
1776

1777
            debug_info = DebugInfo()
4✔
1778
            block_alloc = self.builder.add_block(debug_info)
4✔
1779
            t_malloc = self.builder.add_malloc(block_alloc, total_size)
4✔
1780
            t_ptr = self.builder.add_access(block_alloc, tmp_name, debug_info)
4✔
1781
            self.builder.add_memlet(
4✔
1782
                block_alloc, t_malloc, "_ret", t_ptr, "void", "", ptr_type, debug_info
1783
            )
1784

1785
        loop_vars = []
4✔
1786
        debug_info = DebugInfo()
4✔
1787

1788
        for dim_idx, (orig_dim, start_str, stop_str, step_str) in enumerate(slice_info):
4✔
1789
            loop_var = self.builder.find_new_name(f"_slice_loop_{dim_idx}_")
4✔
1790
            loop_vars.append((loop_var, orig_dim, start_str, step_str))
4✔
1791

1792
            if not self.builder.exists(loop_var):
4✔
1793
                self.builder.add_container(loop_var, Scalar(PrimitiveType.Int64), False)
4✔
1794
                self.container_table[loop_var] = Scalar(PrimitiveType.Int64)
4✔
1795

1796
            # Account for step in loop count: ceil((stop - start) / step)
1797
            if step_str == "1":
4✔
1798
                count_str = f"({stop_str} - {start_str})"
4✔
1799
            else:
1800
                count_str = (
×
1801
                    f"idiv({stop_str} - {start_str} + {step_str} - 1, {step_str})"
1802
                )
1803
            self.builder.begin_for(loop_var, "0", count_str, "1", debug_info)
4✔
1804

1805
        src_indices = [""] * ndim
4✔
1806
        dst_indices = []
4✔
1807

1808
        for orig_dim, index_str in index_info:
4✔
1809
            src_indices[orig_dim] = index_str
×
1810

1811
        for loop_var, orig_dim, start_str, step_str in loop_vars:
4✔
1812
            if step_str == "1":
4✔
1813
                src_indices[orig_dim] = f"({start_str} + {loop_var})"
4✔
1814
            else:
1815
                src_indices[orig_dim] = f"({start_str} + {loop_var} * {step_str})"
×
1816
            dst_indices.append(loop_var)
4✔
1817

1818
        src_linear = self._compute_linear_index(src_indices, shapes, value_str, ndim)
4✔
1819
        if result_ndim > 0:
4✔
1820
            dst_linear = self._compute_linear_index(
4✔
1821
                dst_indices, result_shapes, tmp_name, result_ndim
1822
            )
1823
        else:
1824
            dst_linear = "0"
×
1825

1826
        block = self.builder.add_block(debug_info)
4✔
1827
        t_src = self.builder.add_access(block, value_str, debug_info)
4✔
1828
        t_dst = self.builder.add_access(block, tmp_name, debug_info)
4✔
1829
        t_task = self.builder.add_tasklet(
4✔
1830
            block, TaskletCode.assign, ["_in"], ["_out"], debug_info
1831
        )
1832

1833
        self.builder.add_memlet(
4✔
1834
            block, t_src, "void", t_task, "_in", src_linear, None, debug_info
1835
        )
1836
        self.builder.add_memlet(
4✔
1837
            block, t_task, "_out", t_dst, "void", dst_linear, None, debug_info
1838
        )
1839

1840
        for _ in loop_vars:
4✔
1841
            self.builder.end_for()
4✔
1842

1843
        return tmp_name
4✔
1844

1845
    def _compute_linear_index(self, indices, shapes, array_name, ndim):
4✔
1846
        """Compute linear index from multi-dimensional indices."""
1847
        if ndim == 0:
4✔
1848
            return "0"
×
1849

1850
        linear_index = ""
4✔
1851
        for i in range(ndim):
4✔
1852
            term = str(indices[i])
4✔
1853
            for j in range(i + 1, ndim):
4✔
1854
                shape_val = shapes[j] if j < len(shapes) else f"_{array_name}_shape_{j}"
×
1855
                term = f"(({term}) * {shape_val})"
×
1856

1857
            if i == 0:
4✔
1858
                linear_index = term
4✔
1859
            else:
1860
                linear_index = f"({linear_index} + {term})"
×
1861

1862
        return linear_index
4✔
1863

1864
    def _is_array_index(self, node):
4✔
1865
        """Check if a node represents an array that could be used as an index (gather)."""
1866
        if isinstance(node, ast.Name):
4✔
1867
            return node.id in self.tensor_table
4✔
1868
        return False
4✔
1869

1870
    def _is_boolean_mask_index(self, node):
4✔
1871
        """Check if a subscript index is a boolean mask (e.g. arr[arr <= 0.1])."""
1872
        # A boolean array variable used directly as a mask.
1873
        if isinstance(node, ast.Name) and node.id in self.tensor_table:
4✔
1874
            element_type = self.tensor_table[node.id].element_type
4✔
1875
            return element_type.primitive_type == PrimitiveType.Bool
4✔
1876
        # A whole-array comparison (e.g. arr <= 0.1, x > y) yields a boolean mask.
1877
        if isinstance(node, ast.Compare):
4✔
1878
            operands = [node.left] + list(node.comparators)
4✔
1879
            return any(
4✔
1880
                isinstance(o, ast.Name) and o.id in self.tensor_table for o in operands
1881
            )
1882
        return False
4✔
1883

1884
    def _handle_masked_assignment(
4✔
1885
        self, target, mask_node, value_node, target_name, orig_node
1886
    ):
1887
        """Handle boolean-mask assignment: target[mask] = value.
1888

1889
        Rewritten as ``target[:] = np.where(mask, value, target)`` which has
1890
        identical NumPy semantics (elements where the mask is False keep their
1891
        original value) and reuses the existing np.where lowering.
1892
        """
1893
        ndim = len(self.tensor_table[target_name].shape)
4✔
1894

1895
        full_slice = ast.Slice(lower=None, upper=None, step=None)
4✔
1896
        if ndim > 1:
4✔
1897
            slice_arg = ast.Tuple(
4✔
1898
                elts=[
1899
                    ast.Slice(lower=None, upper=None, step=None) for _ in range(ndim)
1900
                ],
1901
                ctx=ast.Load(),
1902
            )
1903
        else:
1904
            slice_arg = full_slice
4✔
1905

1906
        new_target = ast.Subscript(
4✔
1907
            value=copy.deepcopy(target.value), slice=slice_arg, ctx=ast.Store()
1908
        )
1909

1910
        where_call = ast.Call(
4✔
1911
            func=ast.Attribute(
1912
                value=ast.Name(id="np", ctx=ast.Load()), attr="where", ctx=ast.Load()
1913
            ),
1914
            args=[
1915
                copy.deepcopy(mask_node),
1916
                copy.deepcopy(value_node),
1917
                ast.Name(id=target_name, ctx=ast.Load()),
1918
            ],
1919
            keywords=[],
1920
        )
1921

1922
        new_assign = ast.Assign(targets=[new_target], value=where_call)
4✔
1923
        ast.copy_location(new_assign, orig_node)
4✔
1924
        ast.fix_missing_locations(new_assign)
4✔
1925
        self.visit_Assign(new_assign)
4✔
1926

1927
    def _handle_gather(self, value_str, index_node, debug_info=None):
4✔
1928
        """Handle gather operation: x[indices] where indices is an array."""
1929
        if debug_info is None:
4✔
1930
            debug_info = DebugInfo()
4✔
1931

1932
        if isinstance(index_node, ast.Name):
4✔
1933
            idx_array_name = index_node.id
4✔
1934
        else:
1935
            idx_array_name = self.visit(index_node)
×
1936

1937
        if idx_array_name not in self.tensor_table:
4✔
1938
            raise ValueError(f"Gather index must be an array, got {idx_array_name}")
×
1939

1940
        idx_shapes = self.tensor_table[idx_array_name].shape
4✔
1941
        idx_ndim = len(idx_shapes)
4✔
1942

1943
        if idx_ndim != 1:
4✔
1944
            raise NotImplementedError("Only 1D index arrays supported for gather")
×
1945

1946
        result_shape = idx_shapes[0] if idx_shapes else f"_{idx_array_name}_shape_0"
4✔
1947

1948
        # For runtime evaluation, prefer shapes_runtime_info if available
1949
        # This ensures we use expressions that can be evaluated at runtime
1950
        if idx_array_name in self.shapes_runtime_info:
4✔
1951
            runtime_shapes = self.shapes_runtime_info[idx_array_name]
4✔
1952
            result_shape_runtime = runtime_shapes[0] if runtime_shapes else result_shape
4✔
1953
        else:
1954
            result_shape_runtime = result_shape
×
1955

1956
        dtype = Scalar(PrimitiveType.Double)
4✔
1957
        if value_str in self.container_table:
4✔
1958
            t = self.container_table[value_str]
4✔
1959
            if isinstance(t, Pointer) and t.has_pointee_type():
4✔
1960
                dtype = t.pointee_type
4✔
1961

1962
        idx_dtype = Scalar(PrimitiveType.Int64)
4✔
1963
        if idx_array_name in self.container_table:
4✔
1964
            t = self.container_table[idx_array_name]
4✔
1965
            if isinstance(t, Pointer) and t.has_pointee_type():
4✔
1966
                idx_dtype = t.pointee_type
4✔
1967

1968
        tmp_name = self.builder.find_new_name("_gather_")
4✔
1969

1970
        element_size = self.builder.get_sizeof(dtype)
4✔
1971
        total_size = f"({result_shape} * {element_size})"
4✔
1972

1973
        ptr_type = Pointer(dtype)
4✔
1974
        self.builder.add_container(tmp_name, ptr_type, False)
4✔
1975
        self.container_table[tmp_name] = ptr_type
4✔
1976
        self.tensor_table[tmp_name] = Tensor(dtype, [result_shape])
4✔
1977
        # Store runtime evaluable shape for this gather result
1978
        self.shapes_runtime_info[tmp_name] = [result_shape_runtime]
4✔
1979

1980
        block_alloc = self.builder.add_block(debug_info)
4✔
1981
        t_malloc = self.builder.add_malloc(block_alloc, total_size)
4✔
1982
        t_ptr = self.builder.add_access(block_alloc, tmp_name, debug_info)
4✔
1983
        self.builder.add_memlet(
4✔
1984
            block_alloc, t_malloc, "_ret", t_ptr, "void", "", ptr_type, debug_info
1985
        )
1986

1987
        loop_var = self.builder.find_new_name("_gather_i_")
4✔
1988
        self.builder.add_container(loop_var, Scalar(PrimitiveType.Int64), False)
4✔
1989
        self.container_table[loop_var] = Scalar(PrimitiveType.Int64)
4✔
1990

1991
        idx_var = self.builder.find_new_name("_gather_idx_")
4✔
1992
        self.builder.add_container(idx_var, idx_dtype, False)
4✔
1993
        self.container_table[idx_var] = idx_dtype
4✔
1994

1995
        self.builder.begin_for(loop_var, "0", str(result_shape), "1", debug_info)
4✔
1996

1997
        block_load_idx = self.builder.add_block(debug_info)
4✔
1998
        idx_arr_access = self.builder.add_access(
4✔
1999
            block_load_idx, idx_array_name, debug_info
2000
        )
2001
        idx_var_access = self.builder.add_access(block_load_idx, idx_var, debug_info)
4✔
2002
        tasklet_load = self.builder.add_tasklet(
4✔
2003
            block_load_idx, TaskletCode.assign, ["_in"], ["_out"], debug_info
2004
        )
2005
        self.builder.add_memlet(
4✔
2006
            block_load_idx,
2007
            idx_arr_access,
2008
            "void",
2009
            tasklet_load,
2010
            "_in",
2011
            loop_var,
2012
            None,
2013
            debug_info,
2014
        )
2015
        self.builder.add_memlet(
4✔
2016
            block_load_idx,
2017
            tasklet_load,
2018
            "_out",
2019
            idx_var_access,
2020
            "void",
2021
            "",
2022
            None,
2023
            debug_info,
2024
        )
2025

2026
        block_gather = self.builder.add_block(debug_info)
4✔
2027
        src_access = self.builder.add_access(block_gather, value_str, debug_info)
4✔
2028
        dst_access = self.builder.add_access(block_gather, tmp_name, debug_info)
4✔
2029
        tasklet_gather = self.builder.add_tasklet(
4✔
2030
            block_gather, TaskletCode.assign, ["_in"], ["_out"], debug_info
2031
        )
2032

2033
        self.builder.add_memlet(
4✔
2034
            block_gather,
2035
            src_access,
2036
            "void",
2037
            tasklet_gather,
2038
            "_in",
2039
            idx_var,
2040
            None,
2041
            debug_info,
2042
        )
2043
        self.builder.add_memlet(
4✔
2044
            block_gather,
2045
            tasklet_gather,
2046
            "_out",
2047
            dst_access,
2048
            "void",
2049
            loop_var,
2050
            None,
2051
            debug_info,
2052
        )
2053

2054
        self.builder.end_for()
4✔
2055

2056
        return tmp_name
4✔
2057

2058
    def _get_max_array_ndim_in_expr(self, node):
4✔
2059
        """Get the maximum array dimensionality in an expression."""
2060
        max_ndim = 0
4✔
2061

2062
        class NdimVisitor(ast.NodeVisitor):
4✔
2063
            def __init__(self, tensor_table):
4✔
2064
                self.tensor_table = tensor_table
4✔
2065
                self.max_ndim = 0
4✔
2066

2067
            def visit_Name(self, node):
4✔
2068
                if node.id in self.tensor_table:
4✔
2069
                    ndim = len(self.tensor_table[node.id].shape)
4✔
2070
                    self.max_ndim = max(self.max_ndim, ndim)
4✔
2071
                return self.generic_visit(node)
4✔
2072

2073
        visitor = NdimVisitor(self.tensor_table)
4✔
2074
        visitor.visit(node)
4✔
2075
        return visitor.max_ndim
4✔
2076

2077
    def _handle_broadcast_slice_assignment(
4✔
2078
        self,
2079
        target,
2080
        materialized_rhs,
2081
        target_name,
2082
        indices,
2083
        target_ndim,
2084
        value_ndim,
2085
        debug_info,
2086
    ):
2087
        """Handle slice assignment with broadcasting (e.g., 2D[:,:] = 1D[:]).
2088

2089
        materialized_rhs is the already-evaluated RHS array name (not AST node).
2090
        """
2091
        broadcast_dims = target_ndim - value_ndim
×
2092
        shapes = self.tensor_table[target_name].shape
×
2093
        rhs_tensor = self.tensor_table.get(materialized_rhs)
×
2094
        rhs_shapes = rhs_tensor.shape if rhs_tensor else []
×
2095

2096
        # Create outer loops for broadcast dimensions
2097
        outer_loop_vars = []
×
2098
        for i in range(broadcast_dims):
×
2099
            loop_var = self.builder.find_new_name(f"_bcast_iter_{i}_")
×
2100
            outer_loop_vars.append(loop_var)
×
2101

2102
            if not self.builder.exists(loop_var):
×
2103
                self.builder.add_container(loop_var, Scalar(PrimitiveType.Int64), False)
×
2104
                self.container_table[loop_var] = Scalar(PrimitiveType.Int64)
×
2105

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

2109
        # Create inner loops for value dimensions
2110
        inner_loop_vars = []
×
2111
        for i in range(value_ndim):
×
2112
            loop_var = self.builder.find_new_name(f"_inner_iter_{i}_")
×
2113
            inner_loop_vars.append(loop_var)
×
2114

2115
            if not self.builder.exists(loop_var):
×
2116
                self.builder.add_container(loop_var, Scalar(PrimitiveType.Int64), False)
×
2117
                self.container_table[loop_var] = Scalar(PrimitiveType.Int64)
×
2118

2119
            # Use RHS shape for inner dimension bounds
2120
            dim_size = (
×
2121
                rhs_shapes[i] if i < len(rhs_shapes) else shapes[broadcast_dims + i]
2122
            )
2123
            self.builder.begin_for(loop_var, "0", str(dim_size), "1", debug_info)
×
2124

2125
        # Create assignment block: target[outer_vars, inner_vars] = rhs[inner_vars]
2126
        block = self.builder.add_block(debug_info)
×
2127
        t_src = self.builder.add_access(block, materialized_rhs, debug_info)
×
2128
        t_dst = self.builder.add_access(block, target_name, debug_info)
×
2129
        t_task = self.builder.add_tasklet(
×
2130
            block, TaskletCode.assign, ["_in"], ["_out"], debug_info
2131
        )
2132

2133
        # Source index: just inner loop vars
2134
        src_index = ",".join(inner_loop_vars) if inner_loop_vars else "0"
×
2135

2136
        # Target index: outer_vars + inner_vars combined
2137
        all_target_vars = outer_loop_vars + inner_loop_vars
×
2138
        target_index = ",".join(all_target_vars) if all_target_vars else "0"
×
2139

2140
        self.builder.add_memlet(
×
2141
            block, t_src, "void", t_task, "_in", src_index, rhs_tensor, debug_info
2142
        )
2143

2144
        tensor_dst = self.tensor_table[target_name]
×
2145
        self.builder.add_memlet(
×
2146
            block, t_task, "_out", t_dst, "void", target_index, tensor_dst, debug_info
2147
        )
2148

2149
        # Close all loops (inner first, then outer)
2150
        for _ in inner_loop_vars:
×
2151
            self.builder.end_for()
×
2152
        for _ in outer_loop_vars:
×
2153
            self.builder.end_for()
×
2154

2155
    def _handle_slice_assignment(
4✔
2156
        self, target, value, target_name, indices, debug_info=None
2157
    ):
2158
        if debug_info is None:
4✔
2159
            debug_info = DebugInfo()
×
2160

2161
        # Add missing dimensions
2162
        tensor_info = self.tensor_table[target_name]
4✔
2163
        ndim = len(tensor_info.shape)
4✔
2164
        if len(indices) < ndim:
4✔
2165
            indices = list(indices)
4✔
2166
            for _ in range(ndim - len(indices)):
4✔
2167
                indices.append(ast.Slice(lower=None, upper=None, step=None))
4✔
2168

2169
        # Handle ufunc outer case separately to preserve slice shape info
2170
        has_outer, ufunc_name, outer_node = contains_ufunc_outer(value)
4✔
2171
        if has_outer:
4✔
2172
            self._handle_ufunc_outer_slice_assignment(
4✔
2173
                target, value, target_name, indices, debug_info
2174
            )
2175
            return
4✔
2176

2177
        # Count slice dimensions to determine effective target dimensionality
2178
        target_slice_ndim = sum(1 for idx in indices if isinstance(idx, ast.Slice))
4✔
2179
        value_max_ndim = self._get_max_array_ndim_in_expr(value)
4✔
2180

2181
        # ALWAYS evaluate RHS first (NumPy semantics) - before any loops
2182
        materialized_rhs = self.visit(value)
4✔
2183

2184
        if (
4✔
2185
            target_slice_ndim > 0
2186
            and value_max_ndim > 0
2187
            and target_slice_ndim > value_max_ndim
2188
        ):
2189
            # Broadcasting case: use row-by-row approach with reference memlets
2190
            self._handle_broadcast_slice_assignment(
×
2191
                target,
2192
                materialized_rhs,
2193
                target_name,
2194
                indices,
2195
                target_slice_ndim,
2196
                value_max_ndim,
2197
                debug_info,
2198
            )
2199
            return
×
2200

2201
        loop_vars = []
4✔
2202
        new_target_indices = []
4✔
2203

2204
        for i, idx in enumerate(indices):
4✔
2205
            if isinstance(idx, ast.Slice):
4✔
2206
                loop_var = self.builder.find_new_name(f"_slice_iter_{len(loop_vars)}_")
4✔
2207
                loop_vars.append(loop_var)
4✔
2208

2209
                if not self.builder.exists(loop_var):
4✔
2210
                    self.builder.add_container(
4✔
2211
                        loop_var, Scalar(PrimitiveType.Int64), False
2212
                    )
2213
                    self.container_table[loop_var] = Scalar(PrimitiveType.Int64)
4✔
2214

2215
                start_str = "0"
4✔
2216
                if idx.lower:
4✔
2217
                    start_str = self.visit(idx.lower)
4✔
2218
                    if start_str.startswith("-"):
4✔
2219
                        dim_size = (
×
2220
                            str(tensor_info.shape[i])
2221
                            if i < len(tensor_info.shape)
2222
                            else f"_{target_name}_shape_{i}"
2223
                        )
2224
                        start_str = f"({dim_size} {start_str})"
×
2225

2226
                stop_str = ""
4✔
2227
                if idx.upper and not (
4✔
2228
                    isinstance(idx.upper, ast.Constant) and idx.upper.value is None
2229
                ):
2230
                    stop_str = self.visit(idx.upper)
4✔
2231
                    if stop_str.startswith("-") or stop_str.startswith("(-"):
4✔
2232
                        dim_size = (
×
2233
                            str(tensor_info.shape[i])
2234
                            if i < len(tensor_info.shape)
2235
                            else f"_{target_name}_shape_{i}"
2236
                        )
2237
                        stop_str = f"({dim_size} {stop_str})"
×
2238
                else:
2239
                    stop_str = (
4✔
2240
                        str(tensor_info.shape[i])
2241
                        if i < len(tensor_info.shape)
2242
                        else f"_{target_name}_shape_{i}"
2243
                    )
2244

2245
                step_str = "1"
4✔
2246
                if idx.step:
4✔
2247
                    step_str = self.visit(idx.step)
×
2248

2249
                count_str = f"({stop_str} - {start_str})"
4✔
2250

2251
                self.builder.begin_for(loop_var, "0", count_str, "1", debug_info)
4✔
2252
                self.container_table[loop_var] = Scalar(PrimitiveType.Int64)
4✔
2253
                new_target_indices.append(
4✔
2254
                    ast.Name(
2255
                        id=f"{start_str} + {loop_var} * {step_str}", ctx=ast.Load()
2256
                    )
2257
                )
2258
            else:
2259
                dim_size = (
4✔
2260
                    tensor_info.shape[i]
2261
                    if i < len(tensor_info.shape)
2262
                    else f"_{target_name}_shape_{i}"
2263
                )
2264
                normalized_idx = normalize_negative_index(idx, dim_size)
4✔
2265
                # intermediate computations are placed outside the loops
2266
                idx_str = self.visit(normalized_idx)
4✔
2267
                new_target_indices.append(ast.Name(id=idx_str, ctx=ast.Load()))
4✔
2268

2269
        rewriter = SliceRewriter(loop_vars, self.tensor_table, self)
4✔
2270
        new_value = rewriter.visit(copy.deepcopy(value))
4✔
2271

2272
        new_target = copy.deepcopy(target)
4✔
2273
        if len(new_target_indices) == 1:
4✔
2274
            new_target.slice = new_target_indices[0]
4✔
2275
        else:
2276
            new_target.slice = ast.Tuple(elts=new_target_indices, ctx=ast.Load())
4✔
2277

2278
        rhs_memlet_type = None
4✔
2279
        rhs_indexed_subset = ""
4✔
2280
        if materialized_rhs in self.tensor_table:
4✔
2281
            rhs_tensor = self.tensor_table[materialized_rhs]
4✔
2282
            rhs_ndim = len(rhs_tensor.shape)
4✔
2283
            if rhs_ndim > 0 and rhs_ndim == len(loop_vars):
4✔
2284
                # RHS is an array matching the slice dimensions - index it with loop vars
2285
                rhs_indexed_subset = ",".join(loop_vars)
4✔
2286
                rhs_memlet_type = rhs_tensor
4✔
2287

2288
        block = self.builder.add_block(debug_info)
4✔
2289
        t_task = self.builder.add_tasklet(
4✔
2290
            block, TaskletCode.assign, ["_in"], ["_out"], debug_info
2291
        )
2292

2293
        t_src, src_sub = self._add_read(block, materialized_rhs, debug_info)
4✔
2294
        # Use indexed subset if RHS is an array that needs indexing
2295
        actual_src_sub = rhs_indexed_subset if rhs_indexed_subset else src_sub
4✔
2296
        self.builder.add_memlet(
4✔
2297
            block,
2298
            t_src,
2299
            "void",
2300
            t_task,
2301
            "_in",
2302
            actual_src_sub,
2303
            rhs_memlet_type,
2304
            debug_info,
2305
        )
2306

2307
        lhs_expr = self.visit(new_target)
4✔
2308
        if "(" in lhs_expr and lhs_expr.endswith(")"):
4✔
2309
            subset = lhs_expr[lhs_expr.find("(") + 1 : -1]
4✔
2310
            tensor_dst = self.tensor_table[target_name]
4✔
2311

2312
            t_dst = self.builder.add_access(block, target_name, debug_info)
4✔
2313
            self.builder.add_memlet(
4✔
2314
                block, t_task, "_out", t_dst, "void", subset, tensor_dst, debug_info
2315
            )
2316
        else:
2317
            t_dst = self.builder.add_access(block, target_name, debug_info)
×
2318
            self.builder.add_memlet(
×
2319
                block, t_task, "_out", t_dst, "void", "", None, debug_info
2320
            )
2321

2322
        for _ in loop_vars:
4✔
2323
            self.builder.end_for()
4✔
2324

2325
    def _handle_ufunc_outer_slice_assignment(
4✔
2326
        self, target, value, target_name, indices, debug_info=None
2327
    ):
2328
        """Handle slice assignment where RHS contains a ufunc outer operation.
2329

2330
        Example: path[:] = np.minimum(path[:], np.add.outer(path[:, k], path[k, :]))
2331

2332
        The strategy is:
2333
        1. Evaluate the entire RHS expression, which will create a temporary array
2334
           containing the result of the ufunc outer (potentially wrapped in other ops)
2335
        2. Copy the temporary result to the target slice
2336

2337
        This avoids the loop transformation that would destroy slice shape info.
2338
        """
2339
        if debug_info is None:
4✔
2340
            from docc.sdfg import DebugInfo
×
2341

2342
            debug_info = DebugInfo()
×
2343

2344
        # Evaluate the full RHS expression
2345
        # This will:
2346
        # - Create temp arrays for ufunc outer results
2347
        # - Apply any wrapping operations (np.minimum, etc.)
2348
        # - Return the name of the final result array
2349
        result_name = self.visit(value)
4✔
2350

2351
        # Now we need to copy result to target slice
2352
        # Count slice dimensions to determine if we need loops
2353
        target_slice_ndim = sum(1 for idx in indices if isinstance(idx, ast.Slice))
4✔
2354

2355
        if target_slice_ndim == 0:
4✔
2356
            # No slices on target - just simple assignment
2357
            target_str = self.visit(target)
×
2358
            block = self.builder.add_block(debug_info)
×
2359
            t_src, src_sub = self._add_read(block, result_name, debug_info)
×
2360
            t_dst = self.builder.add_access(block, target_str, debug_info)
×
2361
            t_task = self.builder.add_tasklet(
×
2362
                block, TaskletCode.assign, ["_in"], ["_out"], debug_info
2363
            )
2364
            self.builder.add_memlet(
×
2365
                block, t_src, "void", t_task, "_in", src_sub, None, debug_info
2366
            )
2367
            self.builder.add_memlet(
×
2368
                block, t_task, "_out", t_dst, "void", "", None, debug_info
2369
            )
2370
            return
×
2371

2372
        # We have slices on the target - need to create loops for copying
2373
        # Get target array info
2374
        target_shapes = self.tensor_table[target_name].shape
4✔
2375

2376
        loop_vars = []
4✔
2377
        new_target_indices = []
4✔
2378

2379
        for i, idx in enumerate(indices):
4✔
2380
            if isinstance(idx, ast.Slice):
4✔
2381
                loop_var = self.builder.find_new_name(f"_copy_iter_{len(loop_vars)}_")
4✔
2382
                loop_vars.append(loop_var)
4✔
2383

2384
                if not self.builder.exists(loop_var):
4✔
2385
                    self.builder.add_container(
4✔
2386
                        loop_var, Scalar(PrimitiveType.Int64), False
2387
                    )
2388
                    self.container_table[loop_var] = Scalar(PrimitiveType.Int64)
4✔
2389

2390
                start_str = "0"
4✔
2391
                if idx.lower:
4✔
2392
                    start_str = self.visit(idx.lower)
×
2393

2394
                stop_str = ""
4✔
2395
                if idx.upper and not (
4✔
2396
                    isinstance(idx.upper, ast.Constant) and idx.upper.value is None
2397
                ):
2398
                    stop_str = self.visit(idx.upper)
×
2399
                else:
2400
                    stop_str = (
4✔
2401
                        target_shapes[i]
2402
                        if i < len(target_shapes)
2403
                        else f"_{target_name}_shape_{i}"
2404
                    )
2405

2406
                step_str = "1"
4✔
2407
                if idx.step:
4✔
2408
                    step_str = self.visit(idx.step)
×
2409

2410
                count_str = f"({stop_str} - {start_str})"
4✔
2411

2412
                self.builder.begin_for(loop_var, "0", count_str, "1", debug_info)
4✔
2413
                self.container_table[loop_var] = Scalar(PrimitiveType.Int64)
4✔
2414

2415
                new_target_indices.append(
4✔
2416
                    ast.Name(
2417
                        id=f"{start_str} + {loop_var} * {step_str}", ctx=ast.Load()
2418
                    )
2419
                )
2420
            else:
2421
                # Handle non-slice indices - need to normalize negative indices
2422
                dim_size = (
×
2423
                    target_shapes[i]
2424
                    if i < len(target_shapes)
2425
                    else f"_{target_name}_shape_{i}"
2426
                )
2427
                normalized_idx = normalize_negative_index(idx, dim_size)
×
2428
                # Visit the index NOW before any loops are opened to ensure
2429
                # intermediate computations are placed outside the loops
2430
                idx_str = self.visit(normalized_idx)
×
2431
                new_target_indices.append(ast.Name(id=idx_str, ctx=ast.Load()))
×
2432

2433
        # Create assignment block: target[i,j,...] = result[i,j,...]
2434
        block = self.builder.add_block(debug_info)
4✔
2435

2436
        # Access nodes
2437
        t_src = self.builder.add_access(block, result_name, debug_info)
4✔
2438
        t_dst = self.builder.add_access(block, target_name, debug_info)
4✔
2439
        t_task = self.builder.add_tasklet(
4✔
2440
            block, TaskletCode.assign, ["_in"], ["_out"], debug_info
2441
        )
2442

2443
        # Source index - just use loop vars for flat array from ufunc outer
2444
        # The ufunc outer result is a flat array of size M*N
2445
        if len(loop_vars) == 2:
4✔
2446
            # 2D case: result is indexed as i * N + j
2447
            # Get the second dimension size from target shapes
2448
            n_dim = (
4✔
2449
                target_shapes[1]
2450
                if len(target_shapes) > 1
2451
                else f"_{target_name}_shape_1"
2452
            )
2453
            src_index = f"(({loop_vars[0]}) * ({n_dim}) + ({loop_vars[1]}))"
4✔
2454
        elif len(loop_vars) == 1:
×
2455
            src_index = loop_vars[0]
×
2456
        else:
2457
            # General case - compute linear index
2458
            src_terms = []
×
2459
            stride = "1"
×
2460
            for i in range(len(loop_vars) - 1, -1, -1):
×
2461
                if stride == "1":
×
2462
                    src_terms.insert(0, loop_vars[i])
×
2463
                else:
2464
                    src_terms.insert(0, f"({loop_vars[i]} * {stride})")
×
2465
                if i > 0:
×
2466
                    dim_size = (
×
2467
                        target_shapes[i]
2468
                        if i < len(target_shapes)
2469
                        else f"_{target_name}_shape_{i}"
2470
                    )
2471
                    stride = (
×
2472
                        f"({stride} * {dim_size})" if stride != "1" else str(dim_size)
2473
                    )
2474
            src_index = " + ".join(src_terms) if src_terms else "0"
×
2475

2476
        # Target index - compute linear index (row-major order)
2477
        # For 2D array with shape (M, N): linear_index = i * N + j
2478
        target_index_parts = []
4✔
2479
        for idx in new_target_indices:
4✔
2480
            if isinstance(idx, ast.Name):
4✔
2481
                target_index_parts.append(idx.id)
4✔
2482
            else:
2483
                target_index_parts.append(self.visit(idx))
×
2484

2485
        # Convert to linear index
2486
        if len(target_index_parts) == 2:
4✔
2487
            # 2D case
2488
            n_dim = (
4✔
2489
                target_shapes[1]
2490
                if len(target_shapes) > 1
2491
                else f"_{target_name}_shape_1"
2492
            )
2493
            target_index = (
4✔
2494
                f"(({target_index_parts[0]}) * ({n_dim}) + ({target_index_parts[1]}))"
2495
            )
2496
        elif len(target_index_parts) == 1:
×
2497
            target_index = target_index_parts[0]
×
2498
        else:
2499
            # General case - compute linear index with strides
2500
            stride = "1"
×
2501
            target_index = "0"
×
2502
            for i in range(len(target_index_parts) - 1, -1, -1):
×
2503
                idx_part = target_index_parts[i]
×
2504
                if stride == "1":
×
2505
                    term = idx_part
×
2506
                else:
2507
                    term = f"(({idx_part}) * ({stride}))"
×
2508

2509
                if target_index == "0":
×
2510
                    target_index = term
×
2511
                else:
2512
                    target_index = f"({term} + {target_index})"
×
2513

2514
                if i > 0:
×
2515
                    dim_size = (
×
2516
                        target_shapes[i]
2517
                        if i < len(target_shapes)
2518
                        else f"_{target_name}_shape_{i}"
2519
                    )
2520
                    stride = (
×
2521
                        f"({stride} * {dim_size})" if stride != "1" else str(dim_size)
2522
                    )
2523

2524
        # Connect memlets
2525
        self.builder.add_memlet(
4✔
2526
            block, t_src, "void", t_task, "_in", src_index, None, debug_info
2527
        )
2528
        self.builder.add_memlet(
4✔
2529
            block, t_task, "_out", t_dst, "void", target_index, None, debug_info
2530
        )
2531

2532
        # End loops
2533
        for _ in loop_vars:
4✔
2534
            self.builder.end_for()
4✔
2535

2536
    def _contains_indirect_access(self, node):
4✔
2537
        """Check if an AST node contains any indirect array access."""
2538
        if isinstance(node, ast.Subscript):
4✔
2539
            if isinstance(node.value, ast.Name):
4✔
2540
                arr_name = node.value.id
4✔
2541
                if arr_name in self.tensor_table:
4✔
2542
                    return True
4✔
2543
        elif isinstance(node, ast.BinOp):
4✔
2544
            return self._contains_indirect_access(
4✔
2545
                node.left
2546
            ) or self._contains_indirect_access(node.right)
2547
        elif isinstance(node, ast.UnaryOp):
4✔
2548
            return self._contains_indirect_access(node.operand)
4✔
2549
        return False
4✔
2550

2551
    def _materialize_indirect_access(
4✔
2552
        self, node, debug_info=None, return_original_expr=False
2553
    ):
2554
        """Materialize an array access into a scalar variable using tasklet+memlets."""
2555
        if not self.builder:
4✔
2556
            expr = self.visit(node)
×
2557
            return (expr, expr) if return_original_expr else expr
×
2558

2559
        if debug_info is None:
4✔
2560
            debug_info = DebugInfo()
4✔
2561

2562
        if not isinstance(node, ast.Subscript):
4✔
2563
            expr = self.visit(node)
×
2564
            return (expr, expr) if return_original_expr else expr
×
2565

2566
        if not isinstance(node.value, ast.Name):
4✔
2567
            expr = self.visit(node)
×
2568
            return (expr, expr) if return_original_expr else expr
×
2569

2570
        arr_name = node.value.id
4✔
2571
        if arr_name not in self.tensor_table:
4✔
2572
            expr = self.visit(node)
×
2573
            return (expr, expr) if return_original_expr else expr
×
2574

2575
        dtype = Scalar(PrimitiveType.Int64)
4✔
2576
        if arr_name in self.container_table:
4✔
2577
            t = self.container_table[arr_name]
4✔
2578
            if isinstance(t, Pointer) and t.has_pointee_type():
4✔
2579
                dtype = t.pointee_type
4✔
2580

2581
        tmp_name = self.builder.find_new_name("_idx_")
4✔
2582
        self.builder.add_container(tmp_name, dtype, False)
4✔
2583
        self.container_table[tmp_name] = dtype
4✔
2584

2585
        ndim = len(self.tensor_table[arr_name].shape)
4✔
2586
        shapes = self.tensor_table[arr_name].shape
4✔
2587

2588
        if isinstance(node.slice, ast.Tuple):
4✔
2589
            indices = [self.visit(elt) for elt in node.slice.elts]
×
2590
        else:
2591
            indices = [self.visit(node.slice)]
4✔
2592

2593
        materialized_indices = []
4✔
2594
        for idx_str in indices:
4✔
2595
            if "(" in idx_str and idx_str.endswith(")"):
4✔
2596
                materialized_indices.append(idx_str)
×
2597
            else:
2598
                materialized_indices.append(idx_str)
4✔
2599

2600
        linear_index = self._compute_linear_index(
4✔
2601
            materialized_indices, shapes, arr_name, ndim
2602
        )
2603

2604
        block = self.builder.add_block(debug_info)
4✔
2605
        t_src = self.builder.add_access(block, arr_name, debug_info)
4✔
2606
        t_dst = self.builder.add_access(block, tmp_name, debug_info)
4✔
2607
        t_task = self.builder.add_tasklet(
4✔
2608
            block, TaskletCode.assign, ["_in"], ["_out"], debug_info
2609
        )
2610

2611
        self.builder.add_memlet(
4✔
2612
            block, t_src, "void", t_task, "_in", linear_index, None, debug_info
2613
        )
2614
        self.builder.add_memlet(
4✔
2615
            block, t_task, "_out", t_dst, "void", "", None, debug_info
2616
        )
2617

2618
        if return_original_expr:
4✔
2619
            original_expr = f"{arr_name}({linear_index})"
4✔
2620
            return (tmp_name, original_expr)
4✔
2621

2622
        return tmp_name
×
2623

2624
    def _get_unique_id(self):
4✔
2625
        self._unique_counter_ref[0] += 1
4✔
2626
        return self._unique_counter_ref[0]
4✔
2627

2628
    def _get_memlet_type_for_access(self, expr_str, subset):
4✔
2629
        """Get the Tensor type for an indexed array access expression.
2630

2631
        When accessing an array like "arr(i,j)" with a multi-dimensional subset,
2632
        we need to pass the Tensor type to add_memlet for correct type inference.
2633
        If the expression is a simple scalar variable or constant, returns None.
2634
        """
2635
        if not subset:
4✔
2636
            return None
4✔
2637

2638
        # Check if expr_str is an indexed array access like "arr(i,j)"
2639
        if "(" in expr_str and expr_str.endswith(")"):
×
2640
            name = expr_str.split("(")[0]
×
2641
            if name in self.tensor_table:
×
2642
                return self.tensor_table[name]
×
2643

2644
        # Check if expr_str is a simple array name with a non-empty subset from _add_read
2645
        if expr_str in self.tensor_table:
×
2646
            return self.tensor_table[expr_str]
×
2647

2648
        return None
×
2649

2650
    def _element_type(self, name):
4✔
2651
        if name in self.container_table:
4✔
2652
            return element_type_from_sdfg_type(self.container_table[name])
4✔
2653
        else:  # Constant
2654
            if self._is_int(name):
4✔
2655
                return Scalar(PrimitiveType.Int64)
4✔
2656
            else:
2657
                return Scalar(PrimitiveType.Double)
4✔
2658

2659
    def _is_int(self, operand):
4✔
2660
        try:
4✔
2661
            if operand.lstrip("-").isdigit():
4✔
2662
                return True
4✔
2663
        except ValueError:
×
2664
            pass
×
2665

2666
        name = operand
4✔
2667
        if "(" in operand and operand.endswith(")"):
4✔
2668
            name = operand.split("(")[0]
×
2669

2670
        if name in self.container_table:
4✔
2671
            t = self.container_table[name]
4✔
2672

2673
            def is_int_ptype(pt):
4✔
2674
                return pt in [
4✔
2675
                    PrimitiveType.Int64,
2676
                    PrimitiveType.Int32,
2677
                    PrimitiveType.Int8,
2678
                    PrimitiveType.Int16,
2679
                    PrimitiveType.UInt64,
2680
                    PrimitiveType.UInt32,
2681
                    PrimitiveType.UInt8,
2682
                    PrimitiveType.UInt16,
2683
                ]
2684

2685
            if isinstance(t, Scalar):
4✔
2686
                return is_int_ptype(t.primitive_type)
4✔
2687

2688
            if type(t).__name__ == "Array" and hasattr(t, "element_type"):
×
2689
                et = t.element_type
×
2690
                if callable(et):
×
2691
                    et = et()
×
2692
                if isinstance(et, Scalar):
×
2693
                    return is_int_ptype(et.primitive_type)
×
2694

2695
            if type(t).__name__ == "Pointer":
×
2696
                if hasattr(t, "pointee_type"):
×
2697
                    et = t.pointee_type
×
2698
                    if callable(et):
×
2699
                        et = et()
×
2700
                    if isinstance(et, Scalar):
×
2701
                        return is_int_ptype(et.primitive_type)
×
2702
                if hasattr(t, "element_type"):
×
2703
                    et = t.element_type
×
2704
                    if callable(et):
×
2705
                        et = et()
×
2706
                    if isinstance(et, Scalar):
×
2707
                        return is_int_ptype(et.primitive_type)
×
2708

2709
        return False
4✔
2710

2711
    def _add_read(self, block, expr_str, debug_info=None):
4✔
2712
        try:
4✔
2713
            if (block, expr_str) in self._access_cache:
4✔
2714
                return self._access_cache[(block, expr_str)]
×
2715
        except TypeError:
×
2716
            pass
×
2717

2718
        if debug_info is None:
4✔
2719
            debug_info = DebugInfo()
4✔
2720

2721
        if "(" in expr_str and expr_str.endswith(")"):
4✔
2722
            name = expr_str.split("(")[0]
×
2723
            subset = expr_str[expr_str.find("(") + 1 : -1]
×
2724
            access = self.builder.add_access(block, name, debug_info)
×
2725
            try:
×
2726
                self._access_cache[(block, expr_str)] = (access, subset)
×
2727
            except TypeError:
×
2728
                pass
×
2729
            return access, subset
×
2730

2731
        if self.builder.exists(expr_str):
4✔
2732
            access = self.builder.add_access(block, expr_str, debug_info)
4✔
2733
            subset = ""
4✔
2734
            if expr_str in self.container_table:
4✔
2735
                sym_type = self.container_table[expr_str]
4✔
2736
                if isinstance(sym_type, Pointer):
4✔
2737
                    if expr_str in self.tensor_table:
4✔
2738
                        ndim = len(self.tensor_table[expr_str].shape)
4✔
2739
                        if ndim == 0:
4✔
2740
                            subset = "0"
×
2741
                    else:
2742
                        subset = "0"
×
2743
            try:
4✔
2744
                self._access_cache[(block, expr_str)] = (access, subset)
4✔
2745
            except TypeError:
×
2746
                pass
×
2747
            return access, subset
4✔
2748

2749
        dtype = Scalar(PrimitiveType.Double)
4✔
2750
        if self._is_int(expr_str):
4✔
2751
            dtype = Scalar(PrimitiveType.Int64)
4✔
2752
        elif expr_str == "true" or expr_str == "false":
4✔
2753
            dtype = Scalar(PrimitiveType.Bool)
×
2754

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

© 2026 Coveralls, Inc