From bfe92bfb15a99ad9fba6d6f52347617aef0e51f7 Mon Sep 17 00:00:00 2001 From: Jamie Willis Date: Sat, 28 Dec 2024 16:05:22 +0000 Subject: [PATCH] refactor(debug)!: Frontend -> View --- .../debugger/ConsolePrettyPrinter.scala | 10 ++- .../scala/parsley/debugger/DebugView.scala | 61 ++++++++++++++++++ .../scala/parsley/debugger/combinator.scala | 59 +++++++++-------- .../debugger/frontend/DebugFrontend.scala | 64 ------------------- .../debugger/{frontend => }/ReuseSpec.scala | 11 ++-- 5 files changed, 99 insertions(+), 106 deletions(-) create mode 100644 parsley-debug/shared/src/main/scala/parsley/debugger/DebugView.scala delete mode 100644 parsley-debug/shared/src/main/scala/parsley/debugger/frontend/DebugFrontend.scala rename parsley-debug/shared/src/test/scala/parsley/debugger/{frontend => }/ReuseSpec.scala (80%) diff --git a/parsley-debug/shared/src/main/scala/parsley/debugger/ConsolePrettyPrinter.scala b/parsley-debug/shared/src/main/scala/parsley/debugger/ConsolePrettyPrinter.scala index fbbe6f7f3..f9d4845df 100644 --- a/parsley-debug/shared/src/main/scala/parsley/debugger/ConsolePrettyPrinter.scala +++ b/parsley-debug/shared/src/main/scala/parsley/debugger/ConsolePrettyPrinter.scala @@ -9,8 +9,6 @@ import java.io.{OutputStream, PrintStream} import scala.annotation.tailrec -import frontend.ReusableFrontend - /** A (reusable) console pretty-printer for the debugger. * * It is recommended that all memory-heavy types (e.g. closures) are not stored explicitly. Consult the documentation @@ -18,23 +16,23 @@ import frontend.ReusableFrontend * * @since 5.0.0 */ -object ConsolePrettyPrinter extends ReusableFrontend with ConsolePrettyPrinter { +object ConsolePrettyPrinter extends DebugView.Reusable with ConsolePrettyPrinter { override protected val out: PrintStream = Console.out /** Create a string pretty-printer that outputs to an arbitrary place * * @since 5.0.0 */ - def apply(out: OutputStream): ReusableFrontend = apply(new PrintStream(out)) + def apply(out: OutputStream): DebugView.Reusable = apply(new PrintStream(out)) /** Create a string pretty-printer that outputs to an arbitrary place * * @since 5.0.0 */ - def apply(print: PrintStream): ReusableFrontend = new ConsolePrettyPrinter { + def apply(print: PrintStream): DebugView.Reusable = new ConsolePrettyPrinter { override protected val out: PrintStream = print } } -private [debugger] sealed trait ConsolePrettyPrinter extends ReusableFrontend { +private [debugger] sealed trait ConsolePrettyPrinter extends DebugView.Reusable { protected val out: PrintStream override private [debugger] def process(input: =>String, tree: =>DebugTree): Unit = { out.println(s"${tree.parserName}'s parse tree for input:\n\n$input\n\n") diff --git a/parsley-debug/shared/src/main/scala/parsley/debugger/DebugView.scala b/parsley-debug/shared/src/main/scala/parsley/debugger/DebugView.scala new file mode 100644 index 000000000..307082159 --- /dev/null +++ b/parsley-debug/shared/src/main/scala/parsley/debugger/DebugView.scala @@ -0,0 +1,61 @@ +/* + * Copyright 2020 Parsley Contributors + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package parsley.debugger + +import parsley.debugger.internal.XIllegalStateException + +/** A common interface for a rendering view for a debugger to present the debug tree. Inherit from + * one of the two provided subtraits to use. + * + * Any compliant implementation that handles all nodes of a `parsley.debugger.DebugTree` can be + * used in place of any other implementation (e.g. a serialiser to JSON, a GUI, etc.). + * + * If a view is reusable, one can implement it as either an `object` or a `class`, but an `object` + * is recommended. Either way, it should inherit [[DebugView.Reusable]]. + * + * If a view is single-use (e.g. it has some non-reusable state), never implement it as an `object`. Always + * implement single-use views as a `class` of some sort inheriting from [[DebugView.SingleUse]]. + * + * @since 5.0.0 + */ +sealed trait DebugView { + /** Render a debug tree. + * + * @param input The full input of the parse. + * @param tree Debug tree to render. + */ + private [debugger] def process(input: =>String, tree: =>DebugTree): Unit +} +object DebugView { + /** Signifies that the debug view inheriting from this can be used multiple times. + * + * @see [[DebugView]] + * @since 5.0.0 + */ + trait Reusable extends DebugView + + /** Signifies that the debug view inheriting from this can only be run once. + * + * @see [[DebugView]] + * @since 4.5.0 + */ + trait SingleUse extends DebugView { + private var hasBeenRun = false + final override private[debugger] def process(input: =>String, tree: =>DebugTree): Unit = { + if (hasBeenRun) { + // XXX: There isn't really another way to enforce not running a stateful frontend more than once that isn't just "do nothing". + // Especially since doing nothing turns that action into a silent error, which is generally less preferable to "loud" + // errors. Failing fast may be better for some frontends. + throw new XIllegalStateException("Stateful frontend has already been run.").except // scalastyle:ignore throw + } else { + processImpl(input, tree) + hasBeenRun = true + } + } + /** The implementation of the process method above */ + private[debugger] def processImpl(input: =>String, tree: =>DebugTree): Unit + } +} diff --git a/parsley-debug/shared/src/main/scala/parsley/debugger/combinator.scala b/parsley-debug/shared/src/main/scala/parsley/debugger/combinator.scala index 6ca780b18..65a47bcf2 100644 --- a/parsley-debug/shared/src/main/scala/parsley/debugger/combinator.scala +++ b/parsley-debug/shared/src/main/scala/parsley/debugger/combinator.scala @@ -7,7 +7,6 @@ package parsley.debugger import parsley.Parsley import parsley.Parsley.{atomic, empty, fresh} -import parsley.debugger.frontend.{DebugFrontend, ReusableFrontend} import parsley.debugger.internal.{DebugContext, DivergenceContext} import parsley.internal.deepembedding.frontend.LazyParsley @@ -171,30 +170,30 @@ object combinator { /** Attach a debugger and an explicitly-available frontend in which the debug tree should be * proessed with. * - * You would normally obtain a [[parsley.debugger.frontend.DebugFrontend]] frontend from its + * You would normally obtain a [[parsley.debugger.DebugView]] from its * respective package as either a static object or an instance object depending on whether the * renderer stores state. In the latter case, it is better to regenerate the frontend with - * every new debugged parser. The frontend can be reusable (i.e. inherits from - * [[parsley.debugger.frontend.ReusableFrontend]]) or single-use (i.e. inherits from - * [[parsley.debugger.frontend.SingleUseFrontend]]). + * every new debugged parser. The view can be reusable (i.e. inherits from + * [[parsley.debugger.DebugView.Reusable]]) or single-use (i.e. inherits from + * [[parsley.debugger.DebugView.SingleUse]]). * - * The instrumented parser will automatically call the frontend to render the debug tree, so it + * The instrumented parser will automatically call the view to render the debug tree, so it * may be recommended that you only use this with smaller parsers as large parsers may cause - * large amounts of memory to be used for processing the tree. + * large amounts of memory to be used for rendering the tree. * * @note Do not run a parser through this combinator multiple times. * * @param parser The parser to debug. - * @param frontend The frontend instance to render with. + * @param view The debug view instance to render with. * @param toStringRules If a parser's result matches any of the predicates in this sequence, it * will get turned into a string before storing in the debug tree. This is * usually for memory-usage optimisation. By default, all function-like * objects will be converted into strings, as closures are expensive to store. * @tparam A Output type of parser. - * @return A modified parser which will ask the frontend to process the produced debug tree after + * @return A modified parser which will ask the view to render the produced debug tree after * a call to [[Parsley.parse]] is made. */ - def attach[A](parser: Parsley[A], frontend: DebugFrontend, toStringRules: PartialFunction[Any, Boolean]): Parsley[A] = { + def attach[A](parser: Parsley[A], view: DebugView, toStringRules: PartialFunction[Any, Boolean]): Parsley[A] = { val (tree, attached) = attachDebugger(parser, toStringRules) // Ideally, this should run 'attached', and render the tree regardless of the parser's success. @@ -202,7 +201,7 @@ object combinator { val frozen = tree() val input = frozen.fullInput - frontend.process(input, frozen) + view.process(input, frozen) }.impure atomic(attached <* renderer) <|> (renderer *> empty) @@ -218,29 +217,29 @@ object combinator { * @note Do not run a parser through this combinator multiple times. * @see * The other overload of this method - * ([[parsley.debugger.combinator$.attach[A](parser:parsley\.Parsley[A],frontend:parsley\.debugger\.frontend\.DebugFrontend,toStringRules* attachDebugger]]) + * ([[parsley.debugger.combinator$.attach[A](parser:parsley\.Parsley[A],view:parsley\.debugger\.DebugView,toStringRules* attachDebugger]]) * has more information on its usage. * @param parser The parser to debug. - * @param frontend The frontend instance to render with. + * @param view The debug view instance to render with. * @tparam A Output type of parser. - * @return A modified parser which will ask the frontend to process the produced debug tree after + * @return A modified parser which will ask the view to render the produced debug tree after * a call to [[Parsley.parse]] is made. * */ - def attach[A](parser: Parsley[A], frontend: DebugFrontend): Parsley[A] = attach[A](parser, frontend, DefaultStringRules) + def attach[A](parser: Parsley[A], view: DebugView): Parsley[A] = attach[A](parser, view, DefaultStringRules) - /** Create a closure that freshly attaches a debugger and a tree-processing frontend to a parser every + /** Create a closure that freshly attaches a debugger and a tree-rendering view to a parser every * time it is called. * * @note Do not run a parser through this combinator multiple times. * - * @return Generator closure for frontend-debugged versions of the input parser. + * @return Generator closure for view-debugged versions of the input parser. */ - def attachReusable[A](parser: Parsley[A], frontend: =>ReusableFrontend, toStringRules: PartialFunction[Any, Boolean]): () => Parsley[A] = { - () => attach(parser, frontend, toStringRules) + def attachReusable[A](parser: Parsley[A], view: =>DebugView.Reusable, toStringRules: PartialFunction[Any, Boolean]): () => Parsley[A] = { + () => attach(parser, view, toStringRules) } - /** Create a closure that freshly attaches a debugger and a tree-processing frontend to a parser every + /** Create a closure that freshly attaches a debugger and a tree-rendering view to a parser every * time it is called. * * This assumes the default rules of converting only lambdas and closures into strings when @@ -249,11 +248,11 @@ object combinator { * @note Do not run a parser through this combinator multiple times. * @see * The other overload of this method - * ([[parsley.debugger.combinator$.attachReusable[A](parser:parsley\.Parsley[A],frontend:=>parsley\.debugger\.frontend\.ReusableFrontend,toStringRules* attachDebugger]]) + * ([[parsley.debugger.combinator$.attachReusable[A](parser:parsley\.Parsley[A],view:=>parsley\.debugger\.DebugView\.Reusable,toStringRules* attachDebugger]]) * has more information on its usage. - * @return Generator closure for frontend-debugged versions of the input parser. + * @return Generator closure for view-debugged versions of the input parser. */ - def attachReusable[A](parser: Parsley[A], frontend: =>ReusableFrontend): () => Parsley[A] = attachReusable[A](parser, frontend, DefaultStringRules) + def attachReusable[A](parser: Parsley[A], view: =>DebugView.Reusable): () => Parsley[A] = attachReusable[A](parser, view, DefaultStringRules) /* Attach a debugger and an implicitly-available frontend in which the debug tree should be * processed with. @@ -295,15 +294,15 @@ object combinator { implicit class DebuggerOps[A](par: Parsley[A]) { //def attachDebugger(toStringRules: PartialFunction[Any, Boolean]): DebuggedPair[A] = combinator.attachDebugger(par, toStringRules) //def attachReusable(toStringRules: PartialFunction[Any, Boolean]): () => DebuggedPair[A] = combinator.attachReusable(par, toStringRules) - def attach(frontend: DebugFrontend, toStringRules: PartialFunction[Any, Boolean]): Parsley[A] = combinator.attach(par, frontend, toStringRules) - def attachReusable(frontend: =>ReusableFrontend, toStringRules: PartialFunction[Any, Boolean]): () => Parsley[A] = - combinator.attachReusable(par, frontend, toStringRules) - //def attach(toStringRules: PartialFunction[Any, Boolean])(implicit frontend: DebugFrontend): Parsley[A] = combinator.attach(par, toStringRules) + def attach(view: DebugView, toStringRules: PartialFunction[Any, Boolean]): Parsley[A] = combinator.attach(par, view, toStringRules) + def attachReusable(view: =>DebugView.Reusable, toStringRules: PartialFunction[Any, Boolean]): () => Parsley[A] = + combinator.attachReusable(par, view, toStringRules) + //def attach(toStringRules: PartialFunction[Any, Boolean])(implicit view: DebugView): Parsley[A] = combinator.attach(par, toStringRules) //def attachDebugger: DebuggedPair[A] = combinator.attachDebugger(par, defaultRules) //def attachReusable: () => DebuggedPair[A] = combinator.attachReusable(par, defaultRules) - def attach(frontend: DebugFrontend): Parsley[A] = combinator.attach(par, frontend, DefaultStringRules) - def attachReusable(frontend: =>ReusableFrontend): () => Parsley[A] = combinator.attachReusable(par, frontend, DefaultStringRules) - //def attach(implicit frontend: DebugFrontend): Parsley[A] = combinator.attach(par, defaultRules) + def attach(view: DebugView): Parsley[A] = combinator.attach(par, view, DefaultStringRules) + def attachReusable(view: =>DebugView.Reusable): () => Parsley[A] = combinator.attachReusable(par, view, DefaultStringRules) + //def attach(implicit view: DebugFrontend): Parsley[A] = combinator.attach(par, defaultRules) def named(name: String): Parsley[A] = combinator.named(par, name) } // $COVERAGE-ON$ diff --git a/parsley-debug/shared/src/main/scala/parsley/debugger/frontend/DebugFrontend.scala b/parsley-debug/shared/src/main/scala/parsley/debugger/frontend/DebugFrontend.scala deleted file mode 100644 index 870ef3e7a..000000000 --- a/parsley-debug/shared/src/main/scala/parsley/debugger/frontend/DebugFrontend.scala +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2020 Parsley Contributors - * - * SPDX-License-Identifier: BSD-3-Clause - */ -package parsley.debugger.frontend - -import parsley.debugger.DebugTree -import parsley.debugger.internal.XIllegalStateException - -/** A common interface for a debug frontend for a debugger to present the debug tree. Inherit from - * one of the two provided subtraits to use. - * - * Any compliant implementation that handles all nodes of a `parsley.debugger.DebugTree` can be - * used in place of any other implementation (e.g. a serialiser to JSON, a GUI, etc.). - * - * If a frontend is reusable, one can implement it as either an `object` or a `class`, but an `object` - * is recommended. Either way, it should inherit [[ReusableFrontend]]. - * - * If a frontend is single-use (e.g. it has some non-reusable state), never implement it as an `object`. Always - * implement single-use frontends as a `class` of some sort inheriting from [[SingleUseFrontend]]. - * - * If the results of some processing of a tree are needed out of a frontend, create a frontend class that accepts - * some continuation parameter of `ReturnType => Unit` and call it somewhere within the implementation. - * - * @since 4.5.0 - */ -sealed trait DebugFrontend { - /** Process a debug tree using whatever the frontend is doing to present the tree in some way. - * - * @param input The full input of the parse. - * @param tree Debug tree to process. - */ - private [debugger] def process(input: =>String, tree: =>DebugTree): Unit -} - -/** Signifies that the frontend inheriting from this can be used multiple times. - * - * @see [[DebugFrontend]] - * @since 4.5.0 - */ -trait ReusableFrontend extends DebugFrontend - -/** Signifies that the frontend inheriting from this can only be run once. - * - * @see [[DebugFrontend]] - * @since 4.5.0 - */ -trait SingleUseFrontend extends DebugFrontend { - private var hasBeenRun = false - final override private[debugger] def process(input: =>String, tree: =>DebugTree): Unit = { - if (hasBeenRun) { - // XXX: There isn't really another way to enforce not running a stateful frontend more than once that isn't just "do nothing". - // Especially since doing nothing turns that action into a silent error, which is generally less preferable to "loud" - // errors. Failing fast may be better for some frontends. - throw new XIllegalStateException("Stateful frontend has already been run.").except // scalastyle:ignore throw - } else { - processImpl(input, tree) - hasBeenRun = true - } - } - /** The implementation of the process method above */ - private[debugger] def processImpl(input: =>String, tree: =>DebugTree): Unit -} diff --git a/parsley-debug/shared/src/test/scala/parsley/debugger/frontend/ReuseSpec.scala b/parsley-debug/shared/src/test/scala/parsley/debugger/ReuseSpec.scala similarity index 80% rename from parsley-debug/shared/src/test/scala/parsley/debugger/frontend/ReuseSpec.scala rename to parsley-debug/shared/src/test/scala/parsley/debugger/ReuseSpec.scala index f9bf56066..b56cbe6dd 100644 --- a/parsley-debug/shared/src/test/scala/parsley/debugger/frontend/ReuseSpec.scala +++ b/parsley-debug/shared/src/test/scala/parsley/debugger/ReuseSpec.scala @@ -3,23 +3,22 @@ * * SPDX-License-Identifier: BSD-3-Clause */ -package parsley.debugger.frontend +package parsley.debugger import org.typelevel.scalaccompat.annotation.unused import parsley.ParsleyTest -import parsley.debugger.{DebugTree, ParseAttempt} //noinspection ConvertExpressionToSAM class ReuseSpec extends ParsleyTest { - behavior of "the DebugFrontend class and its re-usability / single-use enforcement" + behavior of "the DebugView class and its re-usability / single-use enforcement" - it should "throw when run multiple times, only if the frontend is marked as single-use" in { + it should "throw when run multiple times, only if the view is marked as single-use" in { // Dummy values. - val reusable: ReusableFrontend = new ReusableFrontend { + val reusable: DebugView.Reusable = new DebugView.Reusable { override private [debugger] def process(input: => String, tree: => DebugTree): Unit = () } - val singleUse: SingleUseFrontend = new SingleUseFrontend { + val singleUse: DebugView.SingleUse = new DebugView.SingleUse { override private [debugger] def processImpl(input: => String, tree: => DebugTree): Unit = () }