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

daisytuner / docc / 28761269778

06 Jul 2026 01:02AM UTC coverage: 62.801% (+0.3%) from 62.469%
28761269778

push

github

web-flow
Merge pull request #835 from daisytuner/pylulesh

[Python] Extends frontend support and adds pylulesh benchmarks

704 of 800 new or added lines in 5 files covered. (88.0%)

4 existing lines in 1 file now uncovered.

40479 of 64456 relevant lines covered (62.8%)

968.2 hits per line

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

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

31

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

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

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

56
        # Context
57
        self.infer_return_type = infer_return_type
4✔
58
        self.globals_dict = globals_dict if globals_dict is not None else {}
4✔
59
        self._unique_counter_ref = (
4✔
60
            unique_counter_ref if unique_counter_ref is not None else [0]
61
        )
62
        self._access_cache = {}
4✔
63
        self.structure_member_info = (
4✔
64
            structure_member_info if structure_member_info is not None else {}
65
        )
66
        self.captured_return_shapes = {}  # Map param name to shape string list
4✔
67
        self.captured_return_strides = {}  # Map param name to stride string list
4✔
68
        self.shapes_runtime_info = (
4✔
69
            {}
70
        )  # Map array name to runtime shapes (separate from Tensor)
71
        # Map variable name -> tuple/list AST for tuple-valued variables
72
        # (e.g. `shp = (a.shape[0], a.shape[1])`), so they can later be resolved
73
        # as array-creation shape arguments.
74
        self.tuple_table = {}
4✔
75

76
        # Memory manager for hoisted allocations (shared with inline parsers)
77
        self.memory_handler = (
4✔
78
            memory_handler
79
            if memory_handler is not None
80
            else ManagedMemoryHandler(builder)
81
        )
82

83
        # Initialize handlers - they receive 'self' to access expression visitor methods
84
        self.numpy_visitor = NumPyHandler(self)
4✔
85
        self.math_handler = MathHandler(self)
4✔
86
        self.python_handler = PythonHandler(self)
4✔
87

88
    def visit_Constant(self, node):
4✔
89
        if isinstance(node.value, bool):
4✔
90
            return "true" if node.value else "false"
4✔
91
        return str(node.value)
4✔
92

93
    def visit_Name(self, node):
4✔
94
        name = node.id
4✔
95
        if name not in self.container_table and self.globals_dict is not None:
4✔
96
            if name in self.globals_dict:
4✔
97
                val = self.globals_dict[name]
4✔
98
                if isinstance(val, (int, float)):
4✔
99
                    return str(val)
4✔
100
                # Module-global constant array (e.g. LULESH's `gamma`). Bake it
101
                # into the SDFG as an initialized array container the first time
102
                # it is referenced.
103
                if isinstance(val, np.ndarray) and self.builder is not None:
4✔
104
                    return self._materialize_global_array(name, val)
4✔
105
        return name
4✔
106

107
    def visit_Add(self, node):
4✔
108
        return "+"
4✔
109

110
    def visit_Sub(self, node):
4✔
111
        return "-"
4✔
112

113
    def visit_Mult(self, node):
4✔
114
        return "*"
4✔
115

116
    def visit_Div(self, node):
4✔
117
        return "/"
4✔
118

119
    def visit_FloorDiv(self, node):
4✔
120
        return "//"
4✔
121

122
    def visit_Mod(self, node):
4✔
123
        return "%"
4✔
124

125
    def visit_Pow(self, node):
4✔
126
        return "**"
4✔
127

128
    def visit_Eq(self, node):
4✔
129
        return "=="
4✔
130

131
    def visit_NotEq(self, node):
4✔
132
        return "!="
4✔
133

134
    def visit_Lt(self, node):
4✔
135
        return "<"
4✔
136

137
    def visit_LtE(self, node):
4✔
138
        return "<="
4✔
139

140
    def visit_Gt(self, node):
4✔
141
        return ">"
4✔
142

143
    def visit_GtE(self, node):
4✔
144
        return ">="
4✔
145

146
    def visit_And(self, node):
4✔
147
        return "&"
4✔
148

149
    def visit_Or(self, node):
4✔
150
        return "|"
4✔
151

152
    def visit_BitAnd(self, node):
4✔
153
        return "&"
4✔
154

155
    def visit_BitOr(self, node):
4✔
156
        return "|"
4✔
157

158
    def visit_BitXor(self, node):
4✔
159
        return "^"
4✔
160

161
    def visit_LShift(self, node):
4✔
162
        return "<<"
4✔
163

164
    def visit_RShift(self, node):
4✔
165
        return ">>"
×
166

167
    def visit_Not(self, node):
4✔
168
        return "!"
4✔
169

170
    def visit_USub(self, node):
4✔
171
        return "-"
4✔
172

173
    def visit_UAdd(self, node):
4✔
174
        return "+"
×
175

176
    def visit_Invert(self, node):
4✔
177
        return "~"
4✔
178

179
    def visit_BoolOp(self, node):
4✔
180
        op = self.visit(node.op)
4✔
181
        values = [f"({self.visit(v)} != 0)" for v in node.values]
4✔
182
        expr_str = f"{f' {op} '.join(values)}"
4✔
183

184
        tmp_name = self.builder.find_new_name()
4✔
185
        dtype = Scalar(PrimitiveType.Bool)
4✔
186
        self.builder.add_container(tmp_name, dtype, False)
4✔
187

188
        self.builder.begin_if(expr_str)
4✔
189
        self._add_assign_constant(tmp_name, "true", dtype)
4✔
190
        self.builder.begin_else()
4✔
191
        self._add_assign_constant(tmp_name, "false", dtype)
4✔
192
        self.builder.end_if()
4✔
193

194
        self.container_table[tmp_name] = dtype
4✔
195
        return tmp_name
4✔
196

197
    def visit_UnaryOp(self, node):
4✔
198
        if (
4✔
199
            isinstance(node.op, ast.USub)
200
            and isinstance(node.operand, ast.Constant)
201
            and isinstance(node.operand.value, (int, float))
202
        ):
203
            return f"-{node.operand.value}"
4✔
204

205
        op = self.visit(node.op)
4✔
206
        operand = self.visit(node.operand)
4✔
207

208
        if operand in self.tensor_table and op == "-":
4✔
209
            return self.numpy_visitor.handle_array_negate(operand)
4✔
210

211
        assert operand in self.container_table, f"Undefined variable: {operand}"
4✔
212
        tmp_name = self.builder.find_new_name()
4✔
213
        dtype = self.container_table[operand]
4✔
214
        self.builder.add_container(tmp_name, dtype, False)
4✔
215
        self.container_table[tmp_name] = dtype
4✔
216

217
        block = self.builder.add_block()
4✔
218
        t_src, src_sub = self._add_read(block, operand)
4✔
219
        t_dst = self.builder.add_access(block, tmp_name)
4✔
220

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

263
        return tmp_name
4✔
264

265
    def visit_BinOp(self, node):
4✔
266
        if isinstance(node.op, ast.MatMult):
4✔
267
            return self.numpy_visitor.handle_numpy_matmul_op(node.left, node.right)
4✔
268

269
        left = self.visit(node.left)
4✔
270
        op = self.visit(node.op)
4✔
271
        right = self.visit(node.right)
4✔
272

273
        left_is_array = left in self.tensor_table
4✔
274
        right_is_array = right in self.tensor_table
4✔
275

276
        if left_is_array or right_is_array:
4✔
277
            op_map = {"+": "add", "-": "sub", "*": "mul", "/": "div", "**": "pow"}
4✔
278
            if op in op_map:
4✔
279
                return self.numpy_visitor.handle_array_binary_op(
4✔
280
                    op_map[op], left, right
281
                )
282
            elif op in ("&", "|", "^", "<<", ">>"):
4✔
283
                return self.numpy_visitor.handle_array_bitwise_op(op, left, right)
4✔
284
            else:
285
                raise NotImplementedError(f"Array operation {op} not supported")
×
286

287
        tmp_name = self.builder.find_new_name()
4✔
288

289
        left_is_int = self._is_int(left)
4✔
290
        right_is_int = self._is_int(right)
4✔
291
        dtype = Scalar(PrimitiveType.Double)
4✔
292
        if left_is_int and right_is_int and op not in ["/", "**"]:
4✔
293
            dtype = Scalar(PrimitiveType.Int64)
4✔
294

295
        if not self.builder.exists(tmp_name):
4✔
296
            self.builder.add_container(tmp_name, dtype, False)
4✔
297
            self.container_table[tmp_name] = dtype
4✔
298

299
        real_left = left
4✔
300
        real_right = right
4✔
301
        if dtype.primitive_type == PrimitiveType.Double:
4✔
302
            if left_is_int:
4✔
303
                left_cast = self.builder.find_new_name()
4✔
304
                self.builder.add_container(
4✔
305
                    left_cast, Scalar(PrimitiveType.Double), False
306
                )
307
                self.container_table[left_cast] = Scalar(PrimitiveType.Double)
4✔
308

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

318
                real_left = left_cast
4✔
319

320
            if right_is_int:
4✔
321
                right_cast = self.builder.find_new_name()
4✔
322
                self.builder.add_container(
4✔
323
                    right_cast, Scalar(PrimitiveType.Double), False
324
                )
325
                self.container_table[right_cast] = Scalar(PrimitiveType.Double)
4✔
326

327
                c_block = self.builder.add_block()
4✔
328
                t_src, src_sub = self._add_read(c_block, right)
4✔
329
                t_dst = self.builder.add_access(c_block, right_cast)
4✔
330
                t_task = self.builder.add_tasklet(
4✔
331
                    c_block, TaskletCode.assign, ["_in"], ["_out"]
332
                )
333
                self.builder.add_memlet(c_block, t_src, "void", t_task, "_in", src_sub)
4✔
334
                self.builder.add_memlet(c_block, t_task, "_out", t_dst, "void", "")
4✔
335

336
                real_right = right_cast
4✔
337

338
        if op == "**":
4✔
339
            block = self.builder.add_block()
4✔
340
            t_left, left_sub = self._add_read(block, real_left)
4✔
341
            t_right, right_sub = self._add_read(block, real_right)
4✔
342
            t_out = self.builder.add_access(block, tmp_name)
4✔
343

344
            t_task = self.builder.add_cmath(
4✔
345
                block, CMathFunction.pow, dtype.primitive_type
346
            )
347
            self.builder.add_memlet(block, t_left, "void", t_task, "_in1", left_sub)
4✔
348
            self.builder.add_memlet(block, t_right, "void", t_task, "_in2", right_sub)
4✔
349
            self.builder.add_memlet(block, t_task, "_out", t_out, "void", "")
4✔
350

351
            return tmp_name
4✔
352
        elif op == "%":
4✔
353
            block = self.builder.add_block()
4✔
354
            t_left, left_sub = self._add_read(block, real_left)
4✔
355
            t_right, right_sub = self._add_read(block, real_right)
4✔
356
            t_out = self.builder.add_access(block, tmp_name)
4✔
357

358
            if dtype.primitive_type == PrimitiveType.Int64:
4✔
359
                t_rem1 = self.builder.add_tasklet(
4✔
360
                    block, TaskletCode.int_srem, ["_in1", "_in2"], ["_out"]
361
                )
362
                self.builder.add_memlet(block, t_left, "void", t_rem1, "_in1", left_sub)
4✔
363
                self.builder.add_memlet(
4✔
364
                    block, t_right, "void", t_rem1, "_in2", right_sub
365
                )
366

367
                rem1_name = self.builder.find_new_name()
4✔
368
                self.builder.add_container(rem1_name, dtype, False)
4✔
369
                t_rem1_out = self.builder.add_access(block, rem1_name)
4✔
370
                self.builder.add_memlet(block, t_rem1, "_out", t_rem1_out, "void", "")
4✔
371

372
                t_add = self.builder.add_tasklet(
4✔
373
                    block, TaskletCode.int_add, ["_in1", "_in2"], ["_out"]
374
                )
375
                self.builder.add_memlet(block, t_rem1_out, "void", t_add, "_in1", "")
4✔
376
                self.builder.add_memlet(
4✔
377
                    block, t_right, "void", t_add, "_in2", right_sub
378
                )
379

380
                add_name = self.builder.find_new_name()
4✔
381
                self.builder.add_container(add_name, dtype, False)
4✔
382
                t_add_out = self.builder.add_access(block, add_name)
4✔
383
                self.builder.add_memlet(block, t_add, "_out", t_add_out, "void", "")
4✔
384

385
                t_rem2 = self.builder.add_tasklet(
4✔
386
                    block, TaskletCode.int_srem, ["_in1", "_in2"], ["_out"]
387
                )
388
                self.builder.add_memlet(block, t_add_out, "void", t_rem2, "_in1", "")
4✔
389
                self.builder.add_memlet(
4✔
390
                    block, t_right, "void", t_rem2, "_in2", right_sub
391
                )
392
                self.builder.add_memlet(block, t_rem2, "_out", t_out, "void", "")
4✔
393

394
                return tmp_name
4✔
395
            else:
396
                t_rem1 = self.builder.add_tasklet(
4✔
397
                    block, TaskletCode.fp_rem, ["_in1", "_in2"], ["_out"]
398
                )
399
                self.builder.add_memlet(block, t_left, "void", t_rem1, "_in1", left_sub)
4✔
400
                self.builder.add_memlet(
4✔
401
                    block, t_right, "void", t_rem1, "_in2", right_sub
402
                )
403

404
                rem1_name = self.builder.find_new_name()
4✔
405
                self.builder.add_container(rem1_name, dtype, False)
4✔
406
                t_rem1_out = self.builder.add_access(block, rem1_name)
4✔
407
                self.builder.add_memlet(block, t_rem1, "_out", t_rem1_out, "void", "")
4✔
408

409
                t_add = self.builder.add_tasklet(
4✔
410
                    block, TaskletCode.fp_add, ["_in1", "_in2"], ["_out"]
411
                )
412
                self.builder.add_memlet(block, t_rem1_out, "void", t_add, "_in1", "")
4✔
413
                self.builder.add_memlet(
4✔
414
                    block, t_right, "void", t_add, "_in2", right_sub
415
                )
416

417
                add_name = self.builder.find_new_name()
4✔
418
                self.builder.add_container(add_name, dtype, False)
4✔
419
                t_add_out = self.builder.add_access(block, add_name)
4✔
420
                self.builder.add_memlet(block, t_add, "_out", t_add_out, "void", "")
4✔
421

422
                t_rem2 = self.builder.add_tasklet(
4✔
423
                    block, TaskletCode.fp_rem, ["_in1", "_in2"], ["_out"]
424
                )
425
                self.builder.add_memlet(block, t_add_out, "void", t_rem2, "_in1", "")
4✔
426
                self.builder.add_memlet(
4✔
427
                    block, t_right, "void", t_rem2, "_in2", right_sub
428
                )
429
                self.builder.add_memlet(block, t_rem2, "_out", t_out, "void", "")
4✔
430

431
                return tmp_name
4✔
432

433
        tasklet_code = None
4✔
434
        if dtype.primitive_type == PrimitiveType.Int64:
4✔
435
            if op == "+":
4✔
436
                tasklet_code = TaskletCode.int_add
4✔
437
            elif op == "-":
4✔
438
                tasklet_code = TaskletCode.int_sub
4✔
439
            elif op == "*":
4✔
440
                tasklet_code = TaskletCode.int_mul
4✔
441
            elif op == "/":
4✔
442
                tasklet_code = TaskletCode.int_sdiv
×
443
            elif op == "//":
4✔
444
                tasklet_code = TaskletCode.int_sdiv
4✔
445
            elif op == "&":
4✔
446
                tasklet_code = TaskletCode.int_and
4✔
447
            elif op == "|":
4✔
448
                tasklet_code = TaskletCode.int_or
4✔
449
            elif op == "^":
4✔
450
                tasklet_code = TaskletCode.int_xor
4✔
451
            elif op == "<<":
×
452
                tasklet_code = TaskletCode.int_shl
×
453
            elif op == ">>":
×
454
                tasklet_code = TaskletCode.int_lshr
×
455
        else:
456
            if op == "+":
4✔
457
                tasklet_code = TaskletCode.fp_add
4✔
458
            elif op == "-":
4✔
459
                tasklet_code = TaskletCode.fp_sub
4✔
460
            elif op == "*":
4✔
461
                tasklet_code = TaskletCode.fp_mul
4✔
462
            elif op == "/":
4✔
463
                tasklet_code = TaskletCode.fp_div
4✔
464
            elif op == "//":
×
465
                tasklet_code = TaskletCode.fp_div
×
466
            else:
467
                raise NotImplementedError(f"Operation {op} not supported for floats")
×
468

469
        block = self.builder.add_block()
4✔
470
        t_left, left_sub = self._add_read(block, real_left)
4✔
471
        t_right, right_sub = self._add_read(block, real_right)
4✔
472
        t_out = self.builder.add_access(block, tmp_name)
4✔
473

474
        t_task = self.builder.add_tasklet(
4✔
475
            block, tasklet_code, ["_in1", "_in2"], ["_out"]
476
        )
477

478
        # For indexed array accesses like "arr(i,j)", we need to pass the Tensor type
479
        # to ensure correct type inference during validation
480
        left_type = self._get_memlet_type_for_access(real_left, left_sub)
4✔
481
        right_type = self._get_memlet_type_for_access(real_right, right_sub)
4✔
482

483
        self.builder.add_memlet(
4✔
484
            block, t_left, "void", t_task, "_in1", left_sub, left_type
485
        )
486
        self.builder.add_memlet(
4✔
487
            block, t_right, "void", t_task, "_in2", right_sub, right_type
488
        )
489
        self.builder.add_memlet(block, t_task, "_out", t_out, "void", "")
4✔
490

491
        return tmp_name
4✔
492

493
    def visit_Attribute(self, node):
4✔
494
        if node.attr == "shape":
4✔
495
            if isinstance(node.value, ast.Name) and node.value.id in self.tensor_table:
4✔
496
                return f"_shape_proxy_{node.value.id}"
4✔
497
            # Array-member attribute: e.g. domain.x.shape
NEW
498
            if isinstance(node.value, ast.Attribute):
×
NEW
499
                resolved = self.visit(node.value)
×
NEW
500
                if isinstance(resolved, str) and resolved in self.tensor_table:
×
NEW
501
                    return f"_shape_proxy_{resolved}"
×
502

503
        if node.attr == "size":
4✔
504
            # arr.size / domain.member.size -> total element count (product of
505
            # the shape dimensions) as a scalar expression string, analogous to
506
            # how `.shape[i]` returns a raw dimension expression. Usable in
507
            # symbolic contexts (comparisons, ranges, arithmetic).
508
            target = None
4✔
509
            if isinstance(node.value, ast.Name) and node.value.id in self.tensor_table:
4✔
510
                target = node.value.id
4✔
NEW
511
            elif isinstance(node.value, ast.Attribute):
×
NEW
512
                resolved = self.visit(node.value)
×
NEW
513
                if isinstance(resolved, str) and resolved in self.tensor_table:
×
NEW
514
                    target = resolved
×
515
            if target is not None:
4✔
516
                shape = self.tensor_table[target].shape
4✔
517
                if not shape:
4✔
NEW
518
                    return "1"
×
519
                size_expr = f"({shape[0]})"
4✔
520
                for d in shape[1:]:
4✔
521
                    size_expr = f"({size_expr} * ({d}))"
4✔
522
                return size_expr
4✔
523

524
        if node.attr == "T":
4✔
525
            value_name = None
4✔
526
            if isinstance(node.value, ast.Name):
4✔
527
                value_name = node.value.id
4✔
528
            elif isinstance(node.value, ast.Subscript):
×
529
                value_name = self.visit(node.value)
×
530

531
            if value_name and value_name in self.tensor_table:
4✔
532
                return self.numpy_visitor.handle_transpose_expr(node)
4✔
533

534
        if isinstance(node.value, ast.Name) and node.value.id == "math":
4✔
535
            val = ""
4✔
536
            if node.attr == "pi":
4✔
537
                val = "M_PI"
4✔
538
            elif node.attr == "e":
4✔
539
                val = "M_E"
4✔
540

541
            if val:
4✔
542
                tmp_name = self.builder.find_new_name()
4✔
543
                dtype = Scalar(PrimitiveType.Double)
4✔
544
                self.builder.add_container(tmp_name, dtype, False)
4✔
545
                self.container_table[tmp_name] = dtype
4✔
546
                self._add_assign_constant(tmp_name, val, dtype)
4✔
547
                return tmp_name
4✔
548

549
        # NumPy floating-point constants: np.inf, np.nan, np.pi, np.e
550
        if isinstance(node.value, ast.Name) and node.value.id in ("numpy", "np"):
4✔
551
            const_map = {
4✔
552
                "inf": "INFINITY",
553
                "infty": "INFINITY",
554
                "Inf": "INFINITY",
555
                "nan": "NAN",
556
                "NAN": "NAN",
557
                "pi": "M_PI",
558
                "e": "M_E",
559
            }
560
            val = const_map.get(node.attr, "")
4✔
561
            if val:
4✔
562
                tmp_name = self.builder.find_new_name()
