Skip to content

Commit 6e8dfb4

Browse files
committed
Handle associativity mismatches for type operator in pretty-printer
* RefinedPrinter: Handle associativity mismatches for Type operators. * Add Documentation/tutorial on how to use atPrec and changePrec correctly, since so much code got it wrong and it takes a while to get it. * Extend tests for Type printing.
1 parent 500a9e5 commit 6e8dfb4

File tree

3 files changed

+100
-19
lines changed

3 files changed

+100
-19
lines changed

compiler/src/dotty/tools/dotc/printing/Printer.scala

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,24 @@ abstract class Printer {
1717

1818
private[this] var prec: Precedence = GlobalPrec
1919

20-
/** The current precedence level */
20+
/** The current precedence level.
21+
* WHen pretty-printing arguments of operator `op`, `currentPrecedence` must equal `op`'s precedence level,
22+
* so that pretty-printing expressions using lower-precedence operators can insert parentheses automatically
23+
* by calling `changePrec`.
24+
*/
2125
def currentPrecedence = prec
2226

23-
/** Generate text using `op`, assuming a given precedence level `prec`. */
27+
/** Generate text using `op`, assuming a given precedence level `prec`.
28+
*
29+
* ### `atPrec` vs `changePrec`
30+
*
31+
* This is to be used when changing precedence inside some sort of parentheses:
32+
* for instance, to print T[A]` use
33+
* `toText(T) ~ '[' ~ atPrec(GlobalPrec) { toText(A) } ~ ']'`.
34+
*
35+
* If the presence of the parentheses depends on precedence, inserting them manually is most certainly a bug.
36+
* Use `changePrec` instead to generate them exactly when needed.
37+
*/
2438
def atPrec(prec: Precedence)(op: => Text): Text = {
2539
val outerPrec = this.prec
2640
this.prec = prec
@@ -30,6 +44,27 @@ abstract class Printer {
3044

3145
/** Generate text using `op`, assuming a given precedence level `prec`.
3246
* If new level `prec` is lower than previous level, put text in parentheses.
47+
*
48+
* ### `atPrec` vs `changePrec`
49+
*
50+
* To pretty-print `A op B`, you need something like
51+
* `changePrec(parsing.precedence(op, isType)) { toText(a) ~ op ~ toText(b) }` // BUGGY
52+
* that will insert parentheses around `A op B` if, for instance, the
53+
* preceding operator has higher precedence.
54+
*
55+
* But that does not handle infix operators with left- or right- associativity.
56+
*
57+
* If op and op' have the same precedence and associativity,
58+
* A op B op' C parses as (A op B) op' C if op and op' are left-associative, and as
59+
* A op (B op' C) if they're right-associative, so we need respectively
60+
* ```scala
61+
* val isType = ??? // is this a term or type operator?
62+
* val prec = parsing.precedence(op, isType)
63+
* // either:
64+
* changePrec(prec) { toText(a) ~ op ~ atPrec(prec + 1) { toText(b) } } // for left-associative op and op'
65+
* // or:
66+
* changePrec(prec) { atPrec(prec + 1) { toText(a) } ~ op ~ toText(b) } // for right-associative op and op'
67+
* ```
3368
*/
3469
def changePrec(prec: Precedence)(op: => Text): Text =
3570
if (prec < this.prec) atPrec(prec) ("(" ~ op ~ ")") else atPrec(prec)(op)

compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -155,17 +155,24 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) {
155155
case _ => false
156156
}
157157

158-
def toTextInfixType(op: Type, args: List[Type]): Text = changePrec(InfixPrec) {
159-
/* SLS 3.2.8: all infix types have the same precedence.
160-
* In A op B op' C, op and op' need the same associativity.
161-
* Therefore, if op is left associative, anything on its right
162-
* needs to be parenthesized if it's an infix type, and vice versa. */
163-
val l :: r :: Nil = args
164-
val isRightAssoc = op.typeSymbol.name.endsWith(":")
165-
val leftArg = if (isRightAssoc) atPrec(InfixPrec + 1) { argText(l) } else argText(l)
166-
val rightArg = if (!isRightAssoc) atPrec(InfixPrec + 1) { argText(r) } else argText(r)
167-
168-
leftArg ~ " " ~ simpleNameString(op.classSymbol) ~ " " ~ rightArg
158+
def tyconName(tp: Type): Name = tp.typeSymbol.name
159+
def checkAssocMismatch(tp: Type, isRightAssoc: Boolean) = tp match {
160+
case AppliedType(tycon, _) => isInfixType(tp) && tyconName(tycon).endsWith(":") != isRightAssoc
161+
case AndType(_, _) => isRightAssoc
162+
case OrType(_, _) => isRightAssoc
163+
case _ => false
164+
}
165+
166+
def toTextInfixType(opName: Name, l: Type, r: Type)(op: => Text): Text = {
167+
val isRightAssoc = opName.endsWith(":")
168+
val opPrec = parsing.precedence(opName, true)
169+
170+
changePrec(opPrec) {
171+
val leftPrec = if (isRightAssoc || checkAssocMismatch(l, isRightAssoc)) opPrec + 1 else opPrec
172+
val rightPrec = if (!isRightAssoc || checkAssocMismatch(r, isRightAssoc)) opPrec + 1 else opPrec
173+
174+
atPrec(leftPrec) { argText(l) } ~ " " ~ op ~ " " ~ atPrec(rightPrec) { argText(r) }
175+
}
169176
}
170177

171178
homogenize(tp) match {
@@ -174,7 +181,20 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) {
174181
if (tycon.isRepeatedParam) return toTextLocal(args.head) ~ "*"
175182
if (defn.isFunctionClass(cls)) return toTextFunction(args, cls.name.isImplicitFunction, cls.name.isErasedFunction)
176183
if (defn.isTupleClass(cls)) return toTextTuple(args)
177-
if (isInfixType(tp)) return toTextInfixType(tycon, args)
184+
if (isInfixType(tp)) {
185+
val l :: r :: Nil = args
186+
val opName = tyconName(tycon)
187+
188+
return toTextInfixType(tyconName(tycon), l, r) { simpleNameString(tycon.classSymbol) }
189+
}
190+
191+
// Since RefinedPrinter, unlike PlainPrinter, can output right-associative type-operators, we must override handling
192+
// of AndType and OrType to account for associativity
193+
case AndType(tp1, tp2) =>
194+
return toTextInfixType(tpnme.raw.AMP, tp1, tp2) { toText(tpnme.raw.AMP) }
195+
case OrType(tp1, tp2) =>
196+
return toTextInfixType(tpnme.raw.BAR, tp1, tp2) { toText(tpnme.raw.BAR) }
197+
178198
case EtaExpansion(tycon) =>
179199
return toText(tycon)
180200
case tp: RefinedType if defn.isFunctionType(tp) =>

compiler/test-resources/type-printer/infix

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,46 @@ scala> def foo: (Int && String) &: Boolean = ???
1616
def foo: (Int && String) &: Boolean
1717
scala> def foo: Int && (Boolean &: String) = ???
1818
def foo: Int && (Boolean &: String)
19+
scala> def foo: (Int &: String) && Boolean = ???
20+
def foo: (Int &: String) && Boolean
21+
scala> def foo: Int &: (Boolean && String) = ???
22+
def foo: Int &: (Boolean && String)
23+
scala> def foo: (Int & String) &: Boolean = ???
24+
def foo: (Int & String) &: Boolean
25+
scala> def foo: Int & (Boolean &: String) = ???
26+
def foo: Int & (Boolean &: String)
27+
scala> def foo: (Int &: String) & Boolean = ???
28+
def foo: (Int &: String) & Boolean
29+
scala> def foo: Int &: (Boolean & String) = ???
30+
def foo: Int &: (Boolean & String)
1931
scala> import scala.annotation.showAsInfix
2032
scala> @scala.annotation.showAsInfix class Mappy[T,U]
2133
// defined class Mappy
34+
scala> def foo: (Int Mappy Boolean) && String = ???
35+
def foo: (Int Mappy Boolean) && String
36+
scala> def foo: Int Mappy Boolean && String = ???
37+
def foo: Int Mappy Boolean && String
2238
scala> def foo: Int Mappy (Boolean && String) = ???
23-
def foo: Int Mappy (Boolean && String)
39+
def foo: Int Mappy Boolean && String
2440
scala> @scala.annotation.showAsInfix(false) class ||[T,U]
2541
// defined class ||
2642
scala> def foo: Int || Boolean = ???
2743
def foo: ||[Int, Boolean]
28-
scala> def foo: Int && (Boolean with String) = ???
44+
scala> def foo: Int && Boolean & String = ???
2945
def foo: Int && Boolean & String
30-
scala> def foo: (Int && Boolean) with String = ???
31-
def foo: (Int && Boolean) & String
3246
scala> def foo: (Int && Boolean) & String = ???
33-
def foo: (Int && Boolean) & String
47+
def foo: Int && Boolean & String
3448
scala> def foo: Int && (Boolean & String) = ???
49+
def foo: Int && (Boolean & String)
50+
scala> def foo: Int && (Boolean with String) = ???
51+
def foo: Int && (Boolean & String)
52+
scala> def foo: (Int && Boolean) with String = ???
3553
def foo: Int && Boolean & String
54+
scala> def foo: Int && Boolean with String = ???
55+
def foo: Int && (Boolean & String)
56+
scala> def foo: Int && Boolean | String = ???
57+
def foo: Int && Boolean | String
58+
scala> def foo: Int && (Boolean | String) = ???
59+
def foo: Int && (Boolean | String)
60+
scala> def foo: (Int && Boolean) | String = ???
61+
def foo: Int && Boolean | String

0 commit comments

Comments
 (0)