This is an automated email from the ASF dual-hosted git repository.
arosien pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/daffodil-vscode.git
The following commit(s) were added to refs/heads/main by this push:
new 188dd3f Refactor to fix event delivery guarantees.
188dd3f is described below
commit 188dd3fa3c368c470fa802d8dde65774ba46fa52
Author: Adam Rosien <[email protected]>
AuthorDate: Fri Apr 26 09:32:09 2024 -0700
Refactor to fix event delivery guarantees.
Events were emitted, or sometimes not emitted at all, before state changes
were committed.
Events were emitted, or sometimes not emitted at all, before state changes
were committed.
Some refactorings to both directly fix this and also improve the code:
- New `Parse.Deliver` class encapsulates translation of `Parse.Event`s into
DAP state and events, delivered via the `Debugee` interface. It also ensures
state is delivered before events so that consumers can react to events and read
the correct state values. `Parse.Deliver` also fixes the bug of not delivering
certain events, like the data position upon the "continue" action, by
delivering events at every kind of stopping action, which includes "step",
"continue", and a hit breakpoint.
- Remove `Debugee.state` and have the `Debugee` implementation emit the DAP
events directly via `Debugee.events`.
- Instead of using `None`-terminated `Queue`, which enqueues with `Some`
and terminates with `None`, use the `Channel` data type which only uses the
actual message type.
- To lift an `IO` into a `Resource`, replace `Resource.eval(io)` with
`io.toResource`.
- Fix Daffodil 3.7.0 upgrade problems.
Fixes #995.
---
.../org.apache.daffodil.debugger.dap/DAPodil.scala | 41 +-
.../org.apache.daffodil.debugger.dap/Parse.scala | 770 +++++++++++----------
.../org.apache.daffodil.debugger.dap/logging.scala | 2 +
3 files changed, 409 insertions(+), 404 deletions(-)
diff --git
a/debugger/src/main/scala/org.apache.daffodil.debugger.dap/DAPodil.scala
b/debugger/src/main/scala/org.apache.daffodil.debugger.dap/DAPodil.scala
index 59aa36e..8b61078 100644
--- a/debugger/src/main/scala/org.apache.daffodil.debugger.dap/DAPodil.scala
+++ b/debugger/src/main/scala/org.apache.daffodil.debugger.dap/DAPodil.scala
@@ -30,7 +30,7 @@ import com.microsoft.java.debug.core.protocol.Responses._
import com.monovore.decline.Opts
import com.monovore.decline.effect.CommandIOApp
import fs2._
-import fs2.concurrent.Signal
+import fs2.concurrent._
import java.io._
import java.net._
import java.nio.file.Path
@@ -82,9 +82,9 @@ object DAPSession {
def resource(socket: Socket): Resource[IO, DAPSession[Request, Response,
DebugEvent]] =
for {
dispatcher <- Dispatcher.parallel[IO]
- requests <- Resource.eval(Queue.bounded[IO, Option[Request]](10))
+ requests <- Channel.bounded[IO, Request](10).toResource
server <- Server.resource(socket.getInputStream, socket.getOutputStream,
dispatcher, requests)
- session = DAPSession(server, Stream.fromQueueNoneTerminated(requests))
+ session = DAPSession(server, requests.stream)
} yield session
/** Wraps an AbstractProtocolServer into an IO-based interface. */
@@ -92,13 +92,13 @@ object DAPSession {
in: InputStream,
out: OutputStream,
dispatcher: Dispatcher[IO],
- requests: QueueSink[IO, Option[Request]]
+ requests: Channel[IO, Request]
) extends AbstractProtocolServer(in, out) {
def dispatchRequest(request: Request): Unit =
dispatcher.unsafeRunSync {
for {
_ <- Logger[IO].info(show"R> $request")
- _ <- requests.offer(Some(request)).recoverWith {
+ _ <- requests.send(request).recoverWith {
// format: off
case t => Logger[IO].error(t)(show"error during handling of
request $request")
// format: on
@@ -114,10 +114,10 @@ object DAPSession {
in: InputStream,
out: OutputStream,
dispatcher: Dispatcher[IO],
- requests: QueueSink[IO, Option[Request]]
+ requests: Channel[IO, Request]
): Resource[IO, AbstractProtocolServer] =
Resource
- .make(IO(new Server(in, out, dispatcher, requests)))(server =>
IO(server.stop()) *> requests.offer(None).void)
+ .make(IO(new Server(in, out, dispatcher, requests)))(server =>
IO(server.stop()) *> requests.close.void)
.flatTap(server => IO.blocking(server.run).background)
}
@@ -514,11 +514,11 @@ object DAPodil extends IOApp {
debugee: Request => EitherNel[String, Resource[IO, Debugee]]
): Resource[IO, IO[Done]] =
for {
- state <- Resource.eval(Ref[IO].of[State](State.Uninitialized))
+ state <- Ref[IO].of[State](State.Uninitialized).toResource
hotswap <- Hotswap
.create[IO, State]
.onFinalizeCase(ec => Logger[IO].debug(s"hotswap: $ec"))
- whenDone <- Resource.eval(Deferred[IO, Done])
+ whenDone <- Deferred[IO, Done].toResource
dapodil = new DAPodil(
session,
state,
@@ -543,7 +543,6 @@ object DAPodil extends IOApp {
trait Debugee {
// TODO: extract "control" interface from "state" interface
def data(): Signal[IO, Data]
- def state(): Stream[IO, Debugee.State]
def events(): Stream[IO, Events.DebugEvent]
def sources(): IO[List[Source]]
@@ -571,6 +570,11 @@ object DAPodil extends IOApp {
case class Stopped(reason: Stopped.Reason) extends State
object Stopped {
+ def entry: Stopped = Stopped(Reason.Entry)
+ def pause: Stopped = Stopped(Reason.Pause)
+ def step: Stopped = Stopped(Reason.Step)
+ def breakpointHit(location: DAPodil.Location): Stopped =
Stopped(Reason.BreakpointHit(location))
+
sealed trait Reason
object Reason {
@@ -628,24 +632,11 @@ object DAPodil extends IOApp {
.lastOrError
.onFinalizeCase(ec => Logger[IO].debug(s"launched: $ec"))
- _ <- Resource.eval(session.sendEvent(new
Events.ThreadEvent("started", 1L)))
+ _ <- session.sendEvent(new Events.ThreadEvent("started",
1L)).toResource
} yield launched
}
def deliverEvents(debugee: Debugee, session: DAPSession[Request, Response,
DebugEvent]): Stream[IO, Unit] = {
- val stoppedEventsDelivery = debugee.state
- .collect {
- case Debugee.State.Stopped(Debugee.State.Stopped.Reason.Entry) =>
- new Events.StoppedEvent("entry", 1L)
- case Debugee.State.Stopped(Debugee.State.Stopped.Reason.Pause) =>
- new Events.StoppedEvent("pause", 1L)
- case Debugee.State.Stopped(Debugee.State.Stopped.Reason.Step) =>
- new Events.StoppedEvent("step", 1L)
- case
Debugee.State.Stopped(Debugee.State.Stopped.Reason.BreakpointHit(location)) =>
- new Events.StoppedEvent("breakpoint", 1L, false, show"Breakpoint
hit at $location", null)
- }
- .onFinalizeCase(ec => Logger[IO].debug(s"deliverStoppedEvents: $ec"))
-
val dapEvents = debugee.events
.onFinalizeCase(ec => Logger[IO].debug(s"dapEvents: $ec"))
@@ -653,7 +644,7 @@ object DAPodil extends IOApp {
.map(source => DAPodil.LoadedSourceEvent("changed", source.toDAP))
.onFinalizeCase(ec => Logger[IO].debug(s"sourceEventsDelivery: $ec"))
- Stream(stoppedEventsDelivery, dapEvents,
sourceEventsDelivery).parJoinUnbounded
+ Stream(dapEvents, sourceEventsDelivery).parJoinUnbounded
.evalMap(session.sendEvent)
.onFinalize(
session.sendEvent(new Events.ThreadEvent("exited", 1L)) *>
diff --git
a/debugger/src/main/scala/org.apache.daffodil.debugger.dap/Parse.scala
b/debugger/src/main/scala/org.apache.daffodil.debugger.dap/Parse.scala
index 1f5f15e..8c7aa58 100644
--- a/debugger/src/main/scala/org.apache.daffodil.debugger.dap/Parse.scala
+++ b/debugger/src/main/scala/org.apache.daffodil.debugger.dap/Parse.scala
@@ -34,7 +34,6 @@ import java.nio.file._
import org.apache.commons.io.FileUtils
import org.apache.daffodil.debugger.dap.{BuildInfo => DAPBuildInfo}
import org.apache.daffodil.runtime1.debugger.Debugger
-import org.apache.daffodil.runtime1.api.InfosetElement
import org.apache.daffodil.runtime1.infoset.{DIDocument, DIElement,
InfosetWalker}
import org.apache.daffodil.runtime1.processors._
import org.apache.daffodil.runtime1.processors.dfa.DFADelimiter
@@ -50,6 +49,7 @@ import org.typelevel.log4cats.slf4j.Slf4jLogger
import scala.collection.JavaConverters._
import scala.util.Try
+/** A Daffodil parse of a schema against data. */
trait Parse {
/** Run the parse, returning the bytes of the final infoset. */
@@ -70,6 +70,7 @@ object Parse {
.mkString("\n")
)
+ /** Create a parse using the given schema, data, etc. */
def apply(
schema: Path,
data: InputStream,
@@ -132,18 +133,14 @@ object Parse {
schema: DAPodil.Source,
data: DAPodil.Source,
outputData: Signal[IO, DAPodil.Data],
- outputState: Stream[IO, DAPodil.Debugee.State],
- stateSink: QueueSink[IO, Option[DAPodil.Debugee.State]],
events: Stream[IO, Events.DebugEvent],
breakpoints: Breakpoints,
- control: Control
+ control: Control,
+ parseEvents: Channel[IO, Parse.Event]
) extends DAPodil.Debugee {
def data(): Signal[IO, DAPodil.Data] =
outputData
- def state(): Stream[IO, DAPodil.Debugee.State] =
- outputState
-
def events(): Stream[IO, Events.DebugEvent] =
events
@@ -160,23 +157,26 @@ object Parse {
Stream.empty
def step(): IO[Unit] =
- control.step() *> stateSink.offer(
- Some(
- DAPodil.Debugee.State
- .Stopped(DAPodil.Debugee.State.Stopped.Reason.Step)
+ control.step() *> parseEvents
+ .send(
+ Parse.Event
+ .Control(DAPodil.Debugee.State.Stopped.step)
)
- )
+ .void
def continue(): IO[Unit] =
- control.continue() *>
stateSink.offer(Some(DAPodil.Debugee.State.Running))
+ control.continue() *>
+
parseEvents.send(Parse.Event.Control(DAPodil.Debugee.State.Running)).void
def pause(): IO[Unit] =
- control.pause() *> stateSink.offer(
- Some(
- DAPodil.Debugee.State
- .Stopped(DAPodil.Debugee.State.Stopped.Reason.Pause)
+ control.pause() *> parseEvents
+ .send(
+ Parse.Event
+ .Control(
+ DAPodil.Debugee.State.Stopped.pause
+ )
)
- )
+ .void
def setBreakpoints(uri: URI, lines: List[DAPodil.Line]): IO[Unit] =
breakpoints.setBreakpoints(uri, lines)
@@ -201,6 +201,7 @@ object Parse {
sealed trait LaunchArgs
object LaunchArgs {
+ // TODO: data type for infosetFormat
case class Manual(
schemaPath: Path,
dataPath: Path,
@@ -740,57 +741,31 @@ object Parse {
def debugee(args: Debugee.LaunchArgs.Manual): Resource[IO, DAPodil.Debugee] =
for {
- data <- Resource.eval(Queue.bounded[IO, Option[DAPodil.Data]](10))
- state <- Resource.eval(Queue.bounded[IO,
Option[DAPodil.Debugee.State]](10))
- dapEvents <- Resource.eval(Queue.bounded[IO,
Option[Events.DebugEvent]](10))
- breakpoints <- Resource.eval(Breakpoints())
- infoset <- Resource.eval(
- Queue.bounded[IO, Option[String]](10)
- ) // TODO: it's a bit incongruous to have a separate channel for infoset
changes, vs. streaming Parse.Event values
- control <- Resource.eval(Control.stopped())
-
- latestData <- Stream
- .fromQueueNoneTerminated(data)
- .holdResource(DAPodil.Data.empty)
-
- latestInfoset <- Resource.eval(SignallingRef[IO, String](""))
- infosetChanges = Stream
- .fromQueueNoneTerminated(infoset)
- .evalTap(latestInfoset.set)
- .evalTap(content =>
- dapEvents.offer(
- Some(
- InfosetEvent(
- content,
- args.infosetFormat match {
- case "xml" => "text/xml"
- case "json" => "application/json"
- }
- )
- )
- )
- )
- .onFinalizeCase(ec => Logger[IO].debug(s"infosetChanges (orig): $ec"))
-
- events <- Resource.eval(Queue.bounded[IO, Option[Event]](10))
+ data <- Channel.bounded[IO, DAPodil.Data](10).toResource
+ dapEvents <- Channel.bounded[IO, Events.DebugEvent](10).toResource
+ breakpoints <- Breakpoints().toResource
+ control <- Control.stopped().toResource
+ events <- Channel.bounded[IO, Event](10).toResource
debugger <- DaffodilDebugger
- .resource(state, events, breakpoints, control, infoset,
args.infosetFormat)
- parse <- Resource.eval(
- args.data.flatMap(in =>
- Parse(
- args.schemaPath,
- in,
- debugger,
- args.infosetFormat,
- args.rootName,
- args.rootNamespace,
- args.variables,
- args.tunables
+ .resource(events, breakpoints, control, args.infosetFormat)
+ parse <-
+ args.data
+ .flatMap(in =>
+ Parse(
+ args.schemaPath,
+ in,
+ debugger,
+ args.infosetFormat,
+ args.rootName,
+ args.rootNamespace,
+ args.variables,
+ args.tunables
+ )
)
- )
- )
+ .toResource
- parsing = args.infosetOutput match {
+ // run the parse, handling the final output (the infoset) in various ways
+ parsing = (args.infosetOutput match {
case Debugee.LaunchArgs.InfosetOutput.None =>
parse.run().drain
case Debugee.LaunchArgs.InfosetOutput.Console =>
@@ -800,74 +775,57 @@ object Parse {
.foldMonoid
.evalTap(xml =>
Logger[IO].debug("done collecting infoset XML output") *>
-
dapEvents.offer(Some(Events.OutputEvent.createConsoleOutput(xml)))
+ dapEvents.send(Events.OutputEvent.createConsoleOutput(xml))
)
case Debugee.LaunchArgs.InfosetOutput.File(path) =>
parse
.run()
.through(fs2.io.file.Files[IO].writeAll(fs2.io.file.Path.fromNioPath(path)))
- }
-
- nextFrameId <- Resource.eval(
- Next.int.map(_.map(DAPodil.Frame.Id.apply)).flatTap(_.next())
- ) // `.flatTap(_.next())`: ids start at 1
- nextRef <- Resource.eval(
- Next.int.map(_.map(DAPodil.VariablesReference.apply)).flatTap(_.next())
- ) // `.flatTap(_.next())`: ids start at 1
-
- // convert Parse.Event values to DAPodil.Data values
- deliverParseData = Stream
- .fromQueueNoneTerminated(events)
- .evalTap {
- case start: Event.StartElement if start.isStepping =>
-
dapEvents.offer(Some(DataEvent(start.state.currentLocation.bytePos1b)))
- case _ => IO.unit
- }
- .through(fromParse(nextFrameId, nextRef))
- .enqueueNoneTerminated(data)
-
+ }).onFinalizeCase {
+ // ensure dapEvents is terminated when the parse is terminated
+ case Resource.ExitCase.Errored(e: Parse.Exception) =>
+ // TODO: when Parse.Exception has source coordinates, include it
into a more structured OutputEvent
+
dapEvents.send(Events.OutputEvent.createConsoleOutput(e.getMessage())) *>
+ dapEvents.close.void
+ case _ => dapEvents.close.void
+ }.onFinalizeCase(ec => Logger[IO].debug(s"parsing: $ec"))
+
+ latestData <- data.stream.holdResource(DAPodil.Data.empty)
debugee = new Debugee(
DAPodil.Source(args.schemaPath, None),
DAPodil.Source(args.dataPath, None),
latestData,
- Stream.fromQueueNoneTerminated(state),
- state,
- Stream.fromQueueNoneTerminated(dapEvents),
+ dapEvents.stream,
breakpoints,
- control
+ control,
+ events
)
- startup = dapEvents.offer(Some(ConfigEvent(args))) *>
+ startup = dapEvents.send(ConfigEvent(args)) *>
(if (args.stopOnEntry)
- control.step() *> state.offer(
- Some(
- DAPodil.Debugee.State
- .Stopped(DAPodil.Debugee.State.Stopped.Reason.Entry)
- )
- ) // don't use debugee.step as we need to send
Stopped.Reason.Entry, not Stopped.Reason.Step
+ control.step() *>
+
events.send(Parse.Event.Control(DAPodil.Debugee.State.Stopped.entry))
+ // don't use debugee.step as we need to send Stopped.entry, not
Stopped.step
else debugee.continue())
+ delivery <- Delivery.to(data, dapEvents).toResource
+ deliverParseData =
+ events.stream
+ .through(delivery.deliver)
+ .onFinalize(data.close.void)
+ .onFinalizeCase {
+ case ec @ Resource.ExitCase.Errored(e) =>
+ Logger[IO].warn(e)(s"deliverParseData: $ec")
+ case ec => Logger[IO].debug(s"deliverParseData: $ec")
+ }
+
_ <- Stream
.emit(debugee)
.concurrently(
Stream(
Stream.eval(startup),
- // ensure dapEvents is terminated when the parse is terminated
- parsing
- .onFinalizeCase {
- case Resource.ExitCase.Errored(e: Parse.Exception) =>
- // TODO: when Parse.Exception has source coordinates,
include it into a more structured OutputEvent
-
dapEvents.offer(Some(Events.OutputEvent.createConsoleOutput(e.getMessage()))) *>
- dapEvents.offer(None)
- case _ => dapEvents.offer(None)
- }
- .onFinalizeCase(ec => Logger[IO].debug(s"parsing: $ec")),
- deliverParseData.onFinalizeCase {
- case ec @ Resource.ExitCase.Errored(e) =>
- Logger[IO].warn(e)(s"deliverParseData: $ec")
- case ec => Logger[IO].debug(s"deliverParseData: $ec")
- },
- infosetChanges
+ parsing,
+ deliverParseData
).parJoinUnbounded
)
.compile
@@ -877,213 +835,265 @@ object Parse {
_ <- Resource.onFinalize(Logger[IO].debug("signalling a stop") *>
parse.close())
} yield debugee
- /** Translate parse events to updated Daffodil state. */
- def fromParse(
+ /** Delivers data and events to a Debugee via channels. */
+ private class Delivery(
frameIds: Next[DAPodil.Frame.Id],
- variableRefs: Next[DAPodil.VariablesReference]
- ): Stream[IO, Parse.Event] => Stream[IO, DAPodil.Data] =
- events =>
- events.evalScan(DAPodil.Data.empty) {
- case (_, Parse.Event.Init(_)) => IO.pure(DAPodil.Data.empty)
- case (prev, startElement: Parse.Event.StartElement) =>
- createFrame(startElement, frameIds, variableRefs).map(prev.push)
- case (prev, Parse.Event.EndElement(_)) => IO.pure(prev.pop)
- case (prev, _: Parse.Event.Fini.type) => IO.pure(prev)
- }
+ variableRefs: Next[DAPodil.VariablesReference],
+ data: Channel[IO, DAPodil.Data],
+ dapEvents: Channel[IO, Events.DebugEvent]
+ ) {
+
+ /** Maintains state related to upstream events that need to be delivered.
+ * - DAPodil.Data updates are delivered when a new element is started.
+ * - We update the data position and infoset when we know it, but only
emit the corresponding DAPevents when a
+ * stop occurs.
+ */
+ def deliver(events: Stream[IO, Event]): Stream[IO, Nothing] =
+ events
+ .evalScan(Delivery.State.empty) {
+ case (state, Parse.Event.Init(_)) => IO.pure(state.copy(data =
DAPodil.Data.empty))
+ case (state, e: Event.StartElement) =>
+ for {
+ frame <- createFrame(e)
+ newState =
+ state.copy(
+ data = state.data.push(frame),
+ bytePos1b = e.state.currentLocation.bytePos1b,
+ infoset = e.infoset
+ )
+ _ <- data.send(newState.data)
+ } yield newState
+ case (state, Parse.Event.EndElement(_)) => IO.pure(state.copy(data =
state.data.pop))
+ case (state, _: Parse.Event.Fini.type) => IO.pure(state)
+ case (state, Event.Control(DAPodil.Debugee.State.Stopped(reason))) =>
+ val events =
+ List(
+ reason match {
+ case DAPodil.Debugee.State.Stopped.Reason.Entry =>
+ new Events.StoppedEvent("entry", 1L)
+ case DAPodil.Debugee.State.Stopped.Reason.Pause =>
+ new Events.StoppedEvent("pause", 1L)
+ case DAPodil.Debugee.State.Stopped.Reason.Step =>
+ new Events.StoppedEvent("step", 1L)
+ case
DAPodil.Debugee.State.Stopped.Reason.BreakpointHit(location) =>
+ new Events.StoppedEvent("breakpoint", 1L, false,
show"Breakpoint hit at $location", null)
+ },
+ DataEvent(state.bytePos1b)
+ ) ++ state.infoset.toList
+ events.traverse(dapEvents.send).as(state)
+ case (state, Event.Control(DAPodil.Debugee.State.Running)) =>
IO.pure(state)
+ }
+ .drain
- /** Transform Daffodil state to a DAP stack frame.
- *
- * @see
- *
https://microsoft.github.io/debug-adapter-protocol/specification#Types_StackFrame
- */
- def createFrame(
- startElement: Parse.Event.StartElement,
- frameIds: Next[DAPodil.Frame.Id],
- variableRefs: Next[DAPodil.VariablesReference]
- ): IO[DAPodil.Frame] =
- for {
- ids <- (frameIds.next, variableRefs.next, variableRefs.next,
variableRefs.next).tupled
- (frameId, parseScopeId, schemaScopeId, dataScopeId) = ids
-
- stackFrame = new Types.StackFrame(
- /* It must be unique across all threads.
- * This id can be used to retrieve the scopes of the frame with the
- * 'scopesRequest' or to restart the execution of a stackframe.
- */
- frameId.value,
- startElement.name.map(_.value).getOrElse("???"),
- /* If sourceReference > 0 the contents of the source must be retrieved
through
- * the SourceRequest (even if a path is specified). */
- Try(
- Paths
- .get(URI.create(startElement.schemaLocation.uriString))
- .toString()
+ /** Transform Daffodil state to a DAP stack frame.
+ *
+ * @see
+ *
https://microsoft.github.io/debug-adapter-protocol/specification#Types_StackFrame
+ */
+ def createFrame(startElement: Parse.Event.StartElement): IO[DAPodil.Frame]
=
+ for {
+ ids <- (frameIds.next, variableRefs.next, variableRefs.next,
variableRefs.next).tupled
+ (frameId, parseScopeId, schemaScopeId, dataScopeId) = ids
+
+ stackFrame = new Types.StackFrame(
+ /* It must be unique across all threads.
+ * This id can be used to retrieve the scopes of the frame with the
+ * 'scopesRequest' or to restart the execution of a stackframe.
+ */
+ frameId.value,
+ startElement.name.map(_.value).getOrElse("???"),
+ /* If sourceReference > 0 the contents of the source must be
retrieved through
+ * the SourceRequest (even if a path is specified). */
+ Try(
+ Paths
+ .get(URI.create(startElement.schemaLocation.uriString))
+ .toString()
+ )
+ .fold(
+ _ =>
+ new Types.Source(
+ startElement.schemaLocation.uriString,
+ null,
+ 0
+ ), // there is no valid path if the location is a schema
contained in a jar file; see #76.
+ path => new Types.Source(path, 0)
+ ),
+ startElement.schemaLocation.lineNumber
+ .map(_.toInt)
+ .getOrElse(1), // line numbers start at 1 according to
InitializeRequest
+ 0 // column numbers start at 1 according to InitializeRequest, but
set to 0 to ignore it; column calculation by Daffodil uses 1 tab = 2 spaces(?),
but breakpoints use 1 character per tab
)
- .fold(
- _ =>
- new Types.Source(
- startElement.schemaLocation.uriString,
- null,
- 0
- ), // there is no valid path if the location is a schema
contained in a jar file; see #76.
- path => new Types.Source(path, 0)
- ),
- startElement.schemaLocation.lineNumber
- .map(_.toInt)
- .getOrElse(1), // line numbers start at 1 according to
InitializeRequest
- 0 // column numbers start at 1 according to InitializeRequest, but set
to 0 to ignore it; column calculation by Daffodil uses 1 tab = 2 spaces(?), but
breakpoints use 1 character per tab
- )
- schemaScope <- schemaScope(schemaScopeId, startElement.state,
variableRefs)
- parseScope <- parseScope(parseScopeId, startElement, variableRefs)
- } yield DAPodil.Frame(
- frameId,
- stackFrame,
- List(
- parseScope,
- schemaScope,
- dataScope(dataScopeId, startElement.state)
+ schemaScope <- schemaScope(schemaScopeId, startElement.state)
+ parseScope <- parseScope(parseScopeId, startElement)
+ } yield DAPodil.Frame(
+ frameId,
+ stackFrame,
+ List(
+ parseScope,
+ schemaScope,
+ dataScope(dataScopeId, startElement.state)
+ )
)
- )
-
- def parseScope(
- ref: DAPodil.VariablesReference,
- event: Event.StartElement,
- refs: Next[DAPodil.VariablesReference]
- ): IO[DAPodil.Frame.Scope] =
- (refs.next, refs.next, refs.next, refs.next, refs.next).mapN {
- (pouRef, delimiterStackRef, diagnosticsRef, diagnosticsValidationsRef,
diagnosticsErrorsRef) =>
- val hidden = event.state.withinHiddenNest
- val childIndex = if (event.state.childPos != -1)
Some(event.state.childPos) else None
- val groupIndex = if (event.state.groupPos != -1)
Some(event.state.groupPos) else None
- val occursIndex = if (event.state.arrayIterationPos != -1)
Some(event.state.arrayIterationPos) else None
- val foundDelimiter = for {
- dpr <- event.state.delimitedParseResult.toScalaOption
- dv <- dpr.matchedDelimiterValue.toScalaOption
- } yield Misc.remapStringToVisibleGlyphs(dv)
- val foundField = for {
- dpr <- event.state.delimitedParseResult.toScalaOption
- f <- dpr.field.toScalaOption
- } yield Misc.remapStringToVisibleGlyphs(f)
-
- val pouRootVariable =
- new Types.Variable("Points of uncertainty", "", null, pouRef.value,
null)
- val pouVariables =
- event.pointsOfUncertainty.map(pou => new
Types.Variable(s"${pou.name.value}", s"${pou.context}"))
-
- val delimiterStackRootVariable =
- new Types.Variable("Delimiters", "", null, delimiterStackRef.value,
null)
- val delimiterStackVariables =
- event.delimiterStack.map(delimiter =>
- new
Types.Variable(s"${delimiter.kind}:${delimiter.value.delimType}",
s"${delimiter.value.lookingFor}")
+
+ def parseScope(
+ ref: DAPodil.VariablesReference,
+ event: Event.StartElement
+ ): IO[DAPodil.Frame.Scope] =
+ (variableRefs.next, variableRefs.next, variableRefs.next,
variableRefs.next, variableRefs.next).mapN {
+ (pouRef, delimiterStackRef, diagnosticsRef, diagnosticsValidationsRef,
diagnosticsErrorsRef) =>
+ val hidden = event.state.withinHiddenNest
+ val childIndex = if (event.state.childPos != -1)
Some(event.state.childPos) else None
+ val groupIndex = if (event.state.groupPos != -1)
Some(event.state.groupPos) else None
+ val occursIndex = if (event.state.arrayIterationPos != -1)
Some(event.state.arrayIterationPos) else None
+ val foundDelimiter = for {
+ dpr <- event.state.delimitedParseResult.toScalaOption
+ dv <- dpr.matchedDelimiterValue.toScalaOption
+ } yield Misc.remapStringToVisibleGlyphs(dv)
+ val foundField = for {
+ dpr <- event.state.delimitedParseResult.toScalaOption
+ f <- dpr.field.toScalaOption
+ } yield Misc.remapStringToVisibleGlyphs(f)
+
+ val pouRootVariable =
+ new Types.Variable("Points of uncertainty", "", null,
pouRef.value, null)
+ val pouVariables =
+ event.pointsOfUncertainty.map(pou => new
Types.Variable(s"${pou.name.value}", s"${pou.context}"))
+
+ val delimiterStackRootVariable =
+ new Types.Variable("Delimiters", "", null,
delimiterStackRef.value, null)
+ val delimiterStackVariables =
+ event.delimiterStack.map(delimiter =>
+ new
Types.Variable(s"${delimiter.kind}:${delimiter.value.delimType}",
s"${delimiter.value.lookingFor}")
+ )
+
+ val diagnosticsRootVariable =
+ new Types.Variable("Diagnostics", "", null, diagnosticsRef.value,
null)
+ val diagnosticsValidationsVariable =
+ new Types.Variable("Validations", "", null,
diagnosticsValidationsRef.value, null)
+ val diagnosticsErrorsVariable =
+ new Types.Variable("Errors", "", null, diagnosticsErrorsRef.value,
null)
+ // TODO: Get better values than toString() when
https://issues.apache.org/jira/browse/DAFFODIL-1200 is completed.
+ val diagnosticsValidations: List[Types.Variable] =
+ event.diagnostics.filter(_.isValidation).zipWithIndex.map { case
(diag, i) =>
+ new Types.Variable(i.toString,
diag.toString().replace("Validation Error: ", ""))
+ }
+ val diagnosticsErrors: List[Types.Variable] =
+ event.diagnostics.filterNot(_.isValidation).zipWithIndex.map {
case (diag, i) =>
+ new Types.Variable(i.toString, diag.toString())
+ }
+
+ val parseVariables: List[Types.Variable] =
+ (List(
+ new Types.Variable("hidden", hidden.toString, "bool", 0, null),
+ pouRootVariable,
+ delimiterStackRootVariable,
+ diagnosticsRootVariable
+ ) ++ childIndex.map(ci => new Types.Variable("childIndex",
ci.toString)).toList
+ ++ groupIndex
+ .map(gi => new Types.Variable("groupIndex", gi.toString))
+ .toList
+ ++ occursIndex
+ .map(oi => new Types.Variable("occursIndex", oi.toString))
+ .toList
+ ++ foundDelimiter.map(fd => new Types.Variable("foundDelimiter",
fd)).toList
+ ++ foundField.map(ff => new Types.Variable("foundField",
ff)).toList)
+ .sortBy(_.name)
+
+ DAPodil.Frame.Scope(
+ "Parse",
+ ref,
+ Map(
+ ref -> parseVariables,
+ pouRef -> pouVariables,
+ delimiterStackRef -> delimiterStackVariables,
+ diagnosticsRef -> List(diagnosticsValidationsVariable,
diagnosticsErrorsVariable),
+ diagnosticsValidationsRef -> diagnosticsValidations,
+ diagnosticsErrorsRef -> diagnosticsErrors
+ )
)
+ }
- val diagnosticsRootVariable =
- new Types.Variable("Diagnostics", "", null, diagnosticsRef.value,
null)
- val diagnosticsValidationsVariable =
- new Types.Variable("Validations", "", null,
diagnosticsValidationsRef.value, null)
- val diagnosticsErrorsVariable =
- new Types.Variable("Errors", "", null, diagnosticsErrorsRef.value,
null)
- // TODO: Get better values than toString() when
https://issues.apache.org/jira/browse/DAFFODIL-1200 is completed.
- val diagnosticsValidations: List[Types.Variable] =
- event.diagnostics.filter(_.isValidation).zipWithIndex.map { case
(diag, i) =>
- new Types.Variable(i.toString, diag.toString().replace("Validation
Error: ", ""))
- }
- val diagnosticsErrors: List[Types.Variable] =
- event.diagnostics.filterNot(_.isValidation).zipWithIndex.map { case
(diag, i) =>
- new Types.Variable(i.toString, diag.toString())
+ // a.k.a. Daffodil variables
+ def schemaScope(
+ scopeRef: DAPodil.VariablesReference,
+ state: StateForDebugger
+ ): IO[DAPodil.Frame.Scope] =
+ state.variableMapForDebugger.qnames.toList
+ .groupBy(_.namespace) // TODO: handle NoNamespace or
UnspecifiedNamespace as top-level?
+ .toList
+ .flatTraverse { case (ns, vs) =>
+ // every namespace is a DAP variable in the current scope, and links
to its set of Daffodil-as-DAP variables
+ variableRefs.next.map { ref =>
+ List(scopeRef -> List(new Types.Variable(ns.toString(), "", null,
ref.value, null))) ++
+ List(
+ ref -> vs
+ .sortBy(_.toPrettyString)
+ .fproduct(state.variableMapForDebugger.find)
+ .map { case (name, value) =>
+ new Types.Variable(
+ name.toQNameString,
+ value
+ .flatMap(v => Option(v.value.value).map(_.toString)
orElse Some("null"))
+ .getOrElse("???"),
+ value
+ .map(_.state match {
+ case VariableDefined => "default"
+ case VariableRead => "read"
+ case VariableSet => "set"
+ case VariableUndefined => "undefined"
+ case VariableBeingDefined => "being defined"
+ case VariableInProcess => "in process"
+ })
+ .getOrElse("???"),
+ 0,
+ null
+ )
+ }
+ )
}
-
- val parseVariables: List[Types.Variable] =
- (List(
- new Types.Variable("hidden", hidden.toString, "bool", 0, null),
- pouRootVariable,
- delimiterStackRootVariable,
- diagnosticsRootVariable
- ) ++ childIndex.map(ci => new Types.Variable("childIndex",
ci.toString)).toList
- ++ groupIndex
- .map(gi => new Types.Variable("groupIndex", gi.toString))
- .toList
- ++ occursIndex
- .map(oi => new Types.Variable("occursIndex", oi.toString))
- .toList
- ++ foundDelimiter.map(fd => new Types.Variable("foundDelimiter",
fd)).toList
- ++ foundField.map(ff => new Types.Variable("foundField",
ff)).toList)
- .sortBy(_.name)
-
- DAPodil.Frame.Scope(
- "Parse",
- ref,
- Map(
- ref -> parseVariables,
- pouRef -> pouVariables,
- delimiterStackRef -> delimiterStackVariables,
- diagnosticsRef -> List(diagnosticsValidationsVariable,
diagnosticsErrorsVariable),
- diagnosticsValidationsRef -> diagnosticsValidations,
- diagnosticsErrorsRef -> diagnosticsErrors
+ }
+ .map { refVars =>
+ val sv = refVars.foldMap(Map(_)) // combine values of map to
accumulate namespaces
+ DAPodil.Frame.Scope(
+ "Schema",
+ scopeRef,
+ sv
)
- )
+ }
+
+ def dataScope(ref: DAPodil.VariablesReference, state: StateForDebugger):
DAPodil.Frame.Scope = {
+ val bytePos1b = state.currentLocation.bytePos1b
+ val dataVariables: List[Types.Variable] =
+ List(new Types.Variable("bytePos1b", bytePos1b.toString, "number", 0,
null))
+
+ DAPodil.Frame.Scope(
+ "Data",
+ ref,
+ Map(ref -> dataVariables)
+ )
}
+ }
- // a.k.a. Daffodil variables
- def schemaScope(
- scopeRef: DAPodil.VariablesReference,
- state: StateForDebugger,
- refs: Next[DAPodil.VariablesReference]
- ): IO[DAPodil.Frame.Scope] =
- state.variableMapForDebugger.qnames.toList
- .groupBy(_.namespace) // TODO: handle NoNamespace or
UnspecifiedNamespace as top-level?
- .toList
- .flatTraverse { case (ns, vs) =>
- // every namespace is a DAP variable in the current scope, and links
to its set of Daffodil-as-DAP variables
- refs.next.map { ref =>
- List(scopeRef -> List(new Types.Variable(ns.toString(), "", null,
ref.value, null))) ++
- List(
- ref -> vs
- .sortBy(_.toPrettyString)
- .fproduct(state.variableMapForDebugger.find)
- .map { case (name, value) =>
- new Types.Variable(
- name.toQNameString,
- value
- .flatMap(v => Option(v.value.value).map(_.toString)
orElse Some("null"))
- .getOrElse("???"),
- value
- .map(_.state match {
- case VariableDefined => "default"
- case VariableRead => "read"
- case VariableSet => "set"
- case VariableUndefined => "undefined"
- case VariableBeingDefined => "being defined"
- case VariableInProcess => "in process"
- })
- .getOrElse("???"),
- 0,
- null
- )
- }
- )
- }
- }
- .map { refVars =>
- val sv = refVars.foldMap(Map(_)) // combine values of map to
accumulate namespaces
- DAPodil.Frame.Scope(
- "Schema",
- scopeRef,
- sv
- )
- }
+ private object Delivery {
+
+ def to(
+ data: Channel[IO, DAPodil.Data],
+ dapEvents: Channel[IO, Events.DebugEvent]
+ ): IO[Delivery] =
+ for {
+ frameIds <-
+ Next.int.map(_.map(DAPodil.Frame.Id.apply)).flatTap(_.next()) //
`.flatTap(_.next())`: ids start at 1
+ variableRefs <-
+
Next.int.map(_.map(DAPodil.VariablesReference.apply)).flatTap(_.next())
+ } yield new Delivery(frameIds, variableRefs, data, dapEvents)
- def dataScope(ref: DAPodil.VariablesReference, state: StateForDebugger):
DAPodil.Frame.Scope = {
- val bytePos1b = state.currentLocation.bytePos1b
- val dataVariables: List[Types.Variable] =
- List(new Types.Variable("bytePos1b", bytePos1b.toString, "number", 0,
null))
+ /** All state to deliver to via the Debugee interface. */
+ case class State(data: DAPodil.Data, bytePos1b: Long, infoset:
Option[InfosetEvent])
- DAPodil.Frame.Scope(
- "Data",
- ref,
- Map(ref -> dataVariables)
- )
+ object State {
+ val empty: State = State(DAPodil.Data.empty, 0, None)
+ }
}
/** An algebraic data type that reifies the Daffodil `Debugger` callbacks. */
@@ -1093,18 +1103,17 @@ object Parse {
case class Init(state: StateForDebugger) extends Event
case class StartElement(
state: StateForDebugger,
- isStepping: Boolean,
name: Option[ElementName],
schemaLocation: SchemaFileLocation,
pointsOfUncertainty: List[PointOfUncertainty],
delimiterStack: List[Delimiter],
- diagnostics: List[org.apache.daffodil.lib.api.Diagnostic]
+ diagnostics: List[org.apache.daffodil.lib.api.Diagnostic],
+ infoset: Option[InfosetEvent]
) extends Event {
// PState is mutable, so we copy all the information we might need
downstream.
- def this(pstate: PState, isStepping: Boolean) =
+ def this(pstate: PState, infoset: Option[InfosetEvent]) =
this(
pstate.copyStateForDebugger,
- isStepping,
pstate.currentNode.toScalaOption.map(element =>
ElementName(element.name)),
pstate.schemaFileLocation,
pstate.pointsOfUncertainty.iterator.toList.map(mark =>
@@ -1118,11 +1127,13 @@ object Parse {
pstate.mpstate.delimiters.toList.zipWithIndex.map { case (delimiter,
i) =>
Delimiter(if (i < pstate.mpstate.delimitersLocalIndexStack.top)
"remote" else "local", delimiter)
},
- pstate.diagnostics
+ pstate.diagnostics,
+ infoset
)
}
case class EndElement(state: StateForDebugger) extends Event
case object Fini extends Event
+ case class Control(state: DAPodil.Debugee.State) extends Event
implicit val show: Show[Event] = Show.fromToString
}
@@ -1251,6 +1262,35 @@ object Parse {
}
case class DataEvent(bytePos1b: Long) extends
Events.DebugEvent("daffodil.data")
case class InfosetEvent(content: String, mimeType: String) extends
Events.DebugEvent("daffodil.infoset")
+ object InfosetEvent {
+ def apply(format: String, node: DIElement): InfosetEvent =
+ InfosetEvent(
+ infosetToString(format, node),
+ format match {
+ case "xml" => "text/xml"
+ case "json" => "application/json"
+ }
+ )
+
+ private def infosetToString(format: String, ie: DIElement): String = {
+ val bos = new java.io.ByteArrayOutputStream()
+ val infosetOutputter = format match {
+ case "xml" => new XMLTextInfosetOutputter(bos, true)
+ case "json" => new JsonInfosetOutputter(bos, true)
+ }
+
+ val iw = InfosetWalker(
+ ie.asInstanceOf[DIElement],
+ infosetOutputter,
+ walkHidden = false,
+ ignoreBlocks = true,
+ releaseUnneededInfoset = false
+ )
+ iw.walk(lastWalk = true)
+ bos.toString("UTF-8")
+ }
+
+ }
/** Behavior of a stepping debugger that can be running or stopped. */
sealed trait Control {
@@ -1341,97 +1381,71 @@ object Parse {
}
}
- /** The Daffodil `Debugger` interface is asynchronously invoked from a
running parse, and always returns `Unit`. In
- * order to invoke effects like `IO` but return `Unit`, we use a
`Dispatcher` to execute the effects at this
- * "outermost" layer (with respect to the effects).
+ /** The Daffodil `Debugger` interface is asynchronously invoked from a
running parse, and its methods always returns
+ * `Unit`. In order to invoke effects like `IO` but return `Unit`, we use a
`Dispatcher` to execute the effects at
+ * this "outermost" layer (with respect to the effects).
+ *
+ * The parse callbacks are converted into [[Parse.Event]] values and sent
into the `events` [[Channel]].
*/
class DaffodilDebugger(
dispatcher: Dispatcher[IO],
- state: QueueSink[IO, Option[DAPodil.Debugee.State]],
breakpoints: Breakpoints,
control: Control,
- events: QueueSink[IO, Option[Event]],
- infoset: QueueSink[IO, Option[String]],
+ events: Channel[IO, Event],
infosetFormat: String
) extends Debugger {
implicit val logger: Logger[IO] = Slf4jLogger.getLogger
override def init(pstate: PState, processor: Parser): Unit =
dispatcher.unsafeRunSync {
- events.offer(Some(Event.Init(pstate.copyStateForDebugger)))
+ events.send(Event.Init(pstate.copyStateForDebugger)).void
}
override def fini(processor: Parser): Unit =
dispatcher.unsafeRunSync {
- events.offer(Some(Event.Fini)) *>
- events.offer(None) *> // no more events after this
- state.offer(None) *> // no more states == the parse is terminated
- infoset.offer(None) *>
+ events.send(Event.Fini) *>
+ events.close *> // no more events after this
Logger[IO].debug("Debugger fini event: completed parse")
}
override def startElement(pstate: PState, processor: Parser): Unit =
dispatcher.unsafeRunSync {
// Generating the infoset requires a PState, not a StateForDebugger,
so we can't generate it later from the Event.StartElement (which contains the
StateForDebugger).
- lazy val setInfoset = {
+ lazy val infoset = {
var node = pstate.infoset
while (node.diParent != null) node = node.diParent
node match {
- case d: DIDocument if d.contents.size == 0 => IO.unit
- case _ =>
infoset.offer(Some(infosetToString(node)))
+ case d: DIDocument if d.contents.size == 0 => None
+ case _ =>
Some(InfosetEvent(infosetFormat, node))
}
}
- logger.debug("pre-control await") *>
- // may block until external control says to unblock, for stepping
behavior
- control.await
- .ifM(
- events.offer(Some(new Event.StartElement(pstate, isStepping =
true))) *> setInfoset,
- events.offer(Some(new Event.StartElement(pstate, isStepping =
false)))
- ) *>
- logger.debug("post-control await") *>
- logger.debug("pre-checkBreakpoints") *>
- // TODO: there's a race between the readers of `events` and `state`,
so somebody could react to a hit breakpoint before the event data was committed
- checkBreakpoints(createLocation(pstate.schemaFileLocation))
+ for {
+ _ <- logger.debug("pre-control await")
+ isStepping <- control.await // may block until external control says
to unblock, for stepping behavior
+ _ <- logger.debug("post-control await")
+ location = createLocation(pstate.schemaFileLocation)
+ shouldBreak <- breakpoints.shouldBreak(location)
+ startElement =
+ if (isStepping || shouldBreak) new Event.StartElement(pstate,
infoset)
+ else new Event.StartElement(pstate, None)
+ _ <- events.send(startElement)
+ _ <- onBreakpointHit(location).whenA(shouldBreak)
+ } yield ()
}
- def infosetToString(ie: InfosetElement): String = {
- val bos = new java.io.ByteArrayOutputStream()
- val infosetOutputter = infosetFormat match {
- case "xml" => new XMLTextInfosetOutputter(bos, true)
- case "json" => new JsonInfosetOutputter(bos, true)
- }
-
- val iw = InfosetWalker(
- ie.asInstanceOf[DIElement],
- infosetOutputter,
- walkHidden = false,
- ignoreBlocks = true,
- releaseUnneededInfoset = false
- )
- iw.walk(lastWalk = true)
- bos.toString("UTF-8")
- }
-
/** If the current location is a breakpoint, pause the control and update
the state to notify the breakpoint was
* hit.
*/
- def checkBreakpoints(location: DAPodil.Location): IO[Unit] =
- breakpoints
- .shouldBreak(location)
- .ifM(
- control.pause() *>
- state
- .offer(
- Some(
- DAPodil.Debugee.State.Stopped(
- DAPodil.Debugee.State.Stopped.Reason
- .BreakpointHit(location)
- )
- )
- ),
- IO.unit
- )
+ def onBreakpointHit(location: DAPodil.Location): IO[Unit] =
+ control.pause() *>
+ events
+ .send(
+ Event.Control(
+ DAPodil.Debugee.State.Stopped.breakpointHit(location)
+ )
+ )
+ .void
def createLocation(loc: SchemaFileLocation): DAPodil.Location =
DAPodil.Location(
@@ -1442,28 +1456,26 @@ object Parse {
override def endElement(pstate: PState, processor: Parser): Unit =
dispatcher.unsafeRunSync {
control.await *> // ensure no events while debugger is paused
- events.offer(Some(Event.EndElement(pstate.copyStateForDebugger)))
+ events.send(Event.EndElement(pstate.copyStateForDebugger)).void
}
}
object DaffodilDebugger {
+
+ /** Create a Daffodil [[Debugger]] that writes events to a channel. */
def resource(
- state: QueueSink[IO, Option[DAPodil.Debugee.State]],
- events: QueueSink[IO, Option[Event]],
+ events: Channel[IO, Event],
breakpoints: Breakpoints,
control: Control,
- infoset: QueueSink[IO, Option[String]],
infosetFormat: String
- ): Resource[IO, DaffodilDebugger] =
+ ): Resource[IO, Debugger] =
for {
dispatcher <- Dispatcher.parallel[IO]
} yield new DaffodilDebugger(
dispatcher,
- state,
breakpoints,
control,
events,
- infoset,
infosetFormat
)
}
diff --git
a/debugger/src/main/scala/org.apache.daffodil.debugger.dap/logging.scala
b/debugger/src/main/scala/org.apache.daffodil.debugger.dap/logging.scala
index 063831c..06061bf 100644
--- a/debugger/src/main/scala/org.apache.daffodil.debugger.dap/logging.scala
+++ b/debugger/src/main/scala/org.apache.daffodil.debugger.dap/logging.scala
@@ -42,6 +42,8 @@ object logging {
case event: Events.ThreadEvent => s"${event.`type`} ${event.reason}"
case event: DAPodil.LoadedSourceEvent =>
s"${event.`type`} ${event.reason} ${JsonUtils.toJson(event.source)}"
+ case event: Parse.DataEvent =>
+ s"${event.`type`} ${event.bytePos1b}"
case event => s"${event.`type`}"
}
}