Skip to content

Commit

Permalink
refactor(debug)!: Frontend -> View
Browse files Browse the repository at this point in the history
  • Loading branch information
j-mie6 committed Dec 28, 2024
1 parent 26c6927 commit bfe92bf
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 106 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,32 +9,30 @@ 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
* on attaching debuggers to find out how to prevent that.
*
* @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")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright 2020 Parsley Contributors <https://github.com/j-mie6/Parsley/graphs/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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -171,38 +170,38 @@ 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.
val renderer = fresh {
val frozen = tree()
val input = frozen.fullInput

frontend.process(input, frozen)
view.process(input, frozen)
}.impure

atomic(attached <* renderer) <|> (renderer *> empty)
Expand All @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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$
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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 = ()
}

Expand Down

0 comments on commit bfe92bf

Please sign in to comment.