4✔
563
                dtype = Scalar(PrimitiveType.Double)
4✔
564
                self.builder.add_container(tmp_name, dtype, False)
4✔
565
                self.container_table[tmp_name] = dtype
4✔
566
                self._add_assign_constant(tmp_name, val, dtype)
4✔
567
                return tmp_name
4✔
568

569
        if isinstance(node.value, ast.Name):
4✔
570
            obj_name = node.value.id
4✔
571
            attr_name = node.attr
4✔
572

573
            if obj_name in self.container_table:
4✔
574
                obj_type = self.container_table[obj_name]
4✔
575
                if isinstance(obj_type, Pointer) and obj_type.has_pointee_type():
4✔
576
                    pointee_type = obj_type.pointee_type
4✔
577
                    if isinstance(pointee_type, Structure):
4✔
578
                        struct_name = pointee_type.name
4✔
579

580
                        if (
4✔
581
                            struct_name in self.structure_member_info
582
                            and attr_name in self.structure_member_info[struct_name]
583
                        ):
584
                            member_index, member_type, member_shape = (
4✔
585
                                self.structure_member_info[struct_name][attr_name]
586
                            )
587
                        else:
588
                            raise RuntimeError(
×
589
                                f"Member '{attr_name}' not found in structure '{struct_name}'. "
590
                                f"Available members: {list(self.structure_member_info.get(struct_name, {}).keys())}"
591
                            )
592

593
                        subset = "0," + str(member_index)
4✔
594

595
                        if isinstance(member_type, Pointer):
4✔
596
                            # Struct-of-arrays pointer member: expose the
597
                            # member's *value* (the element pointer) as a tensor
598
                            # view. This is exactly a GEP + load, encoded with two
599
                            # canonical memlets:
600
                            #   1. a reference memlet computes the address of the
601
                            #      member field  ->  field_ptr = &obj->member  (T**)
602
                            #   2. a dereference memlet loads the stored pointer
603
                            #      ->  tmp = *field_ptr  (T*)
604
                            # Keeping these separate preserves the memlet
605
                            # contracts (reference = address-of, dereference =
606
                            # load) instead of overloading the reference memlet in
607
                            # codegen.
608
                            field_ptr_type = Pointer(member_type)  # T**
4✔
609
                            field_name = self.builder.find_new_name()
4✔
610
                            self.builder.add_container(
4✔
611
                                field_name, field_ptr_type, False
612
                            )
613
                            self.container_table[field_name] = field_ptr_type
4✔
614

615
                            tmp_name = self.builder.find_new_name()  # T*
4✔
616
                            self.builder.add_container(tmp_name, member_type, False)
4✔
617
                            self.container_table[tmp_name] = member_type
4✔
618

619
                            elem_type = member_type.pointee_type
4✔
620
                            shape = list(member_shape) if member_shape else []
4✔
621
                            self.tensor_table[tmp_name] = Tensor(elem_type, shape)
4✔
622

623
                            block = self.builder.add_block()
4✔
624
                            obj_access = self.builder.add_access(block, obj_name)
4✔
625
                            field_access = self.builder.add_access(block, field_name)
4✔
626
                            tmp_access = self.builder.add_access(block, tmp_name)
4✔
627
                            # field_ptr = &obj->member  (address of the field)
628
                            self.builder.add_reference_memlet(
4✔
629
                                block, obj_access, field_access, subset, None
630
                            )
631
                            # tmp = *field_ptr  (load the element pointer)
632
                            self.builder.add_dereference_memlet(
4✔
633
                                block, field_access, tmp_access, True, field_ptr_type
634
                            )
635
                            return tmp_name
4✔
636

637
                        tmp_name = self.builder.find_new_name()
4✔
638

639
                        self.builder.add_container(tmp_name, member_type, False)
4✔
640
                        self.container_table[tmp_name] = member_type
4✔
641

642
                        block = self.builder.add_block()
4✔
643
                        obj_access = self.builder.add_access(block, obj_name)
4✔
644
                        tmp_access = self.builder.add_access(block, tmp_name)
4✔
645

646
                        tasklet = self.builder.add_tasklet(
4✔
647
                            block, TaskletCode.assign, ["_in"], ["_out"]
648
                        )
649

650
                        self.builder.add_memlet(
4✔
651
                            block, obj_access, "", tasklet, "_in", subset
652
                        )
653
                        self.builder.add_memlet(block, tasklet, "_out", tmp_access, "")
4✔
654

655
                        return tmp_name
4✔
656

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

659
    def visit_Compare(self, node):
4✔
660
        left = self.visit(node.left)
4✔
661
        if len(node.ops) > 1:
4✔
662
            raise NotImplementedError("Chained comparisons not supported yet")
×
663

664
        op = self.visit(node.ops[0])
4✔
665
        right = self.visit(node.comparators[0])
4✔
666

667
        left_is_array = left in self.tensor_table
4✔
668
        right_is_array = right in self.tensor_table
4✔
669

670
        if left_is_array or right_is_array:
4✔
671
            return self.numpy_visitor.handle_array_compare(
4✔
672
                left, op, right, left_is_array, right_is_array
673
            )
674

675
        expr_str = f"{left} {op} {right}"
4✔
676

677
        tmp_name = self.builder.find_new_name()
4✔
678
        dtype = Scalar(PrimitiveType.Bool)
4✔
679
        self.builder.add_container(tmp_name, dtype, False)
4✔
680

681
        self.builder.begin_if(expr_str)
4✔
682
        self.builder.add_transition(tmp_name, "true")
4✔
683
        self.builder.begin_else()
4✔
684
        self.builder.add_transition(tmp_name, "false")
4✔
685
        self.builder.end_if()
4✔
686

687
        self.container_table[tmp_name] = dtype
4✔
688
        return tmp_name
4✔
689

690
    def visit_Subscript(self, node):
4✔
691
        # Compile-time constant folding for subscripts on global constant
692
        # collections, e.g. `XI["M"]["mask"]` where XI is a global dict of bit
693
        # masks. Folds to the numeric literal so it can be used in expressions.
694
        ok, const_val = self._try_const_eval(node)
4✔
695
        if (
4✔
696
            ok
697
            and isinstance(const_val, (int, float))
698
            and not isinstance(const_val, bool)
699
        ):
700
            return str(const_val)
4✔
701

702
        value_str = self.visit(node.value)
4✔
703

704
        if value_str.startswith("_shape_proxy_"):
4✔
705
            array_name = value_str[len("_shape_proxy_") :]
4✔
706
            if isinstance(node.slice, ast.Constant):
4✔
707
                idx = node.slice.value
4✔
708
            elif isinstance(node.slice, ast.Index):
×
709
                idx = node.slice.value.value
×
710
            else:
711
                try:
×
712
                    idx = int(self.visit(node.slice))
×
713
                except:
×
714
                    raise NotImplementedError(
×
715
                        "Dynamic shape indexing not fully supported yet"
716
                    )
717

718
            if array_name in self.tensor_table:
4✔
719
                return self.tensor_table[array_name].shape[idx]
4✔
720

721
            return f"_{array_name}_shape_{idx}"
×
722

723
        if value_str in self.tensor_table:
4✔
724
            tensor = self.tensor_table[value_str]
4✔
725
            ndim = len(tensor.shape)
4✔
726
            shapes = tensor.shape
4✔
727

728
            if isinstance(node.slice, ast.Tuple):
4✔
729
                indices_nodes = node.slice.elts
4✔
730
            else:
731
                indices_nodes = [node.slice]
4✔
732

733
            # np.newaxis / None indexing: arr[:, None], arr[None, :], ...
734
            if any(self._is_newaxis(idx) for idx in indices_nodes):
4✔
735
                return self._handle_newaxis_subscript(value_str, indices_nodes)
4✔
736

737
            # Partial indexing: fewer indices than dimensions implies trailing
738
            # full slices, e.g. A[i] on a 2-D array == A[i, :] (row view).
739
            if len(indices_nodes) < ndim:
4✔
740
                indices_nodes = list(indices_nodes) + [
4✔
741
                    ast.Slice(lower=None, upper=None, step=None)
742
                    for _ in range(ndim - len(indices_nodes))
743
                ]
744

745
            all_full_slices = True
4✔
746
            for idx in indices_nodes:
4✔
747
                if isinstance(idx, ast.Slice):
4✔
748
                    if idx.lower is not None or idx.upper is not None:
4✔
749
                        all_full_slices = False
4✔
750
                        break
4✔
751
                    # Also check for non-trivial step (step != None and step != 1)
752
                    if idx.step is not None:
4✔
753
                        # Check if step is a constant 1; if not, it's not a full slice
754
                        if isinstance(idx.step, ast.Constant) and idx.step.value == 1:
4✔
755
                            pass  # step=1 is equivalent to no step
×
756
                        else:
757
                            all_full_slices = False
4✔
758
                            break
4✔
759
                else:
760
                    all_full_slices = False
4✔
761
                    break
4✔
762

763
            if all_full_slices:
4✔
764
                return value_str
4✔
765

766
            # Fancy indexing with a constant integer sequence on one axis
767
            # (e.g. x[:, (1, 2, 3, 4, 5, 7)]). Handle before the slice path,
768
            # which would otherwise treat the sequence as a dimension-dropping
769
            # scalar index.
770
            if any(self._const_int_sequence(idx) is not None for idx in indices_nodes):
4✔
771
                return self._handle_fancy_index(
4✔
772
                    node, value_str, indices_nodes, shapes, ndim
773
                )
774

775
            has_slices = any(isinstance(idx, ast.Slice) for idx in indices_nodes)
4✔
776
            if has_slices:
4✔
777
                return self._handle_expression_slicing(
4✔
778
                    node, value_str, indices_nodes, shapes, ndim
779
                )
780

781
            if len(indices_nodes) == 1 and self._is_array_valued_index(
4✔
782
                indices_nodes[0]
783
            ):
784
                if self.builder:
4✔
785
                    return self._handle_gather(value_str, indices_nodes[0])
4✔
786

787
            if isinstance(node.slice, ast.Tuple):
4✔
788
                indices = [self.visit(elt) for elt in node.slice.elts]
4✔
789
            else:
790
                indices = [self.visit(node.slice)]
4✔
791

792
            if len(indices) != ndim:
4✔
793
                raise ValueError(
×
794
                    f"Array {value_str} has {ndim} dimensions, but accessed with {len(indices)} indices"
795
                )
796

797
            normalized_indices = []
4✔
798
            for i, idx_str in enumerate(indices):
4✔
799
                shape_val = shapes[i]
4✔
800
                if isinstance(idx_str, str) and (
4✔
801
                    idx_str.startswith("-") or idx_str.startswith("(-")
802
                ):
803
                    normalized_indices.append(f"({shape_val} + {idx_str})")
×
804
                else:
805
                    normalized_indices.append(idx_str)
4✔
806

807
            subscript_str = ",".join(normalized_indices)
4✔
808
            access_str = f"{value_str}({subscript_str})"
4✔
809

810
            if isinstance(node.ctx, ast.Load):
4✔
811
                tmp_name = self.builder.find_new_name()
4✔
812
                self.builder.add_container(tmp_name, tensor.element_type, False)
4✔
813
                self.container_table[tmp_name] = tensor.element_type
4✔
814

815
                block = self.builder.add_block()
4✔
816
                t_src = self.builder.add_access(block, value_str)
4✔
817
                t_dst = self.builder.add_access(block, tmp_name)
4✔
818
                t_task = self.builder.add_tasklet(
4✔
819
                    block, TaskletCode.assign, ["_in"], ["_out"]
820
                )
821
                self.builder.add_memlet(
4✔
822
                    block, t_src, "void", t_task, "_in", subscript_str, tensor
823
                )
824
                self.builder.add_memlet(
4✔
825
                    block, t_task, "_out", t_dst, "void", "", tensor.element_type
826
                )
827

828
                return tmp_name
4✔
829

830
            return access_str
4✔
831

832
        slice_val = self.visit(node.slice)
×
833
        access_str = f"{value_str}({slice_val})"
×
834
        return access_str
×
835

836
    def visit_AugAssign(self, node):
4✔
837
        # Scatter-add: arr[idx] += vals where idx is an integer index array.
838
        if isinstance(node.target, ast.Subscript) and isinstance(node.op, ast.Add):
4✔
839
            tgt = node.target
4✔
840
            idx_nodes = (
4✔
841
                tgt.slice.elts if isinstance(tgt.slice, ast.Tuple) else [tgt.slice]
842
            )
843
            if len(idx_nodes) == 1 and self._is_array_index(idx_nodes[0]):
4✔
844
                target_name = self.visit(tgt.value)
4✔
845
                if target_name in self.tensor_table:
4✔
846
                    debug_info = get_debug_info(node, self.filename, self.function_name)
4✔
847
                    self._handle_scatter_assignment(
4✔
848
                        target_name,
849
                        idx_nodes[0],
850
                        node.value,
851
                        accumulate=True,
852
                        debug_info=debug_info,
853
                    )
854
                    return
4✔
855

856
        if isinstance(node.target, ast.Name) and node.target.id in self.tensor_table:
4✔
857
            # Convert to slice assignment: target[:] = target op value
858
            ndim = len(self.tensor_table[node.target.id].shape)
4✔
859

860
            slices = []
4✔
861
            for _ in range(ndim):
4✔
862
                slices.append(ast.Slice(lower=None, upper=None, step=None))
4✔
863

864
            if ndim == 1:
4✔
865
                slice_arg = slices[0]
×
866
            else:
867
                slice_arg = ast.Tuple(elts=slices, ctx=ast.Load())
4✔
868

869
            slice_node = ast.Subscript(
4✔
870
                value=node.target, slice=slice_arg, ctx=ast.Store()
871
            )
872

873
            new_node = ast.Assign(
4✔
874
                targets=[slice_node],
875
                value=ast.BinOp(left=node.target, op=node.op, right=node.value),
876
            )
877
            self.visit_Assign(new_node)
4✔
878
            return
4✔
879

880
        # Array-member attribute target: e.g. `domain.x += domain.xd * dt`.
881
        # Resolve the member to its view container, then rewrite as a whole-array
882
        # slice assignment `member[:] = member op value` (writes propagate back
883
        # through the struct member pointer).
884
        if isinstance(node.target, ast.Attribute):
4✔
885
            resolved = self.visit(node.target)
4✔
886
            if isinstance(resolved, str) and resolved in self.tensor_table:
4✔
887
                ndim = len(self.tensor_table[resolved].shape)
4✔
888
                slices = [
4✔
889
                    ast.Slice(lower=None, upper=None, step=None) for _ in range(ndim)
890
                ]
891
                slice_arg = (
4✔
892
                    slices[0] if ndim == 1 else ast.Tuple(elts=slices, ctx=ast.Load())
893
                )
894
                slice_node = ast.Subscript(
4✔
895
                    value=ast.Name(id=resolved, ctx=ast.Load()),
896
                    slice=slice_arg,
897
                    ctx=ast.Store(),
898
                )
899
                new_node = ast.Assign(
4✔
900
                    targets=[slice_node],
901
                    value=ast.BinOp(
902
                        left=ast.Name(id=resolved, ctx=ast.Load()),
903
                        op=node.op,
904
                        right=node.value,
905
                    ),
906
                )
907
                ast.copy_location(new_node, node)
4✔
908
                self.visit_Assign(new_node)
4✔
909
                return
4✔
910

911
        new_node = ast.Assign(
4✔
912
            targets=[node.target],
913
            value=ast.BinOp(left=node.target, op=node.op, right=node.value),
914
        )
915
        self.visit_Assign(new_node)
4✔
916

917
    def visit_Assign(self, node):
4✔
918
        # Handle multiple targets: a = b = c or a, b = expr
919
        if len(node.targets) > 1:
4✔
920
            rhs_result = self.visit(node.value)
4✔
921
            if isinstance(rhs_result, str) and rhs_result in self.container_table:
4✔
922
                val_node = ast.Name(id=rhs_result, ctx=ast.Load())
4✔
923
                ast.copy_location(val_node, node)
4✔
924
            else:
925
                # Literals / expressions without a container: re-emit directly.
926
                val_node = node.value
×
927

928
            # Assign the evaluated value to each target
929
            for target in node.targets:
4✔
930
                assign = ast.Assign(targets=[target], value=val_node)
4✔
931
                ast.copy_location(assign, node)
4✔
932
                self.visit_Assign(assign)
4✔
933
            return
4✔
934
        target = node.targets[0]
4✔
935

936
        # Handle tuple unpacking: I, J, K = expr1, expr2, expr3
937
        if isinstance(target, ast.Tuple):
4✔
938
            if isinstance(node.value, ast.Tuple):
4✔
939
                if len(target.elts) != len(node.value.elts):
4✔
940
                    raise ValueError("Tuple unpacking size mismatch")
×
941
                for tgt, val in zip(target.elts, node.value.elts):
4✔
942
                    assign = ast.Assign(targets=[tgt], value=val)
4✔
943
                    ast.copy_location(assign, node)
4✔
944
                    self.visit_Assign(assign)
4✔
945
                return
4✔
946

947
            # RHS is not a literal tuple (e.g. a call returning multiple values,
948
            # such as `x1, y1, z1 = collect_nodes(domain, ...)`). Evaluate it;
949
            # inline calls with a tuple return yield a list of result names.
950
            result = self.visit(node.value)
4✔
951
            if isinstance(result, (list, tuple)):
4✔
952
                if len(target.elts) != len(result):
4✔
NEW
953
                    raise ValueError("Tuple unpacking size mismatch")
×
954
                for tgt, name in zip(target.elts, result):
4✔
955
                    assign = ast.Assign(
4✔
956
                        targets=[tgt], value=ast.Name(id=name, ctx=ast.Load())
957
                    )
958
                    ast.copy_location(assign, node)
4✔
959
                    self.visit_Assign(assign)
4✔
960
                return
4✔
NEW
961
            raise NotImplementedError(
×
962
                "Tuple unpacking from non-tuple values not supported"
963
            )
964

965
        # Tuple/list-valued variable: `shp = (a.shape[0], a.shape[1])`. Record
966
        # it so it can later be resolved as an array-creation shape argument.
967
        # Only stored (not lowered); other uses of the variable are unaffected.
968
        if isinstance(target, ast.Name) and isinstance(
4✔
969
            node.value, (ast.Tuple, ast.List)
970
        ):
971
            self.tuple_table[target.id] = node.value
4✔
972
            return
4✔
973

974
        # Special cases, where rhs is not just a simple expression but requires special handling
975
        if self.numpy_visitor.is_gemm(node.value):
4✔
976
            if self.numpy_visitor.handle_gemm(target, node.value):
4✔
977
                return
4✔
978
            if self.numpy_visitor.handle_dot(target, node.value):
4✔
979
                return
×
980
        if self.numpy_visitor.is_outer(node.value):
4✔
981
            if self.numpy_visitor.handle_outer(target, node.value):
4✔
982
                return
4✔
983
        if self.numpy_visitor.is_transpose(node.value):
4✔
984
            if self.numpy_visitor.handle_transpose(target, node.value):
4✔
985
                return
4✔
986

987
        # Handle subscript assignments: a[i] = val or a[i, j] = val
988
        if isinstance(target, ast.Subscript):
4✔
989
            debug_info = get_debug_info(node, self.filename, self.function_name)
4✔
990

991
            target_name = self.visit(target.value)
4✔
992
            indices = []
4✔
993
            if isinstance(target.slice, ast.Tuple):
4✔
994
                indices = target.slice.elts
4✔
995
            else:
996
                indices = [target.slice]
4✔
997

998
            # Partial indexing on the LHS: `b[i] = <sub-array>` on an N-D array
999
            # (N > number of indices) means `b[i, :] = ...`. Pad implicit
1000
            # trailing full slices so it routes to slice-assignment. This only
1001
            # applies to genuine scalar partial indices -- never a boolean mask
1002
            # (`a[a < 0] = ...`) or a fancy/scatter index, which address the
1003
            # array as a whole and must reach their dedicated handlers below.
1004
            if target_name in self.tensor_table:
4✔
1005
                ndim = len(self.tensor_table[target_name].shape)
4✔
1006
                is_special_index = len(indices) == 1 and (
4✔
1007
                    self._is_boolean_mask_index(indices[0])
1008
                    or self._is_array_valued_index(indices[0])
1009
                )
1010
                if not is_special_index and 0 < len(indices) < ndim:
4✔
1011
                    indices = list(indices) + [
4✔
1012
                        ast.Slice(lower=None, upper=None, step=None)
1013
                        for _ in range(ndim - len(indices))
1014
                    ]
1015

1016
            # Handle slice assignment separately
1017
            has_slice = False
4✔
1018
            for idx in indices:
4✔
1019
                if isinstance(idx, ast.Slice):
4✔
1020
                    has_slice = True
4✔
1021
                    break
4✔
1022

1023
            if has_slice:
4✔
1024
                self._handle_slice_assignment(
4✔
1025
                    target, node.value, target_name, indices, debug_info
1026
                )
1027
                return
4✔
1028

1029
            # Handle boolean-mask assignment: target[mask] = value
