Skip to content

Conversation

@DanilaFe
Copy link
Contributor

@DanilaFe DanilaFe commented Jan 24, 2026

Closes #28246 by untangling forwarding from POI.
Closes #28354 by making speculatively resolved calls still count for POI caching.

Consider the program from that issue:

module A {
  record Inner {
    proc foo() {
      return 42;
    }
  }
}

module B {
  import A.Inner;

  record Outer {
    forwarding var inner: Inner;
  }
}

module C {
  import B.Outer;

  record Outermost {
    forwarding var outer: Outer;
  }
}

module D {
  import A.Inner;
  import B.Outer;
  import C.Outermost;

  proc generic(x) {
    writeln(x.foo());
  }

  proc case1() {
    generic(new Outermost());
  }

  proc case2() {
    proc Outer.foo() do return "Outer foo";
    generic(new Outermost());
  }

  proc case3() {
    generic(new Outermost());
  }

  proc main() {
    case1();
    case2();
    case3();
  }
}

While previously, forwarding and POI would be interleaved as follows:

Old Ordering:

  • non-POI candidates for Outermost.
  • POI candidates for Outermost.
  • non-POI candidates for Outer.
  • POI candidates for Outer.
  • non-POI candidates for Inner
  • POI candidates for Inner.

After this PR, they will be interleaved as follows:
New Ordering:

  • non-POI candidates for Outermost.
  • non-POI candidates for Outer.
  • non-POI candidates for Inner
  • POI candidates for Outermost.
  • POI candidates for Outer.
  • POI candidates for Inner.

This ensures that user code cannot "hijack" library procedures by defining POI overloads for types like Outermost, where previously the type forwarded this method from Inner. It also ensures that POI scopes are attempted only if the call would not have otherwise resolved, since this is required for caching instantiations.

There are effectively 4 phases to the new resolution strategy:

Phase Candidates considered
Phase 1 Non-POI candidates for Outermost (`searchOnePoiLevel
Phase 2 (Transitive) Non-POI candidates forwarded by Outermost (resolveForwardedCall(considerPOI=false))
Phase 3 POI candidates for Outermost (findVisibleFunctionsAndCandidates)
Phase 2 (Transitive) POI candidates forwarded by Outermost (resolveForwardedCall(considerPOI=true))

When implementing this, I noticed that we had several functions that threaded through much state. These functions had to be split and interleaved with forwarding handling, which would've required more state to be lifted to the top-level resolveNormalCall function and threaded through. Instead of doing so, I consolidated all the shared state into a new struct, CandidateSearchState. I also tried to improve the comments in the affected code, since I found it somewhat difficult to follow at first.

The implementation details are as follows. Since resolveForwardedCall can recurse (implementing the step from Outer to Inner above, allowing for transitive forwards), which it does by calling resolveNormalCall, and since in phase 2 it is too early to consult POIs, I needed to adjust resolveNormalCall to enable it to ignore POI when needed. Furthermore, since we did want a POI candidate search for forwards eventually (phase 4), I also needed to allow resolveForwardedCall to request POI candidates after all. This led to me threading through considerPoi boolean arguments to both of these functions.

This helps make the data flow more explicit and bundle repeated
arguments.

Signed-off-by: Danila Fedorin <[email protected]>
Suppose there's a forwarding chain A -> B -> C.

Previously, because only a 'considerPoi' flag was used,
the search pattern was roughly like this:

* Search for type A candidates (non-POI)
* Search for type B candidates (non-POI)
* Search tor type C candidates (non-POI)
* Search for type A POI candidates
* Search for type B candidates (non-POI, because POI search entails non-POI search)
* Search for type A candidates (non-POI)
* Search for type B POI candidates
* Search for type A candidates (non-POI, because POI search entails non-POI search)
* Search for Type A POI candidates

Basically, because 'considerPoi' entailed a superset of work that
'!considerPoi' did, we often duplicated effort. I suspect that
this duplication is exponential in the length of the forwarding
chain.

To fix this, adjust 'considerPoi' to instead be tri-state:
default (poi and non-poi), non-poi-only, and poi-only.
This way, we can proceed as:

* Search for type A candidates (non-POI)
* Search for type B candidates (non-POI)
* Search tor type C candidates (non-POI)
* Search for type A POI candidates
* Search for type B POI candidates (because POI-only is set)
* Search for Type C POI candidates (because POI-only is set)

This is exactly the search set we desire, without repetition.

Signed-off-by: Danila Fedorin <[email protected]>
@DanilaFe DanilaFe marked this pull request as ready for review January 29, 2026 01:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Speculative resolution (canResolve) not respected by generic caching. [Bug]: improper interaction between forwarding and POI causes hysteresis

1 participant