Skip to content

Commit bfb018d

Browse files
authored
Narrow types in if-statements (#794)
We narrow the types in the branches of the if-statements by their conditions, and by the negations of the previous conditions, *e.g.*, ``x`` is a ``Child_b`` in the ``else`` of ``if x is None: ... elif not isinstance(x, Child_b): ...``. After an if-statement or a switch, we keep the facts common to the branches which complete normally, which narrows ``x`` after ``if x is None: return False``. An assignment removes the facts it can falsify, following the access paths (``self.y = 5`` keeps ``self.x``, ``items[j] = z`` drops ``items[i]``), and narrows the target by the assigned value.
1 parent bc7de63 commit bfb018d

174 files changed

Lines changed: 31510 additions & 1042 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎aas_core_codegen/intermediate/type_inference.py‎

Lines changed: 1037 additions & 183 deletions
Large diffs are not rendered by default.

‎dev/test_data/common_meta_models/if_statements.py‎

Lines changed: 199 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from enum import Enum
2-
from typing import Optional
2+
from typing import List, Optional, Sequence
33

44
from icontract import DBC, invariant
55

@@ -19,6 +19,43 @@ def __init__(self, name: str, optional_text: Optional[str] = None) -> None:
1919
self.optional_text = optional_text
2020

2121

22+
@abstract
23+
@serialization(with_model_type=True)
24+
class Parent(DBC):
25+
optional_text: Optional[str]
26+
27+
def __init__(self, optional_text: Optional[str] = None) -> None:
28+
self.optional_text = optional_text
29+
30+
31+
class Child_a(Parent):
32+
a_only: int
33+
34+
def __init__(self, a_only: int, optional_text: Optional[str] = None) -> None:
35+
Parent.__init__(self, optional_text)
36+
self.a_only = a_only
37+
38+
39+
class Child_b(Parent):
40+
b_only: int
41+
42+
def __init__(self, b_only: int, optional_text: Optional[str] = None) -> None:
43+
Parent.__init__(self, optional_text)
44+
self.b_only = b_only
45+
46+
47+
class Container(Parent):
48+
children: Optional[List[Parent]]
49+
50+
def __init__(
51+
self,
52+
optional_text: Optional[str] = None,
53+
children: Optional[List[Parent]] = None,
54+
) -> None:
55+
Parent.__init__(self, optional_text)
56+
self.children = children
57+
58+
2259
@verification
2360
def if_with_a_single_branch(text: str) -> bool:
2461
"""Check the if-statement with a single branch and no default."""
@@ -111,6 +148,155 @@ def if_with_continue_in_for(number: int) -> bool:
111148
return count != 3
112149

113150

151+
@verification
152+
def narrowing_in_body(parent: Optional[Parent]) -> bool:
153+
"""Check the narrowing in the body of a branch by its condition."""
154+
if parent is not None and isinstance(parent, Child_a):
155+
return parent.a_only < 100
156+
157+
return True
158+
159+
160+
@verification
161+
def narrowing_in_elif_and_else(parent: Optional[Parent]) -> bool:
162+
"""Check the narrowing by the negation of the previous conditions."""
163+
if parent is None:
164+
return True
165+
elif not isinstance(parent, Child_b):
166+
return parent.optional_text is None or parent.optional_text != "forbidden"
167+
else:
168+
return parent.b_only > 0
169+
170+
171+
@verification
172+
def narrowing_after_early_return(parent: Optional[Parent]) -> bool:
173+
"""Check the narrowing after an if-statement whose branch always returns."""
174+
if parent is None or not isinstance(parent, Child_b):
175+
return True
176+
177+
return parent.b_only < 50
178+
179+
180+
@verification
181+
def narrowing_after_the_only_completing_branch(parent: Optional[Parent]) -> bool:
182+
"""Check the narrowing after an if-statement whose ``else`` always returns."""
183+
if parent is not None and isinstance(parent, Child_a):
184+
pass
185+
else:
186+
return True
187+
188+
return parent.a_only > -10
189+
190+
191+
@verification
192+
def child_as_have_texts(parents: Sequence[Parent]) -> bool:
193+
"""Check the narrowing after the ``continue`` and the early return in a loop."""
194+
for parent in parents:
195+
if not isinstance(parent, Child_a):
196+
continue
197+
198+
if parent.optional_text is None:
199+
return False
200+
201+
if len(parent.optional_text) < 1:
202+
return False
203+
204+
return True
205+
206+
207+
@verification
208+
def texts_before_container_are_short(parents: Sequence[Parent]) -> bool:
209+
"""Check the narrowing after the ``continue`` in a loop with a ``break``."""
210+
total = 0
211+
for parent in parents:
212+
if isinstance(parent, Container):
213+
break
214+
215+
if parent.optional_text is None:
216+
continue
217+
218+
total = total + len(parent.optional_text)
219+
220+
return total < 20
221+
222+
223+
@verification
224+
def text_or_default_is_short(parent: Parent) -> bool:
225+
"""Check the narrowing by the value assigned in a branch."""
226+
text = parent.optional_text
227+
if text is None:
228+
text = "default"
229+
230+
return len(text) < 10
231+
232+
233+
@verification
234+
def last_child_a_is_small(parent: Parent, parents: Sequence[Parent]) -> bool:
235+
"""Check the narrowing of a variable to a class by the assigned value."""
236+
last = parent
237+
for other in parents:
238+
if isinstance(other, Child_a):
239+
last = other
240+
if last.a_only >= 1000:
241+
return False
242+
243+
return True
244+
245+
246+
@verification
247+
def has_marker_in_tree(parent: Parent) -> bool:
248+
"""Check the recursive chain of ``isinstance`` checks with early returns."""
249+
if parent.optional_text is not None and parent.optional_text == "marker":
250+
return True
251+
252+
if isinstance(parent, Container):
253+
return parent.children is not None and any(
254+
has_marker_in_tree(child) for child in parent.children
255+
)
256+
257+
return False
258+
259+
260+
@invariant(
261+
lambda self: narrowing_in_body(self.optional_parent),
262+
"Optional parent as Child_a must have a small a_only",
263+
)
264+
@invariant(
265+
lambda self: narrowing_in_elif_and_else(self.optional_parent),
266+
"Optional parent must have an allowed text or a positive b_only",
267+
)
268+
@invariant(
269+
lambda self: narrowing_after_early_return(self.optional_parent),
270+
"Optional parent as Child_b must have a small b_only",
271+
)
272+
@invariant(
273+
lambda self: narrowing_after_the_only_completing_branch(self.optional_parent),
274+
"Optional parent as Child_a must have a_only above minus ten",
275+
)
276+
@invariant(
277+
lambda self: not (self.parents is not None) or child_as_have_texts(self.parents),
278+
"Parents as Child_a must have non-empty texts",
279+
)
280+
@invariant(
281+
lambda self: not (self.parents is not None)
282+
or texts_before_container_are_short(self.parents),
283+
"Texts of parents before the first container must be short",
284+
)
285+
@invariant(
286+
lambda self: not (self.optional_parent is not None)
287+
or text_or_default_is_short(self.optional_parent),
288+
"Text of the optional parent must be short",
289+
)
290+
@invariant(
291+
lambda self: not (self.optional_parent is not None and self.parents is not None)
292+
or last_child_a_is_small(self.optional_parent, self.parents),
293+
"Parents as Child_a must have a_only below one thousand",
294+
)
295+
@invariant(
296+
lambda self: not (self.optional_parent is not None)
297+
or not has_marker_in_tree(self.optional_parent),
298+
"Optional parent must have no marker in its tree",
299+
)
114300
@invariant(
115301
lambda self: if_with_a_single_branch(self.text),
116302
"Text must be at most 10 characters long",
@@ -145,15 +331,26 @@ class Something(DBC):
145331
number: int
146332
flag: bool
147333
item: Item
334+
optional_parent: Optional[Parent]
335+
parents: Optional[List[Parent]]
148336

149337
def __init__(
150-
self, kind: Kind, text: str, number: int, flag: bool, item: Item
338+
self,
339+
kind: Kind,
340+
text: str,
341+
number: int,
342+
flag: bool,
343+
item: Item,
344+
optional_parent: Optional[Parent] = None,
345+
parents: Optional[List[Parent]] = None,
151346
) -> None:
152347
self.kind = kind
153348
self.text = text
154349
self.number = number
155350
self.flag = flag
156351
self.item = item
352+
self.optional_parent = optional_parent
353+
self.parents = parents
157354

158355

159356
__version__ = "dummy"

0 commit comments

Comments
 (0)