1030
            if (
4✔
1031
                len(indices) == 1
1032
                and target_name in self.tensor_table
1033
                and self._is_boolean_mask_index(indices[0])
1034
            ):
1035
                self._handle_masked_assignment(
4✔
1036
                    target, indices[0], node.value, target_name, node
1037
                )
1038
                return
4✔
1039

1040
            # Scatter assignment: target[idx] = value where idx is an index array.
1041
            if (
4✔
1042
                len(indices) == 1
1043
                and target_name in self.tensor_table
1044
                and self._is_array_index(indices[0])
1045
            ):
1046
                self._handle_scatter_assignment(
4✔
1047
                    target_name,
1048
                    indices[0],
1049
                    node.value,
1050
                    accumulate=False,
1051
                    debug_info=debug_info,
1052
                )
1053
                return
4✔
1054

1055
            # Handle rhs and store in scalar tmp
1056
            rhs_tmp = self.visit(node.value)
4✔
1057

1058
            # Evaluate the LHS (index) expression before creating the store
1059
            # block/tasklet.
1060
            lhs_expr = self.visit(target)
4✔
1061

1062
            block = self.builder.add_block(debug_info)
4✔
1063
            t_task = self.builder.add_tasklet(
4✔
1064
                block, TaskletCode.assign, ["_in"], ["_out"], debug_info
1065
            )
1066

1067
            t_src, src_sub = self._add_read(block, rhs_tmp, debug_info)
4✔
1068
            self.builder.add_memlet(
4✔
1069
                block, t_src, "void", t_task, "_in", src_sub, None, debug_info
1070
            )
1071

1072
            if "(" in lhs_expr and lhs_expr.endswith(")"):
4✔
1073
                subset = lhs_expr[lhs_expr.find("(") + 1 : -1]
4✔
1074
                tensor_dst = self.tensor_table[target_name]
4✔
1075

1076
                t_dst = self.builder.add_access(block, target_name, debug_info)
4✔
1077
                self.builder.add_memlet(
4✔
1078
                    block, t_task, "_out", t_dst, "void", subset, tensor_dst, debug_info
1079
                )
1080
            else:
1081
                t_dst = self.builder.add_access(block, target_name, debug_info)
×
1082
                self.builder.add_memlet(
×
1083
                    block, t_task, "_out", t_dst, "void", "", None, debug_info
1084
                )
1085
            return
4✔
1086

1087
        # Assignment to an array-member attribute: `domain.dxx = value`.
1088
        # Resolve the member view and perform a whole-array slice assignment so
1089
        # the write propagates back through the struct member pointer.
1090
        if isinstance(target, ast.Attribute):
4✔
1091
            # Scalar struct member: `domain.dtcourant = <scalar>`. Store the
1092
            # value back into obj[0, member_index] so it round-trips through the
1093
            # marshalled struct (checked before the array path, which would
1094
            # otherwise emit a spurious member load).
1095
            scalar_member = self._struct_scalar_member(target)
4✔
1096
            if scalar_member is not None:
4✔
1097
                obj_name, member_index, member_type = scalar_member
4✔
1098
                self._store_scalar_member(
4✔
1099
                    obj_name, member_index, member_type, node.value, node
1100
                )
1101
                return
4✔
1102

1103
            resolved = self.visit(target)
4✔
1104
            if isinstance(resolved, str) and resolved in self.tensor_table:
4✔
1105
                ndim = len(self.tensor_table[resolved].shape)
4✔
1106
                slices = [
4✔
1107
                    ast.Slice(lower=None, upper=None, step=None) for _ in range(ndim)
1108
                ]
1109
                slice_arg = (
4✔
1110
                    slices[0] if ndim == 1 else ast.Tuple(elts=slices, ctx=ast.Load())
1111
                )
1112
                slice_node = ast.Subscript(
4✔
1113
                    value=ast.Name(id=resolved, ctx=ast.Load()),
1114
                    slice=slice_arg,
1115
                    ctx=ast.Store(),
1116
                )
1117
                new_assign = ast.Assign(targets=[slice_node], value=node.value)
4✔
1118
                ast.copy_location(new_assign, node)
4✔
1119
                self.visit_Assign(new_assign)
4✔
1120
                return
4✔
1121

1122
        # Fallback: lhs is a simple scalar assignments
1123
        if not isinstance(target, ast.Name):
4✔
1124
            raise NotImplementedError("Only assignment to variables supported")
×
1125

1126
        target_name = target.id
4✔
1127
        rhs_tmp = self.visit(node.value)
4✔
1128
        debug_info = get_debug_info(node, self.filename, self.function_name)
4✔
1129

1130
        if not self.builder.exists(target_name):
4✔
1131
            if isinstance(node.value, ast.Constant):
4✔
1132
                val = node.value.value
4✔
1133
                if isinstance(val, int):
4✔
1134
                    dtype = Scalar(PrimitiveType.Int64)
4✔
1135
                elif isinstance(val, float):
4✔
1136
                    dtype = Scalar(PrimitiveType.Double)
4✔
1137
                elif isinstance(val, bool):
×
1138
                    dtype = Scalar(PrimitiveType.Bool)
×
1139
                else:
1140
                    raise NotImplementedError(f"Cannot infer type for {val}")
×
1141

1142
                self.builder.add_container(target_name, dtype, False)
4✔
1143
                self.container_table[target_name] = dtype
4✔
1144
            else:
1145
                self.builder.add_container(
4✔
1146
                    target_name, self.container_table[rhs_tmp], False
1147
                )
1148
                self.container_table[target_name] = self.container_table[rhs_tmp]
4✔
1149

1150
        if rhs_tmp in self.tensor_table:
4✔
1151
            self.tensor_table[target_name] = self.tensor_table[rhs_tmp]
4✔
1152

1153
        # Also copy shapes_runtime_info if available
1154
        if rhs_tmp in self.shapes_runtime_info:
4✔
1155
            self.shapes_runtime_info[target_name] = self.shapes_runtime_info[rhs_tmp]
4✔
1156

1157
        # Distinguish assignments: scalar -> tasklet, pointer -> reference_memlet
1158
        src_type = self.container_table.get(rhs_tmp)
4✔
1159
        dst_type = self.container_table[target_name]
4✔
1160
        if src_type and isinstance(src_type, Pointer) and isinstance(dst_type, Pointer):
4✔
1161
            block = self.builder.add_block(debug_info)
4✔
1162
            t_src = self.builder.add_access(block, rhs_tmp, debug_info)
4✔
1163
            t_dst = self.builder.add_access(block, target_name, debug_info)
4✔
1164
            self.builder.add_reference_memlet(
4✔
1165
                block, t_src, t_dst, "0", src_type, debug_info
1166
            )
1167
        elif (src_type and isinstance(src_type, Scalar)) or isinstance(
4✔
1168
            dst_type, Scalar
1169
        ):
1170
            block = self.builder.add_block(debug_info)
4✔
1171
            t_dst = self.builder.add_access(block, target_name, debug_info)
4✔
1172
            t_task = self.builder.add_tasklet(
4✔
1173
                block, TaskletCode.assign, ["_in"], ["_out"], debug_info
1174
            )
1175

1176
            if src_type:
4✔
1177
                t_src = self.builder.add_access(block, rhs_tmp, debug_info)
4✔
1178
            else:
1179
                t_src = self.builder.add_constant(block, rhs_tmp, dst_type, debug_info)
4✔
1180

1181
            self.builder.add_memlet(
4✔
1182
                block, t_src, "void", t_task, "_in", "", None, debug_info
1183
            )
1184
            self.builder.add_memlet(
4✔
1185
                block, t_task, "_out", t_dst, "void", "", None, debug_info
1186
            )
1187

1188
    def visit_Raise(self, node):
4✔
1189
        # No-op: `raise` statements are ignored. Exceptions are not modelled in
1190
        # the SDFG IR, so error-signalling paths (e.g. lulesh's
1191
        # `raise VolumeError(...)`) are simply dropped rather than lowered.
1192
        # We deliberately do NOT visit the exception expression so that the
1193
        # exception constructor/attribute (which has no SDFG lowering) is not
1194
        # evaluated.
1195
        pass
4✔
1196

1197
    def visit_Expr(self, node):
4✔
1198
        self.visit(node.value)
4✔
1199

1200
    def visit_IfExp(self, node):
4✔
1201
        # Conditional expression: `body if test else orelse`.
1202
        # Lower it to an if/else that assigns both branches to a temporary,
1203
        # reusing the statement-level if and assignment handling.
1204
        tmp_name = self.builder.find_new_name()
4✔
1205
        assign_body = ast.Assign(
4✔
1206
            targets=[ast.Name(id=tmp_name, ctx=ast.Store())], value=node.body
1207
        )
1208
        assign_else = ast.Assign(
4✔
1209
            targets=[ast.Name(id=tmp_name, ctx=ast.Store())], value=node.orelse
1210
        )
1211
        if_node = ast.If(test=node.test, body=[assign_body], orelse=[assign_else])
4✔
1212
        ast.copy_location(if_node, node)
4✔
1213
        ast.fix_missing_locations(if_node)
4✔
1214
        self.visit(if_node)
4✔
1215
        return tmp_name
4✔
1216

1217
    def visit_If(self, node):
4✔
1218
        cond = self.visit(node.test)
4✔
1219
        debug_info = get_debug_info(node, self.filename, self.function_name)
4✔
1220
        self.builder.begin_if(f"{cond} != false", debug_info)
4✔
1221

1222
        for stmt in node.body:
4✔
1223
            self.visit(stmt)
4✔
1224

1225
        if node.orelse:
4✔
1226
            self.builder.begin_else(debug_info)
4✔
1227
            for stmt in node.orelse:
4✔
1228
                self.visit(stmt)
4✔
1229

1230
        self.builder.end_if()
4✔
1231

1232
    def visit_While(self, node):
4✔
1233
        if node.orelse:
4✔
1234
            raise NotImplementedError("while-else is not supported")
×
1235

1236
        debug_info = get_debug_info(node, self.filename, self.function_name)
4✔
1237
        self.builder.begin_while(debug_info)
4✔
1238

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

1242
        # Create if-break pattern: if condition is false, break
1243
        self.builder.begin_if(f"{cond} == false", debug_info)
4✔
1244
        self.builder.add_break(debug_info)
4✔
1245
        self.builder.end_if()
4✔
1246

1247
        for stmt in node.body:
4✔
1248
            self.visit(stmt)
4✔
1249

1250
        self.builder.end_while()
4✔
1251

1252
    def visit_Break(self, node):
4✔
1253
        debug_info = get_debug_info(node, self.filename, self.function_name)
4✔
1254
        self.builder.add_break(debug_info)
4✔
1255

1256
    def visit_Continue(self, node):
4✔
1257
        debug_info = get_debug_info(node, self.filename, self.function_name)
4✔
1258
        self.builder.add_continue(debug_info)
4✔
1259

1260
    def visit_For(self, node):
4✔
1261
        if node.orelse:
4✔
1262
            raise NotImplementedError("while-else is not supported")
×
1263
        if not isinstance(node.target, ast.Name):
4✔
1264
            raise NotImplementedError("Only simple for loops supported")
×
1265

1266
        var = node.target.id
4✔
1267
        debug_info = get_debug_info(node, self.filename, self.function_name)
4✔
1268

1269
        # Check if iterating over a range() call
1270
        if (
4✔
1271
            isinstance(node.iter, ast.Call)
1272
            and isinstance(node.iter.func, ast.Name)
1273
            and node.iter.func.id == "range"
1274
        ):
1275
            args = node.iter.args
4✔
1276
            if len(args) == 1:
4✔
1277
                start = "0"
4✔
1278
                end = self.visit(args[0])
4✔
1279
                step = "1"
4✔
1280
            elif len(args) == 2:
4✔
1281
                start = self.visit(args[0])
4✔
1282
                end = self.visit(args[1])
4✔
1283
                step = "1"
4✔
1284
            elif len(args) == 3:
4✔
1285
                start = self.visit(args[0])
4✔
1286
                end = self.visit(args[1])
4✔
1287

1288
                # Special handling for step to avoid creating tasklets for constants
1289
                step_node = args[2]
4✔
1290
                if isinstance(step_node, ast.Constant):
4✔
1291
                    step = str(step_node.value)
4✔
1292
                elif (
4✔
1293
                    isinstance(step_node, ast.UnaryOp)
1294
                    and isinstance(step_node.op, ast.USub)
1295
                    and isinstance(step_node.operand, ast.Constant)
1296
                ):
1297
                    step = f"-{step_node.operand.value}"
4✔
1298
                else:
1299
                    step = self.visit(step_node)
×
1300
            else:
1301
                raise ValueError("Invalid range arguments")
×
1302

1303
            if not self.builder.exists(var):
4✔
1304
                self.builder.add_container(var, Scalar(PrimitiveType.Int64), False)
4✔
1305
                self.container_table[var] = Scalar(PrimitiveType.Int64)
4✔
1306

1307
            self.builder.begin_for(var, start, end, step, debug_info)
4✔
1308

1309
            for stmt in node.body:
4✔
1310
                self.visit(stmt)
4✔
1311

1312
            self.builder.end_for()
4✔
1313
            return
4✔
1314

1315
        # Check if iterating over an ndarray (for x in array)
1316
        if isinstance(node.iter, ast.Name):
×
1317
            iter_name = node.iter.id
×
1318
            if iter_name in self.tensor_table:
×
1319
                arr_info = self.tensor_table[iter_name]
×
1320
                if len(arr_info.shape) == 0:
×
1321
                    raise NotImplementedError("Cannot iterate over 0-dimensional array")
×
1322

1323
                # Get the size of the first dimension
1324
                arr_size = arr_info.shape[0]
×
1325

1326
                # Create a hidden index variable for the loop
1327
                idx_var = self.builder.find_new_name()
×
1328
                if not self.builder.exists(idx_var):
×
1329
                    self.builder.add_container(
×
1330
                        idx_var, Scalar(PrimitiveType.Int64), False
1331
                    )
1332
                    self.container_table[idx_var] = Scalar(PrimitiveType.Int64)
×
1333

1334
                # Determine the type of the loop variable (element type)
1335
                # For a 1D array, it's a scalar; for ND array, it's a view of N-1 dimensions
1336
                if len(arr_info.shape) == 1:
×
1337
                    # Element is a scalar - get the element type from the array's type
1338
                    arr_type = self.container_table.get(iter_name)
×
1339
                    if isinstance(arr_type, Pointer):
×
1340
                        elem_type = arr_type.pointee_type
×
1341
                    else:
1342
                        elem_type = Scalar(PrimitiveType.Double)  # Default fallback
×
1343

1344
                    if not self.builder.exists(var):
×
1345
                        self.builder.add_container(var, elem_type, False)
×
1346
                        self.container_table[var] = elem_type
×
1347
                else:
1348
                    # For multi-dimensional arrays, create a view/slice
1349
                    # The loop variable becomes a pointer to the sub-array
1350
                    inner_shapes = arr_info.shape[1:]
×
1351
                    inner_ndim = len(arr_info.shape) - 1
×
1352

1353
                    arr_type = self.container_table.get(iter_name)
×
1354
                    if isinstance(arr_type, Pointer):
×
1355
                        elem_type = arr_type  # Keep as pointer type for views
×
1356
                    else:
1357
                        elem_type = Pointer(Scalar(PrimitiveType.Double))
×
1358

1359
                    if not self.builder.exists(var):
×
1360
                        self.builder.add_container(var, elem_type, False)
×
1361
                        self.container_table[var] = elem_type
×
1362

1363
                    # Register the view in tensor_table
1364
                    self.tensor_table[var] = Tensor(
×
1365
                        element_type_from_sdfg_type(elem_type), inner_shapes
1366
                    )
1367

1368
                # Begin the for loop
1369
                self.builder.begin_for(idx_var, "0", str(arr_size), "1", debug_info)
×
1370

1371
                # Generate the assignment: var = array[idx_var]
1372
                # Create an AST node for the assignment and visit it
1373
                assign_node = ast.Assign(
×
1374
                    targets=[ast.Name(id=var, ctx=ast.Store())],
1375
                    value=ast.Subscript(
1376
                        value=ast.Name(id=iter_name, ctx=ast.Load()),
1377
                        slice=ast.Name(id=idx_var, ctx=ast.Load()),
1378
                        ctx=ast.Load(),
1379
                    ),
1380
                )
1381
                ast.copy_location(assign_node, node)
×
1382
                self.visit_Assign(assign_node)
×
1383

1384
                # Visit the loop body
1385
                for stmt in node.body:
×
1386
                    self.visit(stmt)
×
1387

1388
                self.builder.end_for()
×
1389
                return
×
1390

1391
        raise NotImplementedError(
×
1392
            f"Only range() loops and iteration over ndarrays supported, got: {ast.dump(node.iter)}"
1393
        )
1394

1395
    def visit_Return(self, node):
4✔
1396
        if node.value is None:
4✔
1397
            debug_info = get_debug_info(node, self.filename, self.function_name)
×
1398
            # Emit frees for all deferred allocations before returning
1399
            if self.memory_handler.has_allocations():
×
1400
                self.memory_handler.emit_frees()
×
1401
            self.builder.add_return("", debug_info)
×
1402
            return
×
1403

1404
        if isinstance(node.value, ast.Tuple):
4✔
1405
            values = node.value.elts
4✔
1406
        else:
1407
            values = [node.value]
4✔
1408

1409
        parsed_values = [self.visit(v) for v in values]
4✔
1410
        debug_info = get_debug_info(node, self.filename, self.function_name)
4✔
1411

1412
        if self.infer_return_type:
4✔
1413
            for i, res in enumerate(parsed_values):
4✔
1414
                ret_name = f"_docc_ret_{i}"
4✔
1415
                if not self.builder.exists(ret_name):
4✔
1416
                    dtype = Scalar(PrimitiveType.Double)
4✔
1417
                    if res in self.container_table:
4✔
1418
                        dtype = self.container_table[res]
4✔
1419
                    elif isinstance(values[i], ast.Constant):
×
1420
                        val = values[i].value
×
1421
                        if isinstance(val, int):
×
1422
                            dtype = Scalar(PrimitiveType.Int64)
×
1423
                        elif isinstance(val, float):
×
1424
                            dtype = Scalar(PrimitiveType.Double)
×
1425
                        elif isinstance(val, bool):
×
1426
                            dtype = Scalar(PrimitiveType.Bool)
×
1427

1428
                    # Wrap Scalar in Pointer. Keep Arrays/Pointers as is.
1429
                    arg_type = dtype
4✔
1430
                    if isinstance(dtype, Scalar):
4✔
1431
                        arg_type = Pointer(dtype)
4✔
1432

1433
                    self.builder.add_container(ret_name, arg_type, is_argument=True)
4✔
1434
                    self.container_table[ret_name] = arg_type
4✔
1435

1436
                    if res in self.tensor_table:
4✔
1437
                        self.tensor_table[ret_name] = self.tensor_table[res]
4✔
1438

1439
            self.infer_return_type = False
4✔
1440

1441
        for i, res in enumerate(parsed_values):
4✔
1442
            ret_name = f"_docc_ret_{i}"
4✔
1443
            typ = self.container_table.get(ret_name)
4✔
1444

1445
            is_array_return = False
4✔
1446
            if res in self.tensor_table:
4✔
1447
                # Only treat as array return if it has dimensions
1448
                # 0-d arrays (scalars) should be handled by scalar assignment
1449
                if len(self.tensor_table[res].shape) > 0:
4✔
1450
                    is_array_return = True
4✔
1451
            elif res in self.container_table:
4✔
1452
                if isinstance(self.container_table[res], Pointer):
4✔
1453
                    is_array_return = True
×
1454

1455
            # Simple Scalar Assignment
1456
            if not is_array_return:
4✔
1457
                block = self.builder.add_block(debug_info)
4✔
1458
                t_dst = self.builder.add_access(block, ret_name, debug_info)
4✔
1459

1460
                t_src, src_sub = self._add_read(block, res, debug_info)
4✔
1461

1462
                t_task = self.builder.add_tasklet(
4✔
1463
                    block, TaskletCode.assign, ["_in"], ["_out"], debug_info
1464
                )
1465
                self.builder.add_memlet(
4✔
1466
                    block, t_src, "void", t_task, "_in", src_sub, None, debug_info
1467
                )
1468
                self.builder.add_memlet(
4✔
1469
                    block, t_task, "_out", t_dst, "void", "0", None, debug_info
1470
                )
1471

1472
            # Array Assignment (Copy)
1473
            else:
1474
                # Record shape for metadata
1475
                if res in self.tensor_table:
4✔
1476
                    # Prefer runtime shapes if available (for indirect access patterns)
1477
                    # Fall back to regular shapes otherwise
1478
                    res_info = self.tensor_table[res]
4✔
1479
                    if res in self.shapes_runtime_info:
4✔
1480
                        shape = self.shapes_runtime_info[res]
4✔
1481
                    else:
1482
                        shape = res_info.shape
4✔
1483
                    # Convert to string expressions
1484
                    self.captured_return_shapes[ret_name] = [str(s) for s in shape]
4✔
1485

