Shouldn't affect the result, since in member functions all properties
are assumed to be initialized so the CFG is not needed for reporting
VAL_REASSIGNMENT.
I.e. emit VAL_REASSIGNMENT on repeated assignments to `this.something`,
UNINITIALIZED_VARIABLE on reads of it before any assignment if there is
no initializer, and CAPTURED_MEMBER_VAL_INITIALIZATION on assignments
inside non-called-in-place functions and named classes.
^KT-55528 Fixed
Pretty sure the changes I've made to class graph construction should've
made it so that deadness is propagated through to each member's graph
correctly.
class C {
val x: Int
init {
// valid ways to initialize:
x = 1
this@C.x = 1
// invalid:
someOtherC.x = 1
run { /*this@run.*/x = 1 }
val self = this
self.x = 1
}
}
Class exit node should only unify data flow if it is an anonymous
object, in which case it only has one control flow input. Coversely,
any other class exit node has normal merge semantics for its control
flow inputs, but it won't have any data flow inputs.
If you care about direction, then you also care about labels.
3 out of 6 places that use `traverse` are incorrect, by the way.
Node order does not correspond to scoping boundaries.
UncaughtExceptionPath label out of a finally block matches every label
that is not handled by another edge, and a labeled edge from the middle
of a finally block aborts the jump and so should merge all available
data.
Instead, rely on the variable assignment analyzer to properly restrict
smart casts. This makes error messages more consistent, but otherwise
should have no effect.
Also fix graphs for enums with specialized entries - since we don't
create property subgraphs for FirEnumEntry, there is no body to insert
AnonymousObjectEnterNode, AnonymousObjectExitNode, and
AnonymousObjectExpressionExitNode into.
They are only used in one place that can just as well use kinds.
Especially considering that "the one place" used them incorrectly and
would not attach local functions in property accessors as subgraphs.
function enter -> default 1 -> default 2 -> rest of function
\----------^ \----------^
This probably has no effect (in non-stupid code, at least), but it makes
graph construction more architecturally correct (now value parameters'
subgraphs get attached to a node).
Interpretation: a graph A is a subgraph of B if information available at
nodes of A depends on the paths taken in B. For example, local classes
are subgraphs of a graph in which they are declared, and members of
those classes are subgraphs of the local class itself - because these
members can reference captured values.
Consequences:
* if graph G is a subgraph of node N, then G is a subgraph of N's
owner;
* `ControlFlowAnalysisDiagnosticComponent` will only visit root graphs;
* `graph.traverse` will ignore subgraph boundaries, as if all subgraphs
are inlined into one huge root graph;
* if a control flow checker needs information from a declaration to
which a graph is attached, it must look at subgraphs explicitly.
For example, consider the `callsInPlace` checker. When a function
has a `callsInPlace` contract and a local declaration, the checker must
visit that local declaration to ensure it does not capture the allegedly
called-in-place argument - hence `graph.traverse` will look at the
nodes. However, the local declaration can also be a function with its
own `callsInPlace` contracts, so the checker should also run for it in
isolation. If that sounds quadratic, that's because unfortunately it is.
* `do { continue } while (x)`: block exit is dead, condition is not;
* `try { a?.incomplete() } catch (e) { x }`: when the call is completed
and found to return `Nothing`, the safe call exit node is still live
because `a` could still be null;
* `f(a@{ g { return@a } }, x)`: if call to `g` is not completed, there
should not be a dead data flow edge from the lambda to `f` because
there's no such thing as a dead data flow edge (DeadForward is CF+DF).
The receiver of the provideDelegate call is the same FirExpression as
the delegate itself, so there's only one copy of the nodes in the first
place; trying to remove subgraphs completely detaches objects inside it
from the parent graph, which is not great for checkers.
Note that currently if provideDelegate is not selected, there will be a
stray FunctionCallExit node in the control flow graph. This commit *does
not change that*. It has been there for a while. Don't @ me. I'll try to
fix that. No promises.
Apparently if you add an empty line at the start of the FIR file, that's
not enough of a difference for the test suite to complain about, but
enough for it to not add the FIR_IDENTICAL directive...
While it is theoretically useful to know that `{ while(true) {} }`
returns Nothing, CFG node deadness is not precise enough to do that: if
the entire lambda is dead, it's no longer possible to find out whether
the loop is terminating. Besides, `while (true)` and `if (true)` are
pretty much the only constructs like that anyway.
Note that this commit does not affect resolution for lambdas that end in
a Nothing-returning expression, e.g. `throw`.
* `return` should only be added to the last statement if the return
type is not Unit
* If there is a `return` without an argument, then the expected return
type is Unit and the last expression is not a return argument (unless
it's an incomplete call, in which case it is inferred to return Unit;
this behavior is questionable, but inherited from K1)
* There should be a constraint on return arguments even if the expected
type is Unit, otherwise errors will be missed
* When the expected type is known, using the call completion results
writer is pointless (and probably subtly wrong).
^KT-54742 Fixed
No clue where to put the test for that since diagnostic tests, even
multi-module ones, never touch the metadata serializer. So the test is
a bytecode text one pretending to be about nullability annotations even
though it also affects what resolution in another module will do.
If a class is not referenced by the ABI class file, its InnerClasses
entry should only be kept if it's the current class or a descendant
of the current class.
This is a temporary hack to avoid compiler crashes in some code that
uses builder inference, conditional early returns from lambdas, and
expected types in a certain way. It is not correct - dropping data flow
edges never is - but it is much easier to implement for now than a
proper fix.
This removes the need for hacks around the order in which function
call arguments are visited, fixes called-in-place lambda arguments
for augmented assignment operators, and makes CFG dumps a bit prettier.
I.e. maintain a set of seen lambdas where each marked as either "data
flow only" or "both data and control flow". The latter are truly
parallel with the function being analyzed, while the former have
technically already terminated, we just don't know the types inside them
because they may not have been analyzed yet.
The delegate is resolved in context-dependent mode and thus can be an
incomplete call; if there is no `provideDelegate` method to complete it,
the result is effectively `val x$delegate = y.id()` where `id` is
`fun <T> id(x: T) = x`, except we don't get a real node for `id` so the
DFA edges from lambdas in `y` go who knows where.
Quick quiz:
Q: In a CFG, what does `a -> b -> c -> d` mean?
A: `a`, then `b`, then `c`, then `d`.
Q: In a CFG, what does `a -> b -> d; a -> c -> d` mean?
A: `a`, then `b` or `c`, then `d`.
Q: So how do you encode "a, then (b, then c) or (c, then b), then d`?
A: You can't.
Problem is, you need to, because that's what `a; run2({ b }, { c }); d`
does when `run2` has a contract that it calls both its lambda arguments
in-place: `shuffle(listOf(block1, block2)).forEach { it() }` is a
perfectly valid implementation for it, as little sense as that makes.
So that's what union nodes solve. When a node implements
`UnionNodeMarker`, its inputs are interpreted as "all visited in some
order" instead of the normal "one of the inputs is visited".
Currently this is used for data flow. It *should* also be used for
control flow, but it isn't. But it should be. But that's not so easy.
BTW, `try` exit is NOT a union node; although lambdas in one branch can
be completed according to types' of lambdas in another, data does not
flow between the branches anyway (since we don't know how much of the
`try` executed before jumping into `catch`, and `catch`es are mutually
exclusive) so a `try` expression is more like `when` than a function
call with called-in-place-exactly-once arguments. The fact that
`exitTryExpression` used `processUnionOfArguments` in a weird way
should've hinted at that, but now we know for certain.
The distinction is similar to persistent data structures and their
builders. Only the MutableFlow can be passed to LogicSystem for
modification; when it's ready, it can be converted into PersistentFlow
and attached to a CFG node.
The result is that the API is cleaner, the implementation is a bit more
neat, and hopefully the use of PersistentHashMap.Builder improves
performance a little. Also the node-to-flow map can now be removed in
favor of just storing PersistentFlow inside a node, seeing as it's
explicitly immutable and all that.
If a certain type statement is true on loop entry and all continue
paths, then it is also true on exit if the condition did not reassign
the variable.
^KT-7676 tag fixed-in-k2
It's also not a backwards jump in do-while, unless it's in the loop's
condition, which is a stupid "feature" IMO. As you can probably tell
from the comments added in this commit.
`x?.y != null` does not imply that `x != null` if e.g. an argument to
`y` has reassigned `x` in the meantime.
The same is true for `x == y` and `functionWithContract(x, y)`, but
those are somewhat harder to implement since there is no easy way to
find the last node of a certain argument.
^KT-55096
Thinking of monomorphizing `Flow` (= `PersistentFlow`) and instead
adding a completely separate `MutableFlow` version; then `joinFlow`
would produce a mutable one, data flow analysis would add some
statements, convert to a persistent `Flow` and proceed to the next
node.