Skip to content

Migrate local analysis replay to jaspSyntax#79

Open
FBartos wants to merge 9 commits into
jasp-stats:developmentfrom
FBartos:bridge/jasp-syntax-integration
Open

Migrate local analysis replay to jaspSyntax#79
FBartos wants to merge 9 commits into
jasp-stats:developmentfrom
FBartos:bridge/jasp-syntax-integration

Conversation

@FBartos
Copy link
Copy Markdown
Contributor

@FBartos FBartos commented May 14, 2026

Summary

  • remove the local .jasp option/dataset/QML bridge code and delegate native semantics to jaspSyntax
  • split analysisOptions() into saved, QML-bound runnable options and analysisRuntimeOptions() for backend/runtime inspection
  • route runAnalysis() through jaspBase::runWrappedAnalysis() with explicit source module/QML provenance and reject already-prepared runtime options
  • add subprocess isolation for native/log-heavy bridge work and keep state files on the native callback contract
  • decode JSON result text via jaspSyntax::decodeAnalysisResults() and delegate result-state figure decoding to public jaspBase::decodeJaspResultState()
  • update generated test scaffolding and table expectations for the native encoded-token naming contract

Why

jaspTools should not own Desktop/QML/native option semantics. This PR turns it back into the orchestration layer: it asks jaspSyntax to prepare Desktop-faithful options and asks jaspBase to replay analyses with source-module context. That removes the parallel parser/encoder drift that made local replay fragile.

The last local semantic leak was state-figure decoding: jaspTools walked state$figures and called jaspBase:::decodeplot() directly. That has now moved behind jaspBase::decodeJaspResultState(), leaving jaspTools responsible only for reading the state payload and passing it to the owning package.

Related PRs

Verification

  • Rscript -e "pkgload::load_all('C:/JASP-Packages/jaspTools', quiet = TRUE); testthat::test_dir('tests/testthat', reporter = 'summary')"
  • Rscript -e "pkgload::load_all('C:/JASP-Packages/jasp-desktop/Engine/jaspBase', quiet = TRUE); testthat::test_dir('tests/testthat', reporter = 'summary')"
  • git diff --check

Copy link
Copy Markdown
Contributor

@vandenman vandenman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still need to actually run this, but some feedback below. The subprocesses idea looks interesting and has potential, but the current implementation looks a little fragile. Also, subprocesses are nice in theory but can make errors much harder to debug.

