Skip to content

Commit e937beb

Browse files
authored
Interactivity with parsley-debug-app (#259)
2 parents ebd20cc + 1fca3c1 commit e937beb

File tree

8 files changed

+190
-9
lines changed

8 files changed

+190
-9
lines changed

parsley-debug/shared/src/main/scala/parsley/debug/DebugView.scala

+24
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
package parsley.debug
77

88
import parsley.debug.internal.XIllegalStateException
9+
import org.typelevel.scalaccompat.annotation.unused
910

1011
/** A common interface for a rendering view for a debugger to present the debug tree. Inherit from
1112
* one of the two provided subtraits to use.
@@ -39,6 +40,23 @@ object DebugView {
3940
*/
4041
trait Reusable extends DebugView
4142

43+
/** Signifies that the debug view inheriting from this can wait on a certain render call.
44+
*
45+
* This can be extended to make remote breakpoint stepping possible.
46+
*
47+
* @see [[DebugView]]
48+
*/
49+
trait Pauseable extends DebugView {
50+
/** Render a given debug tree and wait for a response from the remote viewer.
51+
*
52+
* @see [[DebugView]]
53+
* @return n The number of breakpoints to step through after the current breakpoint.
54+
* n == 0 to just step through this breakpoint.
55+
* n >= 1 to step through the next n breakpoints.
56+
*/
57+
private [debug] def renderWait(input: => String, tree: => DebugTree): Int
58+
}
59+
4260
/** Signifies that the debug view inheriting from this can only be run once.
4361
*
4462
* @see [[DebugView]]
@@ -61,3 +79,9 @@ object DebugView {
6179
private [debug] def renderImpl(input: =>String, tree: =>DebugTree): Unit
6280
}
6381
}
82+
83+
/* A no-op implementation for internal use, like testing */
84+
private [parsley] object SilentDebugView extends DebugView {
85+
override private [debug] def render(@unused input: => String, @unused tree: => DebugTree): Unit = ()
86+
private [debug] def renderWait(@unused input: => String, @unused tree: => DebugTree): Int = 0
87+
}

parsley-debug/shared/src/main/scala/parsley/debug/combinator.scala

+16-5
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import parsley.Parsley.{atomic, empty, fresh}
1010
import parsley.debug.internal.{DebugContext, DivergenceContext}
1111

1212
import parsley.internal.deepembedding.frontend.LazyParsley
13-
import parsley.internal.deepembedding.frontend.debug.{TaggedWith, Named}
13+
import parsley.internal.deepembedding.frontend.debug.{TaggedWith, Named, RemoteBreak}
1414
import parsley.internal.deepembedding.backend.debug.{CheckDivergence, Debugging}
1515

1616
/** This object contains the combinators for attaching debuggers to parsers.
@@ -74,8 +74,8 @@ object combinator {
7474
* @tparam A Output type of original parser.
7575
* @return A pair of the finalised tree, and the instrumented parser.
7676
*/
77-
private [parsley] def attachDebugger[A](parser: Parsley[A], toStringRules: PartialFunction[Any, Boolean]): DebuggedPair[A] = {
78-
val context: DebugContext = new DebugContext(toStringRules)
77+
private [parsley] def attachDebugger[A](parser: Parsley[A], toStringRules: PartialFunction[Any, Boolean], view: DebugView): DebuggedPair[A] = {
78+
val context: DebugContext = new DebugContext(toStringRules, view)
7979

8080
val attached: LazyParsley[A] = TaggedWith.tagRecursively(parser.internal, new Debugging(context))
8181
val resetter: Parsley[Unit] = fresh(context.reset()).impure
@@ -107,7 +107,7 @@ object combinator {
107107
* @tparam A Output type of original parser.
108108
* @return A pair of the finalised tree, and the instrumented parser.
109109
*/
110-
private [parsley] def attachDebugger[A](parser: Parsley[A]): DebuggedPair[A] = attachDebugger[A](parser, DefaultStringRules)
110+
private [parsley] def attachDebugger[A](parser: Parsley[A]): DebuggedPair[A] = attachDebugger[A](parser, DefaultStringRules, SilentDebugView)
111111

112112
// $COVERAGE-OFF$
113113
/* Create a closure that freshly attaches a debugger to a parser every time it is called.
@@ -168,7 +168,7 @@ object combinator {
168168
* a call to [[Parsley.parse]] is made.
169169
*/
170170
def attach[A](parser: Parsley[A], view: DebugView, toStringRules: PartialFunction[Any, Boolean]): Parsley[A] = {
171-
val (tree, attached) = attachDebugger(parser, toStringRules)
171+
val (tree, attached) = attachDebugger(parser, toStringRules, view)
172172

173173
// Ideally, this should run 'attached', and render the tree regardless of the parser's success.
174174
val renderer = fresh {
@@ -262,6 +262,16 @@ object combinator {
262262
case _ => new Parsley(Named(parser.internal, name))
263263
}
264264

265+
/** Set a breakpoint on a parser to be used by RemoteView
266+
*
267+
* @param parser The parser to debug.
268+
* @param break Indicate whether to break on entry or exit to the parser.
269+
* @tparam A Output type of original parser.
270+
* @return A modified parser which will pause parsing and ask the view to render the produced
271+
* debug tree after a call to [[Parsley.parse]] is made.
272+
*/
273+
def break[A](parser: Parsley[A], break: Breakpoint): Parsley[A] = new Parsley(new RemoteBreak(parser.internal, break))
274+
265275
/** Dot accessor versions of the combinators. */
266276
implicit class DebuggerOps[A](par: Parsley[A]) {
267277
//def attachDebugger(toStringRules: PartialFunction[Any, Boolean]): DebuggedPair[A] = combinator.attachDebugger(par, toStringRules)
@@ -276,6 +286,7 @@ object combinator {
276286
def attachReusable(view: =>DebugView.Reusable): () => Parsley[A] = combinator.attachReusable(par, view, DefaultStringRules)
277287
//def attach(implicit view: DebugFrontend): Parsley[A] = combinator.attach(par, defaultRules)
278288
def named(name: String): Parsley[A] = combinator.named(par, name)
289+
def break(break: Breakpoint): Parsley[A] = combinator.break(par, break)
279290
}
280291
// $COVERAGE-ON$
281292

parsley-debug/shared/src/main/scala/parsley/debug/internal/DebugContext.scala

+44-1
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@ import scala.collection.mutable
1010
import parsley.XAssert
1111
import parsley.debug.ParseAttempt
1212
import parsley.internal.deepembedding.frontend.LazyParsley
13+
import parsley.internal.deepembedding.frontend.debug.RemoteBreak
14+
import parsley.debug.*
1315

1416
// Class used to hold details about a parser being debugged.
1517
// This is normally held as a value inside an implicit variable.
1618
// Anything caught by the toStringRules will have a parse result of that type toString-ed for memory
1719
// efficiency.
18-
private [parsley] class DebugContext(private val toStringRules: PartialFunction[Any, Boolean]) {
20+
private [parsley] class DebugContext(private val toStringRules: PartialFunction[Any, Boolean], private val view: DebugView) {
1921
// Create a new dummy root of the tree that will act as filler for the rest of the tree to build
2022
// off of (as there is no "nil" representation for the tree... other than null, which should be
2123
// avoided in Scala wherever possible).
@@ -75,12 +77,53 @@ private [parsley] class DebugContext(private val toStringRules: PartialFunction[
7577
uid
7678
}*/
7779

80+
/** The number of breakpoints to skip through.
81+
*
82+
* When breakpointSkips is zero, the next breakpoint will stop the parsing.
83+
*/
84+
private var breakpointSkips: Int = 0
85+
86+
/** Handle a breakpoint.
87+
*
88+
* @param tree The debug tree that has been created thus far.
89+
* @param fullInput The full parser input.
90+
* @param view The DebugView instance. This must extend DebugView.Pauseable to work.
91+
*/
92+
private def handleBreak(tree: TransientDebugTree, fullInput: String): Unit = view match {
93+
case view: DebugView.Pauseable => {
94+
if (breakpointSkips > 0) {
95+
breakpointSkips -= 1
96+
} else {
97+
breakpointSkips = view.renderWait(fullInput, tree)
98+
}
99+
}
100+
case _ =>
101+
}
102+
78103
// Push a new parser onto the parser callstack.
79104
def push(fullInput: String, parser: LazyParsley[_], userAssignedName: Option[String]): Unit = {
105+
// Send the debug tree here if EntryBreak
106+
parser match {
107+
case break: RemoteBreak[_] => break.break match {
108+
case EntryBreak | FullBreak => handleBreak(builderStack.head, fullInput)
109+
case _ =>
110+
}
111+
case _ =>
112+
}
113+
80114
val newTree = new TransientDebugTree(fullInput = fullInput)
81115
newTree.name = Renamer.nameOf(userAssignedName, parser)
82116
newTree.internal = Renamer.internalName(parser)
83117

118+
// Send the debug tree here if ExitBreak
119+
parser match {
120+
case break: RemoteBreak[_] => break.break match {
121+
case ExitBreak | FullBreak => handleBreak(newTree, fullInput)
122+
case _ =>
123+
}
124+
case _ =>
125+
}
126+
84127
//val uid = nextUid()
85128
//builderStack.head.children(s"${newTree.name}-#$uid") = newTree
86129
builderStack.head.children += newTree
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/*
2+
* Copyright 2020 Parsley Contributors <https://github.com/j-mie6/Parsley/graphs/contributors>
3+
*
4+
* SPDX-License-Identifier: BSD-3-Clause
5+
*/
6+
package parsley.internal.deepembedding.frontend.debug
7+
8+
import parsley.internal.deepembedding.backend.StrictParsley
9+
import parsley.internal.deepembedding.frontend.{LazyParsley, LazyParsleyIVisitor, Unary}
10+
import parsley.debug.Breakpoint
11+
12+
private [parsley] final class RemoteBreak[A](p: LazyParsley[A], val break: Breakpoint) extends Unary[A, A](p) {
13+
override def make(p: StrictParsley[A]): StrictParsley[A] = p
14+
15+
override def visit[T, U[+_]](visitor: LazyParsleyIVisitor[T, U], context: T): U[A] = visitor.visitUnknown(this, context) // or visitGeneric
16+
17+
private [parsley] var debugName: String = "remoteBreak"
18+
}

parsley-debug/shared/src/main/scala/parsley/internal/deepembedding/frontend/debug/TaggedWith.scala

+1
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ private [parsley] object TaggedWith {
221221
// parsley, and parsley does not expose naturally transparent combinators.
222222
case _ => throw new IllegalStateException("a transparent parser has been explicitly named, this is non-sensical")
223223
}
224+
case rb: RemoteBreak[A @unchecked] => visitUnary(rb, context)(rb.p)
224225
case _ => handleNoChildren(self, context)
225226
}
226227
}

parsley-debug/shared/src/test/scala/parsley/debug/DebuggerUsageSpec.scala

+2-2
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import parsley.internal.deepembedding.backend.debug.Debugging
2323
class DebuggerUsageSpec extends ParsleyTest {
2424
import DebuggerUsageSpec.Arithmetic
2525
"the Debugged internal frontend class" should "not allow nesting of Debugged nodes" in {
26-
val factory = new Debugging(new DebugContext(parsley.debug.combinator.DefaultStringRules))
26+
val factory = new Debugging(new DebugContext(parsley.debug.combinator.DefaultStringRules, SilentDebugView))
2727
try {
2828
val _ = new TaggedWith(factory)(new TaggedWith(factory)(fresh(()).internal, null, None), null, None)
2929
fail("Debugged nodes have been nested")
@@ -33,7 +33,7 @@ class DebuggerUsageSpec extends ParsleyTest {
3333
}
3434

3535
it should "preserve the prettified names of the parsers" in {
36-
val factory = new Debugging(new DebugContext(parsley.debug.combinator.DefaultStringRules))
36+
val factory = new Debugging(new DebugContext(parsley.debug.combinator.DefaultStringRules, SilentDebugView))
3737
new TaggedWith(factory)(named(fresh(()), "foo").internal, null, None).debugName shouldBe "foo"
3838
new TaggedWith(factory)(fresh(()).internal, null, None).debugName shouldBe "fresh"
3939
new TaggedWith(factory)(fresh(()).internal, null, Some("bar")).debugName shouldBe "bar"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
* Copyright 2020 Parsley Contributors <https://github.com/j-mie6/Parsley/graphs/contributors>
3+
*
4+
* SPDX-License-Identifier: BSD-3-Clause
5+
*/
6+
package parsley.debug
7+
8+
import parsley.Parsley
9+
import parsley.Parsley.*
10+
import parsley.character.string
11+
import parsley.syntax.character.stringLift
12+
import parsley.ParsleyTest
13+
import parsley.debug.combinator.DebuggerOps
14+
import org.scalatest.Assertions
15+
import org.typelevel.scalaccompat.annotation.unused
16+
17+
private [debug] class MockedDebugView(private val exp: Iterator[Int]) extends DebugView.Reusable with DebugView.Pauseable {
18+
override private [debug] def render(@unused input: =>String, @unused tree: =>DebugTree): Unit = ()
19+
override private [debug] def renderWait(@unused input: =>String, @unused tree: =>DebugTree): Int = if (exp.hasNext) exp.next() else Assertions.fail("Hit unexpected breakpoint")
20+
21+
private [debug] def checkMetExpectations(): Unit = if (exp.hasNext) Assertions.fail(s"Did not hit all breakpoints. Still expecting: (${exp.mkString(", ")})")
22+
}
23+
24+
class RemoteBreakSpec extends ParsleyTest {
25+
26+
/* The test runner handling mocking functionality given a parser set with breakpoints, the input, and breakpoint return values */
27+
private def testExpecting(expectations: Int*)(p: Parsley[_], input: String): Unit = {
28+
val mock = new MockedDebugView(expectations.iterator)
29+
p.attach(mock).parse(input)
30+
mock.checkMetExpectations()
31+
}
32+
33+
private def testExpectingNone = testExpecting() _
34+
35+
behavior of "Remote breakpoints"
36+
37+
it should "call renderWait after hitting a breakpoint" in {
38+
val p: Parsley[_] = "J"
39+
testExpecting(0)(p.break(EntryBreak), "J")
40+
testExpecting(0)(p.break(ExitBreak), "J")
41+
}
42+
43+
it should "not break when given NoBreak" in {
44+
val p: Parsley[_] = string("A").break(NoBreak)
45+
testExpectingNone(p, "A")
46+
}
47+
48+
it should "break twice when given FullBreak" in {
49+
val p: Parsley[_] = "M"
50+
testExpecting(0, 0)(p.break(FullBreak), "M")
51+
}
52+
53+
it should "skip one breakpoint" in {
54+
val p: Parsley[_] = string("I").break(ExitBreak)
55+
testExpecting(1)(p ~> p, "II")
56+
}
57+
58+
it should "skip breakpoints many times" in {
59+
val p1: Parsley[_] = string("E").break(FullBreak)
60+
val p2: Parsley[_] = p1 ~> p1 ~> p1
61+
testExpecting(1, 1, 1)(p2, "EEE")
62+
}
63+
64+
it should "skip many breakpoints at once" in {
65+
val p1: Parsley[_] = string("!").break(FullBreak)
66+
val p2: Parsley[_] = p1 ~> p1 ~> p1
67+
testExpecting(7)(p2.break(FullBreak), "!!!")
68+
}
69+
70+
it should "never break if the parser wasn't reached" in {
71+
val p: Parsley[_] = "1" ~> string("2").break(FullBreak)
72+
testExpectingNone(p, "02")
73+
}
74+
75+
it should "break many times with iterative combinators" in {
76+
val p: Parsley[_] = string("5").break(EntryBreak)
77+
testExpecting(0, 0, 0, 0)(many(p), "555")
78+
}
79+
80+
it should "stay silent after skipping more breakpoints than there are" in {
81+
val p: Parsley[_] = string(".").break(FullBreak)
82+
testExpecting(10)(p ~> p, "..")
83+
}
84+
}

parsley-debug/shared/src/test/scala/parsley/internal/deepembedding/RenameSpec.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class RenameSpec extends ParsleyTest {
3939

4040
it should "pass through Debugged parsers and get the inner parser's name" in {
4141
val symbolic = new <**>
42-
val debugged = new TaggedWith[Any](new Debugging(new DebugContext(parsley.debug.combinator.DefaultStringRules)))(symbolic, symbolic, None)
42+
val debugged = new TaggedWith[Any](new Debugging(new DebugContext(parsley.debug.combinator.DefaultStringRules, parsley.debug.SilentDebugView)))(symbolic, symbolic, None)
4343

4444
Renamer.nameOf(None, debugged) shouldBe "<**>"
4545
}

0 commit comments

Comments
 (0)