1486
                    # Return arrays are always contiguous - compute fresh strides
1487
                    contiguous_strides = self.numpy_visitor._compute_strides(shape, "C")
4✔
1488
                    self.captured_return_strides[ret_name] = [
4✔
1489
                        str(s) for s in contiguous_strides
1490
                    ]
1491

1492
                    # Always overwrite tensor_table for return arrays with contiguous strides
1493
                    # (source tensor may have non-standard strides from views/flip)
1494
                    self.tensor_table[ret_name] = Tensor(
4✔
1495
                        res_info.element_type, shape, contiguous_strides
1496
                    )
1497

1498
                # Copy Logic using visit_Assign
1499
                ndim = 1
4✔
1500
                if ret_name in self.tensor_table:
4✔
1501
                    ndim = len(self.tensor_table[ret_name].shape)
4✔
1502

1503
                slice_node = ast.Slice(lower=None, upper=None, step=None)
4✔
1504
                if ndim > 1:
4✔
1505
                    target_slice = ast.Tuple(elts=[slice_node] * ndim, ctx=ast.Load())
4✔
1506
                else:
1507
                    target_slice = slice_node
4✔
1508

1509
                target_sub = ast.Subscript(
4✔
1510
                    value=ast.Name(id=ret_name, ctx=ast.Load()),
1511
                    slice=target_slice,
1512
                    ctx=ast.Store(),
1513
                )
1514

1515
                # Value node reconstruction
1516
                if isinstance(values[i], ast.Name):
4✔
1517
                    val_node = values[i]
4✔
1518
                else:
1519
                    val_node = ast.Name(id=res, ctx=ast.Load())
4✔
1520

1521
                assign_node = ast.Assign(targets=[target_sub], value=val_node)
4✔
1522
                self.visit_Assign(assign_node)
4✔
1523

1524
        # Emit frees for all deferred allocations before returning
1525
        if self.memory_handler.has_allocations():
4✔
1526
            self.memory_handler.emit_frees()
4✔
1527

1528
        # Add control flow return to exit the function/path
1529
        self.builder.add_return("", debug_info)
4✔
1530

1531
    def visit_Call(self, node):
4✔
1532
        func_name = ""
4✔
1533
        module_name = ""
4✔
1534
        submodule_name = ""
4✔
1535
        if isinstance(node.func, ast.Attribute):
4✔
1536
            if isinstance(node.func.value, ast.Name):
4✔
1537
                if node.func.value.id == "math":
4✔
1538
                    module_name = "math"
4✔
1539
                    func_name = node.func.attr
4✔
1540
                elif node.func.value.id in ["numpy", "np"]:
4✔
1541
                    module_name = "numpy"
4✔
1542
                    func_name = node.func.attr
4✔
1543
                else:
1544
                    array_name = node.func.value.id
4✔
1545
                    method_name = node.func.attr
4✔
1546
                    if array_name in self.tensor_table and method_name == "astype":
4✔
1547
                        return self.numpy_visitor.handle_numpy_astype(node, array_name)
4✔
1548
                    elif array_name in self.tensor_table and method_name == "copy":
4✔
1549
                        return self.numpy_visitor.handle_numpy_copy(node, array_name)
4✔
1550
            elif isinstance(node.func.value, ast.Attribute):
4✔
1551
                if (
4✔
1552
                    isinstance(node.func.value.value, ast.Name)
1553
                    and node.func.value.value.id in ["numpy", "np"]
1554
                    and node.func.attr == "outer"
1555
                ):
1556
                    ufunc_name = node.func.value.attr
4✔
1557
                    return self.numpy_visitor.handle_ufunc_outer(node, ufunc_name)
4✔
1558

1559
        elif isinstance(node.func, ast.Name):
4✔
1560
            func_name = node.func.id
4✔
1561

1562
        if module_name == "numpy":
4✔
1563
            if self.numpy_visitor.has_handler(func_name):
4✔
1564
                return self.numpy_visitor.handle_numpy_call(node, func_name)
4✔
1565

1566
        if module_name == "math":
4✔
1567
            if self.math_handler.has_handler(func_name):
4✔
1568
                return self.math_handler.handle_math_call(node, func_name)
4✔
1569

1570
        if self.python_handler.has_handler(func_name):
4✔
1571
            return self.python_handler.handle_python_call(node, func_name)
4✔
1572

1573
        if func_name in self.globals_dict:
4✔
1574
            obj = self.globals_dict[func_name]
4✔
1575
            if inspect.isfunction(obj):
4✔
1576
                return self._handle_inline_call(node, obj)
4✔
1577

1578
        raise NotImplementedError(f"Function call {func_name} not supported")
×
1579

1580
    @staticmethod
4✔
1581
    def _stmts_always_return(body):
4✔
1582
        """True if executing `body` always hits a `return` (so any statements
1583
        that follow it are only reachable when it does not)."""
1584
        if not body:
4✔
NEW
1585
            return False
×
1586
        last = body[-1]
4✔
1587
        if isinstance(last, ast.Return):
4✔
1588
            return True
4✔
NEW
1589
        if isinstance(last, ast.If):
×
NEW
1590
            return ASTParser._stmts_always_return(
×
1591
                last.body
1592
            ) and ASTParser._stmts_always_return(last.orelse)
NEW
1593
        return False
×
1594

1595
    def _lift_early_returns(self, body):
4✔
1596
        """Rewrite early `return`s into if/else so the inliner's return->assign
1597
        conversion preserves control flow.
1598

1599
        The inliner turns each `return X` into `res = X`; without this, a guard
1600
        like ``if c: return a`` followed by ``return b`` would emit
1601
        ``if c: res = a`` then unconditionally ``res = b`` (fallback always
1602
        wins). Here, statements following an `if` whose body always returns are
1603
        moved into that `if`'s `else`, and dead code after a `return` is dropped.
1604
        """
1605
        result = []
4✔
1606
        for i, stmt in enumerate(body):
4✔
1607
            if isinstance(stmt, ast.If):
4✔
1608
                new_if = copy.copy(stmt)
4✔
1609
                new_if.body = self._lift_early_returns(stmt.body)
4✔
1610
                new_if.orelse = self._lift_early_returns(stmt.orelse)
4✔
1611
                rest = body[i + 1 :]
4✔
1612
                if rest and self._stmts_always_return(new_if.body):
4✔
1613
                    # The remaining statements are only reachable when the guard
1614
                    # is false: fold them into the else branch.
1615
                    new_if.orelse = list(new_if.orelse) + self._lift_early_returns(rest)
4✔
1616
                    result.append(new_if)
4✔
1617
                    return result
4✔
NEW
1618
                result.append(new_if)
×
1619
            else:
1620
                result.append(stmt)
4✔
1621
                if isinstance(stmt, ast.Return):
4✔
1622
                    # Statements after a return are unreachable.
1623
                    return result
4✔
1624
        return result
4✔
1625

1626
    def _handle_inline_call(self, node, func_obj):
4✔
1627
        try:
4✔
1628
            source_lines, start_line = inspect.getsourcelines(func_obj)
4✔
1629
            source = textwrap.dedent("".join(source_lines))
4✔
1630
            tree = ast.parse(source)
4✔
1631
            func_def = tree.body[0]
4✔
1632
        except Exception as e:
×
1633
            raise NotImplementedError(
×
1634
                f"Could not parse function {func_obj.__name__}: {e}"
1635
            )
1636

1637
        if len(node.args) != len(func_def.args.args):
4✔
UNCOV
1638
            raise NotImplementedError(
×
1639
                f"Argument count mismatch for {func_obj.__name__}"
1640
            )
1641

1642
        # Compile-time tuple/list literal arguments cannot be resolved to a data
1643
        # container (they have no runtime storage). Substitute them directly
1644
        # into the inlined body instead - e.g. passing `nodes=(0, 1, 2, 3)` so
1645
        # that an internal `a, b, c, d = nodes` unpacks the literal tuple.
1646
        literal_arg_types = (ast.Tuple, ast.List)
4✔
1647
        substitutions = {}
4✔
1648
        arg_vars = []
4✔
1649
        for arg_def, arg in zip(func_def.args.args, node.args):
4✔
1650
            if isinstance(arg, literal_arg_types):
4✔
1651
                substitutions[arg_def.arg] = arg
4✔
1652
                arg_vars.append(None)
4✔
1653
            elif (
4✔
1654
                isinstance(arg, ast.Name)
1655
                and arg.id in self.globals_dict
1656
                and isinstance(self.globals_dict[arg.id], (dict, list, tuple, set))
1657
            ):
1658
                # A global compile-time-constant collection (e.g. a dict of bit
1659
                # masks). It has no runtime container; substitute the reference
1660
                # into the body so constant subscripts fold at compile time.
1661
                substitutions[arg_def.arg] = arg
4✔
1662
                arg_vars.append(None)
4✔
1663
            else:
1664
                arg_vars.append(self.visit(arg))
4✔
1665

1666
        suffix = f"_{func_obj.__name__}_{self._get_unique_id()}"
4✔
1667
        res_name = f"_res{suffix}"
4✔
1668

1669
        # Combine globals with closure variables of the inlined function
1670
        combined_globals = dict(self.globals_dict)
4✔
1671
        closure_constants = {}  # name -> value for numeric closure vars
4✔
1672
        if func_obj.__closure__ is not None and func_obj.__code__.co_freevars:
4✔
1673
            for name, cell in zip(func_obj.__code__.co_freevars, func_obj.__closure__):
4✔
1674
                val = cell.cell_contents
4✔
1675
                combined_globals[name] = val
4✔
1676
                # Track numeric constants for injection
1677
                if isinstance(val, (int, float)) and not isinstance(val, bool):
4✔
1678
                    closure_constants[name] = val
4✔
1679

1680
        class VariableRenamer(ast.NodeTransformer):
4✔
1681
            BUILTINS = {
4✔
1682
                "range",
1683
                "len",
1684
                "int",
1685
                "float",
1686
                "bool",
1687
                "str",
1688
                "list",
1689
                "dict",
1690
                "tuple",
1691
                "set",
1692
                "print",
1693
                "abs",
1694
                "min",
1695
                "max",
1696
                "sum",
1697
                "enumerate",
1698
                "zip",
1699
                "map",
1700
                "filter",
1701
                "sorted",
1702
                "reversed",
1703
                "True",
1704
                "False",
1705
                "None",
1706
            }
1707

1708
            def __init__(self, suffix, globals_dict, substitutions):
4✔
1709
                self.suffix = suffix
4✔
1710
                self.globals_dict = globals_dict
4✔
1711
                self.substitutions = substitutions
4✔
1712

1713
            def visit_Name(self, node):
4✔
1714
                # Inline compile-time literal arguments (e.g. tuple constants).
1715
                if isinstance(node.ctx, ast.Load) and node.id in self.substitutions:
4✔
1716
                    return copy.deepcopy(self.substitutions[node.id])
4✔
1717
                if node.id in self.globals_dict or node.id in self.BUILTINS:
4✔
1718
                    return node
4✔
1719
                return ast.Name(id=f"{node.id}{self.suffix}", ctx=node.ctx)
4✔
1720

1721
            def visit_Return(self, node):
4✔
1722
                if node.value:
4✔
1723
                    val = self.visit(node.value)
4✔
1724
                    if isinstance(val, ast.Tuple):
4✔
1725
                        # Multi-value return: assign each element to its own
1726
                        # result container (res_name_0, res_name_1, ...).
1727
                        self.return_count = len(val.elts)
4✔
1728
                        assigns = []
4✔
1729
                        for i, elt in enumerate(val.elts):
4✔
1730
                            assigns.append(
4✔
1731
                                ast.Assign(
1732
                                    targets=[
1733
                                        ast.Name(id=f"{res_name}_{i}", ctx=ast.Store())
1734
                                    ],
1735
                                    value=elt,
1736
                                )
1737
                            )
1738
                        return assigns
4✔
1739
                    self.return_count = 1
4✔
1740
                    return ast.Assign(
4✔
1741
                        targets=[ast.Name(id=res_name, ctx=ast.Store())],
1742
                        value=val,
1743
                    )
1744
                return node
×
1745

1746
        renamer = VariableRenamer(suffix, combined_globals, substitutions)
4✔
1747
        renamer.return_count = 0
4✔
1748
        new_body = []
4✔
1749
        for stmt in self._lift_early_returns(func_def.body):
4✔
1750
            transformed = renamer.visit(stmt)
4✔
1751
            if isinstance(transformed, list):
4✔
1752
                new_body.extend(transformed)
4✔
1753
            elif transformed is not None:
4✔
1754
                new_body.append(transformed)
4✔
1755

1756
        param_assignments = []
4✔
1757

1758
        # Inject closure constants as assignments
1759
        for name, val in closure_constants.items():
4✔
1760
            if isinstance(val, int):
4✔
1761
                self.container_table[name] = Scalar(PrimitiveType.Int64)
4✔
1762
                self.builder.add_container(name, Scalar(PrimitiveType.Int64), False)
4✔
1763
                val_node = ast.Constant(value=val)
4✔
1764
            else:
1765
                self.container_table[name] = Scalar(PrimitiveType.Double)
×
1766
                self.builder.add_container(name, Scalar(PrimitiveType.Double), False)
×
1767
                val_node = ast.Constant(value=val)
×
1768
            assign = ast.Assign(
4✔
1769
                targets=[ast.Name(id=name, ctx=ast.Store())], value=val_node
1770
            )
1771
            param_assignments.append(assign)
4✔
1772

1773
        for arg_def, arg_val in zip(func_def.args.args, arg_vars):
4✔
1774
            if arg_def.arg in substitutions:
4✔
1775
                # Literal argument already substituted into the body.
1776
                continue
4✔
1777
            param_name = f"{arg_def.arg}{suffix}"
4✔
1778

1779
            if arg_val in self.container_table:
4✔
1780
                self.container_table[param_name] = self.container_table[arg_val]
4✔
1781
                self.builder.add_container(
4✔
1782
                    param_name, self.container_table[arg_val], False
1783
                )
1784
                val_node = ast.Name(id=arg_val, ctx=ast.Load())
4✔
1785
            elif self._is_int(arg_val):
×
1786
                self.container_table[param_name] = Scalar(PrimitiveType.Int64)
×
1787
                self.builder.add_container(
×
1788
                    param_name, Scalar(PrimitiveType.Int64), False
1789
                )
1790
                val_node = ast.Constant(value=int(arg_val))
×
1791
            else:
1792
                try:
×
1793
                    val = float(arg_val)
×
1794
                    self.container_table[param_name] = Scalar(PrimitiveType.Double)
×
1795
                    self.builder.add_container(
×
1796
                        param_name, Scalar(PrimitiveType.Double), False
1797
                    )
1798
                    val_node = ast.Constant(value=val)
×
1799
                except ValueError:
×
1800
                    val_node = ast.Name(id=arg_val, ctx=ast.Load())
×
1801

1802
            assign = ast.Assign(
4✔
1803
                targets=[ast.Name(id=param_name, ctx=ast.Store())], value=val_node
1804
            )
1805
            param_assignments.append(assign)
4✔
1806

1807
        final_body = param_assignments + new_body
4✔
1808

1809
        # Create a new parser instance for the inlined function
1810
        # Share memory_handler so hoisted allocations go to main function entry
1811
        parser = ASTParser(
4✔
1812
            self.builder,
1813
            self.tensor_table,
1814
            self.container_table,
1815
            globals_dict=combined_globals,
1816
            unique_counter_ref=self._unique_counter_ref,
1817
            structure_member_info=self.structure_member_info,
1818
            memory_handler=self.memory_handler,
1819
        )
1820

1821
        for stmt in final_body:
4✔
1822
            parser.visit(stmt)
4✔
1823

1824
        if getattr(renamer, "return_count", 0) > 1:
4✔
1825
            return [f"{res_name}_{i}" for i in range(renamer.return_count)]
4✔
1826
        return res_name
4✔
1827

1828
    def _add_assign_constant(self, target_name, value_str, dtype):
4✔
1829
        block = self.builder.add_block()
4✔
1830
        t_const = self.builder.add_constant(block, value_str, dtype)
4✔
1831
        t_dst = self.builder.add_access(block, target_name)
4✔
1832
        t_task = self.builder.add_tasklet(block, TaskletCode.assign, ["_in"], ["_out"])
4✔
1833
        self.builder.add_memlet(block, t_const, "void", t_task, "_in", "")
4✔
1834
        self.builder.add_memlet(block, t_task, "_out", t_dst, "void", "")
4✔
1835

1836
    def _materialize_global_array(self, name, val):
4✔
1837
        """Bake a module-global constant numpy array into the SDFG.
1838

1839
        Allocates an array container (hoisted to function entry) and initializes
1840
        every element with a constant-assign tasklet. The container is cached
1841
        per parser so repeated references (e.g. `gamma[i]` inside a loop) reuse
1842
        the same array. Only 1-/N-D arrays of scalar dtype are supported.
1843
        """
1844
        if not hasattr(self, "_global_array_cache"):
4✔
1845
            self._global_array_cache = {}
4✔
1846
        if name in self._global_array_cache:
4✔
NEW
1847
            return self._global_array_cache[name]
×
1848

1849
        arr = np.ascontiguousarray(val)
4✔
1850
        try:
4✔
1851
            dtype = sdfg_type_from_type(arr.dtype.type)
4✔
NEW
1852
        except ValueError:
×
NEW
1853
            raise NotImplementedError(
×
1854
                f"Unsupported dtype for global array '{name}': {arr.dtype}"
1855
            )
1856
        if not isinstance(dtype, Scalar):
4✔
NEW
1857
            raise NotImplementedError(
×
1858
                f"Global array '{name}' must have a scalar element type"
1859
            )
1860

1861
        shape = [str(int(d)) for d in arr.shape]
4✔
1862
        tmp_name = self.numpy_visitor._create_array_temp(shape, dtype, zero_init=False)
4✔
1863
        # Register the cache entry before emitting init so any (unexpected)
1864
        # recursive reference reuses the same container.
1865
        self._global_array_cache[name] = tmp_name
4✔
1866

1867
        int_primitives = (
4✔
1868
            PrimitiveType.Int64,
1869
            PrimitiveType.Int32,
1870
            PrimitiveType.Int16,
1871
            PrimitiveType.Int8,
1872
            PrimitiveType.UInt64,
1873
            PrimitiveType.UInt32,
1874
            PrimitiveType.UInt16,
1875
            PrimitiveType.UInt8,
1876
        )
1877
        is_int = dtype.primitive_type in int_primitives
4✔
1878
        is_bool = dtype.primitive_type == PrimitiveType.Bool
4✔
1879

1880
        flat = arr.ravel(order="C")
4✔
1881
        block = self.builder.add_block()
4✔
1882
        t_dst = self.builder.add_access(block, tmp_name)
4✔
1883
        for lin, v in enumerate(flat):
4✔
1884
            if is_bool:
4✔
NEW
1885
                val_str = "true" if bool(v) else "false"
×
1886
            elif is_int:
4✔
1887
                val_str = str(int(v))
4✔
1888
            else:
1889
                val_str = repr(float(v))
4✔
1890
            t_const = self.builder.add_constant(block, val_str, dtype)
4✔
1891
            t_task = self.builder.add_tasklet(
4✔
1892
                block, TaskletCode.assign, ["_in"], ["_out"]
1893
            )
1894
            self.builder.add_memlet(block, t_const, "void", t_task, "_in", "")
4✔
1895
            self.builder.add_memlet(block, t_task, "_out", t_dst, "void", str(lin))
4✔
1896

1897
        return tmp_name
4✔
1898

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

1902
        Uses a zero-copy view when possible (positive step, no indirect access).
1903
        Falls back to copy-based approach for complex cases.
1904
        """
1905
        if not self.builder:
4✔
1906
            raise ValueError("Builder required for expression slicing")
×
1907

1908
        # Try view-based approach first (zero-copy)
1909
        if self._can_use_slice_view(indices_nodes):
4✔
1910
            return self._create_slice_view(value_str, indices_nodes, shapes, ndim)
4✔
1911

1912
        # Fall back to copy-based approach for complex cases
1913
        return self._handle_expression_slicing_copy(
4✔
1914
            node, value_str, indices_nodes, shapes, ndim
1915
        )
1916

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

1920
        Views can be used when:
1921
        - All steps are non-zero constants (positive or negative)
1922
        - No indirect array access in slice parameters
1923

1924
        Returns True if a view can be used, False if a copy is required.
1925
        """
1926
        for idx in indices_nodes:
4✔
1927
            if isinstance(idx, ast.Slice):
4✔
1928
                # Check for zero step (invalid)
1929
                if idx.step is not None:
4✔
1930
                    if isinstance(idx.step, ast.Constant):
4✔
1931
                        if idx.step.value == 0:
4✔
1932
                            return False  # Zero step is invalid
×
1933
                    elif isinstance(idx.step, ast.UnaryOp) and isinstance(
4✔
1934
                        idx.step.op, ast.USub
1935
                    ):
1936
                        # Negative step like -2 is OK
1937
                        pass
4✔
1938
                    elif self._contains_indirect_access(idx.step):
×
1939
                        return False  # Dynamic step requires copy
×
1940

