-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement AppliedTermRef (singleton types for term-level applications) #3887
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hello, and thank you for opening this PR! 🎉
All contributors have signed the CLA, thank you! ❤️
Commit Messages
We want to keep history, but for that to actually be useful we have
some rules on how to format our commit messages (relevant xkcd).
Please stick to these guidelines for commit messages:
- Separate subject from body with a blank line
- When fixing an issue, start your commit message with
Fix #<ISSUE-NBR>:
- Limit the subject line to 72 characters
- Capitalize the subject line
- Do not end the subject line with a period
- Use the imperative mood in the subject line ("Added" instead of "Add")
- Wrap the body at 80 characters
- Use the body to explain what and why vs. how
adapted from https://chris.beams.io/posts/git-commit
Have an awesome day! ☀️
aa29fe1
to
41e3353
Compare
@@ -861,6 +861,11 @@ object Parsers { | |||
} | |||
} | |||
|
|||
def emptyRefinementOrSingletonExpr(): Tree = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe call this refinementOfEmptyOrSingletonExpr()
.
af18383
to
25d7675
Compare
Rebased on top of master, but the most recently merged PR (#3841) seems to interfere, as now the fromtasty version of the added test fails. Investigating. |
25d7675
to
d585468
Compare
Wow! Isn’t this supporting what OCaml calls applicative functors? That would be exciting news, I can elaborate why. Do you also support type members of such applications? That would be a logical consequence. To elaborate a bit (not sure if this will be clear): this would enable usable type members on implicits. Without such a feature, two separate occurrences of For proper credit, this isn’t my idea: it goes back to a talk by Derek Dreyer in 2009, and I and Tillmann Rendel discussed applications to Scala a few years ago. |
Indeed, this PR should enable applicative functors as in OCaml (though I have no experience with OCaml in that regard). I haven't written any test cases involving implicit search, specifically, but in principle that should work too. Feel free to play with the branch and let me know if it doesn't behave as you'd expect! |
I should definitely do that! It’ll take me a while to get productive and be able to do this though — I’m already commenting on more issues I can look at seriously! |
d585468
to
81b4c13
Compare
fd1d5d9
to
2715a53
Compare
test performance please |
performance test scheduled: 1 job(s) in queue, 0 running. |
Performance test finished successfully: Visit http://dotty-bench.epfl.ch/3887/ to see the changes. Benchmarks is based on merging with master (f2bb231) |
@liufengyun When I try to access the performance test results at http://dotty-bench.epfl.ch/3887/ I get a 404, any idea why? |
@gsps That link works here now. |
Right, now it works for me too, I guess there's some post-processing phase. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Your commit messages should contain a blank line between the subject and the body, see https://gist.github.com/robertpainsi/b632364184e70900af4ab688decf6f53 for example.
// Need to be more permissive when checking later phases, applications may have been rewritten | ||
isSubType(tp1, tp2.resType) | ||
case tp1: AppliedTermRef => | ||
tp1.args.size == tp2.args.size && isSubType(tp1.fn, tp2.fn) && |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sameLength(tp1.args, tp2.args)
case tp2: AppliedTermRef => | ||
def compareAppliedTerm = tp1 match { | ||
case _ if !ctx.phase.isTyper => | ||
// Need to be more permissive when checking later phases, applications may have been rewritten |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you give an example?
isSubType(tp1, tp2.resType) | ||
case tp1: AppliedTermRef => | ||
tp1.args.size == tp2.args.size && isSubType(tp1.fn, tp2.fn) && | ||
(tp1.args zip tp2.args).forall(t => isSubType(t._1, t._2)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Slightly cleaner: (tp1.args, tp2.args).zipped.forall(isSubType)
@@ -561,6 +561,17 @@ class TypeComparer(initctx: Context) extends DotClass with ConstraintHandling { | |||
false | |||
} | |||
compareTypeBounds | |||
case tp2: AppliedTermRef => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any reason to have this in thirdTry and not firstTry? Also I think it'd be good to have more testcases that stress-test the subtype checks for AppliedTermRef. For example, what happens when an AppliedTermRef is hidden in a type alias?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We do have a test that relies on a type application:
val t: Id[{a + 1}] = a + 1
I'll also add one that relies on a simple alias.
@@ -148,6 +148,7 @@ object Types { | |||
/** Does this type denote a stable reference (i.e. singleton type)? */ | |||
final def isStable(implicit ctx: Context): Boolean = stripTypeVar match { | |||
case tp: TermRef => tp.termSymbol.isStable && tp.prefix.isStable || tp.info.isStable | |||
case tp: AppliedTermRef => tp.fn.isStable && tp.args.forall(_.isStable) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aren't all AppliedTermRefs stable by definition?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd think they can't be stable if the arguments aren't stable; OTOH, I suspect AppliedTermRef
with unstable arguments shouldn't be just unstable but illegal.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's what I meant.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In principle f(unstableThing)
is still meaningful, if you think of it as the image of f
on set unstableThing
. E.g., we could still type sign(impure()): 1.type
where def impure(): PositiveInt
. That being said, I think it's fine to just make AppliedTermRef
s stable by construction for now, and I'll come back to this in my branch if I find a compelling use-case.
@@ -416,6 +416,7 @@ object TastyFormat { | |||
METHODtype + implicitOffset + erasedOffset | |||
} | |||
|
|||
final val APPLIEDTERMREF = 180 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Need to add this to the comment listing all tags at the top of this file and to bump the MinorVersion of TASTY. Also may need to add a case to TastyPrinter (To check if TastyPrinter is working correctly, enable "pickling" in Printers.scala)
@@ -863,7 +863,7 @@ object Parsers { | |||
makeTupleOrParens(inParens(argTypes(namedOK = false, wildOK = true))) | |||
} | |||
else if (in.token == LBRACE) | |||
atPos(in.offset) { RefinedTypeTree(EmptyTree, refinement()) } | |||
atPos(in.offset) { inBraces(emptyRefinementOrSingletonExpr()) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you're changing the grammar, it'd be nice to update the grammar comment above (and reflect the update in https://github.com/lampepfl/dotty/blob/master/docs/docs/internals/syntax.md)
@@ -60,6 +60,8 @@ class PlainPrinter(_ctx: Context) extends Printer { | |||
homogenize(tp.ref) | |||
case AppliedType(tycon, args) => | |||
tycon.dealias.appliedTo(args) | |||
case tp: AppliedTermRef => | |||
homogenize(tp.underlying) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That seems pretty drastic to me, it means that AppliedTermRef will always be widened before being pretty-printed with -Ytest-pickler, so you may miss differences.
@@ -141,6 +143,8 @@ class PlainPrinter(_ctx: Context) extends Printer { | |||
ParamRefNameString(tp) ~ ".type" | |||
case tp: TypeParamRef => | |||
ParamRefNameString(tp) ~ lambdaHash(tp.binder) | |||
case tp: AppliedTermRef => | |||
toTextRef(tp) ~ ".type" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shouldn't you wrap in {
and }
to reflect the syntax instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed, though it does require some state to track when we hit the first AppliedTermRef
. Is it okay to keep this kind of flag in PlainPrinter
itself?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess so, we already have state for rectypes.
@@ -445,6 +446,9 @@ private class ExtractAPICollector(implicit val ctx: Context) extends ThunkHolder | |||
val apiTycon = apiType(tycon) | |||
val apiArgs = args.map(processArg) | |||
api.Parameterized.of(apiTycon, apiArgs.toArray) | |||
case tp: AppliedTermRef => | |||
val apiTps = (tp.fn :: tp.args).map(apiType) | |||
withMarker(combineApiTypes(apiTps: _*), appliedTermRefMarker) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you add an sbt scripted test to check that this is working properly? See https://github.com/lampepfl/dotty/tree/master/sbt-dotty/sbt-test/source-dependencies/by-name for an example.
This is really exciting for the reason @Blaisorblade mentioned. It could help solve a long-lasting problem in Scala (and other languages with type classes but no dependent types). I've just played a little bit with it, but my experimentation was hampered because of this: scala> class Ord[A]
scala> implicit object io extends Ord[Int]
scala> implicit def lo[A](implicit ord: Ord[A]): Ord[List[A]] = new Ord
scala> val r: {lo(io)} = lo(io)
1 |val r: {lo(io)} = lo(io)
| ^^^^^^^^
| Ord[List[A]] is not stable
|
| where: A is a type variable which is an alias of Int Is there currently a way (annotation) to tell Dotty that the Also, it would be nice and generally more consistent if we could write type |
There isn't in this branch, but the ptyper one has it, i.e., I agree on the syntax, and I'm not sure what would be the preferred notation here. It's a pity that in general we can't just use term-level syntax (without the curly braces) to denote singleton types, since the type and the term namespaces might clash. |
The thing is what comes before |
We have a few exceptions, but they can be tricky to handle — |
I believe if we want to revive this we need a decision on syntax, and a clear goal. For me the goal would be to implement applicative functors. But that should be fleshed out in detail first before we make a move on the underlying type structure. Otherwise we just increase entropy. |
3396b0f
to
aa832f8
Compare
There was no activity on this one for a long while, so let's close it. |
This PR implements
AppliedTermRef
, a singleton type representing a term-level application precisely.AppliedTermRef
s are currently only introduced when they are stable, that is, when both the function and the arguments are.The PR includes several additional changes to allow interaction and testing of
AppliedTermRef
s:AppliedTermRef
s are widened away unless specifically expected on declaration sites. This prompts the need for an explicit syntax of singleton types more complex thanp.a.t.h.type
. For this purpose we add an alternative syntax for singleton types: a type tree of the form{ expr }
is translated to the singleton typeT
ofexpr
, provided thatT
is stable.A simple example:
To Dos:
Bug: Missing denotation / NoType upon accessing underlying type of inter-parameter dependency.Deferred. I suggest tackling this as a separate issue -- I think the problem lies in how parameter types are integrated intoMethodType
s and existed before this PR, but afaict could not be exercised previously.AppliedTermRef
for sbt API extraction. Not sure how to proceed with this. Can we mapAppliedTermRef
s to existingxsbti.api.Tree
s, or do we need a new subclass?