References in a stack graph may resolve differently depending on context.
Problem
Consider the following TypeScript test:
let x = {
f: 11
};
{
let x = {
};
x.f;
//^ defined: 6
// ^ defined:
}
export {};
Two variables x
are defined, one shadowing the other inside the block. The x
reference in x.f
resolves, correctly, only to the inner definition. This shows that shadowing works correctly.
The f
reference in x.f
should not resolve to anything. After all, x
resolved to the inner definition, which does not have fields. Unfortunately, this is not what happens. The reference resolves to the field of the outer definition.
Why is this happening?
Let's have a look at the stack graph:
When resolving x
, there are two possible paths to x
definitions. At the branch point, one outgoing edge has a higher priority than the other, so only one path is selected.
When resolving f
, there is only one possible path, namely via the outer x
to its field. Disambiguation is only applied on complete paths, which end in a definition with an empty symbol stack. Since there is no such path via the inner x
, the one result path is not shadowed at all.
Exercise for the reader
Is this behavior enough to implement SAT solving with stack graphs?
What should we do about this?
The original scope graph work had similar problems, where a reference would resolve differently on its own vs. as part of a qualified reference. I have always been of the opinion that this is incorrect. I do not know of programming languages where a reference is interpreted differently depending on whether you look at it vs. when you look through it. As far as I can see now, this is not something one can fix by adding more precedences either.
Doing resolution such that a reference is always interpreted the same requires that shadowing is applied to subpaths that go from a reference to a definition. Every subpath that goes from a reference to a definition and has the same pre and post condition, should be in the set of resolved paths for that reference (which have and empty pre and post condition) if the pre and post conditions are erased.
My hunch is that this approach would retain all resolution behavior that you would expect.
Implementation
One question is whether we could resolve references we encounter on their own (disregarding the context we are in), and continue based on the shadowed result set for that reference. Some paths only work within a context, e.g. paths ending in a jump node, or the root node, they have non-empty symbol stacks but should definitely be considered.