Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Schedule tree (1/3) #1145

Merged
merged 108 commits into from
Sep 26, 2023
Merged
Show file tree
Hide file tree
Changes from 106 commits
Commits
Show all changes
108 commits
Select commit Hold shift + click to select a range
85aa33b
Type hint
tbennun Nov 8, 2022
e2c227e
(re)scheduling-oriented view of an SDFG as a tree of control/data flo…
tbennun Nov 8, 2022
3c0ad6b
Fix name bug with view and duplicate edge adds to the tree
tbennun Nov 8, 2022
084d203
Merge branch 'master' into schedule-tree
tbennun Nov 11, 2022
329d392
Bugfixes: add else clause, children as list
tbennun Nov 11, 2022
cda81e4
Remove duplicate labels
tbennun Nov 11, 2022
09fbf2d
Revamp edge generation in schedule tree conversion and handle name co…
tbennun Nov 11, 2022
2dcaeb0
Merge branch 'master' into schedule-tree
tbennun Nov 11, 2022
d1774a9
Refactor schedule tree passes and add an example pass
tbennun Nov 11, 2022
4518c34
Support dynamic scope inputs
tbennun Nov 11, 2022
7949ebd
Explicit dynamic scope input copy node
tbennun Nov 11, 2022
d104af8
Merge remote-tracking branch 'origin/master' into schedule-tree
tbennun Dec 9, 2022
adbf8fc
Add memlet parsing and replacement functionality for inter-state edge…
tbennun Dec 9, 2022
828eff3
Started working on ScheduleTree utilities.
alexnick83 Dec 12, 2022
6896955
yapf
alexnick83 Dec 12, 2022
a3dbad8
Merge branch 'schedule-tree' of github.com:spcl/dace into schedule-tree
alexnick83 Dec 12, 2022
2a94d71
WIP: MapFission on ScheduleTree.
alexnick83 Dec 12, 2022
c1ca73c
Updated ScheduleTree MapFission transformation.
alexnick83 Dec 14, 2022
273da6c
Added methos for printing Data (Scalar, Array) as Python arguments.
alexnick83 Dec 14, 2022
25ccc6b
Minor bug (?) fixes related to parsing DaCe programs generated from S…
alexnick83 Dec 14, 2022
d666e55
Added replacement methods for ScheduleTree Copy, Library, and Taskle…
alexnick83 Dec 14, 2022
7489e63
Added the alpha and beta properties to MatMul's init method.
alexnick83 Dec 14, 2022
63ad7c5
Added methods to SDFG that generated DaCe Python signatures.
alexnick83 Dec 14, 2022
5ab2eb4
Updated the ScheduleTree nodes to point to the corresponding (nested)…
alexnick83 Dec 14, 2022
c526195
Removed obsolete ScheduleTree utility methods.
alexnick83 Dec 14, 2022
748f010
Added `as_sdfg` method and tests.
alexnick83 Dec 14, 2022
ab0686c
Merge branch 'master' into schedule-tree
alexnick83 Dec 22, 2022
0f57358
`inner_indices` is a set
alexnick83 Dec 22, 2022
bed168e
Empty `inner_indices` should be an empty set.
alexnick83 Dec 22, 2022
5e6ee5c
Added `is_data_used` method to assist identifying MapScope private va…
alexnick83 Dec 22, 2022
b4b698d
Fixed `is_data_used` for TaskletNodes and LibraryCalls.
alexnick83 Dec 22, 2022
7e06b72
WIP: Adding containers to ScheduleTreeScopes and more MapFission test.
alexnick83 Dec 26, 2022
1e57142
WIP: ScheduleTree updates.
alexnick83 Jan 6, 2023
2754510
Experimenting with new passes.
alexnick83 Jan 10, 2023
4522a9e
Added if-fission transformation.
alexnick83 Jan 10, 2023
2c49e14
Experimenting with if canonicalization.
alexnick83 Jan 12, 2023
754ceea
Split out SDFG "dealiasing" of `remove_name_collisions`. Fixed read-m…
alexnick83 Jan 13, 2023
17d6319
Added pass to fission scopes.
alexnick83 Jan 13, 2023
83ae817
Reworked map-fission to loop-fission (support Maps and ForLoops).
alexnick83 Jan 13, 2023
52831e3
Children must always point to the correct parent.
alexnick83 Jan 13, 2023
eed6777
WIP: Fission with AssignNodes in body
alexnick83 Jan 19, 2023
add80bf
Updates for Fission with AssignNodes.
alexnick83 Jan 21, 2023
5a8c267
Fix for views becoming program parameters and/or return arrays when t…
alexnick83 Jan 21, 2023
0cd779a
WIP: WCR support in TaskletNodes and when converting back to SDFG.
alexnick83 Jan 21, 2023
da03868
Added ScheduleTrree tests: Tasklets
alexnick83 Jan 21, 2023
33df6a1
Always add an access for View/Slices.
alexnick83 Jan 22, 2023
c8c3bff
Added support for WCR in both input and output Memlets.
alexnick83 Jan 22, 2023
db2d8aa
Added more tests.
alexnick83 Jan 22, 2023
8ffcf8f
Allow Views to have only incoming or outgoing edges.
alexnick83 Jan 22, 2023
b3bd6b2
Reverted all changes regarding Views.
alexnick83 Jan 22, 2023
1b2a5e6
TaskletNode.as_python now uses explicit dataflow syntax.
alexnick83 Jan 22, 2023
bbdbdce
Added documentation
alexnick83 Jan 22, 2023
9f1fdee
De-alias the SDFG just before populating the containers.
alexnick83 Jan 22, 2023
3cfb708
Fixes to SDFGs dealiazing (multiple NestedSDFG connectors from/to the…
alexnick83 Jan 22, 2023
d68a016
WIP: `as_python` support for ViewNodes.
alexnick83 Jan 22, 2023
f930a94
Added ScheduleTree conversion tests for Map scopes.
alexnick83 Jan 22, 2023
57d42f7
Ignore reassignment of Views to the same Array slice.
alexnick83 Jan 23, 2023
731a0bf
Support codes with a single statements.
alexnick83 Jan 23, 2023
6dbbd3f
Support library nodes with no connectors.
alexnick83 Jan 23, 2023
cf68922
Reduce node must have `name` as a parameter (like other LibraryNodes).
alexnick83 Jan 23, 2023
81196bd
IfScope should check conditions for potentially data used.
alexnick83 Jan 23, 2023
e8c7665
updated test
alexnick83 Jan 23, 2023
5fbeffd
Print just `bool` instead of `dace::bool` when converting data to boo…
alexnick83 Jan 23, 2023
09134bc
Working on if canonicalization and fission tests.
alexnick83 Jan 23, 2023
5dc9b4a
Merge branch 'master' into schedule-tree
tbennun Jun 29, 2023
b818516
Add schedule-related tests
tbennun Jun 30, 2023
7949956
Fix gblock creation arguments
tbennun Jun 30, 2023
b15fa8d
Fix test names
tbennun Jun 30, 2023
3eeb888
Merge branch 'master' into schedule-tree
tbennun Jul 18, 2023
cef77e9
Fix simple_call for symbolic inputs
tbennun Jul 18, 2023
c7ad062
More optional set property support
tbennun Jul 19, 2023
33f13cf
Traversal methods for strees
tbennun Jul 19, 2023
94781f2
Fix bug in ConstantProp where a symbol_mapping symbol is used in firs…
tbennun Jul 19, 2023
f34b581
No more UB in test
tbennun Jul 19, 2023
250f62b
Naming tests
tbennun Jul 19, 2023
619c21e
Move out elements from this PR to others
tbennun Jul 20, 2023
3c83489
Add set of tests
tbennun Jul 21, 2023
298457d
Add another dealiasing test
tbennun Jul 21, 2023
1812174
Fix stateif printout
tbennun Jul 21, 2023
62789ed
Fix reference set alignment
tbennun Jul 21, 2023
42bc759
Fix printout of tasklets without outputs
tbennun Jul 21, 2023
4b43606
Remove test
tbennun Jul 21, 2023
b4a7984
Schedule tree: fix tests, print empty scopes in a nicer way
tbennun Jul 26, 2023
a4fe542
Fix name collection in collision removal
tbennun Jul 27, 2023
f6e5cd3
Make naming edge case test more complex
tbennun Jul 27, 2023
23d4df1
Better and robust pass to replace symbol mappings
tbennun Jul 27, 2023
71a55c9
Test both simplified and unsimplified modes
tbennun Jul 27, 2023
95c8d86
Make data container dealiasing robust to conflicting replacements
tbennun Jul 27, 2023
1743112
One more test
tbennun Jul 27, 2023
5fa5055
Schedule tree: Fix memlets replacement not addressing both input and …
tbennun Jul 30, 2023
4ea8e32
Merge remote-tracking branch 'origin/master' into schedule-tree
tbennun Aug 25, 2023
5bd8859
Merge branch 'master' into schedule-tree
tbennun Sep 23, 2023
ef7aa4e
Fix dealiasing for data->data edges and interstate edges
tbennun Sep 23, 2023
6cae4a8
Schedule views to show in order of viewing rather than graph
tbennun Sep 24, 2023
1b6a912
Fix constructor
tbennun Sep 24, 2023
b2d6cd3
Fix incorrect subset modification in dealiasing
tbennun Sep 24, 2023
92c7a14
Fix used_symbols for the case of a symbol that is in SDFG.symbols but…
tbennun Sep 24, 2023
07b06f3
Do not inadvertently simplify expressions when computing free symbols
tbennun Sep 24, 2023
7816999
Ignore symbol mappings to unused symbols in used_symbols and nested S…
tbennun Sep 25, 2023
6fc5890
Revert changes made to the Python frontend
tbennun Sep 25, 2023
1c42e24
Remove line that should have been removed in commit 7816999
tbennun Sep 25, 2023
dcac66e
Fix typo
tbennun Sep 25, 2023
bf9b023
Better detection of free symbols in C++ tasklets
tbennun Sep 25, 2023
6162074
Minor fix
tbennun Sep 25, 2023
d2c4370
Consider free symbols in SDFG init and exit code, fix None vs. empty …
tbennun Sep 25, 2023
2943429
Handle struct memlets in normalization, structure views as views
tbennun Sep 25, 2023
afc4e26
Merge branch 'master' into schedule-tree
tbennun Sep 26, 2023
2935f55
Apply review comments
tbennun Sep 26, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions dace/codegen/targets/framecode.py
Original file line number Diff line number Diff line change
Expand Up @@ -886,8 +886,8 @@ def generate_code(self,

# NOTE: NestedSDFGs frequently contain tautologies in their symbol mapping, e.g., `'i': i`. Do not
# redefine the symbols in such cases.
if (not is_top_level and isvarName in sdfg.parent_nsdfg_node.symbol_mapping.keys()
and str(sdfg.parent_nsdfg_node.symbol_mapping[isvarName] == isvarName)):
if (not is_top_level and isvarName in sdfg.parent_nsdfg_node.symbol_mapping
and str(sdfg.parent_nsdfg_node.symbol_mapping[isvarName]) == str(isvarName)):
continue
isvar = data.Scalar(isvarType)
callsite_stream.write('%s;\n' % (isvar.as_arg(with_types=True, name=isvarName)), sdfg)
Expand Down
18 changes: 18 additions & 0 deletions dace/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,10 @@ def __hash__(self):
def as_arg(self, with_types=True, for_call=False, name=None):
"""Returns a string for a C++ function signature (e.g., `int *A`). """
raise NotImplementedError

def as_python_arg(self, with_types=True, for_call=False, name=None):
"""Returns a string for a Data-Centric Python function signature (e.g., `A: dace.int32[M]`). """
raise NotImplementedError

def used_symbols(self, all_symbols: bool) -> Set[symbolic.SymbolicType]:
"""
Expand Down Expand Up @@ -583,6 +587,13 @@ def as_arg(self, with_types=True, for_call=False, name=None):
if not with_types or for_call:
return name
return self.dtype.as_arg(name)

def as_python_arg(self, with_types=True, for_call=False, name=None):
if self.storage is dtypes.StorageType.GPU_Global:
return Array(self.dtype, [1]).as_python_arg(with_types, for_call, name)
if not with_types or for_call:
return name
return f"{name}: {dtypes.TYPECLASS_TO_STRING[self.dtype].replace('::', '.')}"

def sizes(self):
return None
Expand Down Expand Up @@ -849,6 +860,13 @@ def as_arg(self, with_types=True, for_call=False, name=None):
if self.may_alias:
return str(self.dtype.ctype) + ' *' + arrname
return str(self.dtype.ctype) + ' * __restrict__ ' + arrname

def as_python_arg(self, with_types=True, for_call=False, name=None):
arrname = name

if not with_types or for_call:
return arrname
return f"{arrname}: {dtypes.TYPECLASS_TO_STRING[self.dtype].replace('::', '.')}{list(self.shape)}"

def sizes(self):
return [d.name if isinstance(d, symbolic.symbol) else str(d) for d in self.shape]
Expand Down
4 changes: 2 additions & 2 deletions dace/frontend/python/memlet_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ def _fill_missing_slices(das, ast_ndslice, array, indices):
def parse_memlet_subset(array: data.Data,
node: Union[ast.Name, ast.Subscript],
das: Dict[str, Any],
parsed_slice: Any = None) -> Tuple[subsets.Range, List[int]]:
parsed_slice: Any = None) -> Tuple[subsets.Range, List[int], List[int]]:
"""
Parses an AST subset and returns access range, as well as new dimensions to
add.
Expand All @@ -209,7 +209,7 @@ def parse_memlet_subset(array: data.Data,
e.g., negative indices or empty shapes).
:param node: AST node representing whole array or subset thereof.
:param das: Dictionary of defined arrays and symbols mapped to their values.
:return: A 2-tuple of (subset, list of new axis indices).
:return: A 3-tuple of (subset, list of new axis indices, list of index-to-array-dimension correspondence).
"""
# Get memlet range
ndslice = [(0, s - 1, 1) for s in array.shape]
Expand Down
6 changes: 6 additions & 0 deletions dace/frontend/python/newast.py
Original file line number Diff line number Diff line change
Expand Up @@ -3160,6 +3160,12 @@ def _visit_assign(self, node, node_target, op, dtype=None, is_return=False):

if (not is_return and isinstance(target, ast.Name) and true_name and not op
and not isinstance(true_array, data.Scalar) and not (true_array.shape == (1, ))):
if true_name in self.views:
if result in self.sdfg.arrays and self.views[true_name] == (
result, Memlet.from_array(result, self.sdfg.arrays[result])):
continue
else:
raise DaceSyntaxError(self, target, 'Cannot reassign View "{}"'.format(name))
if (isinstance(result, str) and result in self.sdfg.arrays
and self.sdfg.arrays[result].is_equivalent(true_array)):
# Skip error if the arrays are defined exactly in the same way.
Expand Down
47 changes: 33 additions & 14 deletions dace/frontend/python/replacements.py
Original file line number Diff line number Diff line change
Expand Up @@ -617,16 +617,21 @@ def _elementwise(pv: 'ProgramVisitor',

def _simple_call(sdfg: SDFG, state: SDFGState, inpname: str, func: str, restype: dace.typeclass = None):
""" Implements a simple call of the form `out = func(inp)`. """
create_input = True
if isinstance(inpname, (list, tuple)): # TODO investigate this
inpname = inpname[0]
if not isinstance(inpname, str):
if not isinstance(inpname, str) and not symbolic.issymbolic(inpname):
# Constant parameter
cst = inpname
inparr = data.create_datadescriptor(cst)
inpname = sdfg.temp_data_name()
inparr.transient = True
sdfg.add_constant(inpname, cst, inparr)
sdfg.add_datadesc(inpname, inparr)
elif symbolic.issymbolic(inpname):
dtype = symbolic.symtype(inpname)
inparr = data.Scalar(dtype)
create_input = False
else:
inparr = sdfg.arrays[inpname]

Expand All @@ -636,10 +641,17 @@ def _simple_call(sdfg: SDFG, state: SDFGState, inpname: str, func: str, restype:
outarr.dtype = restype
num_elements = data._prod(inparr.shape)
if num_elements == 1:
inp = state.add_read(inpname)
if create_input:
inp = state.add_read(inpname)
inconn_name = '__inp'
else:
inconn_name = symbolic.symstr(inpname)

out = state.add_write(outname)
tasklet = state.add_tasklet(func, {'__inp'}, {'__out'}, '__out = {f}(__inp)'.format(f=func))
state.add_edge(inp, None, tasklet, '__inp', Memlet.from_array(inpname, inparr))
tasklet = state.add_tasklet(func, {'__inp'} if create_input else {}, {'__out'},
f'__out = {func}({inconn_name})')
if create_input:
state.add_edge(inp, None, tasklet, '__inp', Memlet.from_array(inpname, inparr))
state.add_edge(tasklet, '__out', out, None, Memlet.from_array(outname, outarr))
else:
state.add_mapped_tasklet(
Expand Down Expand Up @@ -2158,8 +2170,9 @@ def _matmult(visitor: ProgramVisitor, sdfg: SDFG, state: SDFGState, op1: str, op

res = symbolic.equal(arr1.shape[-1], arr2.shape[-2])
if res is None:
warnings.warn(f'Last mode of first tesnsor/matrix {arr1.shape[-1]} and second-last mode of '
f'second tensor/matrix {arr2.shape[-2]} may not match', UserWarning)
warnings.warn(
f'Last mode of first tesnsor/matrix {arr1.shape[-1]} and second-last mode of '
f'second tensor/matrix {arr2.shape[-2]} may not match', UserWarning)
elif not res:
raise SyntaxError('Matrix dimension mismatch %s != %s' % (arr1.shape[-1], arr2.shape[-2]))

Expand All @@ -2176,8 +2189,9 @@ def _matmult(visitor: ProgramVisitor, sdfg: SDFG, state: SDFGState, op1: str, op

res = symbolic.equal(arr1.shape[-1], arr2.shape[0])
if res is None:
warnings.warn(f'Number of matrix columns {arr1.shape[-1]} and length of vector {arr2.shape[0]} '
f'may not match', UserWarning)
warnings.warn(
f'Number of matrix columns {arr1.shape[-1]} and length of vector {arr2.shape[0]} '
f'may not match', UserWarning)
elif not res:
raise SyntaxError("Number of matrix columns {} must match"
"size of vector {}.".format(arr1.shape[1], arr2.shape[0]))
Expand All @@ -2188,8 +2202,9 @@ def _matmult(visitor: ProgramVisitor, sdfg: SDFG, state: SDFGState, op1: str, op

res = symbolic.equal(arr1.shape[0], arr2.shape[0])
if res is None:
warnings.warn(f'Length of vector {arr1.shape[0]} and number of matrix rows {arr2.shape[0]} '
f'may not match', UserWarning)
warnings.warn(
f'Length of vector {arr1.shape[0]} and number of matrix rows {arr2.shape[0]} '
f'may not match', UserWarning)
elif not res:
raise SyntaxError("Size of vector {} must match number of matrix "
"rows {} must match".format(arr1.shape[0], arr2.shape[0]))
Expand All @@ -2200,8 +2215,9 @@ def _matmult(visitor: ProgramVisitor, sdfg: SDFG, state: SDFGState, op1: str, op

res = symbolic.equal(arr1.shape[0], arr2.shape[0])
if res is None:
warnings.warn(f'Length of first vector {arr1.shape[0]} and length of second vector {arr2.shape[0]} '
f'may not match', UserWarning)
warnings.warn(
f'Length of first vector {arr1.shape[0]} and length of second vector {arr2.shape[0]} '
f'may not match', UserWarning)
elif not res:
raise SyntaxError("Vectors in vector product must have same size: "
"{} vs. {}".format(arr1.shape[0], arr2.shape[0]))
Expand Down Expand Up @@ -4401,10 +4417,13 @@ def _datatype_converter(sdfg: SDFG, state: SDFGState, arg: UfuncInput, dtype: dt

# Set tasklet parameters
impl = {
'name': "_convert_to_{}_".format(dtype.to_string()),
'name':
"_convert_to_{}_".format(dtype.to_string()),
'inputs': ['__inp'],
'outputs': ['__out'],
'code': "__out = dace.{}(__inp)".format(dtype.to_string())
'code':
"__out = {}(__inp)".format(f"dace.{dtype.to_string()}" if dtype not in (dace.bool,
dace.bool_) else dtype.to_string())
}
if dtype in (dace.bool, dace.bool_):
impl['code'] = "__out = dace.bool_(__inp)"
Expand Down
4 changes: 3 additions & 1 deletion dace/libraries/blas/nodes/matmul.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,5 +217,7 @@ class MatMul(dace.sdfg.nodes.LibraryNode):
default=0,
desc="A scalar which will be multiplied with C before adding C")

def __init__(self, name, location=None):
def __init__(self, name, location=None, alpha=1, beta=0):
self.alpha = alpha
self.beta = beta
super().__init__(name, location=location, inputs={"_a", "_b"}, outputs={"_c"})
5 changes: 3 additions & 2 deletions dace/libraries/standard/nodes/reduce.py
Original file line number Diff line number Diff line change
Expand Up @@ -1562,13 +1562,14 @@ class Reduce(dace.sdfg.nodes.LibraryNode):
identity = Property(allow_none=True)

def __init__(self,
name,
wcr='lambda a, b: a',
axes=None,
identity=None,
schedule=dtypes.ScheduleType.Default,
debuginfo=None,
**kwargs):
super().__init__(name='Reduce', **kwargs)
super().__init__(name=name, **kwargs)
self.wcr = wcr
self.axes = axes
self.identity = identity
Expand All @@ -1577,7 +1578,7 @@ def __init__(self,

@staticmethod
def from_json(json_obj, context=None):
ret = Reduce("lambda a, b: a", None)
ret = Reduce('reduce', 'lambda a, b: a', None)
dace.serialize.set_properties_from_json(ret, json_obj, context=context)
return ret

Expand Down
7 changes: 5 additions & 2 deletions dace/properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -1001,8 +1001,11 @@ def get_free_symbols(self, defined_syms: Set[str] = None) -> Set[str]:
if self.language == dace.dtypes.Language.Python:
visitor = TaskletFreeSymbolVisitor(defined_syms)
if self.code:
for stmt in self.code:
visitor.visit(stmt)
if isinstance(self.code, list):
for stmt in self.code:
visitor.visit(stmt)
else:
visitor.visit(self.code)
return visitor.free_symbols

return set()
Expand Down
Empty file.
60 changes: 60 additions & 0 deletions dace/sdfg/analysis/schedule_tree/passes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Copyright 2019-2022 ETH Zurich and the DaCe authors. All rights reserved.
tbennun marked this conversation as resolved.
Show resolved Hide resolved
"""
Assortment of passes for schedule trees.
"""

from dace.sdfg.analysis.schedule_tree import treenodes as tn
from typing import Set


def remove_unused_and_duplicate_labels(stree: tn.ScheduleTreeScope):
"""
Removes unused and duplicate labels from the schedule tree.

:param stree: The schedule tree to remove labels from.
"""

class FindGotos(tn.ScheduleNodeVisitor):

def __init__(self):
self.gotos: Set[str] = set()

def visit_GotoNode(self, node: tn.GotoNode):
if node.target is not None:
self.gotos.add(node.target)

class RemoveLabels(tn.ScheduleNodeTransformer):

def __init__(self, labels_to_keep: Set[str]) -> None:
self.labels_to_keep = labels_to_keep
self.labels_seen = set()

def visit_StateLabel(self, node: tn.StateLabel):
if node.state.name not in self.labels_to_keep:
return None
if node.state.name in self.labels_seen:
return None
self.labels_seen.add(node.state.name)
return node

fg = FindGotos()
fg.visit(stree)
return RemoveLabels(fg.gotos).visit(stree)


def remove_empty_scopes(stree: tn.ScheduleTreeScope):
"""
Removes empty scopes from the schedule tree.

:warning: This pass is not safe to use for for-loops, as it will remove indices that may be used after the loop.
"""

class RemoveEmptyScopes(tn.ScheduleNodeTransformer):

def visit_scope(self, node: tn.ScheduleTreeScope):
if len(node.children) == 0:
return None

return self.generic_visit(node)

return RemoveEmptyScopes().visit(stree)
Loading
Loading