1941
                # Check for indirect access in slice bounds
1942
                if idx.lower is not None and self._contains_indirect_access(idx.lower):
4✔
1943
                    return False
4✔
1944
                if idx.upper is not None and self._contains_indirect_access(idx.upper):
4✔
1945
                    return False
×
1946
            else:
1947
                # Fixed index: check for indirect access
1948
                if self._contains_indirect_access(idx):
4✔
1949
                    return False
×
1950
        return True
4✔
1951

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

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

1958
        For positive step A[start:stop:step, ...] on dimension i:
1959
        - new_shape[i] = ceil((stop - start) / step)
1960
        - new_stride[i] = old_stride[i] * step
1961
        - offset contribution = start * old_stride[i]
1962

1963
        For negative step A[start:stop:step, ...] (e.g., ::-1):
1964
        - Default start = shape - 1 (last element)
1965
        - Default stop = -1 (before first element)
1966
        - new_shape[i] = ceil((start - stop) / abs(step))
1967
        - new_stride[i] = old_stride[i] * step (negative)
1968
        - offset contribution = start * old_stride[i] (points to last element)
1969

1970
        For a fixed index A[k, ...] on dimension i (dimension reduction):
1971
        - offset contribution = k * old_stride[i]
1972
        - dimension is removed from output
1973
        """
1974
        in_tensor = self.tensor_table[value_str]
4✔
1975
        in_shape = in_tensor.shape
4✔
1976
        dtype = in_tensor.element_type
4✔
1977

1978
        # Get input strides (compute if not available)
1979
        in_strides = (
4✔
1980
            in_tensor.strides
1981
            if hasattr(in_tensor, "strides") and in_tensor.strides
1982
            else None
1983
        )
1984
        if in_strides is None:
4✔
1985
            in_strides = self.numpy_visitor._compute_strides(in_shape, "C")
×
1986

1987
        # Get base offset from input tensor
1988
        in_offset = getattr(in_tensor, "offset", "0") or "0"
4✔
1989

1990
        # Build output shape, strides, and compute offset
1991
        out_shape = []
4✔
1992
        out_strides = []
4✔
1993
        offset_terms = []
4✔
1994
        if in_offset != "0":
4✔
1995
            offset_terms.append(str(in_offset))
4✔
1996

1997
        for i, idx in enumerate(indices_nodes):
4✔
1998
            shape_val = shapes[i] if i < len(shapes) else f"_{value_str}_shape_{i}"
4✔
1999
            stride_val = in_strides[i] if i < len(in_strides) else "1"
4✔
2000

2001
            if isinstance(idx, ast.Slice):
4✔
2002
                # Determine step value and sign
2003
                step_str = "1"
4✔
2004
                step_is_negative = False
4✔
2005
                step_value = 1
4✔
2006

2007
                if idx.step is not None:
4✔
2008
                    if isinstance(idx.step, ast.Constant):
4✔
2009
                        step_value = idx.step.value
4✔
2010
                        step_str = str(step_value)
4✔
2011
                        step_is_negative = step_value < 0
4✔
2012
                    elif isinstance(idx.step, ast.UnaryOp) and isinstance(
4✔
2013
                        idx.step.op, ast.USub
2014
                    ):
2015
                        # Handle -N syntax
2016
                        if isinstance(idx.step.operand, ast.Constant):
4✔
2017
                            step_value = -idx.step.operand.value
4✔
2018
                            step_str = str(step_value)
4✔
2019
                            step_is_negative = True
4✔
2020
                        else:
2021
                            step_str = self.visit(idx.step)
×
2022
                    else:
2023
                        step_str = self.visit(idx.step)
×
2024

2025
                if step_is_negative:
4✔
2026
                    # Negative step: iterate from end to start
2027
                    # Default start = shape - 1, default stop = -1 (before 0)
2028
                    if idx.lower is not None:
4✔
2029
                        start_str = self.visit(idx.lower)
×
2030
                        if isinstance(start_str, str) and (
×
2031
                            start_str.startswith("-") or start_str.startswith("(-")
2032
                        ):
2033
                            start_str = f"({shape_val} + {start_str})"
×
2034
                    else:
2035
                        start_str = f"({shape_val} - 1)"
4✔
2036

2037
                    if idx.upper is not None:
4✔
2038
                        stop_str = self.visit(idx.upper)
×
2039
                        if isinstance(stop_str, str) and (
×
2040
                            stop_str.startswith("-") or stop_str.startswith("(-")
2041
                        ):
2042
                            stop_str = f"({shape_val} + {stop_str})"
×
2043
                    else:
2044
                        stop_str = "-1"
4✔
2045

2046
                    # Shape for negative step: ceil((start - stop) / abs(step))
2047
                    abs_step = abs(step_value)
4✔
2048
                    if abs_step == 1:
4✔
2049
                        dim_size = f"({start_str} - {stop_str})"
4✔
2050
                    else:
2051
                        dim_size = f"(({start_str} - {stop_str} + {abs_step} - 1) / {abs_step})"
4✔
2052
                    out_shape.append(dim_size)
4✔
2053

2054
                    # Stride for negative step: old_stride * step (negative)
2055
                    out_strides.append(f"({stride_val} * {step_str})")
4✔
2056

2057
                    # Offset: start * old_stride (points to first element to access)
2058
                    offset_terms.append(f"({start_str} * {stride_val})")
4✔
2059
                else:
2060
                    # Positive step (original logic)
2061
                    start_str = "0"
4✔
2062
                    if idx.lower is not None:
4✔
2063
                        start_str = self.visit(idx.lower)
4✔
2064
                        if isinstance(start_str, str) and (
4✔
2065
                            start_str.startswith("-") or start_str.startswith("(-")
2066
                        ):
2067
                            start_str = f"({shape_val} + {start_str})"
×
2068

2069
                    stop_str = str(shape_val)
4✔
2070
                    if idx.upper is not None:
4✔
2071
                        stop_str = self.visit(idx.upper)
4✔
2072
                        if isinstance(stop_str, str) and (
4✔
2073
                            stop_str.startswith("-") or stop_str.startswith("(-")
2074
                        ):
2075
                            stop_str = f"({shape_val} + {stop_str})"
4✔
2076

2077
                    # Compute new shape: ceil((stop - start) / step)
2078
                    if step_str == "1":
4✔
2079
                        dim_size = f"({stop_str} - {start_str})"
4✔
2080
                    else:
2081
                        dim_size = f"idiv({stop_str} - {start_str} + {step_str} - 1, {step_str})"
4✔
2082
                    out_shape.append(dim_size)
4✔
2083

2084
                    # Compute new stride: old_stride * step
2085
                    if step_str == "1":
4✔
2086
                        out_strides.append(stride_val)
4✔
2087
                    else:
2088
                        out_strides.append(f"({stride_val} * {step_str})")
4✔
2089

2090
                    # Add offset contribution: start * stride
2091
                    if start_str != "0":
4✔
2092
                        offset_terms.append(f"({start_str} * {stride_val})")
4✔
2093
            else:
2094
                # Fixed index: dimension is removed, just add offset
2095
                index_str = self.visit(idx)
4✔
2096
                if isinstance(index_str, str) and (
4✔
2097
                    index_str.startswith("-") or index_str.startswith("(-")
2098
                ):
2099
                    index_str = f"({shape_val} + {index_str})"
4✔
2100
                offset_terms.append(f"({index_str} * {stride_val})")
4✔
2101

2102
        # Combine offset terms
2103
        if not offset_terms:
4✔
2104
            out_offset = "0"
4✔
2105
        elif len(offset_terms) == 1:
4✔
2106
            out_offset = offset_terms[0]
4✔
2107
        else:
2108
            out_offset = " + ".join(offset_terms)
4✔
2109

2110
        # Create new pointer container
2111
        tmp_name = self.builder.find_new_name("_slice_view_")
4✔
2112
        ptr_type = Pointer(dtype)
4✔
2113
        self.builder.add_container(tmp_name, ptr_type, False)
4✔
2114
        self.container_table[tmp_name] = ptr_type
4✔
2115

2116
        # Create output tensor with new shape, strides, and offset
2117
        # Offset is stored in the Tensor (like Tensor.flip() does)
2118
        # Reference memlet just creates the pointer alias with "0" offset
2119
        if out_shape:
4✔
2120
            out_tensor = Tensor(dtype, out_shape, out_strides, out_offset)
4✔
2121
            self.tensor_table[tmp_name] = out_tensor
4✔
2122
        else:
2123
            # Scalar result (all indices were fixed)
2124
            self.builder.add_container(tmp_name, dtype, False)
×
2125
            self.container_table[tmp_name] = dtype
×
2126

2127
        # Create reference memlet (offset is handled by tensor's offset property)
2128
        debug_info = DebugInfo()
4✔
2129
        block = self.builder.add_block(debug_info)
4✔
2130
        t_src = self.builder.add_access(block, value_str, debug_info)
4✔
2131
        t_dst = self.builder.add_access(block, tmp_name, debug_info)
4✔
2132
        self.builder.add_reference_memlet(block, t_src, t_dst, "0", ptr_type)
4✔
2133

2134
        return tmp_name
4✔
2135

2136
    def _handle_expression_slicing_copy(
4✔
2137
        self, node, value_str, indices_nodes, shapes, ndim
2138
    ):
2139
        """Copy-based slicing for cases that cannot use views.
2140

2141
        This allocates a new array and copies elements using nested loops.
2142
        Used for negative steps or indirect access patterns.
2143
        """
2144
        dtype = Scalar(PrimitiveType.Double)
4✔
2145
        if value_str in self.container_table:
4✔
2146
            t = self.container_table[value_str]
4✔
2147
            if isinstance(t, Pointer) and t.has_pointee_type():
4✔
2148
                dtype = t.pointee_type
4✔
2149

2150
        result_shapes = []
4✔
2151
        result_shapes_runtime = []
4✔
2152
        slice_info = []
4✔
2153
        index_info = []
4✔
2154

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

2158
            if isinstance(idx, ast.Slice):
4✔
2159
                start_str = "0"
4✔
2160
                start_str_runtime = "0"
4✔
2161
                if idx.lower is not None:
4✔
2162
                    if self._contains_indirect_access(idx.lower):
4✔
2163
                        start_str, start_str_runtime = (
4✔
2164
                            self._materialize_indirect_access(
2165
                                idx.lower, return_original_expr=True
2166
                            )
2167
                        )
2168
                    else:
2169
                        start_str = self.visit(idx.lower)
×
2170
                        start_str_runtime = start_str
×
2171
                    if isinstance(start_str, str) and (
4✔
2172
                        start_str.startswith("-") or start_str.startswith("(-")
2173
                    ):
2174
                        start_str = f"({shape_val} + {start_str})"
×
2175
                        start_str_runtime = f"({shape_val} + {start_str_runtime})"
×
2176

2177
                stop_str = str(shape_val)
4✔
2178
                stop_str_runtime = str(shape_val)
4✔
2179
                if idx.upper is not None:
4✔
2180
                    if self._contains_indirect_access(idx.upper):
4✔
2181
                        stop_str, stop_str_runtime = self._materialize_indirect_access(
4✔
2182
                            idx.upper, return_original_expr=True
2183
                        )
2184
                    else:
2185
                        stop_str = self.visit(idx.upper)
×
2186
                        stop_str_runtime = stop_str
×
2187
                    if isinstance(stop_str, str) and (
4✔
2188
                        stop_str.startswith("-") or stop_str.startswith("(-")
2189
                    ):
2190
                        stop_str = f"({shape_val} + {stop_str})"
×
2191
                        stop_str_runtime = f"({shape_val} + {stop_str_runtime})"
×
2192

2193
                step_str = "1"
4✔
2194
                if idx.step is not None:
4✔
2195
                    step_str = self.visit(idx.step)
×
2196

2197
                # Compute dimension size accounting for step: ceil((stop - start) / step)
2198
                # For symbolic expressions, use integer ceiling formula: idiv(n + d - 1, d)
2199
                if step_str == "1":
4✔
2200
                    dim_size = f"({stop_str} - {start_str})"
4✔
2201
                    dim_size_runtime = f"({stop_str_runtime} - {start_str_runtime})"
4✔
2202
                else:
2203
                    dim_size = (
×
2204
                        f"idiv({stop_str} - {start_str} + {step_str} - 1, {step_str})"
2205
                    )
2206
                    dim_size_runtime = f"idiv({stop_str_runtime} - {start_str_runtime} + {step_str} - 1, {step_str})"
×
2207
                result_shapes.append(dim_size)
4✔
2208
                result_shapes_runtime.append(dim_size_runtime)
4✔
2209
                slice_info.append((i, start_str, stop_str, step_str))
4✔
2210
            else:
2211
                if self._contains_indirect_access(idx):
×
2212
                    index_str = self._materialize_indirect_access(idx)
×
2213
                else:
2214
                    index_str = self.visit(idx)
×
2215
                if isinstance(index_str, str) and (
×
2216
                    index_str.startswith("-") or index_str.startswith("(-")
2217
                ):
2218
                    index_str = f"({shape_val} + {index_str})"
×
2219
                index_info.append((i, index_str))
×
2220

2221
        tmp_name = self.builder.find_new_name("_slice_tmp_")
4✔
2222
        result_ndim = len(result_shapes)
4✔
2223

2224
        if result_ndim == 0:
4✔
2225
            self.builder.add_container(tmp_name, dtype, False)
×
2226
            self.container_table[tmp_name] = dtype
×
2227
        else:
2228
            size_str = "1"
4✔
2229
            for dim in result_shapes:
4✔
2230
                size_str = f"({size_str} * {dim})"
4✔
2231

2232
            element_size = self.builder.get_sizeof(dtype)
4✔
2233
            total_size = f"({size_str} * {element_size})"
4✔
2234

2235
            ptr_type = Pointer(dtype)
4✔
2236
            self.builder.add_container(tmp_name, ptr_type, False)
4✔
2237
            self.container_table[tmp_name] = ptr_type
4✔
2238
            tensor_info = Tensor(dtype, result_shapes)
4✔
2239
            self.shapes_runtime_info[tmp_name] = (
4✔
2240
                result_shapes_runtime  # Store runtime shapes separately
2241
            )
2242
            self.tensor_table[tmp_name] = tensor_info
4✔
2243

2244
            debug_info = DebugInfo()
4✔
2245
            block_alloc = self.builder.add_block(debug_info)
4✔
2246
            t_malloc = self.builder.add_malloc(block_alloc, total_size)
4✔
2247
            t_ptr = self.builder.add_access(block_alloc, tmp_name, debug_info)
4✔
2248
            self.builder.add_memlet(
4✔
2249
                block_alloc, t_malloc, "_ret", t_ptr, "void", "", ptr_type, debug_info
2250
            )
2251

2252
        loop_vars = []
4✔
2253
        debug_info = DebugInfo()
4✔
2254

2255
        for dim_idx, (orig_dim, start_str, stop_str, step_str) in enumerate(slice_info):
4✔
2256
            loop_var = self.builder.find_new_name(f"_slice_loop_{dim_idx}_")
4✔
2257
            loop_vars.append((loop_var, orig_dim, start_str, step_str))
4✔
2258

2259
            if not self.builder.exists(loop_var):
4✔
2260
                self.builder.add_container(loop_var, Scalar(PrimitiveType.Int64), False)
4✔
2261
                self.container_table[loop_var] = Scalar(PrimitiveType.Int64)
4✔
2262

2263
            # Account for step in loop count: ceil((stop - start) / step)
2264
            if step_str == "1":
4✔
2265
                count_str = f"({stop_str} - {start_str})"
4✔
2266
            else:
2267
                count_str = (
×
2268
                    f"idiv({stop_str} - {start_str} + {step_str} - 1, {step_str})"
2269
                )
2270
            self.builder.begin_for(loop_var, "0", count_str, "1", debug_info)
4✔
2271

2272
        src_indices = [""] * ndim
4✔
2273
        dst_indices = []
4✔
2274

2275
        for orig_dim, index_str in index_info:
4✔
2276
            src_indices[orig_dim] = index_str
×
2277

2278
        for loop_var, orig_dim, start_str, step_str in loop_vars:
4✔
2279
            if step_str == "1":
4✔
2280
                src_indices[orig_dim] = f"({start_str} + {loop_var})"
4✔
2281
            else:
2282
                src_indices[orig_dim] = f"({start_str} + {loop_var} * {step_str})"
×
2283
            dst_indices.append(loop_var)
4✔
2284

2285
        src_linear = self._compute_linear_index(src_indices, shapes, value_str, ndim)
4✔
2286
        if result_ndim > 0:
4✔
2287
            dst_linear = self._compute_linear_index(
4✔
2288
                dst_indices, result_shapes, tmp_name, result_ndim
2289
            )
2290
        else:
2291
            dst_linear = "0"
×
2292

2293
        block = self.builder.add_block(debug_info)
4✔
2294
        t_src = self.builder.add_access(block, value_str, debug_info)
4✔
2295
        t_dst = self.builder.add_access(block, tmp_name, debug_info)
4✔
2296
        t_task = self.builder.add_tasklet(
4✔
2297
            block, TaskletCode.assign, ["_in"], ["_out"], debug_info
2298
        )
2299

2300
        self.builder.add_memlet(
4✔
2301
            block, t_src, "void", t_task, "_in", src_linear, None, debug_info
2302
        )
2303
        self.builder.add_memlet(
4✔
2304
            block, t_task, "_out", t_dst, "void", dst_linear, None, debug_info
2305
        )
2306

2307
        for _ in loop_vars:
4✔
2308
            self.builder.end_for()
4✔
2309

2310
        return tmp_name
4✔
2311

2312
    def _compute_linear_index(self, indices, shapes, array_name, ndim):
4✔
2313
        """Compute linear index from multi-dimensional indices."""
2314
        if ndim == 0:
4✔
2315
            return "0"
×
2316

2317
        linear_index = ""
4✔
2318
        for i in range(ndim):
4✔
2319
            term = str(indices[i])
4✔
2320
            for j in range(i + 1, ndim):
4✔
2321
                shape_val = shapes[j] if j < len(shapes) else f"_{array_name}_shape_{j}"
×
2322
                term = f"(({term}) * {shape_val})"
×
2323

2324
            if i == 0:
4✔
2325
                linear_index = term
4✔
2326
            else:
2327
                linear_index = f"({linear_index} + {term})"
×
2328

2329
        return linear_index
4✔
2330

2331
    def _is_array_index(self, node):
4✔
2332
        """Check if a node represents an array that could be used as an index (gather)."""
2333
        if isinstance(node, ast.Name):
4✔
2334
            return node.id in self.tensor_table
4✔
2335
        # An array-typed struct member used as an index, e.g. domain.xd[domain.nodelist].
2336
        if self._is_array_member_attribute(node):
4✔
2337
            return True
4✔
2338
        return False
4✔
2339

2340
    def _expr_array_ndim(self, node):
4✔
2341
        """Best-effort number of dimensions of an array-valued expression.
2342

2343
        Side-effect-free (inspects metadata only). Returns the ndim, or None if
2344
        the expression is scalar or not a recognizable array. Used to decide
2345
        whether a subscript index (e.g. a column view ``n[:, i]``) is itself an
2346
        array and should be treated as a gather index rather than a scalar.
2347
        """
2348
        if isinstance(node, ast.Name):
4✔
2349
            if node.id in self.tensor_table:
4✔
2350
                return len(self.tensor_table[node.id].shape)
4✔
2351
            return None
4✔
2352
        if self._is_array_member_attribute(node):
4✔
NEW
2353
            obj_type = self.container_table.get(node.value.id)
×
NEW
2354
            member = self.structure_member_info[obj_type.pointee_type.name][node.attr]
×
NEW
2355
            member_shape = member[2]
×
NEW
2356
            return len(member_shape) if member_shape else 1
×
2357
        if isinstance(node, ast.Subscript):
4✔
2358
            base_ndim = self._expr_array_ndim(node.value)
4✔
2359
            if base_ndim is None:
4✔
NEW
2360
                return None
×
2361
            idx_nodes = (
4✔
2362
                node.slice.elts if isinstance(node.slice, ast.Tuple) else [node.slice]
2363
            )
2364
            consumed = 0  # base axes consumed by non-newaxis indices
4✔
2365
            added = 0  # axes contributed by slices/newaxis/array (fancy) indices
4✔
2366
            for idx in idx_nodes:
4✔
2367
                if self._is_newaxis(idx):
4✔
NEW
2368
                    added += 1
×
2369
                elif isinstance(idx, ast.Slice):
4✔
2370
                    added += 1
4✔
2371
                    consumed += 1
4✔
2372
                else:
2373
                    # A scalar index drops one axis; an array-valued (fancy)
2374
                    # index such as `lm[ielem]` replaces one base axis with its
2375
                    # own dimensions (a gather).
2376
                    idx_ndim = self._expr_array_ndim(idx)
4✔
2377
                    if idx_ndim is not None and idx_ndim >= 1:
4✔
2378
                        added += idx_ndim
4✔
2379
                    consumed += 1
4✔
2380
            remaining = base_ndim - consumed
4✔
2381
            if remaining < 0:
4✔
NEW
2382
                return None
×
2383
            return added + remaining
4✔
2384
        return None
4✔
2385

2386
    def _is_array_valued_index(self, node):
4✔
2387
        """True if `node` yields an array (>=1D) usable as a gather index.
