Skip to content

Pretty-printing: handle type-operator precedence correctly #4072

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

Merged
merged 13 commits into from
Aug 21, 2018
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions compiler/src/dotty/tools/dotc/parsing/Parsers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -471,13 +471,13 @@ object Parsers {
syntaxError(MixedLeftAndRightAssociativeOps(op1, op2, op2LeftAssoc), offset)

def reduceStack(base: List[OpInfo], top: Tree, prec: Int, leftAssoc: Boolean, op2: Name, isType: Boolean): Tree = {
if (opStack != base && precedence(opStack.head.operator.name) == prec)
if (opStack != base && precedence(opStack.head.operator.name, isType) == prec)
checkAssoc(opStack.head.offset, opStack.head.operator.name, op2, leftAssoc)
def recur(top: Tree): Tree = {
if (opStack == base) top
else {
val opInfo = opStack.head
val opPrec = precedence(opInfo.operator.name)
val opPrec = precedence(opInfo.operator.name, isType)
if (prec < opPrec || leftAssoc && prec == opPrec) {
opStack = opStack.tail
recur {
Expand Down Expand Up @@ -514,7 +514,7 @@ object Parsers {
var top = first
while (isIdent && isOperator) {
val op = if (isType) typeIdent() else termIdent()
top = reduceStack(base, top, precedence(op.name), isLeftAssoc(op.name), op.name, isType)
top = reduceStack(base, top, precedence(op.name, isType), isLeftAssoc(op.name), op.name, isType)
opStack = OpInfo(top, op, in.offset) :: opStack
newLineOptWhenFollowing(canStartOperand)
if (maybePostfix && !canStartOperand(in.token)) {
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/parsing/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import core.NameOps._

package object parsing {

def precedence(operator: Name): Int =
def precedence(operator: Name, isType: Boolean = false): Int =
Copy link
Contributor

Choose a reason for hiding this comment

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

isType unused?

if (operator eq nme.ERROR) -1
else {
val firstCh = operator.firstPart.head
Expand Down
15 changes: 10 additions & 5 deletions compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,15 @@ class PlainPrinter(_ctx: Context) extends Printer {
(refinementNameString(rt) ~ toTextRHS(rt.refinedInfo)).close

protected def argText(arg: Type): Text = homogenizeArg(arg) match {
case arg: TypeBounds => "_" ~ toTextGlobal(arg)
case arg => toTextGlobal(arg)
case arg: TypeBounds => "_" ~ toText(arg)
case arg => toText(arg)
}

/** Pretty-print comma-separated type arguments for a constructor to be inserted among parentheses or brackets
* (hence with `GlobalPrec` precedence).
*/
protected def argsText(args: List[Type]): Text = atPrec(GlobalPrec) { Text(args.map(arg => argText(arg) ), ", ") }

/** The longest sequence of refinement types, starting at given type
* and following parents.
*/
Expand Down Expand Up @@ -144,7 +149,7 @@ class PlainPrinter(_ctx: Context) extends Printer {
case tp: SingletonType =>
toTextLocal(tp.underlying) ~ "(" ~ toTextRef(tp) ~ ")"
case AppliedType(tycon, args) =>
(toTextLocal(tycon) ~ "[" ~ Text(args map argText, ", ") ~ "]").close
(toTextLocal(tycon) ~ "[" ~ argsText(args) ~ "]").close
case tp: RefinedType =>
val parent :: (refined: List[RefinedType @unchecked]) =
refinementChain(tp).reverse
Expand All @@ -156,9 +161,9 @@ class PlainPrinter(_ctx: Context) extends Printer {
}
finally openRecs = openRecs.tail
case AndType(tp1, tp2) =>
changePrec(AndPrec) { toText(tp1) ~ " & " ~ toText(tp2) }
changePrec(AndTypePrec) { toText(tp1) ~ " & " ~ atPrec(AndTypePrec + 1) { toText(tp2) } }
case OrType(tp1, tp2) =>
changePrec(OrPrec) { toText(tp1) ~ " | " ~ toText(tp2) }
changePrec(OrTypePrec) { toText(tp1) ~ " | " ~ atPrec(OrTypePrec + 1) { toText(tp2) } }
case tp: ErrorType =>
s"<error ${tp.msg.msg}>"
case tp: WildcardType =>
Expand Down
39 changes: 37 additions & 2 deletions compiler/src/dotty/tools/dotc/printing/Printer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,24 @@ abstract class Printer {

private[this] var prec: Precedence = GlobalPrec

/** The current precedence level */
/** The current precedence level.
* WHen pretty-printing arguments of operator `op`, `currentPrecedence` must equal `op`'s precedence level,
Copy link
Contributor

@OlivierBlanvillain OlivierBlanvillain Aug 15, 2018

Choose a reason for hiding this comment

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

s/WHen/When/

* so that pretty-printing expressions using lower-precedence operators can insert parentheses automatically
* by calling `changePrec`.
*/
def currentPrecedence = prec

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

/** Generate text using `op`, assuming a given precedence level `prec`.
* If new level `prec` is lower than previous level, put text in parentheses.
*
* ### `atPrec` vs `changePrec`
*
* To pretty-print `A op B`, you need something like
* `changePrec(parsing.precedence(op, isType)) { toText(a) ~ op ~ toText(b) }` // BUGGY
* that will insert parentheses around `A op B` if, for instance, the
* preceding operator has higher precedence.
*
* But that does not handle infix operators with left- or right- associativity.
*
* If op and op' have the same precedence and associativity,
* A op B op' C parses as (A op B) op' C if op and op' are left-associative, and as
* A op (B op' C) if they're right-associative, so we need respectively
* ```scala
* val isType = ??? // is this a term or type operator?
* val prec = parsing.precedence(op, isType)
* // either:
* changePrec(prec) { toText(a) ~ op ~ atPrec(prec + 1) { toText(b) } } // for left-associative op and op'
* // or:
* changePrec(prec) { atPrec(prec + 1) { toText(a) } ~ op ~ toText(b) } // for right-associative op and op'
* ```
*/
def changePrec(prec: Precedence)(op: => Text): Text =
if (prec < this.prec) atPrec(prec) ("(" ~ op ~ ")") else atPrec(prec)(op)
Expand Down
52 changes: 36 additions & 16 deletions compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) {

override def toText(tp: Type): Text = controlled {
def toTextTuple(args: List[Type]): Text =
"(" ~ Text(args.map(argText), ", ") ~ ")"
"(" ~ argsText(args) ~ ")"

def toTextFunction(args: List[Type], isImplicit: Boolean, isErased: Boolean): Text =
changePrec(GlobalPrec) {
Expand All @@ -155,17 +155,24 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) {
case _ => false
}

def toTextInfixType(op: Type, args: List[Type]): Text = {
/* SLS 3.2.8: all infix types have the same precedence.
* In A op B op' C, op and op' need the same associativity.
* Therefore, if op is left associative, anything on its right
* needs to be parenthesized if it's an infix type, and vice versa. */
val l :: r :: Nil = args
val isRightAssoc = op.typeSymbol.name.endsWith(":")
val leftArg = if (isRightAssoc && isInfixType(l)) "(" ~ argText(l) ~ ")" else argText(l)
val rightArg = if (!isRightAssoc && isInfixType(r)) "(" ~ argText(r) ~ ")" else argText(r)

leftArg ~ " " ~ simpleNameString(op.classSymbol) ~ " " ~ rightArg
def tyconName(tp: Type): Name = tp.typeSymbol.name
def checkAssocMismatch(tp: Type, isRightAssoc: Boolean) = tp match {
case AppliedType(tycon, _) => isInfixType(tp) && tyconName(tycon).endsWith(":") != isRightAssoc
case AndType(_, _) => isRightAssoc
case OrType(_, _) => isRightAssoc
case _ => false
}

def toTextInfixType(opName: Name, l: Type, r: Type)(op: => Text): Text = {
val isRightAssoc = opName.endsWith(":")
val opPrec = parsing.precedence(opName, true)

changePrec(opPrec) {
val leftPrec = if (isRightAssoc || checkAssocMismatch(l, isRightAssoc)) opPrec + 1 else opPrec
val rightPrec = if (!isRightAssoc || checkAssocMismatch(r, isRightAssoc)) opPrec + 1 else opPrec

atPrec(leftPrec) { argText(l) } ~ " " ~ op ~ " " ~ atPrec(rightPrec) { argText(r) }
}
}

homogenize(tp) match {
Expand All @@ -174,7 +181,20 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) {
if (tycon.isRepeatedParam) return toTextLocal(args.head) ~ "*"
if (defn.isFunctionClass(cls)) return toTextFunction(args, cls.name.isImplicitFunction, cls.name.isErasedFunction)
if (defn.isTupleClass(cls)) return toTextTuple(args)
if (isInfixType(tp)) return toTextInfixType(tycon, args)
if (isInfixType(tp)) {
val l :: r :: Nil = args
val opName = tyconName(tycon)

return toTextInfixType(tyconName(tycon), l, r) { simpleNameString(tycon.typeSymbol) }
}

// Since RefinedPrinter, unlike PlainPrinter, can output right-associative type-operators, we must override handling
// of AndType and OrType to account for associativity
case AndType(tp1, tp2) =>
return toTextInfixType(tpnme.raw.AMP, tp1, tp2) { toText(tpnme.raw.AMP) }
case OrType(tp1, tp2) =>
return toTextInfixType(tpnme.raw.BAR, tp1, tp2) { toText(tpnme.raw.BAR) }

case EtaExpansion(tycon) =>
return toText(tycon)
case tp: RefinedType if defn.isFunctionType(tp) =>
Expand All @@ -201,7 +221,7 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) {
// (they don't need to because we keep the original type tree with
// the original annotation anyway. Therefore, there will always be
// one version of the annotation tree that has the correct positions).
withoutPos(super.toText(tp))
return withoutPos(super.toText(tp))
case tp: SelectionProto =>
return "?{ " ~ toText(tp.name) ~
(Str(" ") provided !tp.name.toSimpleName.last.isLetterOrDigit) ~
Expand Down Expand Up @@ -375,9 +395,9 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) {
case SingletonTypeTree(ref) =>
toTextLocal(ref) ~ "." ~ keywordStr("type")
case AndTypeTree(l, r) =>
changePrec(AndPrec) { toText(l) ~ " & " ~ toText(r) }
changePrec(AndTypePrec) { toText(l) ~ " & " ~ atPrec(AndTypePrec + 1) { toText(r) } }
case OrTypeTree(l, r) =>
changePrec(OrPrec) { toText(l) ~ " | " ~ toText(r) }
changePrec(OrTypePrec) { toText(l) ~ " | " ~ atPrec(OrTypePrec + 1) { toText(r) } }
case RefinedTypeTree(tpt, refines) =>
toTextLocal(tpt) ~ " " ~ blockText(refines)
case AppliedTypeTree(tpt, args) =>
Expand Down
4 changes: 3 additions & 1 deletion compiler/src/dotty/tools/dotc/printing/Texts.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import language.implicitConversions

object Texts {

abstract class Text {
sealed abstract class Text {

protected def indentMargin = 2

Expand Down Expand Up @@ -46,9 +46,11 @@ object Texts {
case Str(s1, lines2) => Str(s1 + s2, lines1 union lines2)
case Fluid(Str(s1, lines2) :: prev) => Fluid(Str(s1 + s2, lines1 union lines2) :: prev)
case Fluid(relems) => Fluid(that :: relems)
case Vertical(_) => throw new IllegalArgumentException("Unexpected Vertical.appendToLastLine")
}
case Fluid(relems) =>
(this /: relems.reverse)(_ appendToLastLine _)
case Vertical(_) => throw new IllegalArgumentException("Unexpected Text.appendToLastLine(Vertical(...))")
}

private def appendIndented(that: Text)(width: Int): Text =
Expand Down
5 changes: 3 additions & 2 deletions compiler/src/dotty/tools/dotc/printing/package.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package dotty.tools.dotc

import core.StdNames.nme
import core.StdNames.{nme,tpnme}
import parsing.{precedence, minPrec, maxPrec, minInfixPrec}
import util.Property.Key

Expand All @@ -9,7 +9,8 @@ package object printing {
type Precedence = Int

val DotPrec = parsing.maxPrec
val AndPrec = parsing.precedence(nme.raw.AMP)
val AndTypePrec = parsing.precedence(tpnme.raw.AMP, true)
val OrTypePrec = parsing.precedence(tpnme.raw.BAR, true)
val OrPrec = parsing.precedence(nme.raw.BAR)
val InfixPrec = parsing.minInfixPrec
val GlobalPrec = parsing.minPrec
Expand Down
8 changes: 8 additions & 0 deletions compiler/test-resources/type-printer/functions
Original file line number Diff line number Diff line change
@@ -1,2 +1,10 @@
scala> val toInt: Any => Int = new { def apply(a: Any) = 1; override def toString() = "<func1>" }
val toInt: Any => Int = <func1>
scala> val hoFun: (Int => Int) => Int = new { def apply(a: Int => Int) = 1; override def toString() = "<func2>" }
val hoFun: (Int => Int) => Int = <func2>
scala> val curriedFun: Int => (Int => Int) = new { def apply(a: Int) = _ => 1; override def toString() = "<func3>" }
val curriedFun: Int => Int => Int = <func3>
scala> val tupFun: ((Int, Int)) => Int = new { def apply(a: (Int, Int)) = 1; override def toString() = "<func4>" }
val tupFun: ((Int, Int)) => Int = <func4>
scala> val binFun: (Int, Int) => Int = new { def apply(a: Int, b: Int) = 1; override def toString() = "<func5>" }
val binFun: (Int, Int) => Int = <func5>
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we rewrite these tests as:

- scala> val toInt: Any => Int = new { def apply(a: Any) = 1; override def toString() = "<func1>" }                                                                                                               
- val toInt: Any => Int = <func1>
+ scala> def toInt: Any => Int = ???
+ def toInt: Any => Int

There is too much noise with vals

36 changes: 35 additions & 1 deletion compiler/test-resources/type-printer/infix
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,46 @@ scala> def foo: (Int && String) &: Boolean = ???
def foo: (Int && String) &: Boolean
scala> def foo: Int && (Boolean &: String) = ???
def foo: Int && (Boolean &: String)
scala> def foo: (Int &: String) && Boolean = ???
def foo: (Int &: String) && Boolean
scala> def foo: Int &: (Boolean && String) = ???
def foo: Int &: (Boolean && String)
scala> def foo: (Int & String) &: Boolean = ???
def foo: (Int & String) &: Boolean
scala> def foo: Int & (Boolean &: String) = ???
def foo: Int & (Boolean &: String)
scala> def foo: (Int &: String) & Boolean = ???
def foo: (Int &: String) & Boolean
scala> def foo: Int &: (Boolean & String) = ???
def foo: Int &: (Boolean & String)
scala> import scala.annotation.showAsInfix
scala> @scala.annotation.showAsInfix class Mappy[T,U]
// defined class Mappy
scala> def foo: (Int Mappy Boolean) && String = ???
def foo: (Int Mappy Boolean) && String
scala> def foo: Int Mappy Boolean && String = ???
def foo: Int Mappy Boolean && String
scala> def foo: Int Mappy (Boolean && String) = ???
def foo: Int Mappy (Boolean && String)
def foo: Int Mappy Boolean && String
scala> @scala.annotation.showAsInfix(false) class ||[T,U]
// defined class ||
scala> def foo: Int || Boolean = ???
def foo: ||[Int, Boolean]
scala> def foo: Int && Boolean & String = ???
def foo: Int && Boolean & String
scala> def foo: (Int && Boolean) & String = ???
def foo: Int && Boolean & String
scala> def foo: Int && (Boolean & String) = ???
def foo: Int && (Boolean & String)
scala> def foo: Int && (Boolean with String) = ???
def foo: Int && (Boolean & String)
scala> def foo: (Int && Boolean) with String = ???
def foo: Int && Boolean & String
scala> def foo: Int && Boolean with String = ???
def foo: Int && (Boolean & String)
scala> def foo: Int && Boolean | String = ???
def foo: Int && Boolean | String
scala> def foo: Int && (Boolean | String) = ???
def foo: Int && (Boolean | String)
scala> def foo: (Int && Boolean) | String = ???
def foo: Int && Boolean | String
26 changes: 25 additions & 1 deletion compiler/test/dotty/tools/dotc/printing/PrinterTests.scala
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
package dotty.tools.dotc.printing

import dotty.tools.DottyTest
import dotty.tools.dotc.ast.tpd
import dotty.tools.dotc.ast.{Trees,tpd}
import dotty.tools.dotc.core.Names._
import dotty.tools.dotc.core.Symbols._
import dotty.tools.dotc.core.Decorators._
import org.junit.Assert.assertEquals
import org.junit.Test

class PrinterTests extends DottyTest {

private def newContext = {
initialCtx.setSetting(ctx.settings.color, "never")
}
ctx = newContext

import tpd._

@Test
Expand All @@ -24,4 +31,21 @@ class PrinterTests extends DottyTest {
assertEquals("package object foo", bar.symbol.owner.show)
}
}

@Test
def tpTreeInfixOps: Unit = {
val source = """
|class &&[T,U]
|object Foo {
| def bar1: Int && (Boolean | String) = ???
| def bar2: Int & (Boolean | String) = ???
|}
""".stripMargin

checkCompile("frontend", source) { (tree, context) =>
implicit val ctx = context
val bar @ Trees.DefDef(_, _, _, _, _) = tree.find(tree => tree.symbol.name == termName("bar2")).get
assertEquals("Int & (Boolean | String)", bar.tpt.show)
}
}
}
2 changes: 1 addition & 1 deletion tests/patmat/andtype-opentype-interaction.check
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
31: Pattern Match Exhaustivity: _: SealedTrait & OpenClass, _: Trait & OpenClass
35: Pattern Match Exhaustivity: _: SealedTrait & OpenTrait & OpenClass, _: Trait & OpenTrait & OpenClass
43: Pattern Match Exhaustivity: _: SealedTrait & OpenAbstractClass, _: Trait & OpenAbstractClass
47: Pattern Match Exhaustivity: _: SealedTrait & OpenClass & OpenTrait & OpenClassSubclass, _: Trait & OpenClass & OpenTrait & OpenClassSubclass
47: Pattern Match Exhaustivity: _: SealedTrait & OpenClass & (OpenTrait & OpenClassSubclass), _: Trait & OpenClass & (OpenTrait & OpenClassSubclass)
4 changes: 2 additions & 2 deletions tests/patmat/andtype-refinedtype-interaction.check
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
32: Pattern Match Exhaustivity: _: Trait & C1{x: Int}
48: Pattern Match Exhaustivity: _: Clazz & (C1 | C2 | T1){x: Int} & (C3 | C4 | T2){x: Int}, _: Trait & (C1 | C2 | T1){x: Int} & (C3 | C4 | T2){x: Int}
54: Pattern Match Exhaustivity: _: Trait & (C1 | C2 | T1){x: Int} & C3{x: Int}
48: Pattern Match Exhaustivity: _: Clazz & (C1 | (C2 | T1)){x: Int} & (C3 | (C4 | T2)){x: Int}, _: Trait & (C1 | (C2 | T1)){x: Int} & (C3 | (C4 | T2)){x: Int}
54: Pattern Match Exhaustivity: _: Trait & (C1 | (C2 | T1)){x: Int} & C3{x: Int}
65: Pattern Match Exhaustivity: _: Trait & (C1 | C2){x: Int} & (C3 | SubC1){x: Int}
72: Pattern Match Exhaustivity: _: Trait & (T1 & (C1 | SubC2)){x: Int} & (T2 & (C2 | C3 | SubC1)){x: Int} &
SubSubC1{x: Int}
Expand Down