diff --git a/dace/config_schema.yml b/dace/config_schema.yml index 2b05d45232..5fd3f2a0fb 100644 --- a/dace/config_schema.yml +++ b/dace/config_schema.yml @@ -201,7 +201,7 @@ required: description: > Specify the default data types to use in generating code. If "Python", Python's semantics will be followed (i.e., `float` and `int` - are represented using 64 bits). If the property is set to "C", C's semantcs will be + are represented using 64 bits). If the property is set to "C", C's semantics will be used (`float` and `int` are represented using 32bits). unique_functions: @@ -251,6 +251,18 @@ required: If set, specifies additional arguments to the initial invocation of ``cmake``. + lineinfo: + type: str + default: "inspect" + title: Add line info + description: > + Wether or not to add line info from the parsed code in the + generated SDFG. Valid options are `inspect` and `none`. + "inspect": During parsing, inspect the python call stack and + automatically add line info from the parsed source code in the + resulting SDFG. + "none": Do not save any line info in the resulting SDFG. + ############################################# # CPU compiler cpu: diff --git a/dace/sdfg/state.py b/dace/sdfg/state.py index 2c51d03b6d..fda09f45d5 100644 --- a/dace/sdfg/state.py +++ b/dace/sdfg/state.py @@ -9,8 +9,8 @@ import itertools import warnings import sympy -from typing import (TYPE_CHECKING, Any, AnyStr, Callable, Dict, Iterable, Iterator, List, Optional, Set, Tuple, Type, - Union, overload) +from typing import (TYPE_CHECKING, Any, AnyStr, Callable, Dict, Iterable, Iterator, List, Literal, Optional, Set, Tuple, + Type, Union, overload) import dace from dace.frontend.python import astutils @@ -39,20 +39,43 @@ NodeT = Union[nd.Node, 'ControlFlowBlock'] EdgeT = Union[MultiConnectorEdge[mm.Memlet], Edge['dace.sdfg.InterstateEdge']] GraphT = Union['ControlFlowRegion', 'SDFGState'] +ConfigurableDebugInfo = Literal["config"] | dtypes.DebugInfo | None -def _getdebuginfo(old_dinfo=None) -> dtypes.DebugInfo: - """ Returns a DebugInfo object for the position that called this function. +def _get_debug_info( + debug_info: ConfigurableDebugInfo, + default_lineinfo: dtypes.DebugInfo | None, +) -> dtypes.DebugInfo | None: + """Returns a DebugInfo from the stack, if configured. - :param old_dinfo: Another DebugInfo object that will override the - return value of this function - :return: DebugInfo containing line number and calling file. + If lineinfo is configured in the config, this function inspects the python + stacktrace and returns a DebugInfo object for the position that called this + function. `default_lineinfo` has precedence, if given. """ - if old_dinfo is not None: - return old_dinfo - caller = inspect.getframeinfo(inspect.stack()[2][0], context=0) - return dtypes.DebugInfo(caller.lineno, 0, caller.lineno, 0, caller.filename) + if isinstance(debug_info, dtypes.DebugInfo): + warnings.warn( + "Passing debug_info of type DebugInfo is deprecated. Pass `'config'` and set `compiler.lineinfo` in your DaCe config.", + DeprecationWarning, + stacklevel=3) + return debug_info + + if debug_info is None: + warnings.warn( + "Passing debug_info=None is deprecated. Pass `'config'` and set `compiler.lineinfo` in your DaCe config.", + DeprecationWarning, + stacklevel=3, + ) + + do_inspect = dace.Config.get("compiler", "lineinfo") == "inspect" if debug_info == "config" else False + if (debug_info is None or do_inspect) and default_lineinfo is not None: + return default_lineinfo + + if do_inspect: + caller = inspect.getframeinfo(inspect.stack()[2][0], context=0) + return dtypes.DebugInfo(caller.lineno, 0, caller.lineno, 0, caller.filename) + + return None def _make_iterators(ndrange): @@ -1617,7 +1640,7 @@ def symbols_defined_at(self, node: nd.Node) -> Dict[str, dtypes.typeclass]: # Dynamic SDFG creation API ############################## - def add_read(self, array_or_stream_name: str, debuginfo: Optional[dtypes.DebugInfo] = None) -> nd.AccessNode: + def add_read(self, array_or_stream_name: str, debuginfo: ConfigurableDebugInfo = "config") -> nd.AccessNode: """ Adds an access node to this SDFG state (alias of ``add_access``). @@ -1626,10 +1649,9 @@ def add_read(self, array_or_stream_name: str, debuginfo: Optional[dtypes.DebugIn :return: An array access node. :see: add_access """ - debuginfo = _getdebuginfo(debuginfo or self._default_lineinfo) return self.add_access(array_or_stream_name, debuginfo=debuginfo) - def add_write(self, array_or_stream_name: str, debuginfo: Optional[dtypes.DebugInfo] = None) -> nd.AccessNode: + def add_write(self, array_or_stream_name: str, debuginfo: ConfigurableDebugInfo = "config") -> nd.AccessNode: """ Adds an access node to this SDFG state (alias of ``add_access``). @@ -1638,17 +1660,16 @@ def add_write(self, array_or_stream_name: str, debuginfo: Optional[dtypes.DebugI :return: An array access node. :see: add_access """ - debuginfo = _getdebuginfo(debuginfo or self._default_lineinfo) return self.add_access(array_or_stream_name, debuginfo=debuginfo) - def add_access(self, array_or_stream_name: str, debuginfo: Optional[dtypes.DebugInfo] = None) -> nd.AccessNode: + def add_access(self, array_or_stream_name: str, debuginfo: ConfigurableDebugInfo = "config") -> nd.AccessNode: """ Adds an access node to this SDFG state. :param array_or_stream_name: The name of the array/stream. :param debuginfo: Source line information for this access node. :return: An array access node. """ - debuginfo = _getdebuginfo(debuginfo or self._default_lineinfo) + debuginfo = _get_debug_info(debuginfo, self._default_lineinfo) node = nd.AccessNode(array_or_stream_name, debuginfo=debuginfo) self.add_node(node) return node @@ -1666,10 +1687,10 @@ def add_tasklet( code_exit: str = "", location: dict = None, side_effects: Optional[bool] = None, - debuginfo=None, + debuginfo: ConfigurableDebugInfo = "config", ): """ Adds a tasklet to the SDFG state. """ - debuginfo = _getdebuginfo(debuginfo or self._default_lineinfo) + debuginfo = _get_debug_info(debuginfo, self._default_lineinfo) # Make dictionary of autodetect connector types from set if isinstance(inputs, (set, collections.abc.KeysView)): @@ -1715,7 +1736,7 @@ def add_nested_sdfg( symbol_mapping: Dict[str, Any] = None, name=None, location: Optional[Dict[str, symbolic.SymbolicType]] = None, - debuginfo: Optional[dtypes.DebugInfo] = None, + debuginfo: ConfigurableDebugInfo = "config", external_path: Optional[str] = None, ): """ @@ -1738,7 +1759,7 @@ def add_nested_sdfg( """ if name is None: name = sdfg.label - debuginfo = _getdebuginfo(debuginfo or self._default_lineinfo) + debuginfo = _get_debug_info(debuginfo, self._default_lineinfo) if sdfg is None and external_path is None: raise ValueError('Neither an SDFG nor an external SDFG path has been provided') @@ -1802,7 +1823,7 @@ def add_map( ndrange: Union[Dict[str, Union[str, sbs.Subset]], List[Tuple[str, Union[str, sbs.Subset]]]], schedule=dtypes.ScheduleType.Default, unroll=False, - debuginfo=None, + debuginfo: ConfigurableDebugInfo = "config", ) -> Tuple[nd.MapEntry, nd.MapExit]: """ Adds a map entry and map exit. @@ -1814,21 +1835,23 @@ def add_map( :return: (map_entry, map_exit) node 2-tuple """ - debuginfo = _getdebuginfo(debuginfo or self._default_lineinfo) + debuginfo = _get_debug_info(debuginfo, self._default_lineinfo) map = nd.Map(name, *_make_iterators(ndrange), schedule=schedule, unroll=unroll, debuginfo=debuginfo) map_entry = nd.MapEntry(map) map_exit = nd.MapExit(map) self.add_nodes_from([map_entry, map_exit]) return map_entry, map_exit - def add_consume(self, - name, - elements: Tuple[str, str], - condition: str = None, - schedule=dtypes.ScheduleType.Default, - chunksize=1, - debuginfo=None, - language=dtypes.Language.Python) -> Tuple[nd.ConsumeEntry, nd.ConsumeExit]: + def add_consume( + self, + name, + elements: Tuple[str, str], + condition: str = None, + schedule=dtypes.ScheduleType.Default, + chunksize=1, + debuginfo: ConfigurableDebugInfo = "config", + language=dtypes.Language.Python, + ) -> Tuple[nd.ConsumeEntry, nd.ConsumeExit]: """ Adds consume entry and consume exit nodes. :param name: Label @@ -1850,7 +1873,7 @@ def add_consume(self, "(PE_index, num_PEs)") pe_tuple = (elements[0], SymbolicProperty.from_string(elements[1])) - debuginfo = _getdebuginfo(debuginfo or self._default_lineinfo) + debuginfo = _get_debug_info(debuginfo, self._default_lineinfo) if condition is not None: condition = CodeBlock(condition, language) consume = nd.Consume(name, pe_tuple, condition, schedule, chunksize, debuginfo=debuginfo) @@ -1860,24 +1883,23 @@ def add_consume(self, self.add_nodes_from([entry, exit]) return entry, exit - def add_mapped_tasklet(self, - name: str, - map_ranges: Union[Dict[str, Union[str, sbs.Subset]], List[Tuple[str, Union[str, - sbs.Subset]]]], - inputs: Dict[str, mm.Memlet], - code: str, - outputs: Dict[str, mm.Memlet], - schedule=dtypes.ScheduleType.Default, - unroll_map=False, - location=None, - language=dtypes.Language.Python, - debuginfo=None, - external_edges=False, - input_nodes: Optional[Union[Dict[str, nd.AccessNode], List[nd.AccessNode], - Set[nd.AccessNode]]] = None, - output_nodes: Optional[Union[Dict[str, nd.AccessNode], List[nd.AccessNode], - Set[nd.AccessNode]]] = None, - propagate=True) -> Tuple[nd.Tasklet, nd.MapEntry, nd.MapExit]: + def add_mapped_tasklet( + self, + name: str, + map_ranges: Union[Dict[str, Union[str, sbs.Subset]], List[Tuple[str, Union[str, sbs.Subset]]]], + inputs: Dict[str, mm.Memlet], + code: str, + outputs: Dict[str, mm.Memlet], + schedule=dtypes.ScheduleType.Default, + unroll_map=False, + location=None, + language=dtypes.Language.Python, + debuginfo: ConfigurableDebugInfo = "config", + external_edges=False, + input_nodes: Optional[Union[Dict[str, nd.AccessNode], List[nd.AccessNode], Set[nd.AccessNode]]] = None, + output_nodes: Optional[Union[Dict[str, nd.AccessNode], List[nd.AccessNode], Set[nd.AccessNode]]] = None, + propagate=True, + ) -> Tuple[nd.Tasklet, nd.MapEntry, nd.MapExit]: """ Convenience function that adds a map entry, tasklet, map exit, and the respective edges to external arrays. @@ -1910,7 +1932,7 @@ def add_mapped_tasklet(self, :return: tuple of (tasklet, map_entry, map_exit) """ map_name = name + "_map" - debuginfo = _getdebuginfo(debuginfo or self._default_lineinfo) + debuginfo = _get_debug_info(debuginfo, self._default_lineinfo) # Create appropriate dictionaries from inputs tinputs = {k: None for k, v in inputs.items()} @@ -2030,7 +2052,7 @@ def add_reduce( axes, identity=None, schedule=dtypes.ScheduleType.Default, - debuginfo=None, + debuginfo: ConfigurableDebugInfo = "config", ) -> 'dace.libraries.standard.Reduce': """ Adds a reduction node. @@ -2044,7 +2066,7 @@ def add_reduce( :return: A Reduce node """ import dace.libraries.standard as stdlib # Avoid import loop - debuginfo = _getdebuginfo(debuginfo or self._default_lineinfo) + debuginfo = _get_debug_info(debuginfo, self._default_lineinfo) result = stdlib.Reduce('Reduce', wcr, axes, identity, schedule=schedule, debuginfo=debuginfo) self.add_node(result) return result diff --git a/tests/python_frontend/debug_info_test.py b/tests/python_frontend/debug_info_test.py new file mode 100644 index 0000000000..2f94822490 --- /dev/null +++ b/tests/python_frontend/debug_info_test.py @@ -0,0 +1,68 @@ +from dace import SDFG, SDFGState +from dace import dtypes +from dace.config import Config, temporary_config + +import pytest + + +@pytest.fixture() +def state() -> SDFGState: + """Setup a simple SDFG for testing and return the state.""" + + sdfg = SDFG("tester") + sdfg.add_array("A", [10], dtypes.int64) + sdfg.add_array("B", [15], dtypes.int64) + + return sdfg.add_state("start_here", is_start_block=True) + + +def test_config_compiler_lineinfo_none(state: SDFGState) -> None: + with temporary_config(): + Config.set(*["compiler", "lineinfo"], value="none") + node = state.add_access("A") + assert node.debuginfo is None + + +def test_config_compiler_lineinfo_inspect(state: SDFGState) -> None: + # Ensure "inspect" is the default configuration + assert Config.get(*["compiler", "lineinfo"]) == "inspect" + + # Add an access and expect debug info to be added (from this file) + node = state.add_access("A") + assert node.debuginfo is not None + assert node.debuginfo.filename == __file__ + + +def test_using_default_lineinfo(state: SDFGState) -> None: + line = 42 + filename = "my/test/path.py" + state._default_lineinfo = dtypes.DebugInfo(start_line=line, filename=filename) + + read_A = state.add_read("A") + assert read_A.debuginfo is not None + assert read_A.debuginfo.filename == filename + assert read_A.debuginfo.start_line == line + + with pytest.deprecated_call(): + read_B = state.add_read("B", None) + assert read_B.debuginfo is not None + assert read_B.debuginfo.filename == filename + assert read_B.debuginfo.start_line == line + + +def test_passing_debug_info_warns(state: SDFGState) -> None: + with pytest.deprecated_call(): + state.add_access("A", debuginfo=dtypes.DebugInfo(start_line=42)) + + +def test_passing_None_warns(state: SDFGState) -> None: + with pytest.deprecated_call(): + state.add_access("A", debuginfo=None) + + +if __name__ == "__main__": + test_config_compiler_lineinfo_none() + test_config_compiler_lineinfo_inspect() + test_using_default_lineinfo() + test_passing_debug_info_warns() + test_passing_None_warns()