2388

2389
        Covers array Names, array-member attributes, and array-valued
2390
        subscript expressions such as a column view ``n[:, i]``.
2391
        """
2392
        if self._is_array_index(node):
4✔
2393
            return True
4✔
2394
        ndim = self._expr_array_ndim(node)
4✔
2395
        return ndim is not None and ndim >= 1
4✔
2396

2397
    def _try_const_eval(self, node):
4✔
2398
        """Best-effort compile-time evaluation of an expression built from
2399
        global constants (int/float/str/dict/list/tuple) and constant subscript
2400
        access, e.g. ``XI["M"]["mask"]``. Returns ``(True, value)`` on success,
2401
        ``(False, None)`` otherwise.
2402
        """
2403
        if isinstance(node, ast.Constant):
4✔
2404
            return True, node.value
4✔
2405
        if isinstance(node, ast.Name):
4✔
2406
            if node.id in self.globals_dict:
4✔
2407
                val = self.globals_dict[node.id]
4✔
2408
                if isinstance(val, (int, float, str, dict, list, tuple)):
4✔
2409
                    return True, val
4✔
2410
            return False, None
4✔
2411
        if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub):
4✔
NEW
2412
            ok, v = self._try_const_eval(node.operand)
×
NEW
2413
            return (True, -v) if ok else (False, None)
×
2414
        if isinstance(node, ast.Subscript):
4✔
2415
            ok_base, base = self._try_const_eval(node.value)
4✔
2416
            if not ok_base:
4✔
2417
                return False, None
4✔
2418
            ok_idx, key = self._try_const_eval(node.slice)
4✔
2419
            if not ok_idx:
4✔
NEW
2420
                return False, None
×
2421
            try:
4✔
2422
                return True, base[key]
4✔
NEW
2423
            except (KeyError, IndexError, TypeError):
×
NEW
2424
                return False, None
×
2425
        return False, None
4✔
2426

2427
    def _resolve_tuple(self, node):
4✔
2428
        """Return the element AST list if `node` denotes a tuple/list.
2429

2430
        A tuple is either a literal (``ast.Tuple``/``ast.List``) or a ``Name``
2431
        bound to one via a prior ``shp = (...)`` assignment (tracked in
2432
        ``tuple_table``). Returns ``None`` if `node` is not a tuple. This is the
2433
        single entry point every tuple consumer (shapes, index sequences, ...)
2434
        should use so variable-bound tuples work everywhere literals do.
2435
        """
2436
        if isinstance(node, (ast.Tuple, ast.List)):
4✔
2437
            return node.elts
4✔
2438
        if isinstance(node, ast.Name):
4✔
2439
            bound = self.tuple_table.get(node.id)
4✔
2440
            if bound is not None:
4✔
2441
                return bound.elts
4✔
2442
        return None
4✔
2443

2444
    def _const_int_sequence(self, node):
4✔
2445
        """Return a list of ints if `node` is a constant integer tuple/list.
2446

2447
        Used to detect fancy indexing with a compile-time-constant index list,
2448
        e.g. `x[:, (1, 2, 3, 4, 5, 7)]`. Returns None otherwise.
2449
        """
2450
        elts = self._resolve_tuple(node)
4✔
2451
        if elts is None:
4✔
2452
            return None
4✔
2453
        values = []
4✔
2454
        for elt in elts:
4✔
2455
            if (
4✔
2456
                isinstance(elt, ast.Constant)
2457
                and isinstance(elt.value, int)
2458
                and not isinstance(elt.value, bool)
2459
            ):
2460
                values.append(elt.value)
4✔
NEW
2461
            elif (
×
2462
                isinstance(elt, ast.UnaryOp)
2463
                and isinstance(elt.op, ast.USub)
2464
                and isinstance(elt.operand, ast.Constant)
2465
                and isinstance(elt.operand.value, int)
2466
            ):
NEW
2467
                values.append(-elt.operand.value)
×
2468
            else:
NEW
2469
                return None
×
2470
        return values if values else None
4✔
2471

2472
    def _handle_fancy_index(self, node, value_str, indices_nodes, shapes, ndim):
4✔
2473
        """Handle fancy indexing with a constant integer sequence on one axis.
2474

2475
        Supports the form `A[:, ..., (i0, i1, ...), ..., :]` where exactly one
2476
        axis is indexed by a compile-time-constant list/tuple of integers and
2477
        every other axis is a full slice. Produces a materialized (copied)
2478
        contiguous array whose selected axis has length ``len(sequence)``; for
2479
        each selected position ``k`` the corresponding slab ``A[..., seq[k], ...]``
2480
        is copied into the output. Other fancy-indexing shapes raise clearly.
2481
        """
2482
        # Pad implicit trailing full slices so indices cover all dimensions.
2483
        padded = list(indices_nodes)
4✔
2484
        while len(padded) < ndim:
4✔
NEW
2485
            padded.append(ast.Slice(lower=None, upper=None, step=None))
×
2486
        if len(padded) != ndim:
4✔
NEW
2487
            raise NotImplementedError(
×
2488
                "Fancy indexing with more indices than array dimensions is not "
2489
                "supported"
2490
            )
2491

2492
        fancy_axis = None
4✔
2493
        cols = None
4✔
2494
        for i, idx in enumerate(padded):
4✔
2495
            seq = self._const_int_sequence(idx)
4✔
2496
            if seq is not None:
4✔
2497
                if fancy_axis is not None:
4✔
NEW
2498
                    raise NotImplementedError(
×
2499
                        "Fancy indexing on more than one axis is not supported"
2500
                    )
2501
                fancy_axis = i
4✔
2502
                cols = seq
4✔
2503
            elif isinstance(idx, ast.Slice):
4✔
2504
                is_full = (
4✔
2505
                    idx.lower is None
2506
                    and idx.upper is None
2507
                    and (
2508
                        idx.step is None
2509
                        or (isinstance(idx.step, ast.Constant) and idx.step.value == 1)
2510
                    )
2511
                )
2512
                if not is_full:
4✔
NEW
2513
                    raise NotImplementedError(
×
2514
                        "Fancy indexing combined with a partial slice is not "
2515
                        "supported"
2516
                    )
2517
            else:
NEW
2518
                raise NotImplementedError(
×
2519
                    "Fancy indexing combined with a scalar index is not supported"
2520
                )
2521

2522
        # Normalize negative column indices against the (constant) axis length.
2523
        axis_len_str = str(shapes[fancy_axis])
4✔
2524
        try:
4✔
2525
            axis_len_int = int(axis_len_str)
4✔
2526
        except ValueError:
4✔
2527
            axis_len_int = None
4✔
2528
        norm_cols = []
4✔
2529
        for c in cols:
4✔
2530
            if c < 0:
4✔
NEW
2531
                if axis_len_int is None:
×
NEW
2532
                    raise NotImplementedError(
×
2533
                        "Negative fancy-index values require a statically known "
2534
                        "axis length"
2535
                    )
NEW
2536
                c += axis_len_int
×
2537
            norm_cols.append(c)
4✔
2538

2539
        # Element type of the source.
2540
        dtype = Scalar(PrimitiveType.Double)
4✔
2541
        if value_str in self.container_table:
4✔
2542
            t = self.container_table[value_str]
4✔
2543
            if isinstance(t, Pointer) and t.has_pointee_type():
4✔
2544
                dtype = t.pointee_type
4✔
2545

2546
        # Allocate a contiguous output array (copy).
2547
        out_shape = list(shapes)
4✔
2548
        out_shape[fancy_axis] = str(len(norm_cols))
4✔
2549
        tmp_name = self.builder.find_new_name("_fancy_tmp_")
4✔
2550
        ptr_type = Pointer(dtype)
4✔
2551
        self.builder.add_container(tmp_name, ptr_type, False)
4✔
2552
        self.container_table[tmp_name] = ptr_type
4✔
2553
        self.tensor_table[tmp_name] = Tensor(dtype, out_shape)
4✔
2554

2555
        size_str = "1"
4✔
2556
        for dim in out_shape:
4✔
2557
            size_str = f"({size_str} * {dim})"
4✔
2558
        element_size = self.builder.get_sizeof(dtype)
4✔
2559
        total_size = f"({size_str} * {element_size})"
4✔
2560

2561
        debug_info = DebugInfo()
4✔
2562
        block_alloc = self.builder.add_block(debug_info)
4✔
2563
        t_malloc = self.builder.add_malloc(block_alloc, total_size)
4✔
2564
        t_ptr = self.builder.add_access(block_alloc, tmp_name, debug_info)
4✔
2565
        self.builder.add_memlet(
4✔
2566
            block_alloc, t_malloc, "_ret", t_ptr, "void", "", ptr_type, debug_info
2567
        )
2568

2569
        # Copy each selected slab: out[..., k:k+1, ...] = A[..., col:col+1, ...].
2570
        def _make_subscript(base_name, k):
4✔
2571
            elts = [ast.Slice(lower=None, upper=None, step=None) for _ in range(ndim)]
4✔
2572
            elts[fancy_axis] = ast.Slice(
4✔
2573
                lower=ast.Constant(value=k),
2574
                upper=ast.Constant(value=k + 1),
2575
                step=None,
2576
            )
2577
            sl = elts[0] if ndim == 1 else ast.Tuple(elts=elts, ctx=ast.Load())
4✔
2578
            sub = ast.Subscript(
4✔
2579
                value=ast.Name(id=base_name, ctx=ast.Load()), slice=sl, ctx=ast.Load()
2580
            )
2581
            ast.copy_location(sub, node)
4✔
2582
            ast.fix_missing_locations(sub)
4✔
2583
            return sub
4✔
2584

2585
        for k, col in enumerate(norm_cols):
4✔
2586
            dst = _make_subscript(tmp_name, k)
4✔
2587
            dst.ctx = ast.Store()
4✔
2588
            src = _make_subscript(value_str, col)
4✔
2589
            assign = ast.Assign(targets=[dst], value=src)
4✔
2590
            ast.copy_location(assign, node)
4✔
2591
            ast.fix_missing_locations(assign)
4✔
2592
            self.visit_Assign(assign)
4✔
2593

2594
        return tmp_name
4✔
2595

2596
    def _is_newaxis(self, node):
4✔
2597
        """True if a subscript index is np.newaxis / None (inserts a size-1 axis)."""
2598
        if isinstance(node, ast.Constant) and node.value is None:
4✔
2599
            return True
4✔
2600
        if isinstance(node, ast.Attribute) and node.attr == "newaxis":
4✔
NEW
2601
            return True
×
2602
        return False
4✔
2603

2604
    def _handle_newaxis_subscript(self, value_str, indices_nodes):
4✔
2605
        """Handle np.newaxis / None indexing (arr[:, None], arr[None, :], ...).
2606

2607
        Produces a view of the source array with size-1 axes inserted at the
2608
        newaxis positions. Only full slices may accompany the new axes.
2609
        """
2610
        tensor = self.tensor_table[value_str]
4✔
2611

2612
        new_axis_positions = []
4✔
2613
        for i, idx in enumerate(indices_nodes):
4✔
2614
            if self._is_newaxis(idx):
4✔
2615
                new_axis_positions.append(i)
4✔
2616
                continue
4✔
2617
            is_full_slice = (
4✔
2618
                isinstance(idx, ast.Slice)
2619
                and idx.lower is None
2620
                and idx.upper is None
2621
                and (
2622
                    idx.step is None
2623
                    or (isinstance(idx.step, ast.Constant) and idx.step.value == 1)
2624
                )
2625
            )
2626
            if not is_full_slice:
4✔
NEW
2627
                raise NotImplementedError(
×
2628
                    "np.newaxis indexing is only supported combined with full slices"
2629
                )
2630

2631
        # Insert size-1 axes (in increasing position order so each insertion
2632
        # accounts for the previously inserted axes).
2633
        new_tensor = tensor
4✔
2634
        for pos in sorted(new_axis_positions):
4✔
2635
            new_tensor = new_tensor.newaxis(pos)
4✔
2636

2637
        tmp_name = self.builder.find_new_name("_newaxis_")
4✔
2638
        ptr_type = Pointer(new_tensor.element_type)
4✔
2639
        self.builder.add_container(tmp_name, ptr_type, False)
4✔
2640
        self.container_table[tmp_name] = ptr_type
4✔
2641
        self.tensor_table[tmp_name] = new_tensor
4✔
2642

2643
        block = self.builder.add_block()
4✔
2644
        t_src = self.builder.add_access(block, value_str)
4✔
2645
        t_dst = self.builder.add_access(block, tmp_name)
4✔
2646
        self.builder.add_reference_memlet(block, t_src, t_dst, "0", ptr_type)
4✔
2647
        return tmp_name
4✔
2648

2649
    def _is_array_member_attribute(self, node):
4✔
2650
        """Return True if node is `obj.attr` where attr is an array (pointer) member."""
2651
        if not (isinstance(node, ast.Attribute) and isinstance(node.value, ast.Name)):
4✔
2652
            return False
4✔
2653
        obj_type = self.container_table.get(node.value.id)
4✔
2654
        if not (isinstance(obj_type, Pointer) and obj_type.has_pointee_type()):
4✔
NEW
2655
            return False
×
2656
        pointee = obj_type.pointee_type
4✔
2657
        if not isinstance(pointee, Structure):
4✔
NEW
2658
            return False
×
2659
        member = self.structure_member_info.get(pointee.name, {}).get(node.attr)
4✔
2660
        return member is not None and isinstance(member[1], Pointer)
4✔
2661

2662
    def _struct_scalar_member(self, node):
4✔
2663
        """If `node` is `obj.attr` where attr is a SCALAR struct member (not an
2664
        array/pointer member), return (obj_name, member_index, member_type);
2665
        otherwise None."""
2666
        if not (isinstance(node, ast.Attribute) and isinstance(node.value, ast.Name)):
4✔
NEW
2667
            return None
×
2668
        obj_name = node.value.id
4✔
2669
        obj_type = self.container_table.get(obj_name)
4✔
2670
        if not (isinstance(obj_type, Pointer) and obj_type.has_pointee_type()):
4✔
NEW
2671
            return None
×
2672
        pointee = obj_type.pointee_type
4✔
2673
        if not isinstance(pointee, Structure):
4✔
NEW
2674
            return None
×
2675
        member = self.structure_member_info.get(pointee.name, {}).get(node.attr)
4✔
2676
        if member is None or isinstance(member[1], Pointer):
4✔
2677
            return None
4✔
2678
        member_index, member_type, _member_shape = member
4✔
2679
        return obj_name, member_index, member_type
4✔
2680

2681
    def _store_scalar_member(
4✔
2682
        self, obj_name, member_index, member_type, value_node, node
2683
    ):
2684
        """Store a scalar value into a struct member: `obj.attr = value` becomes
2685
        a write to obj[0, member_index] so it propagates through the struct."""
2686
        debug_info = get_debug_info(node, self.filename, self.function_name)
4✔
2687
        rhs = self.visit(value_node)
4✔
2688
        subset = "0," + str(member_index)
4✔
2689

2690
        block = self.builder.add_block(debug_info)
4✔
2691
        t_task = self.builder.add_tasklet(
4✔
2692
            block, TaskletCode.assign, ["_in"], ["_out"], debug_info
2693
        )
2694
        t_src, src_sub = self._add_read(block, rhs, debug_info)
4✔
2695
        self.builder.add_memlet(
4✔
2696
            block, t_src, "void", t_task, "_in", src_sub, None, debug_info
2697
        )
2698
        obj_access = self.builder.add_access(block, obj_name, debug_info)
4✔
2699
        self.builder.add_memlet(
4✔
2700
            block, t_task, "_out", obj_access, "", subset, None, debug_info
2701
        )
2702

2703
    def _is_boolean_mask_index(self, node):
4✔
2704
        """Check if a subscript index is a boolean mask (e.g. arr[arr <= 0.1])."""
2705
        # A boolean array variable used directly as a mask.
2706
        if isinstance(node, ast.Name) and node.id in self.tensor_table:
4✔
2707
            element_type = self.tensor_table[node.id].element_type
4✔
2708
            return element_type.primitive_type == PrimitiveType.Bool
4✔
2709
        # A whole-array comparison (e.g. arr <= 0.1, x > y) yields a boolean mask.
2710
        if isinstance(node, ast.Compare):
4✔
2711
            operands = [node.left] + list(node.comparators)
4✔
2712
            return any(
4✔
2713
                isinstance(o, ast.Name) and o.id in self.tensor_table for o in operands
2714
            )
2715
        return False
4✔
2716

2717
    def _handle_masked_assignment(
4✔
2718
        self, target, mask_node, value_node, target_name, orig_node
2719
    ):
2720
        """Handle boolean-mask assignment: target[mask] = value.
2721

2722
        Rewritten as ``target[:] = np.where(mask, value, target)`` which has
2723
        identical NumPy semantics (elements where the mask is False keep their
2724
        original value) and reuses the existing np.where lowering.
