Skip to content

Commit

Permalink
Non lazy implementation
Browse files Browse the repository at this point in the history
Laziness can be costly in Scala, switching to a non-lazy implementation
of the pretty-printer should be more efficient.
  • Loading branch information
satabin committed Feb 25, 2017
1 parent 373f9c1 commit 6144d57
Show file tree
Hide file tree
Showing 4 changed files with 71 additions and 100 deletions.
54 changes: 13 additions & 41 deletions src/main/scala/gnieh/pp/Doc.scala
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ sealed trait Doc {
f(that)

/** Concatenates two documents.
* Is left associative with [[gnieh.pp.empty]] as left and right unit.
* Is right associative with [[gnieh.pp.empty]] as left and right unit.
*/
@inline
def ::(that: Doc): Doc =
Expand Down Expand Up @@ -67,72 +67,44 @@ sealed trait Doc {
def ||(that: Doc) =
align(this :|: that)

/** A flatten (no new lines) version of this document */
val flatten: Doc

}

/** Nest document: new lines are indented by the given indentation.
* @author Lucas Satabin
*/
final case class NestDoc(indent: Int, inner: Doc) extends Doc {
lazy val flatten =
NestDoc(indent, inner.flatten)
}

/** Union document: two variations of the same document.
* '''Note''': The `long`-document's first lines must be longer that the `short`-document's ones.
* @author Lucas Satabin
*/
final case class UnionDoc(long: Doc, short: Doc) extends Doc {
val flatten =
long.flatten
}
final case class NestDoc(indent: Int, inner: Doc) extends Doc

/** Empty document.
* @author Lucas Satabin
*/
case object EmptyDoc extends Doc {
val flatten =
this
}
case object EmptyDoc extends Doc

/** Text document: shall not contain any new lines.
* @author Lucas Satabin
*/
final case class TextDoc(text: String) extends Doc {
val flatten =
this
}
final case class TextDoc(text: String) extends Doc

/** Line document: renders as a new line except if discarded by a group.
* @author Lucas Satabin
*/
final case class LineDoc(repl: Doc) extends Doc {
lazy val flatten =
repl
}
final case class LineDoc(repl: String) extends Doc

/** Cons document: Concatenation of two documents.
* @author Lucas Satabin
*/
final case class ConsDoc(first: Doc, second: Doc) extends Doc {
lazy val flatten =
ConsDoc(first.flatten, second.flatten)
}
final case class ConsDoc(first: Doc, second: Doc) extends Doc

/** Align document: aligns the document on the current column.
* @author Lucas Satabin
*/
final case class AlignDoc(inner: Doc) extends Doc {
lazy val flatten =
AlignDoc(inner.flatten)
}
final case class AlignDoc(inner: Doc) extends Doc

/** Column document: creates a document depending on the current column.
* @author Lucas Satabin
*/
final case class ColumnDoc(f: Int => Doc) extends Doc {
lazy val flatten =
ColumnDoc(f.andThen(_.flatten))
}
final case class ColumnDoc(f: Int => Doc) extends Doc

/** Group document: explicit group encoding
* @author Lucas Satabin
*/
final case class GroupDoc(inner: Doc) extends Doc
76 changes: 30 additions & 46 deletions src/main/scala/gnieh/pp/Renderer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*/
package gnieh.pp

import scala.annotation.tailrec

/** A pretty printer, that tries to make the document fit in the page width
*
* @author Lucas Satabin
Expand All @@ -24,37 +26,25 @@ class PrettyRenderer(width: Int) extends (Doc => SimpleDoc) {
private type Docs = List[(Int, Doc)]

def apply(doc: Doc) =
best(width, 0, List((0, doc)))

private def best(width: Int, column: Int, docs: Docs): SimpleDoc = docs match {
case Nil =>
SEmpty
case (_, EmptyDoc) :: tail =>
best(width, column, tail)
case (i, ConsDoc(first, second)) :: tail =>
best(width, column, (i, first) :: (i, second) :: tail)
case (i, NestDoc(j, inner)) :: tail =>
best(width, column, (i + j, inner) :: tail)
case (i, TextDoc(text)) :: tail =>
SText(text, best(width, column + text.length, tail))
case (i, LineDoc(_)) :: tail =>
SLine(i, best(width, i, tail))
case (i, UnionDoc(l, s)) :: tail =>
better(width, column,
best(width, column, (i, l) :: tail),
best(width, column, (i, s) :: tail))
case (i, AlignDoc(inner)) :: tail =>
best(width, column, (column, inner) :: tail)
case (i, ColumnDoc(f)) :: tail =>
best(width, column, (i, f(column)) :: tail)
}

private def better(width: Int, column: Int, d1: SimpleDoc, d2: => SimpleDoc): SimpleDoc =
if (d1.fits(width - column))
d1
else
// d2 is computed only if needed...
d2
format(0, List((0, Break, doc)))

private def format(column: Int, docs: List[(Int, Mode, Doc)]): SimpleDoc =
docs match {
case (i, m, EmptyDoc) :: docs => format(column, docs)
case (i, m, ConsDoc(d1, d2)) :: docs => format(column, (i, m, d1) :: (i, m, d2) :: docs)
case (i, m, NestDoc(j, d)) :: docs => format(column, (i + j, m, d) :: docs)
case (i, m, TextDoc(s)) :: docs => SText(s, format(column + s.length, docs))
case (i, Flat, LineDoc(s)) :: docs => SText(s, format(column + s.length, docs))
case (i, Break, LineDoc(s)) :: docs => SLine(i, format(i, docs))
case (i, m, AlignDoc(d)) :: docs => format(column, (column, m, d) :: docs)
case (i, m, ColumnDoc(f)) :: docs => format(column, (i, m, f(column)) :: docs)
case (i, m, GroupDoc(d)) :: docs =>
if (fits(width - column, column, (i, Flat, d) :: docs))
format(column, (i, Flat, d) :: docs)
else
format(column, (i, Break, d) :: docs)
case Nil => SEmpty
}

}

Expand All @@ -69,17 +59,15 @@ object CompactRenderer extends (Doc => SimpleDoc) {
scan(0, List(doc))

private def scan(column: Int, docs: List[Doc]): SimpleDoc = docs match {
case Nil => SEmpty
case doc :: docs => doc match {
case EmptyDoc => SEmpty
case TextDoc(text) => SText(text, scan(column + text.length, docs))
case LineDoc(_) => scan(column, doc.flatten :: docs)
case ConsDoc(first, second) => scan(column, first :: second :: docs)
case NestDoc(j, doc) => scan(column, doc :: docs)
case UnionDoc(long, _) => scan(column, long :: docs)
case AlignDoc(inner) => scan(column, inner :: docs)
case ColumnDoc(f) => scan(column, (f(column)) :: docs)
}
case Nil => SEmpty
case EmptyDoc :: docs => scan(column, docs)
case TextDoc(s) :: docs => SText(s, scan(column + s.length, docs))
case LineDoc(s) :: docs => SText(s, scan(column + s.length, docs))
case ConsDoc(d1, d2) :: docs => scan(column, d1 :: d2 :: docs)
case NestDoc(j, d) :: docs => scan(column, d :: docs)
case GroupDoc(d) :: docs => scan(column, d :: docs)
case AlignDoc(d) :: docs => scan(column, d :: docs)
case ColumnDoc(f) :: docs => scan(column, f(column) :: docs)
}

}
Expand All @@ -104,10 +92,6 @@ class TruncateRenderer(max: Int, unit: CountUnit, inner: Doc => SimpleDoc) exten
def apply(doc: Doc) =
truncate(inner(doc))

def apply(doc: SimpleDoc) =
truncate(doc)

/** Truncates the simple document, depending on the constructor criterion. */
def truncate(doc: SimpleDoc): SimpleDoc = {
unit match {
case Lines => firstLines(max, doc)
Expand Down
9 changes: 1 addition & 8 deletions src/main/scala/gnieh/pp/SimpleDoc.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ package gnieh.pp
*/
sealed trait SimpleDoc {

def fits(width: Int): Boolean

def layout: String

override def toString = layout
Expand All @@ -35,8 +33,6 @@ sealed trait SimpleDoc {
* @author Lucas Satabin
*/
case object SEmpty extends SimpleDoc {
def fits(width: Int) =
width >= 0 // always fits if there is enough place

val layout =
""
Expand All @@ -46,8 +42,6 @@ case object SEmpty extends SimpleDoc {
* @author Lucas Satabin
*/
final case class SText(text: String, next: SimpleDoc) extends SimpleDoc {
def fits(width: Int) =
next.fits(width - text.length)

lazy val layout =
text + next.layout
Expand All @@ -58,12 +52,11 @@ final case class SText(text: String, next: SimpleDoc) extends SimpleDoc {
* @author Lucas Satabin
*/
final case class SLine(indent: Int, next: SimpleDoc) extends SimpleDoc {
def fits(width: Int) =
width >= 0 // always fits if there is enough place

lazy val layout =
if (next.layout.isEmpty)
""
else
"\n" + (" " * indent) + next.layout

}
32 changes: 27 additions & 5 deletions src/main/scala/gnieh/pp/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,35 @@
*/
package gnieh

import scala.annotation.tailrec

import scala.collection.TraversableLike

import scala.language.implicitConversions

/** Pretty-printer library based on the Wadler's paper "A Prettier Printer". */
/** Pretty-printer library based on the Wadler's paper "A Prettier Printer" and Lindig's paper "Strictly Pretty". */
package object pp {

sealed trait Mode
case object Flat extends Mode
case object Break extends Mode

@tailrec
protected[pp] def fits(width: Int, column: Int, docs: List[(Int, Mode, Doc)]): Boolean =
if (width <= 0)
false
else docs match {
case (i, m, EmptyDoc) :: docs => fits(width, column, docs)
case (i, m, ConsDoc(d1, d2)) :: docs => fits(width, column, (i, m, d1) :: (i, m, d2) :: docs)
case (i, m, NestDoc(j, d)) :: docs => fits(width, column, (i + j, m, d) :: docs)
case (i, m, TextDoc(s)) :: docs => fits(width - s.length, column, docs)
case (i, Flat, LineDoc(s)) :: docs => fits(width - s.length, column, docs)
case (i, m, GroupDoc(d)) :: docs => fits(width, column, (i, Break, d) :: docs)
case (i, m, AlignDoc(d)) :: docs => fits(width, column, (column, m, d) :: docs)
case (i, m, ColumnDoc(f)) :: docs => fits(width, column, (i, m, f(column)) :: docs)
case _ => true
}

/** Indents the document */
@inline
def nest(indent: Int)(inner: Doc): Doc =
Expand All @@ -39,12 +61,12 @@ package object pp {

/** Renders as a new line unless it is discarded by a group, in which case behaves like `space` */
@inline
val line: Doc = LineDoc(TextDoc(" "))
val line: Doc = LineDoc(" ")

/** Renders as a new line unless it is discarded by a group, in which case behaves like `empty` */
@inline
val linebreak: Doc =
LineDoc(EmptyDoc)
LineDoc("")

/** Behaves like `space` if the result fits in the page, otherwise behaves like `line` */
@inline
Expand All @@ -58,7 +80,7 @@ package object pp {

/** Behaves like a new line unless it is discarded by a group, in which case, behaves like the replacement document */
@inline
def lineOr(replacement: => Doc): Doc =
def lineOr(replacement: => String): Doc =
LineDoc(replacement)

/** Renders as an empty string */
Expand Down Expand Up @@ -131,7 +153,7 @@ package object pp {
/** Discards all line breaks in the given document if the result fits in the page, otherwise, renders without any changes */
@inline
def group(doc: Doc): Doc =
UnionDoc(doc.flatten, doc)
GroupDoc(doc)

/** Renders the document as usual, and then fills until `width` with spaces if necessary */
@inline
Expand Down

0 comments on commit 6144d57

Please sign in to comment.