Skip to content

Conversation

@thornoar
Copy link

We use flow analysis to resolve selections of the form .method, where the target is unknown, based on the type into which this selection flows. Examples include

class A
module A with
  fun a = new A
fun f(x : A) = x
f(.a) // <- .a flows into type `A`, hence we find the module A and resolve to A.a
class B
module B with
  fun b = (x) => 5
.b(new B) // <- .b flows into type `B -> Int`, hence we resolve (by traversing the type) to B.b.

See the flows/LeadingDotAccesses.mls file for the examples.
This "generalization" seen in the example with class B can be toggled on and off, as it has both the advantage of flexibility and the disadvantage of potentially conflicting selection resolution candidates. This toggle is currently just a flag in the FlowAnalysis.scala file, and it might be a good idea to have it as part of the user interface, similarly to a language pragma in Haskell.

This is a draft PR, I have the following remaining tasks in mind:

  • Integrating the generalization toggle properly
  • Implementing chained selections of the form .a_1.a_2.<...>.a_n, which doesn't work out of the box unfortunately :(
  • Miscellaneous TODOs and ???s that should be properly implemented
  • Testing

Copy link
Contributor

@LPTK LPTK left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! But I have a bunch of comments. Also, it seems the CI is failing.

if usesResTmp then k(Value.Ref(l))
else k(unit) // * it seems this currently never happens
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👎

val sym = resolveField(nme, preTrm.symbol, nme)
Term.SynthSel(preTrm, nme)(sym, N)
case Sel(Empty(), nme) =>
Term.Sel(Term.LeadingDotTarget, nme)(N, N, S(ctx))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're going to add a new term, it should be Term.LeadingDotSel(nme), tro reduce the amount of illegal states that are representable.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense, and I've implemented it, but there arises a problem with Lowering again. If I understand correctly, leading dot selections should not be lowered at all, but the term function in Lowering.scala still gets called, and when the term was Sel(LeadingDotTarget, nme), this was fine since the term function handles the Sel case. However, on LeadingDotSel... what should it do..?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You just elaborate (expand) it so to make sure no LeadingDotSel appears after the flow analysis stage!

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and probably you would want to add a case in the term function, so that if a LeadingDotSel attempts to get itself lowered, the compiler crashes by calling lastWords (internal error)

),
if (nullary) then nullaryType else Bot,
)
// (binary, unary, nullary) match
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually prefer this version.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what are you referring to? The pattern match or the if block? if it's the latter, do you mean that we should assign the signature only based on the nullary flag?

case Flow(sym: FlowSymbol)
case Fun(lhs: Consumer, rhs: Producer, captures: Ls[(Producer, Consumer)])
case Tup(elems: Ls[Opt[SpreadKind] -> Producer])
case Tup(elems: Ls[(Opt[SpreadKind], Producer)])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't the -> version easier to read?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally, -> looks to me like a function type, so (,) is much more readable. But the call is entirely yours of course, I will follow the project style

case CompanionMember(_, sym) => msg"companion member ${sym.nme}" -> sym.toLoc

def solveConstraints() =
def findConsumerSymbols(cons: Consumer): Ls[Symbol] =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we trying to extract consumer symbols from things like tuples and function left/right-hand sides? 🤔

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the sort of generalization I was talking about, and how resolutions like .b(new B) work, i.e. by solving the constraint _?_.b <: ((B) => <something>) and traversing the consumer of this constraint to find the type B and its module, where b can be found. The findConsumerSymbols function grabs at any symbols found in the consumer and returns them to be inspected for companion modules. Of course, this raises the problem of conflicting methods, which is why you can toggle this off, and we may remove this feature entirely.

case ObjectMember(sym) => msg"object member ${sym.nme}" -> sym.toLoc
case CompanionMember(_, sym) => msg"companion member ${sym.nme}" -> sym.toLoc
msg"Cannot resolve selection" -> sel.toLoc :: Nil
// * An error should alsoready be reported in this case
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"alsoready" kind of makes sense, but is unfortunately not a word.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's your comment, not mine :)

Type.Error

def typeParam(p: Param): C =
val fs = FlowSymbol(p.sym.name)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a fan of this. We should access flow symbols that are reachable from the parameter, so that these can be retrieved later.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. What led me to this was the inability to define a flow symbol directly in the Param class, due to a lack of contextual State in that scope. Would it be better to add an Option[FlowSymbol] field in Param, and set this field to S(FlowSymbol(p.sym.name)) in the typeParam function?

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.

3 participants