2725
        """
2726
        ndim = len(self.tensor_table[target_name].shape)
4✔
2727

2728
        full_slice = ast.Slice(lower=None, upper=None, step=None)
4✔
2729
        if ndim > 1:
4✔
2730
            slice_arg = ast.Tuple(
4✔
2731
                elts=[
2732
                    ast.Slice(lower=None, upper=None, step=None) for _ in range(ndim)
2733
                ],
2734
                ctx=ast.Load(),
2735
            )
2736
        else:
2737
            slice_arg = full_slice
4✔
2738

2739
        new_target = ast.Subscript(
4✔
2740
            value=copy.deepcopy(target.value), slice=slice_arg, ctx=ast.Store()
2741
        )
2742

2743
        where_call = ast.Call(
4✔
2744
            func=ast.Attribute(
2745
                value=ast.Name(id="np", ctx=ast.Load()), attr="where", ctx=ast.Load()
2746
            ),
2747
            args=[
2748
                copy.deepcopy(mask_node),
2749
                copy.deepcopy(value_node),
2750
                ast.Name(id=target_name, ctx=ast.Load()),
2751
            ],
2752
            keywords=[],
2753
        )
2754

2755
        new_assign = ast.Assign(targets=[new_target], value=where_call)
4✔
2756
        ast.copy_location(new_assign, orig_node)
4✔
2757
        ast.fix_missing_locations(new_assign)
4✔
2758
        self.visit_Assign(new_assign)
4✔
2759

2760
    def _handle_gather(self, value_str, index_node, debug_info=None):
4✔
2761
        """Handle gather operation: x[indices] where indices is an array."""
2762
        if debug_info is None:
4✔
2763
            debug_info = DebugInfo()
4✔
2764

2765
        if isinstance(index_node, ast.Name):
4✔
2766
            idx_array_name = index_node.id
4✔
2767
        else:
2768
            idx_array_name = self.visit(index_node)
4✔
2769

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

2773
        idx_shapes = self.tensor_table[idx_array_name].shape
4✔
2774
        idx_ndim = len(idx_shapes)
4✔
2775

2776
        if idx_ndim == 0:
4✔
NEW
2777
            raise NotImplementedError("Scalar index not supported for gather")
×
2778

2779
        # Gather over an N-D index array: the result has the same shape as the
2780
        # index array and is computed elementwise on the flattened arrays:
2781
        #   result_flat[k] = value[idx_flat[k]]
2782
        # Both the (contiguous) index array and result are addressed with a
2783
        # single flat loop variable.
2784
        result_shapes = [
4✔
2785
            idx_shapes[d] if d < len(idx_shapes) else f"_{idx_array_name}_shape_{d}"
2786
            for d in range(idx_ndim)
2787
        ]
2788
        total_count = str(result_shapes[0])
4✔
2789
        for s in result_shapes[1:]:
4✔
2790
            total_count = f"({total_count} * {s})"
4✔
2791

2792
        # For runtime evaluation, prefer shapes_runtime_info if available
2793
        # This ensures we use expressions that can be evaluated at runtime
2794
        if idx_array_name in self.shapes_runtime_info:
4✔
2795
            result_shapes_runtime = list(self.shapes_runtime_info[idx_array_name])
4✔
2796
        else:
2797
            result_shapes_runtime = list(result_shapes)
4✔
2798

2799
        dtype = Scalar(PrimitiveType.Double)
4✔
2800
        if value_str in self.container_table:
4✔
2801
            t = self.container_table[value_str]
4✔
2802
            if isinstance(t, Pointer) and t.has_pointee_type():
4✔
2803
                dtype = t.pointee_type
4✔
2804

2805
        idx_dtype = Scalar(PrimitiveType.Int64)
4✔
2806
        if idx_array_name in self.container_table:
4✔
2807
            t = self.container_table[idx_array_name]
4✔
2808
            if isinstance(t, Pointer) and t.has_pointee_type():
4✔
2809
                idx_dtype = t.pointee_type
4✔
2810

2811
        tmp_name = self.builder.find_new_name("_gather_")
4✔
2812

2813
        element_size = self.builder.get_sizeof(dtype)
4✔
2814
        total_size = f"({total_count} * {element_size})"
4✔
2815

2816
        ptr_type = Pointer(dtype)
4✔
2817
        self.builder.add_container(tmp_name, ptr_type, False)
4✔
2818
        self.container_table[tmp_name] = ptr_type
4✔
2819
        self.tensor_table[tmp_name] = Tensor(dtype, list(result_shapes))
4✔
2820
        # Store runtime evaluable shape for this gather result
2821
        self.shapes_runtime_info[tmp_name] = result_shapes_runtime
4✔
2822

2823
        block_alloc = self.builder.add_block(debug_info)
4✔
2824
        t_malloc = self.builder.add_malloc(block_alloc, total_size)
4✔
2825
        t_ptr = self.builder.add_access(block_alloc, tmp_name, debug_info)
4✔
2826
        self.builder.add_memlet(
4✔
2827
            block_alloc, t_malloc, "_ret", t_ptr, "void", "", ptr_type, debug_info
2828
        )
2829

2830
        loop_var = self.builder.find_new_name("_gather_i_")
4✔
2831
        self.builder.add_container(loop_var, Scalar(PrimitiveType.Int64), False)
4✔
2832
        self.container_table[loop_var] = Scalar(PrimitiveType.Int64)
4✔
2833

2834
        idx_var = self.builder.find_new_name("_gather_idx_")
4✔
2835
        self.builder.add_container(idx_var, idx_dtype, False)
4✔
2836
        self.container_table[idx_var] = idx_dtype
4✔
2837

2838
        self.builder.begin_for(loop_var, "0", str(total_count), "1", debug_info)
4✔
2839

2840
        block_load_idx = self.builder.add_block(debug_info)
4✔
2841
        idx_arr_access = self.builder.add_access(
4✔
2842
            block_load_idx, idx_array_name, debug_info
2843
        )
2844
        idx_var_access = self.builder.add_access(block_load_idx, idx_var, debug_info)
4✔
2845
        tasklet_load = self.builder.add_tasklet(
4✔
2846
            block_load_idx, TaskletCode.assign, ["_in"], ["_out"], debug_info
2847
        )
2848
        # For a 1-D index array pass its Tensor as the memlet base type so a
2849
        # strided index (e.g. a column view `nodelist[:, i]`) is linearized with
2850
        # its stride/offset; a flat (None) base type would ignore the stride and
2851
        # read out of bounds. N-D index arrays are assumed contiguous (flat).
2852
        idx_base_type = self.tensor_table[idx_array_name] if idx_ndim == 1 else None
4✔
2853
        self.builder.add_memlet(
4✔
2854
            block_load_idx,
2855
            idx_arr_access,
2856
            "void",
2857
            tasklet_load,
2858
            "_in",
2859
            loop_var,
2860
            idx_base_type,
2861
            debug_info,
2862
        )
2863
        self.builder.add_memlet(
4✔
2864
            block_load_idx,
2865
            tasklet_load,
2866
            "_out",
2867
            idx_var_access,
2868
            "void",
2869
            "",
2870
            None,
2871
            debug_info,
2872
        )
2873

2874
        block_gather = self.builder.add_block(debug_info)
4✔
2875
        src_access = self.builder.add_access(block_gather, value_str, debug_info)
4✔
2876
        dst_access = self.builder.add_access(block_gather, tmp_name, debug_info)
4✔
2877
        tasklet_gather = self.builder.add_tasklet(
4✔
2878
            block_gather, TaskletCode.assign, ["_in"], ["_out"], debug_info
2879
        )
2880

2881
        self.builder.add_memlet(
4✔
2882
            block_gather,
2883
            src_access,
2884
            "void",
2885
            tasklet_gather,
2886
            "_in",
2887
            idx_var,
2888
            None,
2889
            debug_info,
2890
        )
2891
        self.builder.add_memlet(
4✔
2892
            block_gather,
2893
            tasklet_gather,
2894
            "_out",
2895
            dst_access,
2896
            "void",
2897
            loop_var,
2898
            None,
2899
            debug_info,
2900
        )
2901

2902
        self.builder.end_for()
4✔
2903

2904
        return tmp_name
4✔
2905

2906
    def _handle_scatter_assignment(
4✔
2907
        self, target_name, index_node, value_node, accumulate, debug_info=None
2908
    ):
2909
        """Handle scatter (indexed) assignment: arr[idx] = vals or arr[idx] += vals.
2910

2911
        ``idx`` is an integer index array; the assignment is lowered to a
2912
        sequential loop ``for k: arr[idx[k]] (=|+=) vals[k]``. Tensor base types
2913
        are used on the memlets so strided views (e.g. a column slice) are
2914
        linearized correctly.
2915
        """
2916
        if debug_info is None:
4✔
NEW
2917
            debug_info = DebugInfo()
×
2918

2919
        idx_name = self.visit(index_node)
4✔
2920
        if idx_name not in self.tensor_table:
4✔
NEW
2921
            raise NotImplementedError("Scatter index must be an array")
×
2922

2923
        arr_tensor = self.tensor_table[target_name]
4✔
2924
        elem_type = arr_tensor.element_type
4✔
2925
        is_int = elem_type.primitive_type in (
4✔
2926
            PrimitiveType.Int64,
2927
            PrimitiveType.Int32,
2928
            PrimitiveType.Int16,
2929
            PrimitiveType.Int8,
2930
            PrimitiveType.UInt64,
2931
            PrimitiveType.UInt32,
2932
            PrimitiveType.UInt16,
2933
            PrimitiveType.UInt8,
2934
        )
2935
        add_code = TaskletCode.int_add if is_int else TaskletCode.fp_add
4✔
2936

2937
        idx_tensor = self.tensor_table[idx_name]
4✔
2938
        idx_shapes = idx_tensor.shape
4✔
2939
        total_count = str(idx_shapes[0]) if idx_shapes else "1"
4✔
2940
        for s in idx_shapes[1:]:
4✔
NEW
2941
            total_count = f"({total_count} * {s})"
×
2942

2943
        idx_dtype = Scalar(PrimitiveType.Int64)
4✔
2944
        if idx_name in self.container_table:
4✔
2945
            t = self.container_table[idx_name]
4✔
2946
            if isinstance(t, Pointer) and t.has_pointee_type():
4✔
2947
                idx_dtype = t.pointee_type
4✔
2948

2949
        # The RHS values: an array (indexed per element) or a broadcast scalar.
2950
        vals_name = self.visit(value_node)
4✔
2951
        vals_is_array = vals_name in self.tensor_table
4✔
2952

2953
        loop_var = self.builder.find_new_name("_scatter_i_")
4✔
2954
        self.builder.add_container(loop_var, Scalar(PrimitiveType.Int64), False)
4✔
2955
        self.container_table[loop_var] = Scalar(PrimitiveType.Int64)
4✔
2956

2957
        idx_var = self.builder.find_new_name("_scatter_idx_")
4✔
2958
        self.builder.add_container(idx_var, idx_dtype, False)
4✔
2959
        self.container_table[idx_var] = idx_dtype
4✔
2960

2961
        self.builder.begin_for(loop_var, "0", total_count, "1", debug_info)
4✔
2962

2963
        # Load the destination index: idx_var = idx[loop_var]
2964
        block_idx = self.builder.add_block(debug_info)
4✔
2965
        idx_acc = self.builder.add_access(block_idx, idx_name, debug_info)
4✔
2966
        idx_var_acc = self.builder.add_access(block_idx, idx_var, debug_info)
4✔
2967
        t_load = self.builder.add_tasklet(
4✔
2968
            block_idx, TaskletCode.assign, ["_in"], ["_out"], debug_info
2969
        )
2970
        self.builder.add_memlet(
4✔
2971
            block_idx, idx_acc, "void", t_load, "_in", loop_var, idx_tensor, debug_info
2972
        )
2973
        self.builder.add_memlet(
4✔
2974
            block_idx, t_load, "_out", idx_var_acc, "void", "", None, debug_info
2975
        )
2976

2977
        # Scatter the value into arr[idx_var].
2978
        block = self.builder.add_block(debug_info)
4✔
2979
        if accumulate:
4✔
2980
            arr_in = self.builder.add_access(block, target_name, debug_info)
4✔
2981
            arr_out = self.builder.add_access(block, target_name, debug_info)
4✔
2982
            task = self.builder.add_tasklet(
4✔
2983
                block, add_code, ["_in1", "_in2"], ["_out"], debug_info
2984
            )
2985
            self.builder.add_memlet(
4✔
2986
                block, arr_in, "void", task, "_in1", idx_var, arr_tensor, debug_info
2987
            )
2988
            if vals_is_array:
4✔
2989
                vals_acc = self.builder.add_access(block, vals_name, debug_info)
4✔
2990
                self.builder.add_memlet(
4✔
2991
                    block,
2992
                    vals_acc,
2993
                    "void",
2994
                    task,
2995
                    "_in2",
2996
                    loop_var,
2997
                    self.tensor_table[vals_name],
2998
                    debug_info,
2999
                )
3000
            elif self.builder.exists(vals_name):
4✔
NEW
3001
                vals_acc = self.builder.add_access(block, vals_name, debug_info)
×
NEW
3002
                self.builder.add_memlet(
×
3003
                    block, vals_acc, "void", task, "_in2", "", None, debug_info
3004
                )
3005
            else:
3006
                vals_const = self.builder.add_constant(
4✔
3007
                    block, vals_name, elem_type, debug_info
3008
                )
3009
                self.builder.add_memlet(
4✔
3010
                    block, vals_const, "void", task, "_in2", "", None, debug_info
3011
                )
3012
            self.builder.add_memlet(
4✔
3013
                block, task, "_out", arr_out, "void", idx_var, arr_tensor, debug_info
3014
            )
3015
        else:
3016
            arr_out = self.builder.add_access(block, target_name, debug_info)
4✔
3017
            task = self.builder.add_tasklet(
4✔
3018
                block, TaskletCode.assign, ["_in"], ["_out"], debug_info
3019
            )
3020
            if vals_is_array:
4✔
3021
                vals_acc = self.builder.add_access(block, vals_name, debug_info)
4✔
3022
                self.builder.add_memlet(
4✔
3023
                    block,
3024
                    vals_acc,
3025
                    "void",
3026
                    task,
3027
                    "_in",
3028
                    loop_var,
3029
                    self.tensor_table[vals_name],
3030
                    debug_info,
3031
                )
3032
            elif self.builder.exists(vals_name):
4✔
NEW
3033
                vals_acc = self.builder.add_access(block, vals_name, debug_info)
×
NEW
3034
                self.builder.add_memlet(
×
3035
                    block, vals_acc, "void", task, "_in", "", None, debug_info
3036
                )
3037
            else:
3038
                vals_const = self.builder.add_constant(
4✔
3039
                    block, vals_name, elem_type, debug_info
3040
                )
3041
                self.builder.add_memlet(
4✔
3042
                    block, vals_const, "void", task, "_in", "", None, debug_info
3043
                )
3044
            self.builder.add_memlet(
4✔
3045
                block, task, "_out", arr_out, "void", idx_var, arr_tensor, debug_info
3046
            )
3047

3048
        self.builder.end_for()
4✔
3049

3050
    def _get_max_array_ndim_in_expr(self, node):
4✔
3051
        """Get the maximum array dimensionality in an expression."""
3052
        max_ndim = 0
4✔
3053

3054
        class NdimVisitor(ast.NodeVisitor):
4✔
3055
            def __init__(self, tensor_table):
4✔
3056
                self.tensor_table = tensor_table
4✔
3057
                self.max_ndim = 0
4✔
3058

3059
            def visit_Name(self, node):
4✔
3060
                if node.id in self.tensor_table:
4✔
3061
                    ndim = len(self.tensor_table[node.id].shape)
4✔
3062
                    self.max_ndim = max(self.max_ndim, ndim)
4✔
3063
                return self.generic_visit(node)
4✔
3064

3065
        visitor = NdimVisitor(self.tensor_table)
4✔
3066
        visitor.visit(node)
4✔
3067
        return visitor.max_ndim
4✔
3068

3069
    def _handle_broadcast_slice_assignment(
4✔
3070
        self,
3071
        target,
3072
        materialized_rhs,
3073
        target_name,
3074
        indices,
3075
        target_ndim,
3076
        value_ndim,
3077
        debug_info,
3078
    ):
3079
        """Handle slice assignment with broadcasting (e.g., 2D[:,:] = 1D[:]).
3080

3081
        materialized_rhs is the already-evaluated RHS array name (not AST node).
3082
        """
3083
        broadcast_dims = target_ndim - value_ndim
×
3084
        shapes = self.tensor_table[target_name].shape
×
3085
        rhs_tensor = self.tensor_table.get(materialized_rhs)
×
3086
        rhs_shapes = rhs_tensor.shape if rhs_tensor else []
×
3087

3088
        # Create outer loops for broadcast dimensions
3089
        outer_loop_vars = []
×
3090
        for i in range(broadcast_dims):
×
3091
            loop_var = self.builder.find_new_name(f"_bcast_iter_{i}_")
×
3092
            outer_loop_vars.append(loop_var)
×
3093

3094
            if not self.builder.exists(loop_var):
×
3095
                self.builder.add_container(loop_var, Scalar(PrimitiveType.Int64), False)
×
3096
                self.container_table[loop_var] = Scalar(PrimitiveType.Int64)
×
3097

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

3101
        # Create inner loops for value dimensions
3102
        inner_loop_vars = []
×
3103
        for i in range(value_ndim):
×
3104
            loop_var = self.builder.find_new_name(f"_inner_iter_{i}_")
×
3105
            inner_loop_vars.append(loop_var)
×
3106

3107
            if not self.builder.exists(loop_var):
×
3108
                self.builder.add_container(loop_var, Scalar(PrimitiveType.Int64), False)
×
3109
                self.container_table[loop_var] = Scalar(PrimitiveType.Int64)
×
3110

3111
            # Use RHS shape for inner dimension bounds
3112
            dim_size = (
×
3113
                rhs_shapes[i] if i < len(rhs_shapes) else shapes[broadcast_dims + i]
3114
            )
3115
            self.builder.begin_for(loop_var, "0", str(dim_size), "1", debug_info)
×
3116

3117
        # Create assignment block: target[outer_vars, inner_vars] = rhs[inner_vars]
3118
        block = self.builder.add_block(debug_info)
×
3119
        t_src = self.builder.add_access(block, materialized_rhs, debug_info)
×
3120
        t_dst = self.builder.add_access(block, target_name, debug_info)
×
3121
        t_task = self.builder.add_tasklet(
×
3122
            block, TaskletCode.assign, ["_in"], ["_out"], debug_info
3123
        )
3124

3125
        # Source index: just inner loop vars
3126
        src_index = ",".join(inner_loop_vars) if inner_loop_vars else "0"
×
3127

3128
        # Target index: outer_vars + inner_vars combined
3129
        all_target_vars = outer_loop_vars + inner_loop_vars
×
3130
        target_index = ",".join(all_target_vars) if all_target_vars else "0"
×
3131

3132
        self.builder.add_memlet(
×
3133
            block, t_src, "void", t_task, "_in", src_index, rhs_tensor, debug_info
3134
        )
3135

3136
        tensor_dst = self.tensor_table[target_name]
×
3137
        self.builder.add_memlet(
×
3138
            block, t_task, "_out", t_dst, "void", target_index, tensor_dst, debug_info
3139
        )
3140

3141
        # Close all loops (inner first, then outer)
3142
        for _ in inner_loop_vars:
×
3143
            self.builder.end_for()
×
3144
        for _ in outer_loop_vars:
×
3145
            self.builder.end_for()
×
3146

3147
    def _handle_slice_assignment(
4✔
3148
        self, target, value, target_name, indices, debug_info=None
3149
    ):
3150
        if debug_info is None:
4✔
3151
            debug_info = DebugInfo()
×
3152

3153
        # Add missing dimensions
3154
        tensor_info = self.tensor_table[target_name]
4✔
3155
        ndim = len(tensor_info.shape)
4✔
3156
        if len(indices) < ndim:
4✔
UNCOV
3157
            indices = list(indices)
×
UNCOV
3158
            for _ in range(ndim - len(indices)):
×
UNCOV
3159
                indices.append(ast.Slice(lower=None, upper=None, step=None))
×
3160

3161
        # Handle ufunc outer case separately to preserve slice shape info
3162
        has_outer, ufunc_name, outer_node = contains_ufunc_outer(value)
4✔
3163
        if has_outer:
4✔
3164
            self._handle_ufunc_outer_slice_assignment(
4✔
3165
                target, value, target_name, indices, debug_info
3166
            )
3167
            return
4✔
3168

3169
        # Count slice dimensions to determine effective target dimensionality
3170
        target_slice_ndim = sum(1 for idx in indices if isinstance(idx, ast.Slice))
4✔
3171
        value_max_ndim = self._get_max_array_ndim_in_expr(value)
4✔
3172

3173
        # ALWAYS evaluate RHS first (NumPy semantics) - before any loops
3174
        materialized_rhs = self.visit(value)
4✔
3175

3176
        if (
4✔
3177
            target_slice_ndim > 0
3178
            and value_max_ndim > 0
3179
            and target_slice_ndim > value_max_ndim
3180
        ):
3181
            # Broadcasting case: use row-by-row approach with reference memlets
3182
            self._handle_broadcast_slice_assignment(
×
3183
                target,
3184
                materialized_rhs,
3185
                target_name,
3186
                indices,
3187
                target_slice_ndim,
3188
                value_max_ndim,
3189
                debug_info,
3190
            )
3191
            return
×
3192

3193
        loop_vars = []
4✔
3194
        new_target_indices = []
4✔
3195

3196
        for i, idx in enumerate(indices):
4✔
3197
            if isinstance(idx, ast.Slice):
4✔
3198
                loop_var = self.builder.find_new_name(f"_slice_iter_{len(loop_vars)}_")
4✔
3199
                loop_vars.append(loop_var)
4✔
3200

3201
                if not self.builder.exists(loop_var):
4✔
3202
                    self.builder.add_container(
4✔
3203
                        loop_var, Scalar(PrimitiveType.Int64), False
3204
                    )
3205
                    self.container_table[loop_var] = Scalar(PrimitiveType.Int64)
4✔
3206

3207
                start_str = "0"
4✔
3208
                if idx.lower:
4✔
3209
                    start_str = self.visit(idx.lower)
4✔
3210
                    if start_str.startswith("-"):
4✔
3211
                        dim_size = (
×
3212
                            str(tensor_info.shape[i])
3213
                            if i < len(tensor_info.shape)
3214
                            else f"_{target_name}_shape_{i}"
3215
                        )
3216
                        start_str = f"({dim_size} {start_str})"
×
3217

3218
                stop_str = ""
4✔
3219
                if idx.upper and not (
4✔
3220
                    isinstance(idx.upper, ast.Constant) and idx.upper.value is None
3221
                ):
3222
                    stop_str = self.visit(idx.upper)
4✔
3223
                    if stop_str.startswith("-") or stop_str.startswith("(-"):
4✔
3224
                        dim_size = (
×
3225
                            str(tensor_info.shape[i])
3226
                            if i < len(tensor_info.shape)
3227
                            else f"_{target_name}_shape_{i}"
3228
                        )
3229
                        stop_str = f"({dim_size} {stop_str})"
×
3230
                else:
3231
                    stop_str = (
4✔
3232
                        str(tensor_info.shape[i])
3233
                        if i < len(tensor_info.shape)
3234
                        else f"_{target_name}_shape_{i}"
3235
                    )
3236

3237
                step_str = "1"
4✔
3238
                if idx.step:
4✔
3239
                    step_str = self.visit(idx.step)
×
3240

3241
                count_str = f"({stop_str} - {start_str})"
4✔
3242

3243
                self.builder.begin_for(loop_var, "0", count_str, "1", debug_info)
4✔
3244
                self.container_table[loop_var] = Scalar(PrimitiveType.Int64)
4✔
3245
                new_target_indices.append(
4✔
3246
                    ast.Name(
3247
                        id=f"{start_str} + {loop_var} * {step_str}", ctx=ast.Load()
3248
                    )
3249
                )
3250
            else:
3251
                dim_size = (
4✔
3252
                    tensor_info.shape[i]
3253
                    if i < len(tensor_info.shape)
3254
                    else f"_{target_name}_shape_{i}"
3255
                )
3256
                normalized_idx = normalize_negative_index(idx, dim_size)
4✔
3257
                # intermediate computations are placed outside the loops
3258
                idx_str = self.visit(normalized_idx)
4✔
3259
                new_target_indices.append(ast.Name(id=idx_str, ctx=ast.Load()))
4✔
3260

3261
        rewriter = SliceRewriter(loop_vars, self.tensor_table, self)
4✔
3262
        new_value = rewriter.visit(copy.deepcopy(value))
4✔
3263

3264
        new_target = copy.deepcopy(target)
4✔
3265
        if len(new_target_indices) == 1:
4✔
3266
            new_target.slice = new_target_indices[0]
4✔
3267
        else:
3268
            new_target.slice = ast.Tuple(elts=new_target_indices, ctx=ast.Load())
4✔
3269

3270
        rhs_memlet_type = None
4✔
3271
        rhs_indexed_subset = ""
4✔
3272
        if materialized_rhs in self.tensor_table:
4✔
3273
            rhs_tensor = self.tensor_table[materialized_rhs]
4✔
3274
            rhs_ndim = len(rhs_tensor.shape)
4✔
3275
            if rhs_ndim > 0 and rhs_ndim == len(loop_vars):
4✔
3276
                # RHS is an array matching the slice dimensions - index it with loop vars
3277
                rhs_indexed_subset = ",".join(loop_vars)
4✔
3278
                rhs_memlet_type = rhs_tensor
4✔
3279

3280
        block = self.builder.add_block(debug_info)
4✔
3281
        t_task = self.builder.add_tasklet(
4✔
3282
            block, TaskletCode.assign, ["_in"], ["_out"], debug_info
3283
        )
3284

3285
        t_src, src_sub = self._add_read(block, materialized_rhs, debug_info)
4✔
3286
        # Use indexed subset if RHS is an array that needs indexing
3287
        actual_src_sub = rhs_indexed_subset if rhs_indexed_subset else src_sub
4✔
3288
        self.builder.add_memlet(
4✔
3289
            block,
3290
            t_src,
3291
            "void",
3292
            t_task,
3293
            "_in",
3294
            actual_src_sub,
3295
            rhs_memlet_type,
3296
            debug_info,
3297
        )
3298

3299
        lhs_expr = self.visit(new_target)
4✔
3300
        if "(" in lhs_expr and lhs_expr.endswith(")"):
4✔
3301
            subset = lhs_expr[lhs_expr.find("(") + 1 : -1]
4✔
3302
            tensor_dst = self.tensor_table[target_name]
4✔
3303

3304
            t_dst = self.builder.add_access(block, target_name, debug_info)
4✔
3305
            self.builder.add_memlet(
4✔
3306
                block, t_task, "_out", t_dst, "void", subset, tensor_dst, debug_info
3307
            )
3308
        else:
3309
            t_dst = self.builder.add_access(block, target_name, debug_info)
×
3310
            self.builder.add_memlet(
×
3311
                block, t_task, "_out", t_dst, "void", "", None, debug_info
3312
            )
3313

3314
        for _ in loop_vars:
4✔
3315
            self.builder.end_for()
4✔
3316

3317
    def _handle_ufunc_outer_slice_assignment(
4✔
3318
        self, target, value, target_name, indices, debug_info=None
3319
    ):
3320
        """Handle slice assignment where RHS contains a ufunc outer operation.
