Skip to content

Commit

Permalink
Changes to default names for context bound witnesses
Browse files Browse the repository at this point in the history
 1. Use the constrained type's name as a term name only for single context bounds
 2. Apply the same scheme to deferred givens
  • Loading branch information
odersky committed Feb 28, 2024
1 parent db9a509 commit db16fdc
Show file tree
Hide file tree
Showing 7 changed files with 152 additions and 14 deletions.
13 changes: 7 additions & 6 deletions compiler/src/dotty/tools/dotc/ast/Desugar.scala
Original file line number Diff line number Diff line change
Expand Up @@ -246,13 +246,14 @@ object desugar {
flags: FlagSet,
freshName: => TermName)(using Context): Tree = rhs match
case ContextBounds(tbounds, cxbounds) =>
var useParamName = Feature.enabled(Feature.modularity)
for bound <- cxbounds do
val evidenceName = bound match
case ContextBoundTypeTree(_, _, ownName) if !ownName.isEmpty => ownName
case _ if useParamName => tname.toTermName
case _ => freshName
useParamName = false
case ContextBoundTypeTree(_, _, ownName) if !ownName.isEmpty =>
ownName
case _ if Feature.enabled(Feature.modularity) && cxbounds.tail.isEmpty =>
tname.toTermName
case _ =>
freshName
val evidenceParam = ValDef(evidenceName, bound, EmptyTree).withFlags(flags)
evidenceParam.pushAttachment(ContextBoundParam, ())
evidenceBuf += evidenceParam
Expand Down Expand Up @@ -498,7 +499,7 @@ object desugar {
val evidenceBuf = new ListBuffer[ValDef]
val result = cpy.TypeDef(tdef)(rhs =
desugarContextBounds(tdef.name, tdef.rhs, evidenceBuf,
(tdef.mods.flags.toTermFlags & AccessFlags) | DeferredGivenFlags, EmptyTermName))
(tdef.mods.flags.toTermFlags & AccessFlags) | Lazy | DeferredGivenFlags, EmptyTermName))
if evidenceBuf.isEmpty then result else Thicket(result :: evidenceBuf.toList)

/** The expansion of a class definition. See inline comments for what is involved */
Expand Down
15 changes: 12 additions & 3 deletions docs/_docs/reference/experimental/typeclasses-syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,15 @@ So far, an unnamed context bound for a type parameter gets a synthesized fresh n
xs.foldLeft(A.unit)(_ `combine` _)
```



The use of a name like `A` above in two variants, both as a type name and as a term name is of course familiar to Scala programmers. We use the same convention for classes and companion objects. In retrospect, the idea of generalizing this to also cover type parameters is obvious. It is surprising that it was not brought up before.

**Proposed Rules**

1. The generated evidence parameter for a context bound `A : C as a` has name `a`
2. The generated evidence for a context bound `A : C` without an `as` binding has name `A` (seen as a term name). So, `A : C` is equivalent to `A : C as A`.
3. If there are more than one context bounds for a type parameter, the generated evidence parameter for every context bound except the first one has a fresh synthesized name, unless the context bound carries an `as` clause, in which case rule (1) applies.
3. If there are multiple context bounds for a type parameter, as in `A : {C_1, ..., C_n}`, the generated evidence parameter for every context bound `C_i` has a fresh synthesized name, unless the context bound carries an `as` clause, in which case rule (1) applies.

The default naming convention reduces the need for named context bounds. But named context bounds are still essential, for at least two reasons:

Expand Down Expand Up @@ -183,15 +185,22 @@ The compiler expands this to the following implementation:
```scala
trait Sorted:
type Element
given Ord[Element] = compiletime.deferred
given Ord[Element] as Element = compiletime.deferred

class SortedSet[A](using A: Ord[A]) extends Sorted:
type Element = A
override given Ord[Element] = A // i.e. the A defined by the using clause
override given Ord[Element] as Element = A // i.e. the A defined by the using clause
```

The using clause in class `SortedSet` provides an implementation for the deferred given in trait `Sorted`.

If there is a single context bound, as in
```scala
type T : C
```
the synthesized deferred given will get the (term-)name of the constrained type `T`. If there are multiple bounds,
the standard convention for naming anonymous givens applies.

**Benefits:**

- Better orthogonality, type parameters and abstract type members now accept the same kinds of bounds.
Expand Down
13 changes: 10 additions & 3 deletions docs/_docs/reference/experimental/typeclasses.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ The use of a name like `A` above in two variants, both as a type name and as a t

1. The generated evidence parameter for a context bound `A : C as a` has name `a`
2. The generated evidence for a context bound `A : C` without an `as` binding has name `A` (seen as a term name). So, `A : C` is equivalent to `A : C as A`.
3. If there are more than one context bounds for a type parameter, the generated evidence parameter for every context bound except the first one has a fresh synthesized name, unless the context bound carries an `as` clause, in which case rule (1) applies.
3. If there are multiple context bounds for a type parameter, as in `A : {C_1, ..., C_n}`, the generated evidence parameter for every context bound `C_i` has a fresh synthesized name, unless the context bound carries an `as` clause, in which case rule (1) applies.

The default naming convention reduces the need for named context bounds. But named context bounds are still essential, for at least two reasons:

Expand Down Expand Up @@ -306,15 +306,22 @@ The compiler expands this to the following implementation:
```scala
trait Sorted:
type Element
given Ord[Element] = compiletime.deferred
given Ord[Element] as Element = compiletime.deferred

class SortedSet[A](using A: Ord[A]) extends Sorted:
type Element = A
override given Ord[Element] = A // i.e. the A defined by the using clause
override given Ord[Element] as Element = A // i.e. the A defined by the using clause
```

The using clause in class `SortedSet` provides an implementation for the deferred given in trait `Sorted`.

If there is a single context bound, as in
```scala
type T : C
```
the synthesized deferred given will get the (term-)name of the constrained type `T`. If there are multiple bounds,
the standard convention for naming anonymous givens applies.

**Benefits:**

- Better orthogonality, type parameters and abstract type members now accept the same kinds of bounds.
Expand Down
12 changes: 12 additions & 0 deletions tests/neg/FromString.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//> using options -language:experimental.modularity -source future

trait FromString:
type Self
def fromString(s: String): Self

given Int forms FromString = _.toInt

given Double forms FromString = _.toDouble

def add[N: {FromString, Numeric as num}](a: String, b: String): N =
num.plus(N.fromString(a), N.fromString(b)) // error: Not found: N // error
4 changes: 2 additions & 2 deletions tests/pos/FromString.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ given Int forms FromString = _.toInt

given Double forms FromString = _.toDouble

def add[N: {FromString, Numeric as num}](a: String, b: String): N =
num.plus(N.fromString(a), N.fromString(b))
def add[N: {FromString as fs, Numeric as num}](a: String, b: String): N =
num.plus(fs.fromString(a), fs.fromString(b))
13 changes: 13 additions & 0 deletions tests/pos/deferred-givens.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
//> using options -language:experimental.modularity -source future
import compiletime.*
class Ord[Elem]
given Ord[Double]

trait A:
type Elem : Ord
def foo = summon[Ord[Elem]]

class AC extends A:
type Elem = Double
override given Ord[Elem] as Elem = ???

class AD extends A:
type Elem = Double

trait B:
type Elem
Expand All @@ -22,3 +34,4 @@ class E(using x: Ord[String]) extends B:

class F[X: Ord] extends B:
type Elem = X

96 changes: 96 additions & 0 deletions tests/run/for-desugar-strawman.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@

@main def Test =
println:
for
x <- List(1, 2, 3)
y = x + x
if x >= 2
i <- List.range(0, y)
z = i * i
if z % 2 == 0
yield
i * x

println:
val xs = List(1, 2, 3)
xs.flatMapDefined: x =>
val y = x + x
xs.applyFilter(x >= 2):
val is = List.range(0, y)
is.mapDefined: i =>
val z = i * i
is.applyFilter(z % 2 == 0):
i * x

extension [A](as: List[A])

def applyFilter[B](p: => Boolean)(b: => B) =
if p then Some(b) else None

def flatMapDefined[B](f: A => Option[IterableOnce[B]]): List[B] =
as.flatMap: x =>
f(x).getOrElse(Nil)

def mapDefined[B](f: A => Option[B]): List[B] =
as.flatMap(f)

object UNDEFINED

extension [A](as: Vector[A])

def applyFilter[B](p: => Boolean)(b: => B) =
if p then b else UNDEFINED

def flatMapDefined[B](f: A => IterableOnce[B] | UNDEFINED.type): Vector[B] =
as.flatMap: x =>
f(x) match
case UNDEFINED => Nil
case y: IterableOnce[B] => y

def mapDefined[B](f: A => B | UNDEFINED.type): Vector[B] =
as.flatMap: x =>
f(x) match
case UNDEFINED => Nil
case y: B => y :: Nil

/*
F ::= val x = E; F
x <- E; G
G ::= []
val x = E; G
if E; G
x <- E; G
Translation scheme:
{ for F yield E }c where c = undefined
{ for G yield E }c where c is a reference to the generator preceding the G sequence
{ for [] yield E }c = E
{ for p = Ep; G yield E }c = val p = Ep; { for G yield E }c
{ for if Ep; G yield E}c = c.applyFilter(Ep)({ for G yield E }c)
{ for p <- Ep; G yield E }c = val c1 = Ep; c1.BIND{ case p => { for G yield E }c1 } (c1 fresh)
where BIND = flatMapDefined if isGen(G), isFilter(G)
= mapDefined if !isGen(G), isFilter(G)
= flatMap if isGen(G), !isFilter(G)
= map if !isGen(G), !isFilter(G)
{ for case p <- Ep; G yield E }c = { for $x <- Ep; if $x match case p => true case _ => false; p = $x@RuntimeChecked; G yield E }c
{ for case p = Ep; G yield E }c = { for $x = Ep; if $x match case p => true case _ => false; p = $x@RuntimeChecked; G yield E}c
isFilter(if E; S)
isFilter(val x = E; S) if isFilter(S)
isGen(x <- E; S)
isGen(val x = E; S) if isGen(S)
isGen(if E; S) if isGen(S)
*/

val foo = 1

def main2 =
foo
???
??? match { case _ => 0 }

0 comments on commit db16fdc

Please sign in to comment.