Skip to content

Commit 0fa4155

Browse files
authored
Dev state management (#265)
Changes - State management: Manageable subtrait of DebugView which allows sending, receiving and setting Refs using the codecs - Breakpoints: Better written as a LazyParsley / StrictParsley - DebugTree: New field for highlighting specific nodes
2 parents c84d5d3 + 0b6ddb7 commit 0fa4155

File tree

12 files changed

+548
-62
lines changed

12 files changed

+548
-62
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
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 scala.util.Try
9+
import parsley.state.Ref
10+
11+
abstract class RefCodec {
12+
type A
13+
14+
val ref: Ref[A]
15+
val codec: Codec[A]
16+
}
17+
18+
object RefCodec {
19+
20+
// Ref address and String value passed to RemoteView
21+
type CodedRef = (Int, String)
22+
23+
}
24+
25+
26+
/** An encoder-decoder for `A` and a string representation
27+
*
28+
* @tparam A the type being encoded and decoded
29+
*/
30+
trait Codec[A] {
31+
/** Encode `a` into a string
32+
*
33+
* @param a the value to encode
34+
* @return the string encoding of `a`
35+
*/
36+
def encode(a: A): String
37+
38+
/** Attempt to decode a string into type `A`
39+
*
40+
* @param s the string to decode
41+
* @return a `Option[A]` containing the decoded value if successful
42+
*/
43+
def decode(s: String): Option[A]
44+
}
45+
46+
object Codec {
47+
implicit val booleanCodec: Codec[Boolean] = new Codec[Boolean] {
48+
def encode(b: Boolean): String = b.toString
49+
50+
def decode(s: String): Option[Boolean] = Try(s.toBoolean).toOption
51+
}
52+
53+
implicit val byteCodec: Codec[Byte] = new Codec[Byte] {
54+
def encode(x: Byte): String = x.toString
55+
56+
def decode(s: String): Option[Byte] = Try(s.toByte).toOption
57+
}
58+
59+
implicit val shortCodec: Codec[Short] = new Codec[Short] {
60+
def encode(x: Short): String = x.toString
61+
62+
def decode(s: String): Option[Short] = Try(s.toShort).toOption
63+
}
64+
65+
implicit val intCodec: Codec[Int] = new Codec[Int] {
66+
def encode(x: Int): String = x.toString
67+
68+
def decode(s: String): Option[Int] = Try(s.toInt).toOption
69+
}
70+
71+
implicit val longCodec: Codec[Long] = new Codec[Long] {
72+
def encode(x: Long): String = x.toString
73+
74+
def decode(s: String): Option[Long] = Try(s.toLong).toOption
75+
}
76+
77+
implicit val floatCodec: Codec[Float] = new Codec[Float] {
78+
def encode(x: Float): String = x.toString
79+
80+
def decode(s: String): Option[Float] = Try(s.toFloat).toOption
81+
}
82+
83+
implicit val doubleCodec: Codec[Double] = new Codec[Double] {
84+
def encode(x: Double): String = x.toString
85+
86+
def decode(s: String): Option[Double] = Try(s.toDouble).toOption
87+
}
88+
89+
implicit val charCodec: Codec[Char] = new Codec[Char] {
90+
def encode(c: Char): String = c.toString
91+
92+
def decode(s: String): Option[Char] = if (s.length() == 1) Some(s.charAt(0)) else None
93+
}
94+
95+
implicit val stringCodec: Codec[String] = new Codec[String] {
96+
def encode(s: String): String = s
97+
98+
def decode(s: String): Option[String] = Some(s)
99+
}
100+
}

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

+3
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ private [debug] abstract class DebugTree extends Iterable[DebugTree] {
5050
/** Is this parser iterative? */
5151
def isIterative: Boolean
5252

53+
/** Has this node been generated since the last time it hit a breakpoint */
54+
def isNewlyGenerated: Boolean
55+
5356
// $COVERAGE-OFF$
5457
/** Provides a depth-first view of the tree as an iterator. */
5558
override def iterator: Iterator[DebugTree] = Iterator(this) ++ nodeChildren.iterator.flatMap(_.iterator)

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

+12-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66
package parsley.debug
77

8+
import parsley.debug.RefCodec.CodedRef
89
import parsley.debug.internal.XIllegalStateException
910
import org.typelevel.scalaccompat.annotation.unused
1011

@@ -54,7 +55,17 @@ object DebugView {
5455
* n == 0 to just step through this breakpoint.
5556
* n >= 1 to step through the next n breakpoints.
5657
*/
57-
private [debug] def renderWait(input: => String, tree: => DebugTree): Int
58+
private [debug] def renderWait(input: => String, tree: => DebugTree): Int
59+
}
60+
61+
/** Signifies that the debug view inheriting from this can wait on a certain render call, updating the state of Refs.
62+
*
63+
* This can be extended to make remote state management, breakpoint stepping possible.
64+
*
65+
* @see [[DebugView]]
66+
*/
67+
trait Manageable extends DebugView with Pauseable {
68+
private [debug] def renderManage(input: => String, tree: => DebugTree, refs: CodedRef*): (Int, Seq[CodedRef])
5869
}
5970

6071
/** Signifies that the debug view inheriting from this can only be run once.

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

+5-3
Original file line numberDiff line numberDiff line change
@@ -266,13 +266,15 @@ object combinator {
266266
*
267267
* @param parser The parser to debug.
268268
* @param break Indicate whether to break on entry or exit to the parser.
269+
* @param refs Stateful references to be updated.
269270
* @tparam A Output type of original parser.
270271
* @return A modified parser which will pause parsing and ask the view to render the produced
271272
* debug tree after a call to [[Parsley.parse]] is made.
272273
*/
273-
def break[A](parser: Parsley[A], break: Breakpoint): Parsley[A] = new Parsley(new RemoteBreak(parser.internal, break))
274274

275-
/** Dot accessor versions of the combinators. */
275+
def break[A](parser: Parsley[A], break: Breakpoint, refs: RefCodec*): Parsley[A] = new Parsley(new RemoteBreak(parser.internal, break, refs*))
276+
277+
/** Dot accessor versions of the combinators. */
276278
implicit class DebuggerOps[A](par: Parsley[A]) {
277279
//def attachDebugger(toStringRules: PartialFunction[Any, Boolean]): DebuggedPair[A] = combinator.attachDebugger(par, toStringRules)
278280
//def attachReusable(toStringRules: PartialFunction[Any, Boolean]): () => DebuggedPair[A] = combinator.attachReusable(par, toStringRules)
@@ -286,7 +288,7 @@ object combinator {
286288
def attachReusable(view: =>DebugView.Reusable): () => Parsley[A] = combinator.attachReusable(par, view, DefaultStringRules)
287289
//def attach(implicit view: DebugFrontend): Parsley[A] = combinator.attach(par, defaultRules)
288290
def named(name: String): Parsley[A] = combinator.named(par, name)
289-
def break(break: Breakpoint): Parsley[A] = combinator.break(par, break)
291+
def break(break: Breakpoint, refs: RefCodec*): Parsley[A] = combinator.break(par, break, refs*)
290292
}
291293
// $COVERAGE-ON$
292294

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

+56-30
Original file line numberDiff line numberDiff line change
@@ -10,13 +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.*
13+
import parsley.debug.DebugView
14+
import parsley.debug.RefCodec.CodedRef
1515

1616
// Class used to hold details about a parser being debugged.
1717
// This is normally held as a value inside an implicit variable.
1818
// Anything caught by the toStringRules will have a parse result of that type toString-ed for memory
1919
// efficiency.
20+
2021
private [parsley] class DebugContext(private val toStringRules: PartialFunction[Any, Boolean], private val view: DebugView) {
2122
// Create a new dummy root of the tree that will act as filler for the rest of the tree to build
2223
// off of (as there is no "nil" representation for the tree... other than null, which should be
@@ -45,7 +46,7 @@ private [parsley] class DebugContext(private val toStringRules: PartialFunction[
4546
XAssert.assert(!(ch.size > 1), s"The root tree has somehow gained multiple children. (${ch.size})")
4647

4748
// This should never fail.
48-
ch.head
49+
ch.head.withoutBreakpoints().withoutNewlyGeneratedFlags()
4950
}
5051

5152
// Add an attempt of parsing at the current stack point.
@@ -83,48 +84,73 @@ private [parsley] class DebugContext(private val toStringRules: PartialFunction[
8384
*/
8485
private var breakpointSkips: Int = 0
8586

86-
/** Handle a breakpoint.
87+
/** True if associated DebugView extends the Manageable trait */
88+
def manageableView: Boolean = view match {
89+
case _: DebugView.Manageable => true
90+
case _ => false
91+
}
92+
93+
private var firstBreakpoint: Boolean = true
94+
95+
/** Trigger a breakpoint.
8796
*
88-
* @param tree The debug tree that has been created thus far.
8997
* @param fullInput The full parser input.
90-
* @param view The DebugView instance. This must extend DebugView.Pauseable to work.
98+
* @param refs References managed by this breakpoint.
9199
*/
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)
100+
def triggerBreak(fullInput: String, isAfter: Boolean, codedRefs: Option[Seq[CodedRef]]): Option[Seq[CodedRef]] = {
101+
if (firstBreakpoint) {
102+
builderStack.head.resetNewlyGeneratedFlags()
103+
firstBreakpoint = false
104+
}
105+
106+
val debugTree: TransientDebugTree = (if (isAfter) builderStack.head else {
107+
// FIXME: Instead of `init`, this should take(n) where n is the number of children the next node would have.
108+
// This can only be known after the next parser runs, and needs to be passed in after saving/restoring the builderStack
109+
builderStack.head.copy(children = builderStack.tail.init)
110+
}).copy(parse = Some(new ParseAttempt("", 0, 0, (0,0), (0,0), Some(()))))
111+
112+
113+
val newRefs: Option[Seq[CodedRef]] = view match {
114+
case view: DebugView.Pauseable => {
115+
if (breakpointSkips > 0) { // Skip to next breakpoint
116+
breakpointSkips -= 1
117+
None
118+
} else if (breakpointSkips != -1) { // Breakpoint exit
119+
view match {
120+
case view: DebugView.Manageable => {
121+
122+
// Wait for RemoteView to return breakpoint skips and updated state
123+
val (newSkips, newRefs): (Int, Seq[CodedRef]) = view.renderManage(fullInput, debugTree, codedRefs.get*)
124+
125+
// Update breakpoint skips
126+
breakpointSkips = newSkips
127+
Some(newRefs)
128+
}
129+
130+
// Update breakpoint using Pausable render call
131+
case _ => {
132+
breakpointSkips = view.renderWait(fullInput, debugTree)
133+
None
134+
}
135+
}
136+
} else None
98137
}
138+
139+
case _ => None
99140
}
100-
case _ =>
141+
142+
debugTree.resetNewlyGeneratedFlags()
143+
newRefs
101144
}
102145

103146
// Push a new parser onto the parser callstack.
104147
def push(fullInput: String, parser: LazyParsley[_], isIterative: Boolean, 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-
}
113148

114149
val newTree = new TransientDebugTree(fullInput = fullInput)
115150
newTree.name = Renamer.nameOf(userAssignedName, parser)
116151
newTree.internal = Renamer.internalName(parser)
117152
newTree.iterative = isIterative
118153

119-
// Send the debug tree here if ExitBreak
120-
parser match {
121-
case break: RemoteBreak[_] => break.break match {
122-
case ExitBreak | FullBreak => handleBreak(newTree, fullInput)
123-
case _ =>
124-
}
125-
case _ =>
126-
}
127-
128154
//val uid = nextUid()
129155
//builderStack.head.children(s"${newTree.name}-#$uid") = newTree
130156
builderStack.head.children += newTree

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

+28-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import parsley.debug.{DebugTree, ParseAttempt}
2222
private [parsley] class TransientDebugTree(var name: String = "", var internal: String = "", val fullInput: String,
2323
var parse: Option[ParseAttempt] = None, var cNumber: Option[Long] = None,
2424
val children: mutable.ListBuffer[TransientDebugTree] = mutable.ListBuffer.empty,
25-
var iterative: Boolean = false) extends DebugTree {
25+
var iterative: Boolean = false, var newlyGenerated: Boolean = true) extends DebugTree {
2626
// These are user-facing, and will depend heavily on what the parser looks like.
2727
// $COVERAGE-OFF$
2828
override def parserName: String = name
@@ -44,6 +44,8 @@ private [parsley] class TransientDebugTree(var name: String = "", var internal:
4444

4545
override def isIterative: Boolean = iterative
4646

47+
override def isNewlyGenerated: Boolean = newlyGenerated
48+
4749
// Factors out inputs or results for parsers with children.
4850
private type Augment = (Long, (Int, Int))
4951
private var augmentId = 0L
@@ -76,4 +78,29 @@ private [parsley] class TransientDebugTree(var name: String = "", var internal:
7678
})
7779
}
7880
}
81+
82+
// Make a copy with possibly different fields
83+
private [debug] def copy(name: String = name, internal: String = internal, fullInput: String = fullInput,
84+
parse: Option[ParseAttempt] = parse, cNumber: Option[Long] = cNumber,
85+
children: mutable.ListBuffer[TransientDebugTree] = children,
86+
iterative: Boolean = iterative, newlyGenerated: Boolean = newlyGenerated): TransientDebugTree
87+
= new TransientDebugTree(name, internal, fullInput, parse, cNumber, children, iterative, isNewlyGenerated)
88+
89+
// Strips all `remoteBreak` nodes from the tree
90+
private [debug] def withoutBreakpoints(): TransientDebugTree = {
91+
val childrenWithoutBreak = children.map(_.withoutBreakpoints())
92+
if (internalName == "remoteBreak") {
93+
childrenWithoutBreak.head // Debug nodes *should* only ever have one and only one child
94+
} else new TransientDebugTree(name, internal, fullInput, parse, cNumber, childrenWithoutBreak, iterative)
95+
}
96+
97+
private [debug] def resetNewlyGeneratedFlags(): Unit = {
98+
newlyGenerated = false
99+
children.foreach(_.resetNewlyGeneratedFlags())
100+
}
101+
102+
private [debug] def withoutNewlyGeneratedFlags(): TransientDebugTree = {
103+
resetNewlyGeneratedFlags()
104+
this
105+
}
79106
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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.backend
7+
8+
import parsley.debug.{Breakpoint, EntryBreak, ExitBreak, FullBreak, RefCodec}
9+
import parsley.debug.internal.DebugContext
10+
import parsley.internal.deepembedding.ContOps
11+
import parsley.internal.deepembedding.ContOps.{suspend, ContAdapter}
12+
import parsley.internal.machine.instructions.debug.TriggerBreakpoint
13+
14+
private [deepembedding] final class InertBreak[A](p: StrictParsley[A], break: Breakpoint, refs: RefCodec*) extends StrictParsley[A] {
15+
override protected[backend] def codeGen[M[_, +_]: ContOps, R](producesResults: Boolean)(implicit instrs: StrictParsley.InstrBuffer, state: CodeGenState): M[R,Unit] = p.codeGen(producesResults)
16+
17+
override private [deepembedding] def inlinable: Boolean = p.inlinable
18+
19+
override private [deepembedding] def pretty: String = f"inertBreak(${p.pretty})"
20+
21+
private [deepembedding] def activate(dbgCtx: DebugContext) = new ActiveBreak[A](p, break, dbgCtx, refs*)
22+
}
23+
24+
private [deepembedding] final class ActiveBreak[A](p: StrictParsley[A], break: Breakpoint, dbgCtx: DebugContext, refs: RefCodec*) extends StrictParsley[A] {
25+
override protected[backend] def codeGen[M[_, +_]: ContOps, R](producesResults: Boolean)(implicit instrs: StrictParsley.InstrBuffer, state: CodeGenState): M[R,Unit] = {
26+
break match {
27+
case EntryBreak | FullBreak => instrs += new TriggerBreakpoint(dbgCtx, false, refs*)
28+
case _ =>
29+
}
30+
suspend[M, R, Unit](p.codeGen[M, R](producesResults)) |> {
31+
break match {
32+
case ExitBreak | FullBreak => instrs += new TriggerBreakpoint(dbgCtx, true, refs*)
33+
case _ =>
34+
}
35+
}
36+
}
37+
38+
override private [deepembedding] def inlinable: Boolean = p.inlinable
39+
40+
override private [deepembedding] def pretty: String = f"activeBreak(${p.pretty})"
41+
}

0 commit comments

Comments
 (0)