3321

3322
        Example: path[:] = np.minimum(path[:], np.add.outer(path[:, k], path[k, :]))
3323

3324
        The strategy is:
3325
        1. Evaluate the entire RHS expression, which will create a temporary array
3326
           containing the result of the ufunc outer (potentially wrapped in other ops)
3327
        2. Copy the temporary result to the target slice
3328

3329
        This avoids the loop transformation that would destroy slice shape info.
3330
        """
3331
        if debug_info is None:
4✔
3332
            from docc.sdfg import DebugInfo
×
3333

3334
            debug_info = DebugInfo()
×
3335

3336
        # Evaluate the full RHS expression
3337
        # This will:
3338
        # - Create temp arrays for ufunc outer results
3339
        # - Apply any wrapping operations (np.minimum, etc.)
3340
        # - Return the name of the final result array
3341
        result_name = self.visit(value)
4✔
3342

3343
        # Now we need to copy result to target slice
3344
        # Count slice dimensions to determine if we need loops
3345
        target_slice_ndim = sum(1 for idx in indices if isinstance(idx, ast.Slice))
4✔
3346

3347
        if target_slice_ndim == 0:
4✔
3348
            # No slices on target - just simple assignment
3349
            target_str = self.visit(target)
×
3350
            block = self.builder.add_block(debug_info)
×
3351
            t_src, src_sub = self._add_read(block, result_name, debug_info)
×
3352
            t_dst = self.builder.add_access(block, target_str, debug_info)
×
3353
            t_task = self.builder.add_tasklet(
×
3354
                block, TaskletCode.assign, ["_in"], ["_out"], debug_info
3355
            )
3356
            self.builder.add_memlet(
×
3357
                block, t_src, "void", t_task, "_in", src_sub, None, debug_info
3358
            )
3359
            self.builder.add_memlet(
×
3360
                block, t_task, "_out", t_dst, "void", "", None, debug_info
3361
            )
3362
            return
×
3363

3364
        # We have slices on the target - need to create loops for copying
3365
        # Get target array info
3366
        target_shapes = self.tensor_table[target_name].shape
4✔
3367

3368
        loop_vars = []
4✔
3369
        new_target_indices = []
4✔
3370

3371
        for i, idx in enumerate(indices):
4✔
3372
            if isinstance(idx, ast.Slice):
4✔
3373
                loop_var = self.builder.find_new_name(f"_copy_iter_{len(loop_vars)}_")
4✔
3374
                loop_vars.append(loop_var)
4✔
3375

3376
                if not self.builder.exists(loop_var):
4✔
3377
                    self.builder.add_container(
4✔
3378
                        loop_var, Scalar(PrimitiveType.Int64), False
3379
                    )
3380
                    self.container_table[loop_var] = Scalar(PrimitiveType.Int64)
4✔
3381

3382
                start_str = "0"
4✔
3383
                if idx.lower:
4✔
3384
                    start_str = self.visit(idx.lower)
×
3385

3386
                stop_str = ""
4✔
3387
                if idx.upper and not (
4✔
3388
                    isinstance(idx.upper, ast.Constant) and idx.upper.value is None
3389
                ):
3390
                    stop_str = self.visit(idx.upper)
×
3391
                else:
3392
                    stop_str = (
4✔
3393
                        target_shapes[i]
3394
                        if i < len(target_shapes)
3395
                        else f"_{target_name}_shape_{i}"
3396
                    )
3397

3398
                step_str = "1"
4✔
3399
                if idx.step:
4✔
3400
                    step_str = self.visit(idx.step)
×
3401

3402
                count_str = f"({stop_str} - {start_str})"
4✔
3403

3404
                self.builder.begin_for(loop_var, "0", count_str, "1", debug_info)
4✔
3405
                self.container_table[loop_var] = Scalar(PrimitiveType.Int64)
4✔
3406

3407
                new_target_indices.append(
4✔
3408
                    ast.Name(
3409
                        id=f"{start_str} + {loop_var} * {step_str}", ctx=ast.Load()
3410
                    )
3411
                )
3412
            else:
3413
                # Handle non-slice indices - need to normalize negative indices
3414
                dim_size = (
×
3415
                    target_shapes[i]
3416
                    if i < len(target_shapes)
3417
                    else f"_{target_name}_shape_{i}"
3418
                )
3419
                normalized_idx = normalize_negative_index(idx, dim_size)
×
3420
                # Visit the index NOW before any loops are opened to ensure
3421
                # intermediate computations are placed outside the loops
3422
                idx_str = self.visit(normalized_idx)
×
3423
                new_target_indices.append(ast.Name(id=idx_str, ctx=ast.Load()))
×
3424

3425
        # Create assignment block: target[i,j,...] = result[i,j,...]
3426
        block = self.builder.add_block(debug_info)
4✔
3427

3428
        # Access nodes
3429
        t_src = self.builder.add_access(block, result_name, debug_info)
4✔
3430
        t_dst = self.builder.add_access(block, target_name, debug_info)
4✔
3431
        t_task = self.builder.add_tasklet(
4✔
3432
            block, TaskletCode.assign, ["_in"], ["_out"], debug_info
3433
        )
3434

3435
        # Source index - just use loop vars for flat array from ufunc outer
3436
        # The ufunc outer result is a flat array of size M*N
3437
        if len(loop_vars) == 2:
4✔
3438
            # 2D case: result is indexed as i * N + j
3439
            # Get the second dimension size from target shapes
3440
            n_dim = (
4✔
3441
                target_shapes[1]
3442
                if len(target_shapes) > 1
3443
                else f"_{target_name}_shape_1"
3444
            )
3445
            src_index = f"(({loop_vars[0]}) * ({n_dim}) + ({loop_vars[1]}))"
4✔
3446
        elif len(loop_vars) == 1:
×
3447
            src_index = loop_vars[0]
×
3448
        else:
3449
            # General case - compute linear index
3450
            src_terms = []
×
3451
            stride = "1"
×
3452
            for i in range(len(loop_vars) - 1, -1, -1):
×
3453
                if stride == "1":
×
3454
                    src_terms.insert(0, loop_vars[i])
×
3455
                else:
3456
                    src_terms.insert(0, f"({loop_vars[i]} * {stride})")
×
3457
                if i > 0:
×
3458
                    dim_size = (
×
3459
                        target_shapes[i]
3460
                        if i < len(target_shapes)
3461
                        else f"_{target_name}_shape_{i}"
3462
                    )
3463
                    stride = (
×
3464
                        f"({stride} * {dim_size})" if stride != "1" else str(dim_size)
3465
                    )
3466
            src_index = " + ".join(src_terms) if src_terms else "0"
×
3467

3468
        # Target index - compute linear index (row-major order)
3469
        # For 2D array with shape (M, N): linear_index = i * N + j
3470
        target_index_parts = []
4✔
3471
        for idx in new_target_indices:
4✔
3472
            if isinstance(idx, ast.Name):
4✔
3473
                target_index_parts.append(idx.id)
4✔
3474
            else:
3475
                target_index_parts.append(self.visit(idx))
×
3476

3477
        # Convert to linear index
3478
        if len(target_index_parts) == 2:
4✔
3479
            # 2D case
3480
            n_dim = (
4✔
3481
                target_shapes[1]
3482
                if len(target_shapes) > 1
3483
                else f"_{target_name}_shape_1"
3484
            )
3485
            target_index = (
4✔
3486
                f"(({target_index_parts[0]}) * ({n_dim}) + ({target_index_parts[1]}))"
3487
            )
3488
        elif len(target_index_parts) == 1:
×
3489
            target_index = target_index_parts[0]
×
3490
        else:
3491
            # General case - compute linear index with strides
3492
            stride = "1"
×
3493
            target_index = "0"
×
3494
            for i in range(len(target_index_parts) - 1, -1, -1):
×
3495
                idx_part = target_index_parts[i]
×
3496
                if stride == "1":
×
3497
                    term = idx_part
×
3498
                else:
3499
                    term = f"(({idx_part}) * ({stride}))"
×
3500

3501
                if target_index == "0":
×
3502
                    target_index = term
×
3503
                else:
3504
                    target_index = f"({term} + {target_index})"
×
3505

3506
                if i > 0:
×
3507
                    dim_size = (
×
3508
                        target_shapes[i]
3509
                        if i < len(target_shapes)
3510
                        else f"_{target_name}_shape_{i}"
3511
                    )
3512
                    stride = (
×
3513
                        f"({stride} * {dim_size})" if stride != "1" else str(dim_size)
3514
                    )
3515

3516
        # Connect memlets
3517
        self.builder.add_memlet(
4✔
3518
            block, t_src, "void", t_task, "_in", src_index, None, debug_info
3519
        )
3520
        self.builder.add_memlet(
4✔
3521
            block, t_task, "_out", t_dst, "void", target_index, None, debug_info
3522
        )
3523

3524
        # End loops
3525
        for _ in loop_vars:
4✔
3526
            self.builder.end_for()
4✔
3527

3528
    def _contains_indirect_access(self, node):
4✔
3529
        """Check if an AST node contains any indirect array access."""
3530
        if isinstance(node, ast.Subscript):
4✔
3531
            if isinstance(node.value, ast.Name):
4✔
3532
                arr_name = node.value.id
4✔
3533
                if arr_name in self.tensor_table:
4✔
3534
                    return True
4✔
3535
        elif isinstance(node, ast.BinOp):
4✔
3536
            return self._contains_indirect_access(
4✔
3537
                node.left
3538
            ) or self._contains_indirect_access(node.right)
3539
        elif isinstance(node, ast.UnaryOp):
4✔
3540
            return self._contains_indirect_access(node.operand)
4✔
3541
        return False
4✔
3542

3543
    def _materialize_indirect_access(
4✔
3544
        self, node, debug_info=None, return_original_expr=False
3545
    ):
3546
        """Materialize an array access into a scalar variable using tasklet+memlets."""
3547
        if not self.builder:
4✔
3548
            expr = self.visit(node)
×
3549
            return (expr, expr) if return_original_expr else expr
×
3550

3551
        if debug_info is None:
4✔
3552
            debug_info = DebugInfo()
4✔
3553

3554
        if not isinstance(node, ast.Subscript):
4✔
3555
            expr = self.visit(node)
×
3556
            return (expr, expr) if return_original_expr else expr
×
3557

3558
        if not isinstance(node.value, ast.Name):
4✔
3559
            expr = self.visit(node)
×
3560
            return (expr, expr) if return_original_expr else expr
×
3561

3562
        arr_name = node.value.id
4✔
3563
        if arr_name not in self.tensor_table:
4✔
3564
            expr = self.visit(node)
×
3565
            return (expr, expr) if return_original_expr else expr
×
3566

3567
        dtype = Scalar(PrimitiveType.Int64)
4✔
3568
        if arr_name in self.container_table:
4✔
3569
            t = self.container_table[arr_name]
4✔
3570
            if isinstance(t, Pointer) and t.has_pointee_type():
4✔
3571
                dtype = t.pointee_type
4✔
3572

3573
        tmp_name = self.builder.find_new_name("_idx_")
4✔
3574
        self.builder.add_container(tmp_name, dtype, False)
4✔
3575
        self.container_table[tmp_name] = dtype
4✔
3576

3577
        ndim = len(self.tensor_table[arr_name].shape)
4✔
3578
        shapes = self.tensor_table[arr_name].shape
4✔
3579

3580
        if isinstance(node.slice, ast.Tuple):
4✔
3581
            indices = [self.visit(elt) for elt in node.slice.elts]
×
3582
        else:
3583
            indices = [self.visit(node.slice)]
4✔
3584

3585
        materialized_indices = []
4✔
3586
        for idx_str in indices:
4✔
3587
            if "(" in idx_str and idx_str.endswith(")"):
4✔
3588
                materialized_indices.append(idx_str)
×
3589
            else:
3590
                materialized_indices.append(idx_str)
4✔
3591

3592
        linear_index = self._compute_linear_index(
4✔
3593
            materialized_indices, shapes, arr_name, ndim
3594
        )
3595

3596
        block = self.builder.add_block(debug_info)
4✔
3597
        t_src = self.builder.add_access(block, arr_name, debug_info)
4✔
3598
        t_dst = self.builder.add_access(block, tmp_name, debug_info)
4✔
3599
        t_task = self.builder.add_tasklet(
4✔
3600
            block, TaskletCode.assign, ["_in"], ["_out"], debug_info
3601
        )
3602

3603
        self.builder.add_memlet(
4✔
3604
            block, t_src, "void", t_task, "_in", linear_index, None, debug_info
3605
        )
3606
        self.builder.add_memlet(
4✔
3607
            block, t_task, "_out", t_dst, "void", "", None, debug_info
3608
        )
3609

3610
        if return_original_expr:
4✔
3611
            original_expr = f"{arr_name}({linear_index})"
4✔
3612
            return (tmp_name, original_expr)
4✔
3613

3614
        return tmp_name
×
3615

3616
    def _get_unique_id(self):
4✔
3617
        self._unique_counter_ref[0] += 1
4✔
3618
        return self._unique_counter_ref[0]
4✔
3619

3620
    def _get_memlet_type_for_access(self, expr_str, subset):
4✔
3621
        """Get the Tensor type for an indexed array access expression.
3622

3623
        When accessing an array like "arr(i,j)" with a multi-dimensional subset,
3624
        we need to pass the Tensor type to add_memlet for correct type inference.
3625
        If the expression is a simple scalar variable or constant, returns None.
3626
        """
3627
        if not subset:
4✔
3628
            return None
4✔
3629

3630
        # Check if expr_str is an indexed array access like "arr(i,j)"
3631
        if "(" in expr_str and expr_str.endswith(")"):
×
3632
            name = expr_str.split("(")[0]
×
3633
            if name in self.tensor_table:
×
3634
                return self.tensor_table[name]
×
3635

3636
        # Check if expr_str is a simple array name with a non-empty subset from _add_read
3637
        if expr_str in self.tensor_table:
×
3638
            return self.tensor_table[expr_str]
×
3639

3640
        return None
×
3641

3642
    def _element_type(self, name):
4✔
3643
        if name in self.container_table:
4✔
3644
            return element_type_from_sdfg_type(self.container_table[name])
4✔
3645
        else:  # Constant
3646
            if self._is_int(name):
4✔
3647
                return Scalar(PrimitiveType.Int64)
4✔
3648
            else:
3649
                return Scalar(PrimitiveType.Double)
4✔
3650

3651
    def _is_int(self, operand):
4✔
3652
        try:
4✔
3653
            if operand.lstrip("-").isdigit():
4✔
3654
                return True
4✔
3655
        except ValueError:
×
3656
            pass
×
3657

3658
        name = operand
4✔
3659
        if "(" in operand and operand.endswith(")"):
4✔
3660
            name = operand.split("(")[0]
×
3661

3662
        if name in self.container_table:
4✔
3663
            t = self.container_table[name]
4✔
3664

3665
            def is_int_ptype(pt):
4✔
3666
                return pt in [
4✔
3667
                    PrimitiveType.Int64,
3668
                    PrimitiveType.Int32,
3669
                    PrimitiveType.Int8,
3670
                    PrimitiveType.Int16,
3671
                    PrimitiveType.UInt64,
3672
                    PrimitiveType.UInt32,
3673
                    PrimitiveType.UInt8,
3674
                    PrimitiveType.UInt16,
3675
                ]
3676

3677
            if isinstance(t, Scalar):
4✔
3678
                return is_int_ptype(t.primitive_type)
4✔
3679

3680
            if type(t).__name__ == "Array" and hasattr(t, "element_type"):
×
3681
                et = t.element_type
×
3682
                if callable(et):
×
3683
                    et = et()
×
3684
                if isinstance(et, Scalar):
×
3685
                    return is_int_ptype(et.primitive_type)
×
3686

3687
            if type(t).__name__ == "Pointer":
×
3688
                if hasattr(t, "pointee_type"):
×
3689
                    et = t.pointee_type
×
3690
                    if callable(et):
×
3691
                        et = et()
×
3692
                    if isinstance(et, Scalar):
×
3693
                        return is_int_ptype(et.primitive_type)
×
3694
                if hasattr(t, "element_type"):
×
3695
                    et = t.element_type
×
3696
                    if callable(et):
×
3697
                        et = et()
×
3698
                    if isinstance(et, Scalar):
×
3699
                        return is_int_ptype(et.primitive_type)
×
3700

3701
        return False
4✔
3702

3703
    def _add_read(self, block, expr_str, debug_info=None):
4✔
3704
        try:
4✔
3705
            if (block, expr_str) in self._access_cache:
4✔
3706
                return self._access_cache[(block, expr_str)]
×
3707
        except TypeError:
×
3708
            pass
×
3709

3710
        if debug_info is None:
4✔
3711
            debug_info = DebugInfo()
4✔
3712

3713
        if "(" in expr_str and expr_str.endswith(")"):
4✔
3714
            name = expr_str.split("(")[0]
×
3715
            subset = expr_str[expr_str.find("(") + 1 : -1]
×
3716
            access = self.builder.add_access(block, name, debug_info)
×
3717
            try:
×
3718
                self._access_cache[(block, expr_str)] = (access, subset)
×
3719
            except TypeError:
×
3720
                pass
×
3721
            return access, subset
×
3722

3723
        if self.builder.exists(expr_str):
4✔
3724
            access = self.builder.add_access(block, expr_str, debug_info)
4✔
3725
            subset = ""
4✔
3726
            if expr_str in self.container_table:
4✔
3727
                sym_type = self.container_table[expr_str]
4✔
3728
                if isinstance(sym_type, Pointer):
4✔
3729
                    if expr_str in self.tensor_table:
4✔
3730
                        ndim = len(self.tensor_table[expr_str].shape)
4✔
3731
                        if ndim == 0:
4✔
3732
                            subset = "0"
×
3733
                    else:
3734
                        subset = "0"
×
3735
            try:
4✔
3736
                self._access_cache[(block, expr_str)] = (access, subset)
4✔
3737
            except TypeError:
×
3738
                pass
×
3739
            return access, subset
4✔
3740

3741
        dtype = Scalar(PrimitiveType.Double)
4✔
3742
        if self._is_int(expr_str):
4✔
3743
            dtype = Scalar(PrimitiveType.Int64)
4✔
3744
        elif expr_str == "true" or expr_str == "false":
4✔
3745
            dtype = Scalar(PrimitiveType.Bool)
×
3746

3747
        const_node = self.builder.add_constant(block, expr_str, dtype, debug_info)
4✔
3748
        try:
4✔
3749
            self._access_cache[(block, expr_str)] = (const_node, "")
4✔
3750
        except TypeError:
×
3751
            pass
×
3752
        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