-
Notifications
You must be signed in to change notification settings - Fork 5
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
Flow analysis #5
Comments
If I've understood correctly, I think a simple solution might be to run multiple passes I.e. Write in current inferred state to the trees, then create a new semantic which now uses the inferred annotations. GetSpeculativeSemanticModel could potentially help where only a single referenced related file had changed since the last pass - I'm assuming it's faster but haven't checked. |
That would work in a system that starts with everything as nullable, infers some non-null types, then run multiple passes (until fixed point) finding more non-null types. It doesn't really work in the current "minimize compiler warnings" system -- Roslyn flow analysis seeing something as non-null causes us to omit edges, which could then cause us to switch a non-null type back to nullable. There's no guarantee the passes would converge to anything. |
Of course we could keep notes on which types were marked non-null by an earlier run, and leave those fixed in the following passes. That should make the iterations converge. Also, with a custom flow analysis I have an idea that I think will let me infer
|
Ah yes, I was assuming the former at the time of writing despite the extensive documentation indicating the latter! |
This initial version does not support loops, and instead resets the complete flow-state whenever a future version of flow-state would need to be merged. It also does not yet support value-dependent flow-state, with VisitCondition() just being a stub that returns the same state for both the true and false branches.
I just pushed a very simple flow-analysis that is barely sufficient to handle the case from my previous comment: I'll still need to add support for the return value of a condition implying a particular flow-state (as with |
And now with 3e8ca2b, the original example from this issue: 9: Dictionary<T, Node> mapping = new Dictionary<T, Node>();
11: Node GetNode(T element)
{
13: Node? node;
14: if (!mapping.TryGetValue(element, out node))
15: {
16: node = new Node();
17: mapping.Add(element, node);
18: }
19: return node;
} results in this graph: |
The overall idea here is: * in addition to the primary node for the parameter's type, we have two additional nodes whenTrue/whenFalse for out parameters on methods that return a boolean. * on return true|false;, connect the current flow-state of the parameter with the whenTrue/whenFalse node. * at the call side, if the method is called in a condition, use the whenTrue/whenFalse nodes as flow-states for the assigned variable * after nullabilities are inferred for nodes, if the primary node is nullable and exactly one of whenTrue/whenFalse is non-nullable, emit the `[NotNullWhen(..)]` attribute
Currently our inference re-uses Roslyn's flow-analysis.
However, this has some fundamental problems.
There's no edges created for line 16/17 because here Roslyn knows that
node
is non-null.Line 14 creates two edges: one from
<nullable>
becauseTryGetValue
will assignnull
when returningfalse
; the other frommapping!1#2
(the mapping field'sNode
type argument) whenTryGetValue
returns true.The
return
statement creates an edge from the variable's type, because Roslyn's flow analysis can't guarantee us that the variable is non-null -- our Roslyn code analysis runs under the pessimistic assumption that all types-to-be-inferred might end up nullable, so it considersmapping
to beDictionary<T, Node?>
, which leaves open the possibility thatGetNode
returns null.However, after our inference decides that
mapping!1#2
is non-null, it would be correct to also indicate that theGetNode
return value is non-null. After all, if no node exists yet, the function will create one.The issue here is that Roslyn's flow analysis isn't aware of our types-to-be-inferred.
It would be better if, instead of using Roslyn's flow analysis, we had our own that keeps track of
node
's nullability.The idea would be to create additional "helper" graph nodes for the different flow states of a local variable of reference type.
After
TryGetValue
initializesnode
, it's flow-state would be (true:mapping!1#2
, false:<nullable>
). Within theif
body, the flow-state would initially be<nullable>
, but after line 16 would change to<nonnull>
.After the if, the flow-state from both alternatives could be re-combined by creating a new node "node-after-line-18" and edges from the nodes from the two if-branches -- in this case
<nonnull>
from the then-branch andmapping!1#2
from the else branch.Then the
return
statement would create an edge from this "node-after-line-18" instead of the node for the variable's declared type.All flow-state nodes associated with a variable would have an edge to the variable's declared type node.
We'd end up with a graph somewhat like this:
Thus in the end,
node
would be inferred as nullable, but theGetNode
return type would only depend onmapping!1#2
and thus can be inferred depending on whether there's amapping.Add(x, null)
access somewhere else in the program.The text was updated successfully, but these errors were encountered: