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

daisytuner / docc / 31175641015

07 Aug 2026 11:49AM UTC coverage: 65.128% (+0.02%) from 65.113%
31175641015

push

github

web-flow
Compile time reporting (#940)

* Added DoccMetrics class to emit a properties file containing among other things compile times for python, mlir and pytorch frontends
* DoccMetrics prepares a few metrics reused across the different frontends and captures CI specific env vars to allow matching output folders up with CI actions
 ! done, so that if REUSE_BINARIES is on, the properties will get appended with the run that reuses them, even if it was first built (and had compile times recorded) during the build stage

32 of 34 new or added lines in 3 files covered. (94.12%)

1 existing line in 1 file now uncovered.

46379 of 71212 relevant lines covered (65.13%)

717.15 hits per line

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

64.43
/python/docc/python/python_program.py
1
import inspect
4✔
2
import json
4✔
3
import shutil
4✔
4
import textwrap
4✔
5
import ast
4✔
6
import os
4✔
7
import getpass
4✔
8
import hashlib
4✔
9
import ml_dtypes
4✔
10
import numpy as np
4✔
11
from typing import Annotated, get_origin, get_args, Any, Optional
4✔
12
import time
4✔
13

14
from docc.sdfg import (
4✔
15
    Scalar,
16
    PrimitiveType,
17
    Pointer,
18
    Structure,
19
    Array,
20
    Type,
21
    Tensor,
22
    StructuredSDFG,
23
    StructuredSDFGBuilder,
24
    DoccMetrics,
25
)
26
from docc.compiler.docc_program import DoccProgram
4✔
27
from docc.compiler.compiled_sdfg import CompiledSDFG
4✔
28
from docc.python.ast_parser import ASTParser
4✔
29
from docc.python.types import element_type_from_sdfg_type
4✔
30

31

32
def _compile_wrapper(self, output_folder=None):
4✔
33
    """Wrapper to allow StructuredSDFG.compile() to return a CompiledSDFG."""
34
    lib_path = self._compile(output_folder)
×
35
    return CompiledSDFG(lib_path, self)
×
36

37

38
# Monkey-patch StructuredSDFG to add compile method
39
StructuredSDFG.compile = _compile_wrapper
4✔
40

41

42
def _map_python_type(dtype):
4✔
43
    """Map Python/numpy types to SDFG types."""
44
    # If it is already a sdfg Type, return it
45
    if isinstance(dtype, Type):
4✔
46
        return dtype
×
47

48
    # Handle Annotated for Arrays
49
    if get_origin(dtype) is Annotated:
4✔
50
        args = get_args(dtype)
×
51
        base_type = args[0]
×
52
        metadata = args[1:]
×
53

54
        if base_type is np.ndarray:
×
55
            # Convention: Annotated[np.ndarray, shape, dtype]
56
            shape = metadata[0]
×
57
            elem_type = Scalar(PrimitiveType.Double)  # Default
×
58

59
            if len(metadata) > 1:
×
60
                possible_dtype = metadata[1]
×
61
                elem_type = _map_python_type(possible_dtype)
×
62

63
            return Pointer(elem_type)
×
64

65
    # Handle numpy.ndarray[Shape, DType]
66
    if get_origin(dtype) is np.ndarray:
4✔
67
        args = get_args(dtype)
×
68
        # args[0] is shape, args[1] is dtype
69
        if len(args) >= 2:
×
70
            elem_type = _map_python_type(args[1])
×
71
            return Pointer(elem_type)
×
72

73
    # Handle a parametrized numpy dtype generic, e.g. numpy.dtype[numpy.float64]
74
    # (produced by npt.NDArray[RealT] -> ndarray[Any, dtype[float64]]).
75
    if get_origin(dtype) is np.dtype:
4✔
76
        inner = get_args(dtype)
×
77
        if inner:
×
78
            return _map_python_type(inner[0])
×
79

80
    # Simple mapping for python types
81
    if dtype is float or dtype is np.float64:
4✔
82
        return Scalar(PrimitiveType.Double)
4✔
83
    elif dtype is np.float32:
4✔
84
        return Scalar(PrimitiveType.Float)
×
85
    elif dtype is bool or dtype is np.bool_:
4✔
86
        return Scalar(PrimitiveType.Bool)
4✔
87
    elif dtype is int or dtype is np.int64:
4✔
88
        return Scalar(PrimitiveType.Int64)
4✔
89
    elif dtype is np.int32:
×
90
        return Scalar(PrimitiveType.Int32)
×
91
    elif dtype is np.int16:
×
92
        return Scalar(PrimitiveType.Int16)
×
93
    elif dtype is np.int8:
×
94
        return Scalar(PrimitiveType.Int8)
×
95
    elif dtype is np.uint64:
×
96
        return Scalar(PrimitiveType.UInt64)
×
97
    elif dtype is np.uint32:
×
98
        return Scalar(PrimitiveType.UInt32)
×
99
    elif dtype is np.uint16:
×
100
        return Scalar(PrimitiveType.UInt16)
×
101
    elif dtype is np.uint8:
×
102
        return Scalar(PrimitiveType.UInt8)
×
103

104
    # Handle Python classes - map to Structure type
105
    if inspect.isclass(dtype):
×
106
        # Use the class name as the structure name
107
        return Pointer(Structure(dtype.__name__))
×
108

109
    return dtype
×
110

111

112
class PythonProgram(DoccProgram):
4✔
113

114
    def __init__(
4✔
115
        self,
116
        func,
117
        target: str = "none",
118
        category: str = "server",
119
        instrumentation_mode: Optional[str] = None,
120
        capture_args: Optional[bool] = None,
121
        remote_tuning: bool = False,
122
    ):
123
        super().__init__(
4✔
124
            name=func.__name__,
125
            target=target,
126
            category=category,
127
            instrumentation_mode=instrumentation_mode,
128
            capture_args=capture_args,
129
            remote_tuning=remote_tuning,
130
        )
131
        self.func = func
4✔
132
        self._last_structure_member_info = {}
4✔
133

134
    def __call__(self, *args: Any) -> Any:
4✔
135
        # JIT compile and run. CompiledSDFG validates the call mode (numpy /
136
        # cupy / torch) and rejects GPU arrays on non-device-resident artifacts.
137
        compiled = self.compile(*args)
4✔
138
        res = compiled(*args)
4✔
139

140
        # Handle return value conversion based on annotation
141
        sig = inspect.signature(self.func)
4✔
142
        ret_annotation = sig.return_annotation
4✔
143

144
        if ret_annotation is not inspect.Signature.empty:
4✔
145
            if get_origin(ret_annotation) is Annotated:
4✔
146
                type_args = get_args(ret_annotation)
×
147
                if len(type_args) >= 1 and type_args[0] is np.ndarray:
×
148
                    shape = None
×
149
                    if len(type_args) >= 2:
×
150
                        shape = type_args[1]
×
151

152
                    if shape is not None:
×
153
                        try:
×
154
                            return np.ctypeslib.as_array(res, shape=shape)
×
155
                        except Exception:
×
156
                            pass
×
157

158
        # Try to infer return shape from metadata
159
        if hasattr(compiled, "get_return_shape"):
4✔
160
            shape = compiled.get_return_shape(*args)
4✔
161
            if shape is not None:
4✔
162
                try:
×
163
                    return np.ctypeslib.as_array(res, shape=shape)
×
164
                except Exception:
×
165
                    pass
×
166

167
        return res
4✔
168

169
    def compile(
4✔
170
        self,
171
        *args: Any,
172
        output_folder: Optional[str] = None,
173
        instrumentation_mode: Optional[str] = None,
174
        capture_args: Optional[bool] = None,
175
        remote_tuning: Optional[bool] = None,
176
    ) -> CompiledSDFG:
177
        original_output_folder = output_folder
4✔
178

179
        metrics = DoccMetrics()
4✔
180
        compile_start_time = time.perf_counter()
4✔
181
        metrics.add_metric("function", self.name, "source")
4✔
182
        metrics.add_frontend_source_info("python")
4✔
183

184
        # Resolve options
185
        instrumentation_mode, capture_args, remote_tuning = (
4✔
186
            self._resolve_compile_options(
187
                instrumentation_mode, capture_args, remote_tuning
188
            )
189
        )
190

191
        # When binary reuse is requested, the build run must persist the
192
        # normalized SDFG (py4.norm.json) so a later run can reload it without
193
        # re-parsing/recompiling. Force the dump if instrumentation/capture
194
        # would not already produce it.
195
        docc_reuse_binaries = os.environ.get("DOCC_REUSE_BINARIES")
4✔
196
        if docc_reuse_binaries and not self.debug_dump:
4✔
197
            self.debug_dump = True
×
198

199
        # 1. Analyze arguments and shapes
200
        arg_types = []
4✔
201
        shape_values = []  # List of unique shape values found
4✔
202
        shape_sources = []  # List of (arg_idx, dim_idx) for each unique shape value
4✔
203

204
        # Mapping from (arg_idx, dim_idx) -> unique_shape_idx
205
        arg_shape_mapping = {}
4✔
206

207
        # First pass: collect scalar integer arguments and their values
208
        sig = inspect.signature(self.func)
4✔
209
        params = list(sig.parameters.items())
4✔
210
        scalar_int_params = {}  # Maps value -> parameter name (first one wins)
4✔
211
        for i, ((name, param), arg) in enumerate(zip(params, args)):
4✔
212
            if isinstance(arg, (int, np.integer)) and not isinstance(
4✔
213
                arg, (bool, np.bool_)
214
            ):
215
                val = int(arg)
4✔
216
                if val not in scalar_int_params:
4✔
217
                    scalar_int_params[val] = name
4✔
218

219
        for i, arg in enumerate(args):
4✔
220
            t = self._infer_type(arg)
4✔
221
            arg_types.append(t)
4✔
222

223
            if isinstance(arg, np.ndarray):
4✔
224
                for dim_idx, dim_val in enumerate(arg.shape):
4✔
225
                    # Check if we've seen this value
226
                    if dim_val in shape_values:
4✔
227
                        # Reuse
228
                        u_idx = shape_values.index(dim_val)
4✔
229
                    else:
230
                        # New
231
                        u_idx = len(shape_values)
4✔
232
                        shape_values.append(dim_val)
4✔
233
                        shape_sources.append((i, dim_idx))
4✔
234

235
                    arg_shape_mapping[(i, dim_idx)] = u_idx
4✔
236

237
        # 2. Signature - include scalar-shape equivalences for correct caching
238
        mapping_sig = sorted(arg_shape_mapping.items())
4✔
239
        type_sig = ", ".join(self._type_to_str(t) for t in arg_types)
4✔
240
        signature = f"{type_sig}|{mapping_sig}"
4✔
241

242
        # In-memory cache key: the structural signature plus the resolved compile
243
        # options, so repeated in-process compiles with different
244
        # instrumentation/arg-capture/remote-tuning do not alias to the first
245
        # built binary (the on-disk hash already accounts for these options).
246
        mem_cache_key = (
4✔
247
            f"{signature}|{capture_args}|{instrumentation_mode}|{remote_tuning}"
248
        )
249

250
        if output_folder is None:
4✔
251
            source_path = inspect.getsourcefile(self.func)
4✔
252
            hash_input = f"{source_path}|{self.name}|{self.target}|{self.category}|{capture_args}|{instrumentation_mode}|{remote_tuning}|{signature}".encode(
4✔
253
                "utf-8"
254
            )
255
            stable_id = hashlib.sha256(hash_input).hexdigest()[:16]
4✔
256
            filename = os.path.basename(inspect.getsourcefile(self.func))
4✔
257

258
            docc_tmp = os.environ.get("DOCC_TMP")
4✔
259
            if docc_tmp:
4✔
260
                output_folder = (
×
261
                    f"{docc_tmp}/{filename}-{self.name}-{self.target}-{stable_id}"
262
                )
263
            else:
264
                user = os.getenv("USER")
4✔
265
                if not user:
4✔
266
                    user = getpass.getuser()
4✔
267
                output_folder = f"/tmp/{user}/DOCC/{self.name}-{stable_id}"
4✔
268

269
        if original_output_folder is None and mem_cache_key in self.cache:
4✔
270
            return self.cache[mem_cache_key]
4✔
271

272
        # 3. Reuse a previously built binary if requested and available.
273
        # Structure arguments need per-member layout info that is only produced
274
        # while parsing the kernel, so reuse is limited to plain array/scalar
275
        # kernels; anything else falls through to a full rebuild.
276
        has_struct_args = any(
4✔
277
            isinstance(t, Pointer)
278
            and t.has_pointee_type()
279
            and isinstance(t.pointee_type, Structure)
280
            for t in arg_types
281
        )
282
        if docc_reuse_binaries and not has_struct_args:
4✔
283
            reused = self._try_reuse_binary(output_folder, shape_sources)
×
284
            if reused is not None:
×
285
                if original_output_folder is None:
×
286
                    self.cache[mem_cache_key] = reused
×
287

NEW
288
                metrics.capture_env_vars()
×
NEW
289
                metrics.append_to(output_folder)
×
UNCOV
290
                return reused
×
291

292
        # 4. Build SDFG
293
        if os.path.exists(output_folder):
4✔
294
            # Multiple python processes running the same code?
295
            shutil.rmtree(output_folder)
4✔
296
        sdfg, out_args, out_shapes, out_strides = self._build_sdfg(
4✔
297
            arg_types, args, arg_shape_mapping, shape_values
298
        )
299

300
        lib_path = self.sdfg_pipe(
4✔
301
            sdfg,
302
            output_folder,
303
            instrumentation_mode,
304
            capture_args,
305
            remote_tuning,
306
            metrics=metrics,
307
        )
308

309
        # Persist the return-value layout so a later DOCC_REUSE_BINARIES run can
310
        # rebuild the CompiledSDFG without re-parsing the kernel.
311
        if output_folder:
4✔
312
            self._persist_return_layout(output_folder, sdfg, out_shapes, out_strides)
4✔
313

314
        # 5. Create CompiledSDFG
315
        compiled = CompiledSDFG(
4✔
316
            lib_path,
317
            sdfg,
318
            shape_sources,
319
            self._last_structure_member_info,
320
            out_args,
321
            out_shapes,
322
            out_strides,
323
            device_resident=self._device_resident,
324
            device_backend=self._device_backend,
325
            target=self.target,
326
        )
327

328
        # Cache if using default output folder
329
        if original_output_folder is None:
4✔
330
            self.cache[mem_cache_key] = compiled
4✔
331

332
        compile_time_ms = round((time.perf_counter() - compile_start_time) * 1000)
4✔
333
        metrics.add_metric("compile_time_ms", compile_time_ms, "compile")
4✔
334
        metrics.capture_env_vars()
4✔
335
        metrics.append_to(output_folder)
4✔
336

337
        return compiled
4✔
338

339
    def _persist_return_layout(
4✔
340
        self, output_folder: str, sdfg: StructuredSDFG, out_shapes, out_strides
341
    ) -> None:
342
        """Stamp the return-value layout into the persisted SDFG metadata.
343

344
        The return shapes/strides are discovered while parsing the kernel and
345
        are not otherwise recoverable from the SDFG structure. Persisting them
346
        (into the same ``py4.norm.json`` the reuse path loads) lets a later
347
        ``DOCC_REUSE_BINARIES`` run reconstruct the CompiledSDFG without
348
        re-parsing/recompiling.
349
        """
350
        json_path = os.path.join(output_folder, f"{sdfg.name}.py4.norm.json")
4✔
351
        if not os.path.exists(json_path):
4✔
352
            return
4✔
353
        try:
×
354
            with open(json_path) as f:
×
355
                data = json.load(f)
×
356
            metadata = data.setdefault("metadata", {})
×
357
            metadata["output_shapes"] = json.dumps(out_shapes)
×
358
            metadata["output_strides"] = json.dumps(out_strides)
×
359
            with open(json_path, "w") as f:
×
360
                json.dump(data, f)
×
361
        except (OSError, ValueError):
×
362
            pass
×
363

364
    def _try_reuse_binary(
4✔
365
        self, output_folder: Optional[str], shape_sources
366
    ) -> Optional[CompiledSDFG]:
367
        """Reload a cached ``.so`` + normalized SDFG instead of recompiling.
368

369
        Mirrors the strictness of the pytorch/mlir binary-reuse path: when the
370
        cache directory does not exist yet this returns ``None`` so the caller
371
        performs a first build (no error); but when the directory *does* exist
372
        and a required artifact is missing, it raises ``ValueError`` so a broken
373
        or stale cache surfaces loudly instead of silently recompiling. The
374
        calling convention (device residency) and return-value layout are
375
        restored from the persisted SDFG metadata so arguments are marshalled
376
        exactly as they were at build time.
377
        """
378
        if not output_folder:
×
379
            return None
×
380

381
        # Cache directory absent -> first build. Let the caller build it; this
382
        # is the one case the mlir/pytorch frontends also treat as non-fatal.
383
        if not os.path.exists(output_folder):
×
384
            return None
×
385

386
        sdfg_name = f"{self.name}_sdfg"
×
387
        lib_path = os.path.join(output_folder, f"lib{sdfg_name}.so")
×
388
        json_path = os.path.join(output_folder, f"{sdfg_name}.py4.norm.json")
×
389
        if not os.path.exists(lib_path):
×
390
            raise ValueError(f"Tried reusing binary '{lib_path}' but does not exist")
×
391
        if not os.path.exists(json_path):
×
392
            raise ValueError(f"Tried loading SDFG '{json_path}' but does not exist")
×
393

394
        sdfg = StructuredSDFG.from_file(json_path)
×
395

396
        # Return arguments are recoverable directly from the SDFG signature.
397
        out_args = [name for name in sdfg.arguments if name.startswith("_docc_ret_")]
×
398

399
        # Return-value layout was persisted into the SDFG metadata at build time.
400
        out_shapes = {}
×
401
        out_strides = {}
×
402
        shapes_meta = sdfg.metadata("output_shapes")
×
403
        strides_meta = sdfg.metadata("output_strides")
×
404
        if shapes_meta:
×
405
            try:
×
406
                out_shapes = json.loads(shapes_meta)
×
407
            except ValueError:
×
408
                out_shapes = {}
×
409
        if strides_meta:
×
410
            try:
×
411
                out_strides = json.loads(strides_meta)
×
412
            except ValueError:
×
413
                out_strides = {}
×
414

415
        # Restore the device-residency calling convention chosen at compile time;
416
        # otherwise a device-resident binary would be fed host pointers.
417
        self._device_resident = sdfg.metadata("device_resident") == "1"
×
418
        backend = sdfg.metadata("device_backend")
×
419
        self._device_backend = backend or None
×
420

421
        return CompiledSDFG(
×
422
            lib_path,
423
            sdfg,
424
            shape_sources,
425
            {},
426
            out_args,
427
            out_shapes,
428
            out_strides,
429
            device_resident=self._device_resident,
430
            device_backend=self._device_backend,
431
            target=self.target,
432
        )
433

434
    def to_sdfg(self, *args: Any) -> StructuredSDFG:
4✔
435
        arg_types = [self._infer_type(arg) for arg in args]
×
436

437
        # Build shape mapping
438
        shape_values = []
×
439
        shape_sources = []
×
440
        arg_shape_mapping = {}
×
441

442
        sig = inspect.signature(self.func)
×
443
        params = list(sig.parameters.items())
×
444
        scalar_int_params = {}
×
445
        for i, ((name, param), arg) in enumerate(zip(params, args)):
×
446
            if isinstance(arg, (int, np.integer)) and not isinstance(
×
447
                arg, (bool, np.bool_)
448
            ):
449
                val = int(arg)
×
450
                if val not in scalar_int_params:
×
451
                    scalar_int_params[val] = name
×
452

453
        for i, arg in enumerate(args):
×
454
            if isinstance(arg, np.ndarray):
×
455
                for dim_idx, dim_val in enumerate(arg.shape):
×
456
                    if dim_val in shape_values:
×
457
                        u_idx = shape_values.index(dim_val)
×
458
                    else:
459
                        u_idx = len(shape_values)
×
460
                        shape_values.append(dim_val)
×
461
                        shape_sources.append((i, dim_idx))
×
462
                    arg_shape_mapping[(i, dim_idx)] = u_idx
×
463

464
        sdfg, _, _, _ = self._build_sdfg(
×
465
            arg_types, args, arg_shape_mapping, shape_values
466
        )
467
        return sdfg
×
468

469
    def _convert_inputs(self, args: tuple) -> tuple:
4✔
470
        return args
×
471

472
    def _convert_outputs(self, result: Any, original_args: tuple) -> Any:
4✔
473
        return result
×
474

475
    def _get_signature(self, arg_types):
4✔
476
        return ", ".join(self._type_to_str(t) for t in arg_types)
×
477

478
    def _type_to_str(self, t):
4✔
479
        if isinstance(t, Scalar):
4✔
480
            return f"Scalar({t.primitive_type})"
4✔
481
        elif isinstance(t, Array):
4✔
482
            return f"Array({self._type_to_str(t.element_type)}, {t.num_elements})"
×
483
        elif isinstance(t, Pointer):
4✔
484
            return f"Pointer({self._type_to_str(t.pointee_type)})"
4✔
485
        elif isinstance(t, Structure):
4✔
486
            return f"Structure({t.name})"
4✔
487
        return str(t)
×
488

489
    def _infer_type(self, arg):
4✔
490
        if isinstance(arg, (float, np.float64)):
4✔
491
            return Scalar(PrimitiveType.Double)
4✔
492
        elif isinstance(arg, np.float32):
4✔
493
            return Scalar(PrimitiveType.Float)
4✔
494
        elif isinstance(arg, (bool, np.bool_)):
4✔
495
            return Scalar(PrimitiveType.Bool)
4✔
496
        elif isinstance(arg, (int, np.int64)):
4✔
497
            return Scalar(PrimitiveType.Int64)
4✔
498
        elif isinstance(arg, np.int32):
4✔
499
            return Scalar(PrimitiveType.Int32)
4✔
500
        elif isinstance(arg, np.int16):
4✔
501
            return Scalar(PrimitiveType.Int16)
×
502
        elif isinstance(arg, np.int8):
4✔
503
            return Scalar(PrimitiveType.Int8)
×
504
        elif isinstance(arg, np.uint64):
4✔
505
            return Scalar(PrimitiveType.UInt64)
×
506
        elif isinstance(arg, np.uint32):
4✔
507
            return Scalar(PrimitiveType.UInt32)
×
508
        elif isinstance(arg, np.uint16):
4✔
509
            return Scalar(PrimitiveType.UInt16)
×
510
        elif isinstance(arg, np.uint8):
4✔
511
            return Scalar(PrimitiveType.UInt8)
×
512
        elif isinstance(arg, np.ndarray):
4✔
513
            # Map dtype
514
            if arg.dtype == np.float64:
4✔
515
                elem_type = Scalar(PrimitiveType.Double)
4✔
516
            elif arg.dtype == np.float32:
4✔
517
                elem_type = Scalar(PrimitiveType.Float)
4✔
518
            elif arg.dtype == np.float16:
4✔
519
                elem_type = Scalar(PrimitiveType.Half)
×
520
            elif arg.dtype == ml_dtypes.bfloat16:
4✔
521
                elem_type = Scalar(PrimitiveType.BFloat)
4✔
522
            elif arg.dtype == np.bool_:
4✔
523
                elem_type = Scalar(PrimitiveType.Bool)
4✔
524
            elif arg.dtype == np.int64:
4✔
525
                elem_type = Scalar(PrimitiveType.Int64)
4✔
526
            elif arg.dtype == np.int32:
4✔
527
                elem_type = Scalar(PrimitiveType.Int32)
4✔
528
            elif arg.dtype == np.int16:
×
529
                elem_type = Scalar(PrimitiveType.Int16)
×
530
            elif arg.dtype == np.int8:
×
531
                elem_type = Scalar(PrimitiveType.Int8)
×
532
            elif arg.dtype == np.uint64:
×
533
                elem_type = Scalar(PrimitiveType.UInt64)
×
534
            elif arg.dtype == np.uint32:
×
535
                elem_type = Scalar(PrimitiveType.UInt32)
×
536
            elif arg.dtype == np.uint16:
×
537
                elem_type = Scalar(PrimitiveType.UInt16)
×
538
            elif arg.dtype == np.uint8:
×
539
                elem_type = Scalar(PrimitiveType.UInt8)
×
540
            else:
541
                raise ValueError(f"Unsupported numpy dtype: {arg.dtype}")
×
542

543
            return Pointer(elem_type)
4✔
544
        elif isinstance(arg, str):
4✔
545
            # Explicitly reject strings - they are not supported
546
            raise ValueError(f"Unsupported argument type: {type(arg)}")
4✔
547
        else:
548
            # Check if it's a class instance
549
            if hasattr(arg, "__class__") and not isinstance(arg, type):
4✔
550
                # It's an instance of a class, return pointer to Structure
551
                return Pointer(Structure(arg.__class__.__name__))
4✔
552
            raise ValueError(f"Unsupported argument type: {type(arg)}")
×
553

554
    def _build_sdfg(
4✔
555
        self,
556
        arg_types,
557
        args,
558
        arg_shape_mapping,
559
        shape_values,
560
    ):
561
        sig = inspect.signature(self.func)
4✔
562

563
        # Handle return type - always void for SDFG, output args used for returns
564
        return_type = Scalar(PrimitiveType.Void)
4✔
565
        infer_return_type = True
4✔
566

567
        # Parse return annotation to determine output arguments if possible
568
        explicit_returns = []
4✔
569
        if sig.return_annotation is not inspect.Signature.empty:
4✔
570
            infer_return_type = False
4✔
571

572
            # Helper to normalize annotation to list of types
573
            def normalize_annotation(ann):
4✔
574
                # Handle Tuple[type, ...]
575
                origin = get_origin(ann)
4✔
576
                if origin is tuple:
4✔
577
                    type_args = get_args(ann)
×
578
                    # Tuple[()] or Tuple w/o args
579
                    if not type_args:
×
580
                        return []
×
581
                    # Tuple[int, float]
582
                    if len(type_args) > 0 and type_args[-1] is not Ellipsis:
×
583
                        return [_map_python_type(t) for t in type_args]
×
584
                    # Tuple[int, ...] - not supported for fixed number of returns yet?
585
                    # For now assume fixed tuple
586
                    return [_map_python_type(t) for t in type_args]
×
587
                else:
588
                    return [_map_python_type(ann)]
4✔
589

590
            explicit_returns = normalize_annotation(sig.return_annotation)
4✔
591
            for rt in explicit_returns:
4✔
592
                if not isinstance(rt, Type):
4✔
593
                    # Fallback if map failed (e.g. invalid annotation)
594
                    infer_return_type = True
×
595
                    explicit_returns = []
×
596
                    break
×
597

598
        builder = StructuredSDFGBuilder(f"{self.name}_sdfg", return_type)
4✔
599

600
        # Add pre-defined return arguments if we know them
601
        if not infer_return_type:
4✔
602
            for i, dtype in enumerate(explicit_returns):
4✔
603
                # Scalar -> Pointer(Scalar)
604
                # Array -> Already Pointer(Scalar). Keep it.
605
                arg_type = dtype
4✔
606
                if isinstance(dtype, Scalar):
4✔
607
                    arg_type = Pointer(dtype)
4✔
608

609
                builder.add_container(f"_docc_ret_{i}", arg_type, is_argument=True)
4✔
610

611
        # Register structure types for any class arguments
612
        # Also track member name to index mapping for each structure
613
        structures_to_register = {}
4✔
614
        structure_member_info = {}  # Maps struct_name -> {member_name: (index, type)}
4✔
615
        for i, (arg, dtype) in enumerate(zip(args, arg_types)):
4✔
616
            if isinstance(dtype, Pointer) and dtype.has_pointee_type():
4✔
617
                pointee = dtype.pointee_type
4✔
618
                if isinstance(pointee, Structure):
4✔
619
                    struct_name = pointee.name
4✔
620
                    if struct_name not in structures_to_register:
4✔
621
                        # Get class from arg to introspect members
622
                        if hasattr(arg, "__dict__"):
4✔
623
                            # Use __dict__ to get only instance attributes
624
                            # Sort by name to ensure consistent ordering
625
                            # Note: This alphabetical ordering is used to define the
626
                            # structure layout and must match the order expected by
627
                            # the backend code generation
628
                            member_types = []
4✔
629
                            member_names = []
4✔
630
                            member_shapes = []
4✔
631
                            for attr_name, attr_value in sorted(arg.__dict__.items()):
4✔
632
                                if not attr_name.startswith("_"):
4✔
633
                                    # Infer member type from instance attribute
634
                                    # Check bool before int since bool is subclass of int
635
                                    member_type = None
4✔
636
                                    member_shape = None
4✔
637
                                    if isinstance(attr_value, bool):
4✔
638
                                        member_type = Scalar(PrimitiveType.Bool)
×
639
                                    elif isinstance(attr_value, (int, np.int64)):
4✔
640
                                        member_type = Scalar(PrimitiveType.Int64)
4✔
641
                                    elif isinstance(attr_value, (float, np.float64)):
4✔
642
                                        member_type = Scalar(PrimitiveType.Double)
4✔
643
                                    elif isinstance(attr_value, np.int32):
4✔
644
                                        member_type = Scalar(PrimitiveType.Int32)
×
645
                                    elif isinstance(attr_value, np.float32):
4✔
646
                                        member_type = Scalar(PrimitiveType.Float)
×
647
                                    elif isinstance(attr_value, np.ndarray):
4✔
648
                                        # Array member: stored as a pointer field
649
                                        # (struct-of-arrays). Record the concrete
650
                                        # shape so attribute access can build a
651
                                        # tensor view over the member pointer.
652
                                        member_type = self._infer_type(attr_value)
4✔
653
                                        member_shape = [
4✔
654
                                            str(int(s)) for s in attr_value.shape
655
                                        ]
656
                                    # TODO: Consider using np.integer and np.floating abstract types
657
                                    # for more comprehensive numpy type coverage
658
                                    # TODO: Add support for nested structures
659

660
                                    if member_type is not None:
4✔
661
                                        member_types.append(member_type)
4✔
662
                                        member_names.append(attr_name)
4✔
663
                                        member_shapes.append(member_shape)
4✔
664

665
                            if member_types:
4✔
666
                                structures_to_register[struct_name] = member_types
4✔
667
                                # Build member name to (index, type, shape) mapping.
668
                                # shape is None for scalar members and a list of
669
                                # dimension-size strings for array members.
670
                                structure_member_info[struct_name] = {
4✔
671
                                    name: (idx, mtype, shape)
672
                                    for idx, (name, mtype, shape) in enumerate(
673
                                        zip(member_names, member_types, member_shapes)
674
                                    )
675
                                }
676

677
        # Store structure_member_info for later use in CompiledSDFG
678
        self._last_structure_member_info = structure_member_info
4✔
679

680
        # Register all discovered structures with the builder
681
        for struct_name, member_types in structures_to_register.items():
4✔
682
            builder.add_structure(struct_name, member_types)
4✔
683

684
        # Register arguments
685
        params = list(sig.parameters.items())
4✔
686
        if len(params) != len(arg_types):
4✔
687
            raise ValueError(
×
688
                f"Argument count mismatch: expected {len(params)}, got {len(arg_types)}"
689
            )
690

691
        # Add regular arguments
692
        tensor_table = {}
4✔
693
        for i, ((name, param), dtype, arg) in enumerate(zip(params, arg_types, args)):
4✔
694
            builder.add_container(name, dtype, is_argument=True)
4✔
695

696
            # Store layout information for arrays
697
            if isinstance(arg, np.ndarray):
4✔
698
                element_type = element_type_from_sdfg_type(dtype)
4✔
699

700
                shapes = []
4✔
701
                for dim_idx in range(arg.ndim):
4✔
702
                    dim_val = arg.shape[dim_idx]
4✔
703
                    if dim_val == 1:
4✔
704
                        # Always use literal "1" for size-1 dimensions to enable
705
                        # proper broadcasting detection
706
                        shapes.append("1")
4✔
707
                    else:
708
                        u_idx = arg_shape_mapping[(i, dim_idx)]
4✔
709
                        shapes.append(f"_s{u_idx}")
4✔
710

711
                strides = []
4✔
712
                if arg.flags["C_CONTIGUOUS"]:
4✔
713
                    # Row-major: stride[i] = product of shapes[i+1:]
714
                    for dim_idx in range(arg.ndim):
4✔
715
                        if dim_idx == arg.ndim - 1:
4✔
716
                            strides.append("1")
4✔
717
                        else:
718
                            suffix_shapes = shapes[dim_idx + 1 :]
4✔
719
                            if len(suffix_shapes) == 1:
4✔
720
                                strides.append(suffix_shapes[0])
4✔
721
                            else:
722
                                strides.append("(" + " * ".join(suffix_shapes) + ")")
4✔
723
                elif arg.flags["F_CONTIGUOUS"]:
4✔
724
                    # Column-major: stride[i] = product of shapes[:i]
725
                    for dim_idx in range(arg.ndim):
4✔
726
                        if dim_idx == 0:
4✔
727
                            strides.append("1")
4✔
728
                        else:
729
                            prefix_shapes = shapes[:dim_idx]
4✔
730
                            if len(prefix_shapes) == 1:
4✔
731
                                strides.append(prefix_shapes[0])
4✔
732
                            else:
733
                                strides.append("(" + " * ".join(prefix_shapes) + ")")
4✔
734
                else:
735
                    # Non-contiguous: use actual stride values
736
                    for dim_idx in range(arg.ndim):
4✔
737
                        stride_val = arg.strides[dim_idx] // arg.itemsize
4✔
738
                        strides.append(f"{stride_val}")
4✔
739

740
                offset = "0"
4✔
741
                tensor_table[name] = Tensor(element_type, shapes, strides, offset)
4✔
742

743
            elif isinstance(arg, np.generic):
4✔
744
                # NumPy scalar types (np.float64, np.int32, etc.) should be treated
745
                # as 0-d arrays for type promotion purposes - they trigger full
746
                # promotion, unlike Python literals which adapt to the array dtype
747
                element_type = element_type_from_sdfg_type(dtype)
4✔
748
                tensor_table[name] = Tensor(element_type, [], [], "0")
4✔
749

750
        # Add unified shape arguments only for shapes without scalar equivalents
751
        # and skip size-1 dimensions (they use literal "1" instead)
752
        for i in range(len(shape_values)):
4✔
753
            if shape_values[i] != 1:
4✔
754
                builder.add_container(
4✔
755
                    f"_s{i}", Scalar(PrimitiveType.Int64), is_argument=True
756
                )
757
                builder.add_assumption_lb(f"_s{i}", "1")  # Shapes must be positive
4✔
758
                builder.add_assumption_const(f"_s{i}", True)  # Shapes are constant
4✔
759

760
        # Create symbol table for parser
761
        container_table = {}
4✔
762
        for i, ((name, param), dtype, arg) in enumerate(zip(params, arg_types, args)):
4✔
763
            container_table[name] = dtype
4✔
764

765
        for i in range(len(shape_values)):
4✔
766
            if shape_values[i] != 1:
4✔
767
                container_table[f"_s{i}"] = Scalar(PrimitiveType.Int64)
4✔
768

769
        # Parse AST
770
        source_lines, start_line = inspect.getsourcelines(self.func)
4✔
771
        source = textwrap.dedent("".join(source_lines))
4✔
772
        tree = ast.parse(source)
4✔
773
        ast.increment_lineno(tree, start_line - 1)
4✔
774
        func_def = tree.body[0]
4✔
775

776
        filename = inspect.getsourcefile(self.func)
4✔
777
        function_name = self.func.__name__
4✔
778

779
        # Combine globals with closure variables (closure takes precedence)
780
        combined_globals = dict(self.func.__globals__)
4✔
781
        if self.func.__closure__ is not None and self.func.__code__.co_freevars:
4✔
782
            for name, cell in zip(
4✔
783
                self.func.__code__.co_freevars, self.func.__closure__
784
            ):
785
                combined_globals[name] = cell.cell_contents
4✔
786

787
        parser = ASTParser(
4✔
788
            builder,
789
            tensor_table,
790
            container_table,
791
            filename,
792
            function_name,
793
            infer_return_type=infer_return_type,
794
            globals_dict=combined_globals,
795
            structure_member_info=structure_member_info,
796
        )
797
        for node in func_def.body:
4✔
798
            parser.visit(node)
4✔
799

800
        # Emit hoisted allocations at function entry
801
        parser.memory_handler.emit_allocations()
4✔
802

803
        sdfg = builder.move()
4✔
804
        # Mark return arguments metadata
805
        out_args = []
4✔
806
        for name in sdfg.arguments:
4✔
807
            if name.startswith("_docc_ret_"):
4✔
808
                out_args.append(name)
4✔
809

810
        return (
4✔
811
            sdfg,
812
            out_args,
813
            parser.captured_return_shapes,
814
            parser.captured_return_strides,
815
        )
816

817

818
def native(
4✔
819
    func=None,
820
    *,
821
    target="none",
822
    category="server",
823
    instrumentation_mode=None,
824
    capture_args=None,
825
    remote_tuning=False,
826
):
827
    """Decorator to create a PythonProgram from a Python function.
828

829
    Example:
830
        @native
831
        def my_function(x: np.ndarray) -> np.ndarray:
832
            return x * 2
833

834
        result = my_function(np.array([1.0, 2.0, 3.0]))
835
    """
836
    if func is None:
4✔
837
        return lambda f: PythonProgram(
4✔
838
            f,
839
            target=target,
840
            category=category,
841
            instrumentation_mode=instrumentation_mode,
842
            capture_args=capture_args,
843
            remote_tuning=remote_tuning,
844
        )
845
    return PythonProgram(
4✔
846
        func,
847
        target=target,
848
        category=category,
849
        instrumentation_mode=instrumentation_mode,
850
        capture_args=capture_args,
851
        remote_tuning=remote_tuning,
852
    )
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc