Skip to content

Conversation

@DanilaFe
Copy link
Contributor

@DanilaFe DanilaFe commented Dec 17, 2025

Consider the program:

module Lib {
  proc outermost(x) {
    compilerWarning("In outermost with argument of type " + x.type:string);
  }
}

module A {
  proc a(x) {
    outermost(x);
  }
}
module B {
  private use C;
  import A.a;

  proc b(x) do a(x);
}

module C {
  private use Lib;
  import B.b;

  proc c(x) do b(x);
}

module poichain {
  import C.c;

  record R {}

  proc main() {
    c(new R());
  }
}

The three->way chain is required, because this bug is about collapsing POI scopes.

Basically, by POI, this program resolves: a(...) is resolved in the scope of b(…), which is resolved in the scope of c(…), which has a private use Lib. Thus, a(…) has access to outermost(…).

However, we try to avoid building up long chains of POI scopes (a-in-scope-of-b-in-scope-of-c). As an optimization, if one module uses another, and both are in the POI chain, we remove the usee from the POI chain, since anything in its scope will be found via the use. In this case, B uses C, so we remove C from the POI scope lookup chain. Unfortunately, this is not sound, as C has private use of Lib, which does not propagate through use C. As a result, the call to a misses outermost.

This PR adjusts the logic of isWholeScopeVisibleFromScope, which is what used to implement the optimization. In particular, I found it to have two odd behaviors

  • the private use issue outlined above: not caring about definitions brought in privately (and thus, not re-exported)
  • assuming child modules re-export parent modules' symbols. Eg., use MParent.MChild, where MParent had a public use Lib, was treated as finding Lib. However, I don't believe this is accurate. See:
    module A {
      var x = 42;
      module B {
      }
    }
    module C {
      use A.B;
    
      proc main() {
        writeln(x);
      }
    }

This PR reimplements isWholeScopeVisibleFromScope from first principles. I consider two ways for one scope to be wholly visible from another:

  1. A child scope nested in an outer scope can see all of the variables in the outer scope, even if they are private.
  2. Any scope that is within reach of a use M can see all of M but only if M doesn't have private definitions. For example, if M has a private proc, use M will not bring that in, and thus, it's not true that the scope of M is "fully visible" from the user.
  • The use M can be transitive; either there's a use M right in the scope, OR there's a use Intermediate, where Intermediate has a public use M, OR Intermediate has a public use OtherIntermediate which has a public use M.... Note that except for the first use, all uses must be public.

To help reduce the impact of this method, I kept it conservative. As a result, I expect isWholeScopeVisibleFromScope to now be stricter than it used to be (certainly it is in the buggy case). This technically expanded existing POI chains in some cases and might've affected caching of instantiations. However, I observed no noticeable performance impact from that, either.

Reviewed by @arifthpe -- thanks!

Testing

  • new dyno test

@DanilaFe DanilaFe force-pushed the fix-poi-merging branch 2 times, most recently from a5b40d9 to a3ef76f Compare December 18, 2025 18:16
@arifthpe arifthpe self-requested a review January 5, 2026 19:39
DanilaFe added a commit that referenced this pull request Jan 6, 2026
Depends on #28243 only for
convenience.

In investigating the bug fixed by
#28226, I noticed that some of
our module code relied on POI where it probably didn't intend to do so
(or at least, where it seemed brittle). I adjusted the compiler to
identify such cases, and found a number of generic functions that
claimed to use POI that weren't actually using POI.

It turned out that Dyno was considering methods found via forwarding to
be "POI". Besides being inaccurate, this tainted the resulting `PoiInfo`
of the resolved function, thereby reducing its eligibility for being
used in caching. E.g., any method that (transitively) relied on DSI
methods (e.g., creating array literals, copying arrays, array indexing)
was marked as "non-cache-eligible" because it "relied on POI" (even
though it just used forwarding to the array instance).

It's not clear to me how impactful this issue was, since many calls
remain uncacheable after this PR because they contain recursion.
However, I did confirm that _some_ uncacheable calls became cacheable
after this PR. Thus, we are bound to see an improvement, if a tiny one.

## Testing
- [x] dyno tests
- [x] paratest `--dyno-resolve-only`
- [ ] paratest
Signed-off-by: Danila Fedorin <[email protected]>
Encountering the same candidate N times through a PoI scope
should not baloon the number of reported rejected candidates.

Signed-off-by: Danila Fedorin <[email protected]>
Signed-off-by: Danila Fedorin <[email protected]>
Signed-off-by: Danila Fedorin <[email protected]>
@DanilaFe DanilaFe merged commit b4fb38c into chapel-lang:main Jan 7, 2026
10 checks passed
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.

2 participants