Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions build/yarn-scripts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ const fs = require('fs')
const { glob } = require('glob')
const concurrently = require('concurrently')
const child_process = require('child_process')
const jsoncParse = require('jsonc-parser').parse

const packageData = jsoncParse(
fs.readFileSync(path.resolve('package.json'), 'utf8')
)
const pkg_version = packageData['version']

function rmFileOrDirectory(path) {
if (fs.existsSync(path)) fs.rmSync(path, { recursive: true })
Expand Down Expand Up @@ -325,6 +331,37 @@ function packageVsix() {
process.exit(result.status === null ? 1 : result.status)
}

function getScalaVersions() {
const scalaVersions = ['2.12', '2.13']

// The scala 3 version of the debugger should only exist if JDK >= 17 is being used
if (fs.existsSync(`debugger/target/jvm-3/universal/stage`)) {
scalaVersions.push('3')
}

return scalaVersions
}

function moveDebuggers() {
getScalaVersions().forEach(async (scalaVersion) => {
const serverPackage = `daffodil-debugger-${scalaVersion}-${pkg_version}`
const jvmFolderName = `jvm-${scalaVersion}`
const stageFilePath = path.resolve(
`debugger/target/${jvmFolderName}/universal/stage`
)

const serverPackageFolder = path.join('dist/debuggers', serverPackage)

// remove debugger package folder if exists
if (fs.existsSync(serverPackageFolder)) {
fs.rmSync(serverPackageFolder, { recursive: true, force: true })
}

// Copy staged debugger files to desired location
fs.cpSync(stageFilePath, serverPackageFolder, { recursive: true })
})
}

/* START SECTION: Update version */
// helper function to get the version passed in
function parseArgs() {
Expand Down Expand Up @@ -487,4 +524,5 @@ module.exports = {
packageVsix: packageVsix,
checkMissingLicenseData: checkMissingLicenseData,
checkLicenseCompatibility: checkLicenseCompatibility,
moveDebuggers: moveDebuggers,
}
175 changes: 132 additions & 43 deletions debugger/src/main/scala/org.apache.daffodil.debugger.dap/Parse.scala
Original file line number Diff line number Diff line change
Expand Up @@ -818,8 +818,13 @@ object Parse {
)
_ <- 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, e: Parse.Event.EndElement) =>
val newState = state.copy(
data = state.data.pop(),
infoset = e.infoset // Update the state with the new infoset
)
data.send(newState.data).as(newState)
case (state, _: Parse.Event.Fini.type) => IO.pure(state)
case (state, Event.Control(DAPodil.Debugee.State.Stopped(reason))) =>
val events =
List(
Expand Down Expand Up @@ -1092,7 +1097,7 @@ object Parse {
infoset
)
}
case class EndElement(state: StateForDebugger) extends Event
case class EndElement(state: StateForDebugger, infoset: Option[InfosetEvent]) extends Event
case object Fini extends Event
case class Control(state: DAPodil.Debugee.State) extends Event
case class Error(message: String) extends Events.DebugEvent("daffodil.parseError")
Expand Down Expand Up @@ -1255,7 +1260,18 @@ object Parse {
/** Blocks if the current state is stopped, unblocking when a `step` or `continue` happens. Returns true if blocking
* happened, false otherwise.
*/
def await(): IO[Boolean]
// def await(): IO[Boolean]
def await(onStop: IO[Unit] = IO.unit): IO[Boolean] // Add onStop here

/** Update the control with the current element depth (set by the parser thread before awaiting). Depth is used to
* implement step/stepOut semantics.
*/
def setCurrentDepth(depth: Int): IO[Unit]

/** Indicate the kind of the upcoming await (e.g. "start" or "end"). Parser hooks should set this before calling
* await().
*/
def setAwaitingKind(kind: String): IO[Unit]

/** Start running. */
def continue(): IO[Unit]
Expand Down Expand Up @@ -1286,26 +1302,61 @@ object Parse {
for {
waiterArrived <- Deferred[IO, Unit]
state <- Ref[IO].of[State](AwaitingFirstAwait(waiterArrived))
currentDepth <- Ref[IO].of[Int](0)
awaitingKind <- Ref[IO].of[String]("")
stopTarget <- Ref[IO].of[Option[(Int, String)]](None)
} yield new Control {
def await(): IO[Boolean] =
def await(onStop: IO[Unit] = IO.unit): IO[Boolean] =
for {
nextContinue <- Deferred[IO, Unit]
nextAwaitStarted <- Deferred[IO, Unit]
awaited <- state.modify {
case AwaitingFirstAwait(waiterArrived) =>
Stopped(nextContinue, nextAwaitStarted) -> waiterArrived
.complete(()) *> nextContinue.get.as(true)
// Stopped(nextContinue, nextAwaitStarted) -> waiterArrived
// .complete(()) *> nextContinue.get.as(true)
Stopped(nextContinue, nextAwaitStarted) -> (waiterArrived
.complete(()) *> onStop *> nextAwaitStarted.complete(()).void *> nextContinue.get.as(true))
case Running => Running -> IO.pure(false)
case s @ Stopped(whenContinued, nextAwaitStarted) =>
s -> nextAwaitStarted.complete(()) *> // signal next await happened
whenContinued.get.as(true) // block
s -> stopTarget.get.flatMap {
case None =>
// No step target, pause immediately and signal that we've stopped
onStop *> nextAwaitStarted.complete(()).void *> whenContinued.get.as(true)
case Some((targetDepth, mode)) =>
for {
cur <- currentDepth.get
kind <- awaitingKind.get
res <- mode match {
// stepIn: Stop at the very next 'start' event, completely ignoring 'end' events
case "stepIn" =>
if (kind == "start")
stopTarget.set(None) *> onStop *> nextAwaitStarted.complete(()).void *> whenContinued.get
.as(true)
else IO.pure(false)

// stepOver / stepOut: Run invisibly until we hit a 'start' event at the target depth or shallower
case "stepOver" | "stepOut" =>
if (kind == "start" && cur <= targetDepth)
stopTarget.set(None) *> onStop *> nextAwaitStarted.complete(()).void *> whenContinued.get
.as(true)
else
IO.pure(false) // Ignore ALL 'end' events and any 'start' events deeper than our target

// Block once and clear the stopTarget so subsequent awaits don't re-trigger
case _ =>
stopTarget.set(None) *> onStop *> nextAwaitStarted.complete(()).void *> whenContinued.get.as(
true
)
}
} yield res
}
}.flatten
} yield awaited

def performStep(stepType: String): IO[Unit] =
def performStep(stepType: String, addedDepth: Int): IO[Unit] =
for {
nextContinue <- Deferred[IO, Unit]
nextAwaitStarted <- Deferred[IO, Unit]
newAwaitStarted <- Deferred[IO, Unit]
_ <- state.modify {
case s @ AwaitingFirstAwait(waiterArrived) =>
s -> waiterArrived.get *> (stepType match {
Expand All @@ -1315,35 +1366,46 @@ object Parse {
})
case Running => Running -> IO.unit
case Stopped(whenContinued, _) =>
Stopped(nextContinue, nextAwaitStarted) -> (
whenContinued.complete(()) *> // wake up await-ers
nextAwaitStarted.get // block until next await is invoked
Stopped(nextContinue, newAwaitStarted) -> (
for {
d <- currentDepth.get
_ <- stopTarget.set(Some((d + addedDepth, stepType)))
_ <- whenContinued.complete(()) // Unblock the parser to continue running invisibly
_ <- newAwaitStarted.get // WAIT here until the parser hits the target 'start' event and stops
} yield ()
)
}.flatten
} yield ()

def stepOver(): IO[Unit] = performStep("stepOver")
def stepIn(): IO[Unit] = performStep("stepIn")
def stepOut(): IO[Unit] = performStep("stepOut")
def stepOver(): IO[Unit] = performStep("stepOver", 0)
def stepIn(): IO[Unit] = performStep("stepIn", 1)
def stepOut(): IO[Unit] = performStep("stepOut", -1)

// Helper functions for stepping
def setCurrentDepth(depth: Int): IO[Unit] = currentDepth.set(depth)
def setAwaitingKind(kind: String): IO[Unit] = awaitingKind.set(kind)

def continue(): IO[Unit] =
state.modify {
case s @ AwaitingFirstAwait(waiterArrived) =>
s -> waiterArrived.get *> continue()
case Running => Running -> IO.unit
case Stopped(whenContinued, _) =>
Running -> whenContinued.complete(()).void // wake up await-ers
case Running => Running -> IO.unit
case Stopped(whenContinued, nextAwaitStarted) =>
Running -> (stopTarget.set(None) *> nextAwaitStarted.complete(()).void *> whenContinued.complete(())).void
}.flatten

def pause(): IO[Unit] =
for {
nextContinue <- Deferred[IO, Unit]
nextAwaitStarted <- Deferred[IO, Unit]
_ <- state.update {
case Running => Stopped(nextContinue, nextAwaitStarted)
case s: AwaitingFirstAwait => s
case s: Stopped => s
}
newAwaitStarted <- Deferred[IO, Unit]
_ <- state.modify {
case Running =>
Stopped(nextContinue, newAwaitStarted) -> IO.unit
case s: AwaitingFirstAwait => s -> IO.unit
case s @ Stopped(_, nextAwaitStarted) =>
// If we are hit by a breakpoint during a step, clear target and unblock performStep
s -> (stopTarget.set(None) *> nextAwaitStarted.complete(()).void)
}.flatten
} yield ()
}
}
Expand All @@ -1363,6 +1425,34 @@ object Parse {
) extends Debugger {
implicit val logger: Logger[IO] = Slf4jLogger.getLogger

// Helper function to get the infoset
private def getFullInfoset(pstate: PState): Option[InfosetEvent] = {
var node = pstate.infoset
while (node.diParent != null) node = node.diParent
node match {
case d: DIDocument if d.numChildren == 0 => None
case _ => Some(InfosetEvent(infosetFormat, node))
}
}

// Helper function to calculate the current depth
private def calculateDepth(pstate: PState): Int =
Convert
.daffodilMaybeToOption(pstate.currentNode)
.map { node =>
var d = 0
var n = node
while (n.diParent != null) {
n = n.diParent
// Only count actual elements and document root, ignore hidden DIArray wrappers
if (n.isInstanceOf[DIElement] || n.isInstanceOf[DIDocument]) {
d += 1
}
}
d
}
.getOrElse(0)

override def init(pstate: PState, processor: Parser): Unit =
dispatcher.unsafeRunSync {
events.send(Event.Init(pstate.copyStateForDebugger)).void
Expand All @@ -1377,26 +1467,17 @@ object 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 infoset = {
var node = pstate.infoset
while (node.diParent != null) node = node.diParent
node match {
case d: DIDocument if d.numChildren == 0 => None
case _ => Some(InfosetEvent(infosetFormat, node))
}
}

for {
_ <- logger.debug("pre-control await")
isStepping <- control.await() // may block until external control says to unblock, for stepping behavior
_ <- control.setCurrentDepth(calculateDepth(pstate))
_ <- control.setAwaitingKind("start")
isStepping <- control.await(
onStop = events.send(new Event.StartElement(pstate, getFullInfoset(pstate))).void
)
_ <- 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)
_ <- events.send(new Event.StartElement(pstate, None)).unlessA(isStepping)
_ <- onBreakpointHit(location).whenA(shouldBreak)
} yield ()
}
Expand All @@ -1422,8 +1503,16 @@ object Parse {

override def endElement(pstate: PState, processor: Parser): Unit =
dispatcher.unsafeRunSync {
control.await() *> // ensure no events while debugger is paused
events.send(Event.EndElement(pstate.copyStateForDebugger)).void
for {
_ <- logger.debug("pre-control await")
_ <- control.setCurrentDepth(calculateDepth(pstate))
_ <- control.setAwaitingKind("end")
isStepping <- control.await(
onStop = events.send(new Event.EndElement(pstate, getFullInfoset(pstate))).void
)
_ <- logger.debug("post-control await")
_ <- events.send(new Event.EndElement(pstate, None)).unlessA(isStepping)
} yield ()
}
}

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"test": "sbt test && yarn test:svelte && node ./out/tests/omegaEditServerLifecycle.js && node ./out/tests/runTest.js",
"test:svelte": "mocha --import=tsx ./src/svelte/tests/**/*.test.ts",
"sbt": "sbt Universal/stage",
"move-debuggers": "run-func build/yarn-scripts.ts moveDebuggers",
"svelte:check": "svelte-check --tsconfig ./src/svelte/tsconfig.json",
"svelte:build": "cd src/svelte && vite build --config ./vite.config.mjs --mode production --emptyOutDir",
"update-version": "run-func build/yarn-scripts.ts updateVersion",
Expand Down
17 changes: 17 additions & 0 deletions src/infoset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,23 @@ export async function activate(ctx: vscode.ExtensionContext) {
}
sid = undefined
await openInfosetFilePrompt()

const tabs: vscode.Tab[] = vscode.window.tabGroups.all
.map((tg) => tg.tabs)
.flat()

// Find the tab that matches the file Uri
const foundTab = tabs.find(
(tab) =>
tab.input instanceof vscode.TabInputText &&
tab.input.uri.path === doc?.fileName
)

// If the tab is found, close it
if (foundTab) {
// The close method can take a single tab or an array of tabs
await vscode.window.tabGroups.close(foundTab)
}
})
)

Expand Down
Loading