From f9a66a8c24946d754616341ca1bcf4c825fa9509 Mon Sep 17 00:00:00 2001 From: Lucas Satabin Date: Mon, 21 Oct 2024 14:09:31 +0200 Subject: [PATCH 01/10] Add benchmarks for the json rendering --- .../data/benchmarks/PrinterBenchmarks.scala | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 benchmarks/src/main/scala/fs2/data/benchmarks/PrinterBenchmarks.scala diff --git a/benchmarks/src/main/scala/fs2/data/benchmarks/PrinterBenchmarks.scala b/benchmarks/src/main/scala/fs2/data/benchmarks/PrinterBenchmarks.scala new file mode 100644 index 000000000..67be0df5b --- /dev/null +++ b/benchmarks/src/main/scala/fs2/data/benchmarks/PrinterBenchmarks.scala @@ -0,0 +1,85 @@ +package fs2.data.benchmarks + +import cats.effect.SyncIO +import cats.effect.IO +import fs2.data.json.Token +import fs2.data.json.circe.* +import fs2.{Fallible, Stream} +import io.circe.Json +import org.openjdk.jmh.annotations.* +import org.openjdk.jmh.infra.Blackhole +import cats.effect.unsafe.implicits.global + +import java.util.concurrent.TimeUnit + +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@BenchmarkMode(Array(Mode.AverageTime)) +@State(org.openjdk.jmh.annotations.Scope.Benchmark) +@Fork(value = 1) +@Warmup(iterations = 15, time = 5) +@Measurement(iterations = 10, time = 5) +class PrinterBenchmarks { + + val intArrayStream = + Stream.emits( + Token.StartArray :: + (List + .range(0, 1000000) + .map(i => Token.NumberValue(i.toString())) :+ Token.EndArray)) + + val objectStream = + Stream.emits( + Token.StartObject :: + (List + .range(0, 1000000) + .flatMap(i => List(Token.Key(s"key:$i"), Token.NumberValue(i.toString()))) :+ Token.EndObject)) + + @Benchmark + def intArrayCompact(bh: Blackhole) = + bh.consume( + intArrayStream + .through(fs2.data.json.render.compact) + .compile + .drain) + + @Benchmark + def objectCompact(bh: Blackhole) = + bh.consume( + objectStream + .through(fs2.data.json.render.compact) + .compile + .drain) + + @Benchmark + def intArrayPretty(bh: Blackhole) = + bh.consume( + intArrayStream + .through(fs2.data.json.render.prettyPrint()) + .compile + .drain) + + @Benchmark + def objectPretty(bh: Blackhole) = + bh.consume( + objectStream + .through(fs2.data.json.render.prettyPrint()) + .compile + .drain) + + @Benchmark + def intArrayPrettyLegacy(bh: Blackhole) = + bh.consume( + intArrayStream + .through(fs2.data.json.render.pretty()) + .compile + .drain) + + @Benchmark + def objectPrettyLegacy(bh: Blackhole) = + bh.consume( + objectStream + .through(fs2.data.json.render.pretty()) + .compile + .drain) + +} From b9db552e1de32f6a0292653a2f99f213e8ef09b3 Mon Sep 17 00:00:00 2001 From: Lucas Satabin Date: Mon, 21 Oct 2024 14:41:20 +0200 Subject: [PATCH 02/10] Implement rendering as a `Pull` This avoids many closure and tuple creations, that are costly. --- .../text/render/internal/StreamPrinter.scala | 143 +++++++++++++----- 1 file changed, 105 insertions(+), 38 deletions(-) diff --git a/text/shared/src/main/scala/fs2/data/text/render/internal/StreamPrinter.scala b/text/shared/src/main/scala/fs2/data/text/render/internal/StreamPrinter.scala index 22d01ee92..a90f044ad 100644 --- a/text/shared/src/main/scala/fs2/data/text/render/internal/StreamPrinter.scala +++ b/text/shared/src/main/scala/fs2/data/text/render/internal/StreamPrinter.scala @@ -178,7 +178,7 @@ private[render] class StreamPrinter[F[_], Event](width: Int, indentSize: Int)(im pos, NonEmptyList(aligns.head - 1, aligns.tail), hpl, - indent, + indent - 1, groups) } else { // there is an open group, append the event to the current group @@ -225,42 +225,109 @@ private[render] class StreamPrinter[F[_], Event](width: Int, indentSize: Int)(im } } - def apply(events: Stream[F, Event]): Stream[F, String] = - annotate( - Chunk.empty, - 0, - Stream.suspend(Stream.emit(render.newRenderer())).flatMap(renderer => events.flatMap(renderer.doc(_))), - 0, - NonEmptyList.one(0), - 0, - 0, - Dequeue.empty - ).stream - .mapAccumulate((0, width, NonEmptyList.one(""), 0)) { case (acc @ (fit, hpl, lines, col), evt) => - evt match { - case Annotated.Text(text, _) => ((fit, hpl, lines, col + text.size), Some(text)) - case Annotated.Line(pos) if fit == 0 => ((fit, pos + width, lines, lines.head.size), Some("\n" + lines.head)) - case Annotated.Line(_) => ((fit, hpl, lines, col + 1), Some(" ")) - case Annotated.LineBreak(pos) if fit == 0 => - ((fit, pos + width, lines, lines.head.size), Some("\n" + lines.head)) - case Annotated.LineBreak(_) => (acc, None) - case Annotated.GroupBegin(Position.TooFar) if fit == 0 => ((0, hpl, lines, col), None) - case Annotated.GroupBegin(Position.Small(pos)) if fit == 0 => - ((if (pos <= hpl) 1 else 0, hpl, lines, col), None) - case Annotated.GroupBegin(_) => ((fit + 1, hpl, lines, col), None) - case Annotated.GroupEnd(_) if fit == 0 => (acc, None) - case Annotated.GroupEnd(_) => ((fit - 1, hpl, lines, col), None) - case Annotated.IndentBegin(_) => - ((fit, hpl, NonEmptyList(lines.head + (" " * indentSize), lines.tail), col), None) - case Annotated.IndentEnd(_) => - ((fit, hpl, NonEmptyList(lines.head.drop(indentSize), lines.tail), col), None) - case Annotated.AlignBegin(_) => - ((fit, hpl, (" " * col) :: lines, col), None) - case Annotated.AlignEnd(_) => - ((fit, hpl, NonEmptyList.fromList(lines.tail).getOrElse(NonEmptyList.one("")), col), None) - } - + private def renderAnnotated(chunk: Chunk[Annotated], + chunkSize: Int, + idx: Int, + rest: Stream[F, Annotated], + fit: Int, + hpl: Int, + lines: NonEmptyList[String], + col: Int, + chunkAcc: StringBuilder): Pull[F, String, Unit] = + if (idx >= chunkSize) { + Pull.output1(chunkAcc.result()) >> rest.pull.uncons.flatMap { + case Some((hd, tl)) => + chunkAcc.setLength(0) + renderAnnotated(hd, hd.size, 0, tl, fit, hpl, lines, col, chunkAcc) + case None => + Pull.done + } + } else { + chunk(idx) match { + case Annotated.Text(text, _) => + renderAnnotated(chunk, chunkSize, idx + 1, rest, fit, hpl, lines, col + text.size, chunkAcc.append(text)) + case Annotated.Line(pos) if fit == 0 => + renderAnnotated(chunk, + chunkSize, + idx + 1, + rest, + fit, + pos + width, + lines, + lines.head.size, + chunkAcc.append('\n').append(lines.head)) + case Annotated.Line(_) => + renderAnnotated(chunk, chunkSize, idx + 1, rest, fit, hpl, lines, col + 1, chunkAcc.append(' ')) + case Annotated.LineBreak(pos) if fit == 0 => + renderAnnotated(chunk, + chunkSize, + idx + 1, + rest, + fit, + pos + width, + lines, + lines.head.size, + chunkAcc.append('\n').append(lines.head)) + case Annotated.LineBreak(_) => + renderAnnotated(chunk, chunkSize, idx + 1, rest, fit, hpl, lines, col, chunkAcc) + case Annotated.GroupBegin(Position.TooFar) if fit == 0 => + renderAnnotated(chunk, chunkSize, idx + 1, rest, 0, hpl, lines, col, chunkAcc) + case Annotated.GroupBegin(Position.Small(pos)) if fit == 0 => + renderAnnotated(chunk, chunkSize, idx + 1, rest, if (pos <= hpl) 1 else 0, hpl, lines, col, chunkAcc) + case Annotated.GroupBegin(_) => + renderAnnotated(chunk, chunkSize, idx + 1, rest, fit + 1, hpl, lines, col, chunkAcc) + case Annotated.GroupEnd(_) if fit == 0 => + renderAnnotated(chunk, chunkSize, idx + 1, rest, fit, hpl, lines, col, chunkAcc) + case Annotated.GroupEnd(_) => + renderAnnotated(chunk, chunkSize, idx + 1, rest, fit - 1, hpl, lines, col, chunkAcc) + case Annotated.IndentBegin(_) => + renderAnnotated(chunk, + chunkSize, + idx + 1, + rest, + fit, + hpl, + NonEmptyList(lines.head + (" " * indentSize), lines.tail), + col, + chunkAcc) + case Annotated.IndentEnd(_) => + renderAnnotated(chunk, + chunkSize, + idx + 1, + rest, + fit, + hpl, + NonEmptyList(lines.head.drop(indentSize), lines.tail), + col, + chunkAcc) + case Annotated.AlignBegin(_) => + renderAnnotated(chunk, chunkSize, idx + 1, rest, fit, hpl, (" " * col) :: lines, col, chunkAcc) + case Annotated.AlignEnd(_) => + renderAnnotated(chunk, + chunkSize, + idx + 1, + rest, + fit, + hpl, + NonEmptyList.fromList(lines.tail).getOrElse(NonEmptyList.one("")), + col, + chunkAcc) } - .map(_._2) - .unNone + } + + def apply(events: Stream[F, Event]): Stream[F, String] = { + val annotated = + annotate( + Chunk.empty, + 0, + Stream.suspend(Stream.emit(render.newRenderer())).flatMap(renderer => events.flatMap(renderer.doc(_))), + 0, + NonEmptyList.one(0), + 0, + 0, + Dequeue.empty + ).stream + + renderAnnotated(Chunk.empty, 0, 0, annotated, 0, width, NonEmptyList.one(""), 0, new StringBuilder).stream + } } From 6320b1c987198512ea797350e6cf90edca862e23 Mon Sep 17 00:00:00 2001 From: Lucas Satabin Date: Mon, 21 Oct 2024 14:49:32 +0200 Subject: [PATCH 03/10] Drop unused annotation parameters --- .../data/text/render/internal/Annotated.scala | 16 ++--- .../text/render/internal/StreamPrinter.scala | 72 +++++++++---------- 2 files changed, 41 insertions(+), 47 deletions(-) diff --git a/text/shared/src/main/scala/fs2/data/text/render/internal/Annotated.scala b/text/shared/src/main/scala/fs2/data/text/render/internal/Annotated.scala index 5a81353c5..2fb5ee250 100644 --- a/text/shared/src/main/scala/fs2/data/text/render/internal/Annotated.scala +++ b/text/shared/src/main/scala/fs2/data/text/render/internal/Annotated.scala @@ -18,13 +18,13 @@ package fs2.data.text.render.internal private sealed trait Annotated private object Annotated { - case class Text(text: String, hp: Int) extends Annotated - case class Line(hp: Int) extends Annotated - case class LineBreak(hp: Int) extends Annotated + case class Text(text: String) extends Annotated + case class Line(pos: Int) extends Annotated + case class LineBreak(pos: Int) extends Annotated case class GroupBegin(hpl: Position) extends Annotated - case class GroupEnd(hp: Int) extends Annotated - case class IndentBegin(hp: Int) extends Annotated - case class IndentEnd(hp: Int) extends Annotated - case class AlignBegin(hp: Int) extends Annotated - case class AlignEnd(hp: Int) extends Annotated + case object GroupEnd extends Annotated + case object IndentBegin extends Annotated + case object IndentEnd extends Annotated + case object AlignBegin extends Annotated + case object AlignEnd extends Annotated } diff --git a/text/shared/src/main/scala/fs2/data/text/render/internal/StreamPrinter.scala b/text/shared/src/main/scala/fs2/data/text/render/internal/StreamPrinter.scala index a90f044ad..b1233afc8 100644 --- a/text/shared/src/main/scala/fs2/data/text/render/internal/StreamPrinter.scala +++ b/text/shared/src/main/scala/fs2/data/text/render/internal/StreamPrinter.scala @@ -84,10 +84,10 @@ private[render] class StreamPrinter[F[_], Event](width: Int, indentSize: Int)(im if (groups.isEmpty) { // no open group we can emit immediately Pull - .output1(Annotated.Text(text, pos1)) >> annotate(chunk, idx + 1, rest, pos1, aligns, hpl, indent, groups) + .output1(Annotated.Text(text)) >> annotate(chunk, idx + 1, rest, pos1, aligns, hpl, indent, groups) } else { // there is an open group, append the event to the current group - check(hpl, indent, push(groups, Annotated.Text(text, pos1)), pos1).flatMap { case (hpl, gindent, groups) => + check(hpl, indent, push(groups, Annotated.Text(text)), pos1).flatMap { case (hpl, gindent, groups) => annotate(chunk, idx + 1, rest, pos1, aligns, hpl, gindent, groups) } } @@ -142,7 +142,7 @@ private[render] class StreamPrinter[F[_], Event](width: Int, indentSize: Int)(im case Some(((hpl, indent, group), groups)) => // closing a group, pop it from the buffer dequeue, and continue - pop(groups, group.prepend(Annotated.GroupBegin(Position.Small(pos))).append(Annotated.GroupEnd(pos))) + pop(groups, group.prepend(Annotated.GroupBegin(Position.Small(pos))).append(Annotated.GroupEnd)) .flatMap { groups => annotate(chunk, idx + 1, rest, pos, aligns, hpl, indent, groups) } @@ -153,17 +153,17 @@ private[render] class StreamPrinter[F[_], Event](width: Int, indentSize: Int)(im // increment the current indentation level if (groups.isEmpty) { // no open group we can emit immediately a new line - Pull.output1(Annotated.IndentBegin(pos)) >> annotate(chunk, - idx + 1, - rest, - pos, - NonEmptyList(aligns.head + 1, aligns.tail), - hpl, - indent + 1, - groups) + Pull.output1(Annotated.IndentBegin) >> annotate(chunk, + idx + 1, + rest, + pos, + NonEmptyList(aligns.head + 1, aligns.tail), + hpl, + indent + 1, + groups) } else { // there is an open group, append the event to the current group - check(hpl, indent, push(groups, Annotated.IndentBegin(pos)), pos).flatMap { case (hpl, indent, groups) => + check(hpl, indent, push(groups, Annotated.IndentBegin), pos).flatMap { case (hpl, indent, groups) => annotate(chunk, idx + 1, rest, pos, NonEmptyList(aligns.head + 1, aligns.tail), hpl, indent, groups) } } @@ -172,17 +172,17 @@ private[render] class StreamPrinter[F[_], Event](width: Int, indentSize: Int)(im // decrement the current indentation level if (groups.isEmpty) { // no open group we can emit immediately a new line - Pull.output1(Annotated.IndentEnd(pos)) >> annotate(chunk, - idx + 1, - rest, - pos, - NonEmptyList(aligns.head - 1, aligns.tail), - hpl, - indent - 1, - groups) + Pull.output1(Annotated.IndentEnd) >> annotate(chunk, + idx + 1, + rest, + pos, + NonEmptyList(aligns.head - 1, aligns.tail), + hpl, + indent - 1, + groups) } else { // there is an open group, append the event to the current group - check(hpl, indent, push(groups, Annotated.IndentEnd(pos)), pos).flatMap { case (hpl, indent, groups) => + check(hpl, indent, push(groups, Annotated.IndentEnd), pos).flatMap { case (hpl, indent, groups) => annotate(chunk, idx + 1, rest, pos, NonEmptyList(aligns.head - 1, aligns.tail), hpl, indent, groups) } } @@ -191,17 +191,11 @@ private[render] class StreamPrinter[F[_], Event](width: Int, indentSize: Int)(im // push new indentation level if (groups.isEmpty) { // no open group we can emit immediately a new line - Pull.output1(Annotated.AlignBegin(pos)) >> annotate(chunk, - idx + 1, - rest, - pos, - pos :: aligns, - hpl, - indent, - groups) + Pull + .output1(Annotated.AlignBegin) >> annotate(chunk, idx + 1, rest, pos, pos :: aligns, hpl, indent, groups) } else { // there is an open group, append the event to the current group - check(hpl, indent, push(groups, Annotated.AlignBegin(pos)), pos).flatMap { case (hpl, indent, groups) => + check(hpl, indent, push(groups, Annotated.AlignBegin), pos).flatMap { case (hpl, indent, groups) => annotate(chunk, idx + 1, rest, pos, pos :: aligns, hpl, indent, groups) } } @@ -215,10 +209,10 @@ private[render] class StreamPrinter[F[_], Event](width: Int, indentSize: Int)(im } if (groups.isEmpty) { // no open group we can emit immediately a new line - Pull.output1(Annotated.AlignEnd(pos)) >> annotate(chunk, idx + 1, rest, pos, aligns1, hpl, indent, groups) + Pull.output1(Annotated.AlignEnd) >> annotate(chunk, idx + 1, rest, pos, aligns1, hpl, indent, groups) } else { // there is an open group, append the event to the current group - check(hpl, indent, push(groups, Annotated.AlignEnd(pos)), pos).flatMap { case (hpl, indent, groups) => + check(hpl, indent, push(groups, Annotated.AlignEnd), pos).flatMap { case (hpl, indent, groups) => annotate(chunk, idx + 1, rest, pos, aligns1, hpl, indent, groups) } } @@ -244,7 +238,7 @@ private[render] class StreamPrinter[F[_], Event](width: Int, indentSize: Int)(im } } else { chunk(idx) match { - case Annotated.Text(text, _) => + case Annotated.Text(text) => renderAnnotated(chunk, chunkSize, idx + 1, rest, fit, hpl, lines, col + text.size, chunkAcc.append(text)) case Annotated.Line(pos) if fit == 0 => renderAnnotated(chunk, @@ -276,11 +270,11 @@ private[render] class StreamPrinter[F[_], Event](width: Int, indentSize: Int)(im renderAnnotated(chunk, chunkSize, idx + 1, rest, if (pos <= hpl) 1 else 0, hpl, lines, col, chunkAcc) case Annotated.GroupBegin(_) => renderAnnotated(chunk, chunkSize, idx + 1, rest, fit + 1, hpl, lines, col, chunkAcc) - case Annotated.GroupEnd(_) if fit == 0 => + case Annotated.GroupEnd if fit == 0 => renderAnnotated(chunk, chunkSize, idx + 1, rest, fit, hpl, lines, col, chunkAcc) - case Annotated.GroupEnd(_) => + case Annotated.GroupEnd => renderAnnotated(chunk, chunkSize, idx + 1, rest, fit - 1, hpl, lines, col, chunkAcc) - case Annotated.IndentBegin(_) => + case Annotated.IndentBegin => renderAnnotated(chunk, chunkSize, idx + 1, @@ -290,7 +284,7 @@ private[render] class StreamPrinter[F[_], Event](width: Int, indentSize: Int)(im NonEmptyList(lines.head + (" " * indentSize), lines.tail), col, chunkAcc) - case Annotated.IndentEnd(_) => + case Annotated.IndentEnd => renderAnnotated(chunk, chunkSize, idx + 1, @@ -300,9 +294,9 @@ private[render] class StreamPrinter[F[_], Event](width: Int, indentSize: Int)(im NonEmptyList(lines.head.drop(indentSize), lines.tail), col, chunkAcc) - case Annotated.AlignBegin(_) => + case Annotated.AlignBegin => renderAnnotated(chunk, chunkSize, idx + 1, rest, fit, hpl, (" " * col) :: lines, col, chunkAcc) - case Annotated.AlignEnd(_) => + case Annotated.AlignEnd => renderAnnotated(chunk, chunkSize, idx + 1, From eb349a1a68129871b2c62edd1cc0d0cd3dd47c71 Mon Sep 17 00:00:00 2001 From: Lucas Satabin Date: Mon, 21 Oct 2024 15:07:46 +0200 Subject: [PATCH 04/10] Use a dedicated mutable internal context for rendering --- .../text/render/internal/StreamPrinter.scala | 106 +++++++----------- 1 file changed, 40 insertions(+), 66 deletions(-) diff --git a/text/shared/src/main/scala/fs2/data/text/render/internal/StreamPrinter.scala b/text/shared/src/main/scala/fs2/data/text/render/internal/StreamPrinter.scala index b1233afc8..705a323ef 100644 --- a/text/shared/src/main/scala/fs2/data/text/render/internal/StreamPrinter.scala +++ b/text/shared/src/main/scala/fs2/data/text/render/internal/StreamPrinter.scala @@ -21,6 +21,8 @@ import cats.collections.Dequeue import cats.data.{Chain, NonEmptyList} import fs2.{Chunk, Pipe, Pull, Stream} +private class RenderingContext(var fit: Int, var hpl: Int, var lines: NonEmptyList[String], var col: Int) + private[render] class StreamPrinter[F[_], Event](width: Int, indentSize: Int)(implicit render: Renderable[Event]) extends Pipe[F, Event, String] { @@ -223,89 +225,59 @@ private[render] class StreamPrinter[F[_], Event](width: Int, indentSize: Int)(im chunkSize: Int, idx: Int, rest: Stream[F, Annotated], - fit: Int, - hpl: Int, - lines: NonEmptyList[String], - col: Int, + ctx: RenderingContext, chunkAcc: StringBuilder): Pull[F, String, Unit] = if (idx >= chunkSize) { Pull.output1(chunkAcc.result()) >> rest.pull.uncons.flatMap { case Some((hd, tl)) => chunkAcc.setLength(0) - renderAnnotated(hd, hd.size, 0, tl, fit, hpl, lines, col, chunkAcc) + renderAnnotated(hd, hd.size, 0, tl, ctx, chunkAcc) case None => Pull.done } } else { chunk(idx) match { case Annotated.Text(text) => - renderAnnotated(chunk, chunkSize, idx + 1, rest, fit, hpl, lines, col + text.size, chunkAcc.append(text)) - case Annotated.Line(pos) if fit == 0 => - renderAnnotated(chunk, - chunkSize, - idx + 1, - rest, - fit, - pos + width, - lines, - lines.head.size, - chunkAcc.append('\n').append(lines.head)) + ctx.col += text.size + renderAnnotated(chunk, chunkSize, idx + 1, rest, ctx, chunkAcc.append(text)) + case Annotated.Line(pos) if ctx.fit == 0 => + ctx.hpl = pos + width + ctx.col = ctx.lines.head.size + renderAnnotated(chunk, chunkSize, idx + 1, rest, ctx, chunkAcc.append('\n').append(ctx.lines.head)) case Annotated.Line(_) => - renderAnnotated(chunk, chunkSize, idx + 1, rest, fit, hpl, lines, col + 1, chunkAcc.append(' ')) - case Annotated.LineBreak(pos) if fit == 0 => - renderAnnotated(chunk, - chunkSize, - idx + 1, - rest, - fit, - pos + width, - lines, - lines.head.size, - chunkAcc.append('\n').append(lines.head)) + ctx.col += 1 + renderAnnotated(chunk, chunkSize, idx + 1, rest, ctx, chunkAcc.append(' ')) + case Annotated.LineBreak(pos) if ctx.fit == 0 => + ctx.hpl = pos + width + ctx.col = ctx.lines.head.size + renderAnnotated(chunk, chunkSize, idx + 1, rest, ctx, chunkAcc.append('\n').append(ctx.lines.head)) case Annotated.LineBreak(_) => - renderAnnotated(chunk, chunkSize, idx + 1, rest, fit, hpl, lines, col, chunkAcc) - case Annotated.GroupBegin(Position.TooFar) if fit == 0 => - renderAnnotated(chunk, chunkSize, idx + 1, rest, 0, hpl, lines, col, chunkAcc) - case Annotated.GroupBegin(Position.Small(pos)) if fit == 0 => - renderAnnotated(chunk, chunkSize, idx + 1, rest, if (pos <= hpl) 1 else 0, hpl, lines, col, chunkAcc) + renderAnnotated(chunk, chunkSize, idx + 1, rest, ctx, chunkAcc) + case Annotated.GroupBegin(Position.TooFar) if ctx.fit == 0 => + renderAnnotated(chunk, chunkSize, idx + 1, rest, ctx, chunkAcc) + case Annotated.GroupBegin(Position.Small(pos)) if ctx.fit == 0 => + ctx.fit = if (pos <= ctx.hpl) 1 else 0 + renderAnnotated(chunk, chunkSize, idx + 1, rest, ctx, chunkAcc) case Annotated.GroupBegin(_) => - renderAnnotated(chunk, chunkSize, idx + 1, rest, fit + 1, hpl, lines, col, chunkAcc) - case Annotated.GroupEnd if fit == 0 => - renderAnnotated(chunk, chunkSize, idx + 1, rest, fit, hpl, lines, col, chunkAcc) + ctx.fit += 1 + renderAnnotated(chunk, chunkSize, idx + 1, rest, ctx, chunkAcc) + case Annotated.GroupEnd if ctx.fit == 0 => + renderAnnotated(chunk, chunkSize, idx + 1, rest, ctx, chunkAcc) case Annotated.GroupEnd => - renderAnnotated(chunk, chunkSize, idx + 1, rest, fit - 1, hpl, lines, col, chunkAcc) + ctx.fit -= 1 + renderAnnotated(chunk, chunkSize, idx + 1, rest, ctx, chunkAcc) case Annotated.IndentBegin => - renderAnnotated(chunk, - chunkSize, - idx + 1, - rest, - fit, - hpl, - NonEmptyList(lines.head + (" " * indentSize), lines.tail), - col, - chunkAcc) + ctx.lines = NonEmptyList(ctx.lines.head + (" " * indentSize), ctx.lines.tail) + renderAnnotated(chunk, chunkSize, idx + 1, rest, ctx, chunkAcc) case Annotated.IndentEnd => - renderAnnotated(chunk, - chunkSize, - idx + 1, - rest, - fit, - hpl, - NonEmptyList(lines.head.drop(indentSize), lines.tail), - col, - chunkAcc) + ctx.lines = NonEmptyList(ctx.lines.head.drop(indentSize), ctx.lines.tail) + renderAnnotated(chunk, chunkSize, idx + 1, rest, ctx, chunkAcc) case Annotated.AlignBegin => - renderAnnotated(chunk, chunkSize, idx + 1, rest, fit, hpl, (" " * col) :: lines, col, chunkAcc) + ctx.lines = (" " * ctx.col) :: ctx.lines + renderAnnotated(chunk, chunkSize, idx + 1, rest, ctx, chunkAcc) case Annotated.AlignEnd => - renderAnnotated(chunk, - chunkSize, - idx + 1, - rest, - fit, - hpl, - NonEmptyList.fromList(lines.tail).getOrElse(NonEmptyList.one("")), - col, - chunkAcc) + ctx.lines = NonEmptyList.fromList(ctx.lines.tail).getOrElse(NonEmptyList.one("")) + renderAnnotated(chunk, chunkSize, idx + 1, rest, ctx, chunkAcc) } } @@ -320,8 +292,10 @@ private[render] class StreamPrinter[F[_], Event](width: Int, indentSize: Int)(im 0, 0, Dequeue.empty - ).stream + ).streamNoScope - renderAnnotated(Chunk.empty, 0, 0, annotated, 0, width, NonEmptyList.one(""), 0, new StringBuilder).stream + Stream + .suspend(Stream.emit(new RenderingContext(0, width, NonEmptyList.one(""), 0))) + .flatMap(renderAnnotated(Chunk.empty, 0, 0, annotated, _, new StringBuilder).stream) } } From fe79117f44059fee9a3bb3e97e3234cea6434eb9 Mon Sep 17 00:00:00 2001 From: Lucas Satabin Date: Mon, 21 Oct 2024 15:28:22 +0200 Subject: [PATCH 05/10] Prepare steps for fusing --- .../text/render/internal/StreamPrinter.scala | 204 +++++++++++++----- 1 file changed, 145 insertions(+), 59 deletions(-) diff --git a/text/shared/src/main/scala/fs2/data/text/render/internal/StreamPrinter.scala b/text/shared/src/main/scala/fs2/data/text/render/internal/StreamPrinter.scala index 705a323ef..a56c9ac66 100644 --- a/text/shared/src/main/scala/fs2/data/text/render/internal/StreamPrinter.scala +++ b/text/shared/src/main/scala/fs2/data/text/render/internal/StreamPrinter.scala @@ -65,6 +65,7 @@ private[render] class StreamPrinter[F[_], Event](width: Int, indentSize: Int)(im } private def annotate(chunk: Chunk[DocEvent], + chunkSize: Int, idx: Int, rest: Stream[F, DocEvent], pos: Int, @@ -72,9 +73,9 @@ private[render] class StreamPrinter[F[_], Event](width: Int, indentSize: Int)(im hpl: Int, indent: Int, groups: Dequeue[(Int, Int, Chain[Annotated])]): Pull[F, Annotated, Unit] = - if (idx >= chunk.size) { + if (idx >= chunkSize) { rest.pull.uncons.flatMap { - case Some((hd, tl)) => annotate(hd, 0, tl, pos, aligns, hpl, indent, groups) + case Some((hd, tl)) => annotate(hd, hd.size, 0, tl, pos, aligns, hpl, indent, groups) case None => Pull.done } } else { @@ -86,11 +87,19 @@ private[render] class StreamPrinter[F[_], Event](width: Int, indentSize: Int)(im if (groups.isEmpty) { // no open group we can emit immediately Pull - .output1(Annotated.Text(text)) >> annotate(chunk, idx + 1, rest, pos1, aligns, hpl, indent, groups) + .output1(Annotated.Text(text)) >> annotate(chunk, + chunkSize, + idx + 1, + rest, + pos1, + aligns, + hpl, + indent, + groups) } else { // there is an open group, append the event to the current group check(hpl, indent, push(groups, Annotated.Text(text)), pos1).flatMap { case (hpl, gindent, groups) => - annotate(chunk, idx + 1, rest, pos1, aligns, hpl, gindent, groups) + annotate(chunk, chunkSize, idx + 1, rest, pos1, aligns, hpl, gindent, groups) } } @@ -98,22 +107,38 @@ private[render] class StreamPrinter[F[_], Event](width: Int, indentSize: Int)(im if (groups.isEmpty) { // no open group we can emit immediately a new line Pull - .output1(Annotated.Line(pos + 1)) >> annotate(chunk, idx + 1, rest, pos + 1, aligns, hpl, indent, groups) + .output1(Annotated.Line(pos + 1)) >> annotate(chunk, + chunkSize, + idx + 1, + rest, + pos + 1, + aligns, + hpl, + indent, + groups) } else { // there is an open group, append the event to the current group check(hpl, indent, push(groups, Annotated.Line(pos + 1)), pos + 1).flatMap { case (hpl, indent, groups) => - annotate(chunk, idx + 1, rest, pos + 1, aligns, hpl, indent, groups) + annotate(chunk, chunkSize, idx + 1, rest, pos + 1, aligns, hpl, indent, groups) } } case DocEvent.LineBreak => if (groups.isEmpty) { // no open group we can emit immediately a new line - Pull.output1(Annotated.LineBreak(pos)) >> annotate(chunk, idx + 1, rest, pos, aligns, hpl, indent, groups) + Pull.output1(Annotated.LineBreak(pos)) >> annotate(chunk, + chunkSize, + idx + 1, + rest, + pos, + aligns, + hpl, + indent, + groups) } else { // there is an open group, append the event to the current group check(hpl, indent, push(groups, Annotated.LineBreak(pos)), pos).flatMap { case (hpl, indent, groups) => - annotate(chunk, idx + 1, rest, pos, aligns, hpl, indent, groups) + annotate(chunk, chunkSize, idx + 1, rest, pos, aligns, hpl, indent, groups) } } @@ -122,6 +147,7 @@ private[render] class StreamPrinter[F[_], Event](width: Int, indentSize: Int)(im if (groups.isEmpty) // this is the top-level group, turn on the buffer mechanism annotate(chunk, + chunkSize, idx + 1, rest, pos, @@ -133,20 +159,20 @@ private[render] class StreamPrinter[F[_], Event](width: Int, indentSize: Int)(im // starting a new group, puts a new empty buffer in the group dequeue, and check for overflow check(hpl, indent, groups.snoc((hpl1, aligns.head, Chain.empty)), pos).flatMap { case (hpl, indent, groups) => - annotate(chunk, idx + 1, rest, pos, aligns, hpl, indent, groups) + annotate(chunk, chunkSize, idx + 1, rest, pos, aligns, hpl, indent, groups) } case DocEvent.GroupEnd => groups.unsnoc match { case None => // closing unknown group, just ignore it - annotate(chunk, idx + 1, rest, pos, aligns, hpl, indent, groups) + annotate(chunk, chunkSize, idx + 1, rest, pos, aligns, hpl, indent, groups) - case Some(((hpl, indent, group), groups)) => + case Some(((newhpl, newindent, group), groups)) => // closing a group, pop it from the buffer dequeue, and continue pop(groups, group.prepend(Annotated.GroupBegin(Position.Small(pos))).append(Annotated.GroupEnd)) .flatMap { groups => - annotate(chunk, idx + 1, rest, pos, aligns, hpl, indent, groups) + annotate(chunk, chunkSize, idx + 1, rest, pos, aligns, newhpl, newindent, groups) } } @@ -156,6 +182,7 @@ private[render] class StreamPrinter[F[_], Event](width: Int, indentSize: Int)(im if (groups.isEmpty) { // no open group we can emit immediately a new line Pull.output1(Annotated.IndentBegin) >> annotate(chunk, + chunkSize, idx + 1, rest, pos, @@ -166,7 +193,15 @@ private[render] class StreamPrinter[F[_], Event](width: Int, indentSize: Int)(im } else { // there is an open group, append the event to the current group check(hpl, indent, push(groups, Annotated.IndentBegin), pos).flatMap { case (hpl, indent, groups) => - annotate(chunk, idx + 1, rest, pos, NonEmptyList(aligns.head + 1, aligns.tail), hpl, indent, groups) + annotate(chunk, + chunkSize, + idx + 1, + rest, + pos, + NonEmptyList(aligns.head + 1, aligns.tail), + hpl, + indent, + groups) } } @@ -175,6 +210,7 @@ private[render] class StreamPrinter[F[_], Event](width: Int, indentSize: Int)(im if (groups.isEmpty) { // no open group we can emit immediately a new line Pull.output1(Annotated.IndentEnd) >> annotate(chunk, + chunkSize, idx + 1, rest, pos, @@ -185,7 +221,15 @@ private[render] class StreamPrinter[F[_], Event](width: Int, indentSize: Int)(im } else { // there is an open group, append the event to the current group check(hpl, indent, push(groups, Annotated.IndentEnd), pos).flatMap { case (hpl, indent, groups) => - annotate(chunk, idx + 1, rest, pos, NonEmptyList(aligns.head - 1, aligns.tail), hpl, indent, groups) + annotate(chunk, + chunkSize, + idx + 1, + rest, + pos, + NonEmptyList(aligns.head - 1, aligns.tail), + hpl, + indent, + groups) } } @@ -194,11 +238,19 @@ private[render] class StreamPrinter[F[_], Event](width: Int, indentSize: Int)(im if (groups.isEmpty) { // no open group we can emit immediately a new line Pull - .output1(Annotated.AlignBegin) >> annotate(chunk, idx + 1, rest, pos, pos :: aligns, hpl, indent, groups) + .output1(Annotated.AlignBegin) >> annotate(chunk, + chunkSize, + idx + 1, + rest, + pos, + pos :: aligns, + hpl, + indent, + groups) } else { // there is an open group, append the event to the current group check(hpl, indent, push(groups, Annotated.AlignBegin), pos).flatMap { case (hpl, indent, groups) => - annotate(chunk, idx + 1, rest, pos, pos :: aligns, hpl, indent, groups) + annotate(chunk, chunkSize, idx + 1, rest, pos, pos :: aligns, hpl, indent, groups) } } @@ -211,16 +263,80 @@ private[render] class StreamPrinter[F[_], Event](width: Int, indentSize: Int)(im } if (groups.isEmpty) { // no open group we can emit immediately a new line - Pull.output1(Annotated.AlignEnd) >> annotate(chunk, idx + 1, rest, pos, aligns1, hpl, indent, groups) + Pull.output1(Annotated.AlignEnd) >> annotate(chunk, + chunkSize, + idx + 1, + rest, + pos, + aligns1, + hpl, + indent, + groups) } else { // there is an open group, append the event to the current group check(hpl, indent, push(groups, Annotated.AlignEnd), pos).flatMap { case (hpl, indent, groups) => - annotate(chunk, idx + 1, rest, pos, aligns1, hpl, indent, groups) + annotate(chunk, chunkSize, idx + 1, rest, pos, aligns1, hpl, indent, groups) } } } } + // rendering + + private def renderText(text: String, ctx: RenderingContext, chunkAcc: StringBuilder): Unit = { + ctx.col += text.size + chunkAcc.append(text) + } + + private def renderLine(pos: Int, ctx: RenderingContext, chunkAcc: StringBuilder): Unit = if (ctx.fit == 0) { + ctx.hpl = pos + width + ctx.col = ctx.lines.head.size + chunkAcc.append('\n').append(ctx.lines.head): Unit + } else { + ctx.col += 1 + chunkAcc.append(' ') + } + + private def renderLineBreak(pos: Int, ctx: RenderingContext, chunkAcc: StringBuilder): Unit = + if (ctx.fit == 0) { + ctx.hpl = pos + width + ctx.col = ctx.lines.head.size + chunkAcc.append('\n').append(ctx.lines.head): Unit + } + + private def renderGroupBegin(pos: Position, ctx: RenderingContext): Unit = + if (ctx.fit == 0) { + pos match { + case Position.TooFar => + // too far, do nothing + case Position.Small(pos) => + ctx.fit = if (pos <= ctx.hpl) 1 else 0 + } + } else { + ctx.fit += 1 + } + + private def renderGroupEnd(ctx: RenderingContext): Unit = + if (ctx.fit > 0) { + ctx.fit -= 1 + } + + private def renderIndentBegin(ctx: RenderingContext): Unit = { + ctx.lines = NonEmptyList(ctx.lines.head + (" " * indentSize), ctx.lines.tail) + } + + private def renderIndentEnd(ctx: RenderingContext): Unit = { + ctx.lines = NonEmptyList(ctx.lines.head.drop(indentSize), ctx.lines.tail) + } + + private def renderAlignBegin(ctx: RenderingContext): Unit = { + ctx.lines = (" " * ctx.col) :: ctx.lines + } + + private def renderAlignEnd(ctx: RenderingContext): Unit = { + ctx.lines = NonEmptyList.fromList(ctx.lines.tail).getOrElse(NonEmptyList.one("")) + } + private def renderAnnotated(chunk: Chunk[Annotated], chunkSize: Int, idx: Int, @@ -237,48 +353,17 @@ private[render] class StreamPrinter[F[_], Event](width: Int, indentSize: Int)(im } } else { chunk(idx) match { - case Annotated.Text(text) => - ctx.col += text.size - renderAnnotated(chunk, chunkSize, idx + 1, rest, ctx, chunkAcc.append(text)) - case Annotated.Line(pos) if ctx.fit == 0 => - ctx.hpl = pos + width - ctx.col = ctx.lines.head.size - renderAnnotated(chunk, chunkSize, idx + 1, rest, ctx, chunkAcc.append('\n').append(ctx.lines.head)) - case Annotated.Line(_) => - ctx.col += 1 - renderAnnotated(chunk, chunkSize, idx + 1, rest, ctx, chunkAcc.append(' ')) - case Annotated.LineBreak(pos) if ctx.fit == 0 => - ctx.hpl = pos + width - ctx.col = ctx.lines.head.size - renderAnnotated(chunk, chunkSize, idx + 1, rest, ctx, chunkAcc.append('\n').append(ctx.lines.head)) - case Annotated.LineBreak(_) => - renderAnnotated(chunk, chunkSize, idx + 1, rest, ctx, chunkAcc) - case Annotated.GroupBegin(Position.TooFar) if ctx.fit == 0 => - renderAnnotated(chunk, chunkSize, idx + 1, rest, ctx, chunkAcc) - case Annotated.GroupBegin(Position.Small(pos)) if ctx.fit == 0 => - ctx.fit = if (pos <= ctx.hpl) 1 else 0 - renderAnnotated(chunk, chunkSize, idx + 1, rest, ctx, chunkAcc) - case Annotated.GroupBegin(_) => - ctx.fit += 1 - renderAnnotated(chunk, chunkSize, idx + 1, rest, ctx, chunkAcc) - case Annotated.GroupEnd if ctx.fit == 0 => - renderAnnotated(chunk, chunkSize, idx + 1, rest, ctx, chunkAcc) - case Annotated.GroupEnd => - ctx.fit -= 1 - renderAnnotated(chunk, chunkSize, idx + 1, rest, ctx, chunkAcc) - case Annotated.IndentBegin => - ctx.lines = NonEmptyList(ctx.lines.head + (" " * indentSize), ctx.lines.tail) - renderAnnotated(chunk, chunkSize, idx + 1, rest, ctx, chunkAcc) - case Annotated.IndentEnd => - ctx.lines = NonEmptyList(ctx.lines.head.drop(indentSize), ctx.lines.tail) - renderAnnotated(chunk, chunkSize, idx + 1, rest, ctx, chunkAcc) - case Annotated.AlignBegin => - ctx.lines = (" " * ctx.col) :: ctx.lines - renderAnnotated(chunk, chunkSize, idx + 1, rest, ctx, chunkAcc) - case Annotated.AlignEnd => - ctx.lines = NonEmptyList.fromList(ctx.lines.tail).getOrElse(NonEmptyList.one("")) - renderAnnotated(chunk, chunkSize, idx + 1, rest, ctx, chunkAcc) + case Annotated.Text(text) => renderText(text, ctx, chunkAcc) + case Annotated.Line(pos) => renderLine(pos, ctx, chunkAcc) + case Annotated.LineBreak(pos) => renderLineBreak(pos, ctx, chunkAcc) + case Annotated.GroupBegin(pos) => renderGroupBegin(pos, ctx) + case Annotated.GroupEnd => renderGroupEnd(ctx) + case Annotated.IndentBegin => renderIndentBegin(ctx) + case Annotated.IndentEnd => renderIndentEnd(ctx) + case Annotated.AlignBegin => renderAlignBegin(ctx) + case Annotated.AlignEnd => renderAlignEnd(ctx) } + renderAnnotated(chunk, chunkSize, idx + 1, rest, ctx, chunkAcc) } def apply(events: Stream[F, Event]): Stream[F, String] = { @@ -286,6 +371,7 @@ private[render] class StreamPrinter[F[_], Event](width: Int, indentSize: Int)(im annotate( Chunk.empty, 0, + 0, Stream.suspend(Stream.emit(render.newRenderer())).flatMap(renderer => events.flatMap(renderer.doc(_))), 0, NonEmptyList.one(0), From 0faf8766f93ebaa433399ecad44644c8490c01d7 Mon Sep 17 00:00:00 2001 From: Lucas Satabin Date: Mon, 21 Oct 2024 16:11:18 +0200 Subject: [PATCH 06/10] Fuse annotation and rendering phases --- .../text/render/internal/StreamPrinter.scala | 343 +++++++----------- 1 file changed, 126 insertions(+), 217 deletions(-) diff --git a/text/shared/src/main/scala/fs2/data/text/render/internal/StreamPrinter.scala b/text/shared/src/main/scala/fs2/data/text/render/internal/StreamPrinter.scala index a56c9ac66..3067d68bb 100644 --- a/text/shared/src/main/scala/fs2/data/text/render/internal/StreamPrinter.scala +++ b/text/shared/src/main/scala/fs2/data/text/render/internal/StreamPrinter.scala @@ -21,264 +21,194 @@ import cats.collections.Dequeue import cats.data.{Chain, NonEmptyList} import fs2.{Chunk, Pipe, Pull, Stream} +private class AnnotationContext(var pos: Int, + var aligns: NonEmptyList[Int], + var hpl: Int, + var indent: Int, + var groups: Dequeue[(Int, Int, Chain[Annotated])]) private class RenderingContext(var fit: Int, var hpl: Int, var lines: NonEmptyList[String], var col: Int) private[render] class StreamPrinter[F[_], Event](width: Int, indentSize: Int)(implicit render: Renderable[Event]) extends Pipe[F, Event, String] { - private val emptyGroups = (0, 0, Dequeue.empty[(Int, Int, Chain[Annotated])]) - - private def push(groups: Dequeue[(Int, Int, Chain[Annotated])], - evt: Annotated): Dequeue[(Int, Int, Chain[Annotated])] = - groups.unsnoc match { - case Some(((ghpl, gindent, group), groups)) => groups.snoc((ghpl, gindent, group.append(evt))) - case None => Dequeue.empty // should never happen + private def push(annctx: AnnotationContext, evt: Annotated): Unit = + annctx.groups.unsnoc match { + case Some(((ghpl, gindent, group), groups)) => annctx.groups = groups.snoc((ghpl, gindent, group.append(evt))) + case None => // should never happen } - private def pop(groups: Dequeue[(Int, Int, Chain[Annotated])], - buffer: Chain[Annotated]): Pull[F, Annotated, (Dequeue[(Int, Int, Chain[Annotated])])] = - groups.unsnoc match { + private def pop(buffer: Chain[Annotated], + annctx: AnnotationContext, + rctx: RenderingContext, + chunkAcc: StringBuilder): Unit = + annctx.groups.unsnoc match { case Some(((ghpl, gindent, group), groups)) => - Pull.pure((groups.snoc((ghpl, gindent, group.concat(buffer))))) + annctx.groups = groups.snoc((ghpl, gindent, group.concat(buffer))) case None => - Pull.output(Chunk.chain(buffer)).as(Dequeue.empty) + annctx.groups = Dequeue.empty + buffer.iterator.foreach(renderAnnotated(_, rctx, chunkAcc)) } - private def check(hpl: Int, - indent: Int, - groups: Dequeue[(Int, Int, Chain[Annotated])], - ghpl: Int): Pull[F, Annotated, (Int, Int, Dequeue[(Int, Int, Chain[Annotated])])] = - if (ghpl <= hpl - (indent * indentSize) && groups.size <= width - (indent * indentSize)) { + private def check(annctx: AnnotationContext, rctx: RenderingContext, chunkAcc: StringBuilder): Unit = + if (annctx.pos <= annctx.hpl - (annctx.indent * indentSize) && annctx.groups.size <= width - (annctx.indent * indentSize)) { // groups still fits - Pull.pure((hpl, indent, groups)) } else { // group does not fit, uncons first buffer - groups.uncons match { + annctx.groups.uncons match { case Some(((_, _, buffer), groups)) => - Pull.output(Chunk.chain(buffer.prepend(Annotated.GroupBegin(Position.TooFar)))) >> (groups.uncons match { - case Some(((hpl, indent, _), _)) => check(hpl, indent, groups, ghpl) // check inner groups recursively - case None => Pull.pure(emptyGroups) - }) + renderGroupBegin(Position.TooFar, rctx) + buffer.iterator.foreach(renderAnnotated(_, rctx, chunkAcc)) + groups.uncons match { + case Some(((newhpl, newindent, _), _)) => + annctx.hpl = newhpl + annctx.indent = newindent + annctx.groups = groups + check(annctx, rctx, chunkAcc) // check inner groups recursively + case None => + annctx.hpl = 0 + annctx.indent = 0 + annctx.groups = Dequeue.empty + } case None => - Pull.pure(emptyGroups) // should never happen + // should never happen } } - private def annotate(chunk: Chunk[DocEvent], - chunkSize: Int, - idx: Int, - rest: Stream[F, DocEvent], - pos: Int, - aligns: NonEmptyList[Int], - hpl: Int, - indent: Int, - groups: Dequeue[(Int, Int, Chain[Annotated])]): Pull[F, Annotated, Unit] = + private def process(chunk: Chunk[DocEvent], + chunkSize: Int, + idx: Int, + rest: Stream[F, DocEvent], + annctx: AnnotationContext, + rctx: RenderingContext, + chunkAcc: StringBuilder): Pull[F, String, Unit] = if (idx >= chunkSize) { - rest.pull.uncons.flatMap { - case Some((hd, tl)) => annotate(hd, hd.size, 0, tl, pos, aligns, hpl, indent, groups) - case None => Pull.done - } + Pull.output1(chunkAcc.result()) >> + rest.pull.uncons.flatMap { + case Some((hd, tl)) => + chunkAcc.setLength(0) + process(hd, hd.size, 0, tl, annctx, rctx, chunkAcc) + case None => Pull.done + } } else { val evt = chunk(idx) evt match { case DocEvent.Text(text) => val size = text.size - val pos1 = pos + size - if (groups.isEmpty) { + annctx.pos += size + if (annctx.groups.isEmpty) { // no open group we can emit immediately - Pull - .output1(Annotated.Text(text)) >> annotate(chunk, - chunkSize, - idx + 1, - rest, - pos1, - aligns, - hpl, - indent, - groups) + renderText(text, rctx, chunkAcc) } else { // there is an open group, append the event to the current group - check(hpl, indent, push(groups, Annotated.Text(text)), pos1).flatMap { case (hpl, gindent, groups) => - annotate(chunk, chunkSize, idx + 1, rest, pos1, aligns, hpl, gindent, groups) - } + push(annctx, Annotated.Text(text)) + check(annctx, rctx, chunkAcc) } case DocEvent.Line => - if (groups.isEmpty) { + annctx.pos += 1 + if (annctx.groups.isEmpty) { // no open group we can emit immediately a new line - Pull - .output1(Annotated.Line(pos + 1)) >> annotate(chunk, - chunkSize, - idx + 1, - rest, - pos + 1, - aligns, - hpl, - indent, - groups) + renderLine(annctx.pos, rctx, chunkAcc) } else { // there is an open group, append the event to the current group - check(hpl, indent, push(groups, Annotated.Line(pos + 1)), pos + 1).flatMap { case (hpl, indent, groups) => - annotate(chunk, chunkSize, idx + 1, rest, pos + 1, aligns, hpl, indent, groups) - } + push(annctx, Annotated.Line(annctx.pos)) + check(annctx, rctx, chunkAcc) } case DocEvent.LineBreak => - if (groups.isEmpty) { + if (annctx.groups.isEmpty) { // no open group we can emit immediately a new line - Pull.output1(Annotated.LineBreak(pos)) >> annotate(chunk, - chunkSize, - idx + 1, - rest, - pos, - aligns, - hpl, - indent, - groups) + renderLineBreak(annctx.pos, rctx, chunkAcc) } else { // there is an open group, append the event to the current group - check(hpl, indent, push(groups, Annotated.LineBreak(pos)), pos).flatMap { case (hpl, indent, groups) => - annotate(chunk, chunkSize, idx + 1, rest, pos, aligns, hpl, indent, groups) - } + push(annctx, Annotated.LineBreak(annctx.pos)) + check(annctx, rctx, chunkAcc) } case DocEvent.GroupBegin => - val hpl1 = pos + width - aligns.head - if (groups.isEmpty) + val hpl1 = annctx.pos + width - annctx.aligns.head + if (annctx.groups.isEmpty) { // this is the top-level group, turn on the buffer mechanism - annotate(chunk, - chunkSize, - idx + 1, - rest, - pos, - aligns, - hpl1, - aligns.head, - groups.snoc((hpl1, aligns.head, Chain.empty))) - else + annctx.hpl = hpl1 + annctx.indent = annctx.aligns.head + annctx.groups = annctx.groups.snoc((hpl1, annctx.indent, Chain.empty)) + } else { // starting a new group, puts a new empty buffer in the group dequeue, and check for overflow - check(hpl, indent, groups.snoc((hpl1, aligns.head, Chain.empty)), pos).flatMap { - case (hpl, indent, groups) => - annotate(chunk, chunkSize, idx + 1, rest, pos, aligns, hpl, indent, groups) - } + annctx.groups = annctx.groups.snoc((hpl1, annctx.aligns.head, Chain.empty)) + check(annctx, rctx, chunkAcc) + } case DocEvent.GroupEnd => - groups.unsnoc match { + annctx.groups.unsnoc match { case None => - // closing unknown group, just ignore it - annotate(chunk, chunkSize, idx + 1, rest, pos, aligns, hpl, indent, groups) + // closing unknown group, just ignore it case Some(((newhpl, newindent, group), groups)) => // closing a group, pop it from the buffer dequeue, and continue - pop(groups, group.prepend(Annotated.GroupBegin(Position.Small(pos))).append(Annotated.GroupEnd)) - .flatMap { groups => - annotate(chunk, chunkSize, idx + 1, rest, pos, aligns, newhpl, newindent, groups) - } + annctx.groups = groups + pop(group.prepend(Annotated.GroupBegin(Position.Small(annctx.pos))).append(Annotated.GroupEnd), + annctx, + rctx, + chunkAcc) + annctx.hpl = newhpl + annctx.indent = newindent } case DocEvent.IndentBegin => // increment the current indentation level - if (groups.isEmpty) { + annctx.aligns = NonEmptyList(annctx.aligns.head + 1, annctx.aligns.tail) + if (annctx.groups.isEmpty) { // no open group we can emit immediately a new line - Pull.output1(Annotated.IndentBegin) >> annotate(chunk, - chunkSize, - idx + 1, - rest, - pos, - NonEmptyList(aligns.head + 1, aligns.tail), - hpl, - indent + 1, - groups) + renderIndentBegin(rctx) + annctx.indent += 1 } else { // there is an open group, append the event to the current group - check(hpl, indent, push(groups, Annotated.IndentBegin), pos).flatMap { case (hpl, indent, groups) => - annotate(chunk, - chunkSize, - idx + 1, - rest, - pos, - NonEmptyList(aligns.head + 1, aligns.tail), - hpl, - indent, - groups) - } + push(annctx, Annotated.IndentBegin) + check(annctx, rctx, chunkAcc) } case DocEvent.IndentEnd => // decrement the current indentation level - if (groups.isEmpty) { + annctx.aligns = NonEmptyList(annctx.aligns.head - 1, annctx.aligns.tail) + if (annctx.groups.isEmpty) { // no open group we can emit immediately a new line - Pull.output1(Annotated.IndentEnd) >> annotate(chunk, - chunkSize, - idx + 1, - rest, - pos, - NonEmptyList(aligns.head - 1, aligns.tail), - hpl, - indent - 1, - groups) + renderIndentEnd(rctx) + annctx.indent -= 1 } else { // there is an open group, append the event to the current group - check(hpl, indent, push(groups, Annotated.IndentEnd), pos).flatMap { case (hpl, indent, groups) => - annotate(chunk, - chunkSize, - idx + 1, - rest, - pos, - NonEmptyList(aligns.head - 1, aligns.tail), - hpl, - indent, - groups) - } + push(annctx, Annotated.IndentEnd) + check(annctx, rctx, chunkAcc) } case DocEvent.AlignBegin => // push new indentation level - if (groups.isEmpty) { + annctx.aligns = annctx.pos :: annctx.aligns + if (annctx.groups.isEmpty) { // no open group we can emit immediately a new line - Pull - .output1(Annotated.AlignBegin) >> annotate(chunk, - chunkSize, - idx + 1, - rest, - pos, - pos :: aligns, - hpl, - indent, - groups) + renderAlignBegin(rctx) } else { // there is an open group, append the event to the current group - check(hpl, indent, push(groups, Annotated.AlignBegin), pos).flatMap { case (hpl, indent, groups) => - annotate(chunk, chunkSize, idx + 1, rest, pos, pos :: aligns, hpl, indent, groups) - } + push(annctx, Annotated.AlignBegin) + check(annctx, rctx, chunkAcc) } case DocEvent.AlignEnd => // restore to previous indentation level - val aligns1 = - aligns match { - case NonEmptyList(_, i :: is) => NonEmptyList(i, is) - case NonEmptyList(_, Nil) => NonEmptyList.one(0) - } - if (groups.isEmpty) { + annctx.aligns = annctx.aligns match { + case NonEmptyList(_, i :: is) => NonEmptyList(i, is) + case NonEmptyList(_, Nil) => NonEmptyList.one(0) + } + if (annctx.groups.isEmpty) { // no open group we can emit immediately a new line - Pull.output1(Annotated.AlignEnd) >> annotate(chunk, - chunkSize, - idx + 1, - rest, - pos, - aligns1, - hpl, - indent, - groups) + renderAlignEnd(rctx) } else { // there is an open group, append the event to the current group - check(hpl, indent, push(groups, Annotated.AlignEnd), pos).flatMap { case (hpl, indent, groups) => - annotate(chunk, chunkSize, idx + 1, rest, pos, aligns1, hpl, indent, groups) - } + push(annctx, Annotated.AlignEnd) + check(annctx, rctx, chunkAcc) } } + process(chunk, chunkSize, idx + 1, rest, annctx, rctx, chunkAcc) } // rendering @@ -337,51 +267,30 @@ private[render] class StreamPrinter[F[_], Event](width: Int, indentSize: Int)(im ctx.lines = NonEmptyList.fromList(ctx.lines.tail).getOrElse(NonEmptyList.one("")) } - private def renderAnnotated(chunk: Chunk[Annotated], - chunkSize: Int, - idx: Int, - rest: Stream[F, Annotated], - ctx: RenderingContext, - chunkAcc: StringBuilder): Pull[F, String, Unit] = - if (idx >= chunkSize) { - Pull.output1(chunkAcc.result()) >> rest.pull.uncons.flatMap { - case Some((hd, tl)) => - chunkAcc.setLength(0) - renderAnnotated(hd, hd.size, 0, tl, ctx, chunkAcc) - case None => - Pull.done - } - } else { - chunk(idx) match { - case Annotated.Text(text) => renderText(text, ctx, chunkAcc) - case Annotated.Line(pos) => renderLine(pos, ctx, chunkAcc) - case Annotated.LineBreak(pos) => renderLineBreak(pos, ctx, chunkAcc) - case Annotated.GroupBegin(pos) => renderGroupBegin(pos, ctx) - case Annotated.GroupEnd => renderGroupEnd(ctx) - case Annotated.IndentBegin => renderIndentBegin(ctx) - case Annotated.IndentEnd => renderIndentEnd(ctx) - case Annotated.AlignBegin => renderAlignBegin(ctx) - case Annotated.AlignEnd => renderAlignEnd(ctx) - } - renderAnnotated(chunk, chunkSize, idx + 1, rest, ctx, chunkAcc) + private def renderAnnotated(annotated: Annotated, ctx: RenderingContext, chunkAcc: StringBuilder): Unit = + annotated match { + case Annotated.Text(text) => renderText(text, ctx, chunkAcc) + case Annotated.Line(pos) => renderLine(pos, ctx, chunkAcc) + case Annotated.LineBreak(pos) => renderLineBreak(pos, ctx, chunkAcc) + case Annotated.GroupBegin(pos) => renderGroupBegin(pos, ctx) + case Annotated.GroupEnd => renderGroupEnd(ctx) + case Annotated.IndentBegin => renderIndentBegin(ctx) + case Annotated.IndentEnd => renderIndentEnd(ctx) + case Annotated.AlignBegin => renderAlignBegin(ctx) + case Annotated.AlignEnd => renderAlignEnd(ctx) } - def apply(events: Stream[F, Event]): Stream[F, String] = { - val annotated = - annotate( + def apply(events: Stream[F, Event]): Stream[F, String] = + Stream.suspend(Stream.emit(render.newRenderer())).flatMap { renderer => + process( Chunk.empty, 0, 0, - Stream.suspend(Stream.emit(render.newRenderer())).flatMap(renderer => events.flatMap(renderer.doc(_))), - 0, - NonEmptyList.one(0), - 0, - 0, - Dequeue.empty - ).streamNoScope + events.flatMap(renderer.doc(_)), + new AnnotationContext(0, NonEmptyList.one(0), 0, 0, Dequeue.empty), + new RenderingContext(0, width, NonEmptyList.one(""), 0), + new StringBuilder + ).stream - Stream - .suspend(Stream.emit(new RenderingContext(0, width, NonEmptyList.one(""), 0))) - .flatMap(renderAnnotated(Chunk.empty, 0, 0, annotated, _, new StringBuilder).stream) - } + } } From 5fa1b8d1c54ce3384903d467db8fcddc370d95c4 Mon Sep 17 00:00:00 2001 From: Lucas Satabin Date: Mon, 21 Oct 2024 16:31:31 +0200 Subject: [PATCH 07/10] Avoid int boxing in the contexts --- .../render/internal/NonEmptyIntList.scala | 36 +++++++++++++++++++ .../text/render/internal/StreamPrinter.scala | 35 +++++++++--------- 2 files changed, 53 insertions(+), 18 deletions(-) create mode 100644 text/shared/src/main/scala/fs2/data/text/render/internal/NonEmptyIntList.scala diff --git a/text/shared/src/main/scala/fs2/data/text/render/internal/NonEmptyIntList.scala b/text/shared/src/main/scala/fs2/data/text/render/internal/NonEmptyIntList.scala new file mode 100644 index 000000000..9e72748b5 --- /dev/null +++ b/text/shared/src/main/scala/fs2/data/text/render/internal/NonEmptyIntList.scala @@ -0,0 +1,36 @@ +/* + * Copyright 2024 fs2-data Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fs2.data.text.render.internal + +private sealed trait NonEmptyIntList { + def head: Int + def ::(i: Int): NonEmptyIntList = + More(i, this) + def incHead: NonEmptyIntList + def decHead: NonEmptyIntList + def pop: NonEmptyIntList +} +private final case class One(head: Int) extends NonEmptyIntList { + override def incHead: NonEmptyIntList = One(head + 1) + override def decHead: NonEmptyIntList = One(head - 1) + override lazy val pop: NonEmptyIntList = One(0) +} +private final case class More(head: Int, tail: NonEmptyIntList) extends NonEmptyIntList { + override def incHead: NonEmptyIntList = More(head + 1, tail) + override def decHead: NonEmptyIntList = More(head - 1, tail) + override def pop: NonEmptyIntList = tail +} diff --git a/text/shared/src/main/scala/fs2/data/text/render/internal/StreamPrinter.scala b/text/shared/src/main/scala/fs2/data/text/render/internal/StreamPrinter.scala index 3067d68bb..ea54a1232 100644 --- a/text/shared/src/main/scala/fs2/data/text/render/internal/StreamPrinter.scala +++ b/text/shared/src/main/scala/fs2/data/text/render/internal/StreamPrinter.scala @@ -21,11 +21,12 @@ import cats.collections.Dequeue import cats.data.{Chain, NonEmptyList} import fs2.{Chunk, Pipe, Pull, Stream} +private case class OpenGroup(hpl: Int, indent: Int, group: Chain[Annotated]) private class AnnotationContext(var pos: Int, - var aligns: NonEmptyList[Int], + var aligns: NonEmptyIntList, var hpl: Int, var indent: Int, - var groups: Dequeue[(Int, Int, Chain[Annotated])]) + var groups: Dequeue[OpenGroup]) private class RenderingContext(var fit: Int, var hpl: Int, var lines: NonEmptyList[String], var col: Int) private[render] class StreamPrinter[F[_], Event](width: Int, indentSize: Int)(implicit render: Renderable[Event]) @@ -33,8 +34,9 @@ private[render] class StreamPrinter[F[_], Event](width: Int, indentSize: Int)(im private def push(annctx: AnnotationContext, evt: Annotated): Unit = annctx.groups.unsnoc match { - case Some(((ghpl, gindent, group), groups)) => annctx.groups = groups.snoc((ghpl, gindent, group.append(evt))) - case None => // should never happen + case Some((OpenGroup(ghpl, gindent, group), groups)) => + annctx.groups = groups.snoc(OpenGroup(ghpl, gindent, group.append(evt))) + case None => // should never happen } private def pop(buffer: Chain[Annotated], @@ -42,8 +44,8 @@ private[render] class StreamPrinter[F[_], Event](width: Int, indentSize: Int)(im rctx: RenderingContext, chunkAcc: StringBuilder): Unit = annctx.groups.unsnoc match { - case Some(((ghpl, gindent, group), groups)) => - annctx.groups = groups.snoc((ghpl, gindent, group.concat(buffer))) + case Some((OpenGroup(ghpl, gindent, group), groups)) => + annctx.groups = groups.snoc(OpenGroup(ghpl, gindent, group.concat(buffer))) case None => annctx.groups = Dequeue.empty buffer.iterator.foreach(renderAnnotated(_, rctx, chunkAcc)) @@ -55,11 +57,11 @@ private[render] class StreamPrinter[F[_], Event](width: Int, indentSize: Int)(im } else { // group does not fit, uncons first buffer annctx.groups.uncons match { - case Some(((_, _, buffer), groups)) => + case Some((OpenGroup(_, _, buffer), groups)) => renderGroupBegin(Position.TooFar, rctx) buffer.iterator.foreach(renderAnnotated(_, rctx, chunkAcc)) groups.uncons match { - case Some(((newhpl, newindent, _), _)) => + case Some((OpenGroup(newhpl, newindent, _), _)) => annctx.hpl = newhpl annctx.indent = newindent annctx.groups = groups @@ -131,10 +133,10 @@ private[render] class StreamPrinter[F[_], Event](width: Int, indentSize: Int)(im // this is the top-level group, turn on the buffer mechanism annctx.hpl = hpl1 annctx.indent = annctx.aligns.head - annctx.groups = annctx.groups.snoc((hpl1, annctx.indent, Chain.empty)) + annctx.groups = annctx.groups.snoc(OpenGroup(hpl1, annctx.indent, Chain.empty)) } else { // starting a new group, puts a new empty buffer in the group dequeue, and check for overflow - annctx.groups = annctx.groups.snoc((hpl1, annctx.aligns.head, Chain.empty)) + annctx.groups = annctx.groups.snoc(OpenGroup(hpl1, annctx.aligns.head, Chain.empty)) check(annctx, rctx, chunkAcc) } @@ -143,7 +145,7 @@ private[render] class StreamPrinter[F[_], Event](width: Int, indentSize: Int)(im case None => // closing unknown group, just ignore it - case Some(((newhpl, newindent, group), groups)) => + case Some((OpenGroup(newhpl, newindent, group), groups)) => // closing a group, pop it from the buffer dequeue, and continue annctx.groups = groups pop(group.prepend(Annotated.GroupBegin(Position.Small(annctx.pos))).append(Annotated.GroupEnd), @@ -157,7 +159,7 @@ private[render] class StreamPrinter[F[_], Event](width: Int, indentSize: Int)(im case DocEvent.IndentBegin => // increment the current indentation level - annctx.aligns = NonEmptyList(annctx.aligns.head + 1, annctx.aligns.tail) + annctx.aligns = annctx.aligns.incHead if (annctx.groups.isEmpty) { // no open group we can emit immediately a new line renderIndentBegin(rctx) @@ -170,7 +172,7 @@ private[render] class StreamPrinter[F[_], Event](width: Int, indentSize: Int)(im case DocEvent.IndentEnd => // decrement the current indentation level - annctx.aligns = NonEmptyList(annctx.aligns.head - 1, annctx.aligns.tail) + annctx.aligns = annctx.aligns.decHead if (annctx.groups.isEmpty) { // no open group we can emit immediately a new line renderIndentEnd(rctx) @@ -195,10 +197,7 @@ private[render] class StreamPrinter[F[_], Event](width: Int, indentSize: Int)(im case DocEvent.AlignEnd => // restore to previous indentation level - annctx.aligns = annctx.aligns match { - case NonEmptyList(_, i :: is) => NonEmptyList(i, is) - case NonEmptyList(_, Nil) => NonEmptyList.one(0) - } + annctx.aligns = annctx.aligns.pop if (annctx.groups.isEmpty) { // no open group we can emit immediately a new line renderAlignEnd(rctx) @@ -287,7 +286,7 @@ private[render] class StreamPrinter[F[_], Event](width: Int, indentSize: Int)(im 0, 0, events.flatMap(renderer.doc(_)), - new AnnotationContext(0, NonEmptyList.one(0), 0, 0, Dequeue.empty), + new AnnotationContext(0, One(0), 0, 0, Dequeue.empty), new RenderingContext(0, width, NonEmptyList.one(""), 0), new StringBuilder ).stream From 5b52c89d9996185b3c58cae9066925b549a7dadb Mon Sep 17 00:00:00 2001 From: Lucas Satabin Date: Thu, 24 Oct 2024 10:15:04 +0200 Subject: [PATCH 08/10] Switch to direct compact rendering --- .../main/scala/fs2/data/json/package.scala | 66 ++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/json/src/main/scala/fs2/data/json/package.scala b/json/src/main/scala/fs2/data/json/package.scala index 9a74b83d5..1e5c359af 100644 --- a/json/src/main/scala/fs2/data/json/package.scala +++ b/json/src/main/scala/fs2/data/json/package.scala @@ -154,7 +154,71 @@ package object json { * You can use this to write the Json stream to a file. */ def compact[F[_]]: Pipe[F, Token, String] = - _.through(fs2.data.text.render.pretty(width = Int.MaxValue)(Token.compact)) + _.scanChunks((0, false)) { case (state, chunk) => + val builder = new StringBuilder + val state1 = + chunk.foldLeft(state) { + case ((level, comma), Token.StartObject) => + if (comma) { + builder.append(',') + } + builder.append(('{')) + (level + 1, false) + case ((level, _), Token.EndObject) => + builder.append('}') + (level - 1, level > 1) + case ((level, comma), Token.StartArray) => + if (comma) { + builder.append(',') + } + builder.append(('[')) + (level + 1, false) + case ((level, _), Token.EndArray) => + builder.append(']') + (level - 1, level > 1) + case ((level, comma), Token.Key(key)) => + if (comma) { + builder.append(',') + } + builder.append('"') + Token.renderString(key, 0, builder) + builder.append("\":") + (level, false) + case ((level, comma), Token.StringValue(key)) => + if (comma) { + builder.append(',') + } + builder.append('"') + Token.renderString(key, 0, builder) + builder.append('"') + (level, level > 0) + case ((level, comma), Token.NumberValue(n)) => + if (comma) { + builder.append(',') + } + builder.append(n) + (level, level > 0) + case ((level, comma), Token.TrueValue) => + if (comma) { + builder.append(',') + } + builder.append("true") + (level, level > 0) + case ((level, comma), Token.FalseValue) => + if (comma) { + builder.append(',') + } + builder.append("false") + (level, level > 0) + case ((level, comma), Token.NullValue) => + if (comma) { + builder.append(',') + } + builder.append("null") + (level, level > 0) + } + (state1, Chunk.singleton(builder.result())) + } /** Renders a pretty-printed representation of the token stream with the given * indentation size. From 4455b0639468da7a61fac71cee32e859caee7e00 Mon Sep 17 00:00:00 2001 From: Lucas Satabin Date: Thu, 24 Oct 2024 11:33:51 +0200 Subject: [PATCH 09/10] Make Scala 3 happy --- .../scala/fs2/data/text/render/internal/StreamPrinter.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/text/shared/src/main/scala/fs2/data/text/render/internal/StreamPrinter.scala b/text/shared/src/main/scala/fs2/data/text/render/internal/StreamPrinter.scala index ea54a1232..0d6aff388 100644 --- a/text/shared/src/main/scala/fs2/data/text/render/internal/StreamPrinter.scala +++ b/text/shared/src/main/scala/fs2/data/text/render/internal/StreamPrinter.scala @@ -220,7 +220,7 @@ private[render] class StreamPrinter[F[_], Event](width: Int, indentSize: Int)(im private def renderLine(pos: Int, ctx: RenderingContext, chunkAcc: StringBuilder): Unit = if (ctx.fit == 0) { ctx.hpl = pos + width ctx.col = ctx.lines.head.size - chunkAcc.append('\n').append(ctx.lines.head): Unit + val _ = chunkAcc.append('\n').append(ctx.lines.head) } else { ctx.col += 1 chunkAcc.append(' ') @@ -230,7 +230,7 @@ private[render] class StreamPrinter[F[_], Event](width: Int, indentSize: Int)(im if (ctx.fit == 0) { ctx.hpl = pos + width ctx.col = ctx.lines.head.size - chunkAcc.append('\n').append(ctx.lines.head): Unit + val _ = chunkAcc.append('\n').append(ctx.lines.head) } private def renderGroupBegin(pos: Position, ctx: RenderingContext): Unit = From 6d743932e3128423e98b5abca463f99648627dc6 Mon Sep 17 00:00:00 2001 From: Lucas Satabin Date: Thu, 24 Oct 2024 13:51:52 +0200 Subject: [PATCH 10/10] Add MiMa exceptions for private classes --- build.sbt | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 212b46741..25ee3d72d 100644 --- a/build.sbt +++ b/build.sbt @@ -142,7 +142,46 @@ lazy val text = crossProject(JVMPlatform, JSPlatform, NativePlatform) ProblemFilters.exclude[IncompatibleMethTypeProblem]("fs2.data.text.CharLikeStringChunks.pullNext"), ProblemFilters.exclude[IncompatibleMethTypeProblem]("fs2.data.text.CharLikeStringChunks.advance"), ProblemFilters.exclude[IncompatibleMethTypeProblem]("fs2.data.text.CharLikeStringChunks.current"), - ProblemFilters.exclude[MissingClassProblem]("fs2.data.text.CharLikeStringChunks$StringContext") + ProblemFilters.exclude[MissingClassProblem]("fs2.data.text.CharLikeStringChunks$StringContext"), + ProblemFilters.exclude[MissingClassProblem]("fs2.data.text.render.internal.Annotated$AlignBegin"), + ProblemFilters.exclude[MissingTypesProblem]("fs2.data.text.render.internal.Annotated$AlignBegin$"), + ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.data.text.render.internal.Annotated#AlignBegin.apply"), + ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.data.text.render.internal.Annotated#AlignBegin.unapply"), + ProblemFilters.exclude[IncompatibleResultTypeProblem]( + "fs2.data.text.render.internal.Annotated#AlignBegin.fromProduct"), + ProblemFilters.exclude[MissingClassProblem]("fs2.data.text.render.internal.Annotated$AlignEnd"), + ProblemFilters.exclude[MissingTypesProblem]("fs2.data.text.render.internal.Annotated$AlignEnd$"), + ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.data.text.render.internal.Annotated#AlignEnd.apply"), + ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.data.text.render.internal.Annotated#AlignEnd.unapply"), + ProblemFilters.exclude[IncompatibleResultTypeProblem]( + "fs2.data.text.render.internal.Annotated#AlignEnd.fromProduct"), + ProblemFilters.exclude[MissingClassProblem]("fs2.data.text.render.internal.Annotated$GroupEnd"), + ProblemFilters.exclude[MissingTypesProblem]("fs2.data.text.render.internal.Annotated$GroupEnd$"), + ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.data.text.render.internal.Annotated#GroupEnd.apply"), + ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.data.text.render.internal.Annotated#GroupEnd.unapply"), + ProblemFilters.exclude[IncompatibleResultTypeProblem]( + "fs2.data.text.render.internal.Annotated#GroupEnd.fromProduct"), + ProblemFilters.exclude[MissingClassProblem]("fs2.data.text.render.internal.Annotated$IndentBegin"), + ProblemFilters.exclude[MissingTypesProblem]("fs2.data.text.render.internal.Annotated$IndentBegin$"), + ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.data.text.render.internal.Annotated#IndentBegin.apply"), + ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.data.text.render.internal.Annotated#IndentBegin.unapply"), + ProblemFilters.exclude[IncompatibleResultTypeProblem]( + "fs2.data.text.render.internal.Annotated#IndentBegin.fromProduct"), + ProblemFilters.exclude[MissingClassProblem]("fs2.data.text.render.internal.Annotated$IndentEnd"), + ProblemFilters.exclude[MissingTypesProblem]("fs2.data.text.render.internal.Annotated$IndentEnd$"), + ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.data.text.render.internal.Annotated#IndentEnd.apply"), + ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.data.text.render.internal.Annotated#IndentEnd.unapply"), + ProblemFilters.exclude[IncompatibleResultTypeProblem]( + "fs2.data.text.render.internal.Annotated#IndentEnd.fromProduct"), + ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.data.text.render.internal.Annotated#Line.hp"), + ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.data.text.render.internal.Annotated#LineBreak.hp"), + ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.data.text.render.internal.Annotated#Text.hp"), + ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.data.text.render.internal.Annotated#Text.copy"), + ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.data.text.render.internal.Annotated#Text.copy$default$2"), + ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.data.text.render.internal.Annotated#Text.this"), + ProblemFilters.exclude[MissingTypesProblem]("fs2.data.text.render.internal.Annotated$Text$"), + ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.data.text.render.internal.Annotated#Text.apply"), + ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.data.text.render.internal.Annotated#Text._2") ) ) .nativeSettings(