Comment thread R/options.R
#'
#' @export analysisOptions
analysisOptions <- function(source) {
analysisOptions <- function(source, modulePath = NULL) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this always clear from context? E.g., when running jaspdescriptives it's clear that that is also always the module path?

Copy link
Copy Markdown
Contributor Author

@FBartos FBartos May 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly yes, and the default path still does that inference from module.dirs / the active module context. I kept modulePath as an optional provenance override because .jasp files record module identity/version, not the local source checkout path. That matters for source-branch replay, generated tests, and multi-module archives; normal module workflows should not need to pass it.

Comment thread R/options.R

modulePath <- .modulePathForAnalysisName(analysis, modulePath)
options <- jaspSyntax::readDefaultAnalysisOptions(
modulePath = modulePath,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or is this the reason for the above? Still think we could infer the module path automatically?

Copy link
Copy Markdown
Contributor Author

@FBartos FBartos May 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is the reason for the optional argument. The implementation still infers when modulePath = NULL; explicit modulePath only pins the source checkout or disambiguates a named module/analysis path collection. I kept this as orchestration/provenance, not extra QML semantics in jaspTools.

Comment thread R/options.R Outdated
rFuncLocExpr <- paste0("\\{[^\\{\\}]*func:\\s*", name, "[^\\{\\}]*\\}")
if (!grepl(rFuncLocExpr, fileContents))
stop("Could not locate qml file for R function ", name, " in inst/qml directory and did not find the R function in inst/Description.qml to look for an alternative name for the qml file")
analysisOptionsFromQMLFileSubprocess <- function(analysis, modulePath = NULL) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we use subprocesses? That feels a little over engineered? I mean not a bad idea to be able to run tests in parallel in the future, but don't think that is your motivation right now?

Copy link
Copy Markdown
Contributor Author

@FBartos FBartos May 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed for analysisOptions(). I removed the jaspTools subprocess path there; QML defaults now call jaspSyntax::readDefaultAnalysisOptions() directly. The .jasp extraction isolation remains in jaspSyntax, where the native bridge state is owned.

Comment thread R/options.R Outdated

return(list(value = value, types = typesStructure))
}
`%||%` <- function(x, y) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This exists in base R already since R 4.5.0 or so, so maybe use that one instead (and definitely avoid shadowing that one otherwise).

Copy link
Copy Markdown
Contributor Author

@FBartos FBartos May 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. I removed the custom %||% helper so we do not shadow base R. The only remaining use case was a label fallback, now handled by a named internal helper.

Comment thread R/subprocess.R Outdated

.jaspToolsLaunchSubprocess <- function(scriptPath, inputPath, outputPath, logPath) {
rscript <- file.path(R.home("bin"), if (.Platform$OS.type == "windows") "Rscript.exe" else "Rscript")
system2(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah so if we really want this we should consider mirai or processx rather than doing it "manually".

Copy link
Copy Markdown
Contributor Author

@FBartos FBartos May 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. I replaced the manual Rscript/system2 runner with callr, which uses processx underneath. I kept subprocess containment only for runAnalysis(): while checking this against jaspMixedModels, direct in-process replay of the Larks and Owls GLMM still crashes R with access violation 0xC0000005, so the child process is currently protecting the parent/test session from native crashes rather than preparing for parallelism.

Comment thread R/subprocess.R Outdated

.jaspToolsSubprocessScript <- function(resultLines, saveLines,
beforeResultLines = character(0),
statusExpression = "inherits(result, 'jaspTools.subprocessError')") {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This much code as a string seems very prone to breakage and hard to debug?

Copy link
Copy Markdown
Contributor Author

@FBartos FBartos May 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. The long generated child script is gone. The child process now runs normal internal R functions via callr, so the code is easier to debug and covered like ordinary R code.

Comment thread R/testthat-helper-tables.R Outdated
if (!is.character(x) || length(x) == 0L)
return(x)

tokenPattern <- "(JaspColumn_[[:alnum:]_]+_Encoded|jaspColumn[0-9]+)"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this still necessary? Decoding is handled by jasp Syntax?

Copy link
Copy Markdown
Contributor Author

@FBartos FBartos May 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. I removed canonicalizeJaspColumnTokens() and the compatibility test around native/legacy encoded tokens. Runtime result decoding is handled by jaspSyntax::decodeAnalysisResults(); expect_equal_tables() now compares strings literally again.

Comment thread R/run.R Outdated
runAnalysisSubprocessScript <- function() {
.jaspToolsSubprocessScript(
beforeResultLines = "warnings <- character(0)",
resultLines = c(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same stuff about very long code as text

Copy link
Copy Markdown
Contributor Author

@FBartos FBartos May 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. The long code-as-text path in runAnalysis() is gone as part of the callr refactor.

@FBartos
Copy link
Copy Markdown
Contributor Author

FBartos commented May 14, 2026

Pushed follow-up commit e464286 addressing the review pass.

Summary:

  • kept modulePath as an optional source-checkout/provenance override, while preserving automatic inference for the normal module workflow;
  • removed the analysisOptions() subprocess path entirely;
  • replaced the manual Rscript/string-script runner with callr for runAnalysis() crash containment only;
  • removed the custom %||% helper to avoid shadowing base R;
  • removed canonicalizeJaspColumnTokens() because result decoding belongs to jaspSyntax.

Verification:

  • focused jaspTools bridge tests pass: test-analysisOptions.R, test-expect-equal-tables.R, test-rbridge-shim.R, test-jaspSyntax-lifecycle.R;
  • git diff --check passes;
  • checked the jaspMixedModels Larks and Owls GLMM path: options and dataset extraction succeed, but direct in-process runAnalysis() still hard-crashes R with access violation 0xC0000005; the new callr path contains that crash and reports a parent-side subprocess failure instead of taking down the session.

@FBartos
Copy link
Copy Markdown
Contributor Author

FBartos commented May 14, 2026

Follow-up from the jaspMixedModels replay crash reported against jaspTools::runAnalysis():

I reproduced the failure through this PR's public path, but the fix belongs in the lower bridge layer and was pushed to the linked jaspSyntax PR:

No new jaspTools patch was needed. The important behavior is that jaspTools::extractDatasetFromJASPFile() now receives a data frame with .jasp provenance from jaspSyntax; when runAnalysis() preloads it, jaspSyntax::loadAnalysisDataset() reuses the saved .jasp archive as the native dataset source. That preserves Desktop column typing for real replay instead of forcing jaspTools to know or reconstruct QML/data semantics.

Verified locally with jaspMixedModels/examples/Larks and Owls.jasp: jaspTools::runAnalysis('MixedModelsGLMM', dataset, opts) now returns results,status,typeRequest,state instead of the callr/native crash.

@FBartos
Copy link
Copy Markdown
Contributor Author

FBartos commented May 14, 2026

Final cross-repo follow-up from the senior pass: no additional jaspTools code changes were needed here.

The remaining failures were below the orchestration layer:

With those branches installed together, the reported MixedModels path now completes:

  • analysisOptions("Larks and Owls.jasp") returns saved options.
  • extractDatasetFromJASPFile("Larks and Owls.jasp") returns the expected 260 x 9 dataset.
  • runAnalysis("MixedModelsGLMM", dataset, opts) returns a result list with status = "complete".

This keeps jaspTools as orchestration only, which matches the review direction: QML/native semantics stay in jaspSyntax and Desktop rather than being reimplemented here.

@FBartos
Copy link
Copy Markdown
Contributor Author

FBartos commented May 14, 2026

I chased the noisy focused-test warnings through the bridge stack. There are no jaspTools code changes for this follow-up; the fixes landed in the owning layers:

  • Desktop PR: guards the headless QML layout edge case, adds terminal SyntaxInterface shutdown, and avoids eager parse-only jaspBase/friendly-helper startup.
  • jaspSyntax PR: calls the terminal shutdown on unload/session exit and fixes Windows DLL bundling order so a rebuilt build/R-Interface/libR-InterfaceNoRInside.dll is not overwritten by a stale build-root DLL.

With those PRs pushed, fresh installed jaspSyntax focused tests are clean for the prior GridLayout, qml:, QThreadStorage, and stack imbalance noise. No orchestration change was needed in jaspTools.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants