Carry native encoder context through analysis replay#79
Conversation
vandenman
left a comment
There was a problem hiding this comment.
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.
| #' | ||
| #' @export analysisOptions | ||
| analysisOptions <- function(source) { | ||
| analysisOptions <- function(source, modulePath = NULL) { |
There was a problem hiding this comment.
Isn't this always clear from context? E.g., when running jaspdescriptives it's clear that that is also always the module path?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Mostly yes for the ordinary module workflow, and the default still infers that path when modulePath = NULL. I kept the argument as an explicit provenance/source-checkout override for cases where the saved .jasp file identifies the module but not the local checkout to replay against. Clarified that in the analysisOptions() docs in e5e9411.
|
|
||
| modulePath <- .modulePathForAnalysisName(analysis, modulePath) | ||
| options <- jaspSyntax::readDefaultAnalysisOptions( | ||
| modulePath = modulePath, |
There was a problem hiding this comment.
Or is this the reason for the above? Still think we could infer the module path automatically?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Yes, automatic inference remains the normal path. The explicit modulePath is only for pinning or disambiguating source replay: generated tests, local branch checkouts, or multi-module .jasp archives. I tightened the docs in e5e9411 to say that it should only be passed for those cases.
| 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) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
|
|
||
| return(list(value = value, types = typesStructure)) | ||
| } | ||
| `%||%` <- function(x, y) { |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
|
|
||
| .jaspToolsLaunchSubprocess <- function(scriptPath, inputPath, outputPath, logPath) { | ||
| rscript <- file.path(R.home("bin"), if (.Platform$OS.type == "windows") "Rscript.exe" else "Rscript") | ||
| system2( |
There was a problem hiding this comment.
Yeah so if we really want this we should consider mirai or processx rather than doing it "manually".
There was a problem hiding this comment.
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.
|
|
||
| .jaspToolsSubprocessScript <- function(resultLines, saveLines, | ||
| beforeResultLines = character(0), | ||
| statusExpression = "inherits(result, 'jaspTools.subprocessError')") { |
There was a problem hiding this comment.
This much code as a string seems very prone to breakage and hard to debug?
There was a problem hiding this comment.
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.
| if (!is.character(x) || length(x) == 0L) | ||
| return(x) | ||
|
|
||
| tokenPattern <- "(JaspColumn_[[:alnum:]_]+_Encoded|jaspColumn[0-9]+)" |
There was a problem hiding this comment.
Is this still necessary? Decoding is handled by jasp Syntax?
There was a problem hiding this comment.
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.
| runAnalysisSubprocessScript <- function() { | ||
| .jaspToolsSubprocessScript( | ||
| beforeResultLines = "warnings <- character(0)", | ||
| resultLines = c( |
There was a problem hiding this comment.
Same stuff about very long code as text
There was a problem hiding this comment.
Agreed. The long code-as-text path in runAnalysis() is gone as part of the callr refactor.
|
Pushed follow-up commit Summary:
Verification:
|
|
Follow-up from the 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
No new Verified locally with |
|
Final cross-repo follow-up from the senior pass: no additional The remaining failures were below the orchestration layer:
With those branches installed together, the reported MixedModels path now completes:
This keeps |
|
I chased the noisy focused-test warnings through the bridge stack. There are no
With those PRs pushed, fresh installed |
|
Architecture re-check done after the latest jaspBase/jaspSyntax/native-decoder changes. I found and fixed one remaining boundary leak: jaspTools was still attempting to call a public jaspBase state decoder, but that decoder is intentionally internal in jaspBase PR #204. jaspBase already decodes result state before saving it, so jaspTools now only reads the saved callback state and leaves state traversal/decoding in jaspBase. Pushed as aaf02f1.\n\nThe current ownership boundaries are now:\n- jasp-desktop PR #6249 owns native token decoding through SyntaxInterface / ColumnEncoder.\n- jaspSyntax PR #8 exposes the native bridge and handles JSON result payload decoding via that bridge.\n- jaspBase PR #204 owns R-facing result/state traversal and keeps .decodeJaspResultState internal.\n- this jaspTools PR orchestrates options, dataset loading, and runWrappedAnalysis replay without reimplementing token/state decoding.\n\nFocused checks pass locally with source-loaded jaspTools:\n- test-rbridge-shim.R\n- test-jaspSyntax-lifecycle.R (3 real Descriptives integration tests skipped unless explicitly enabled)\n- test-generated-example-tests.R |
|
Follow-up on the silent fallback review: no jaspTools code changes were needed for this part. The fallback removal landed in the owning packages:
I reran the jaspTools bridge checks against source-loaded local jaspSyntax/jaspBase and they pass:
|
vandenman
left a comment
There was a problem hiding this comment.
Looks good, mostly a few smaller things.
| if (missing(installJaspCorePkgs)) { | ||
| title <- if (jaspBaseIsLegacyVersion()) { | ||
| "- Would you like jaspTools to install jaspResults, jaspBase and jaspGraphs? If you opt no, you must install them yourself." | ||
| "- Would you like jaspTools to install jaspResults, jaspBase, jaspSyntax and jaspGraphs? If you opt no, you must install them yourself." |
There was a problem hiding this comment.
This is not going to work. Remove the jaspSyntax from here and do not install it below.
There was a problem hiding this comment.
Agreed. Addressed in e5e9411: setupJaspTools() no longer mentions or installs jaspSyntax, and the startup update check no longer recommends updating it through the jaspTools core-package path. jaspSyntax remains in DESCRIPTION as an ordinary import because the bridge APIs are still required by this PR.
| .jaspToolsRunSubprocess <- function(task, payload, failureMessage, | ||
| isError = function(result) FALSE) { | ||
| logPath <- tempfile(paste0("jaspTools-", task, "-"), fileext = ".log") | ||
| result <- tryCatch( | ||
| callr::r( |
There was a problem hiding this comment.
Why do we run stuff in subprocesses? This means things like "browser()" will no longer work?
There was a problem hiding this comment.
Agreed after re-checking the tradeoff. Addressed in e5e9411 by removing the subprocess path entirely: runAnalysis() now stays in-process, R/subprocess.R is deleted, the subprocess tests/options are gone, and callr was removed from DESCRIPTION. This keeps browser() and normal interactive debugging as the default behavior.
| @@ -0,0 +1,39 @@ | |||
| import QtQuick | |||
There was a problem hiding this comment.
So one conceptual question about this qml file which is used for tests.
Is it really the responsibility of jaspTools to test this? I guess we could, but it feels like something that belongs in jaspSyntax.
An alternative is to just do git submodule on an actual jasp module (and set it to the latest released tag or so).
There was a problem hiding this comment.
Agreed. The direct jaspSyntax::readAnalysisOptionsFromQml() fixture test was testing jaspSyntax parser behavior rather than jaspTools orchestration. Removed that synthetic minimal module fixture and test in e5e9411; the remaining jaspTools tests cover delegation/wiring boundaries instead. I did not add a module submodule here because that would turn this narrow bridge coverage into heavier integration setup.
Remove runAnalysis subprocess containment so local replay remains debuggable in-process. Stop setupJaspTools from installing or update-checking jaspSyntax while keeping it as an Imports dependency, and remove the synthetic QML parser fixture that belonged in jaspSyntax coverage rather than jaspTools.
|
Changes look good but this needs jaspSyntax to be available/ installable first. |
Summary
This PR updates jaspTools as the orchestration layer for source-module analysis replay:
runAnalysis(..., verbose = ...)controls for separating analysis diagnostics from Desktop/QML/native chatter.jaspfile or source datasetdecodeAnalysisResults()so result replay uses the dataset/QML provenance that produced the analysisWhy
Encoding and decoding should be invisible to module authors and test authors. The lower-level PRs make Desktop the source of truth for token replacement; jaspTools only needs to keep the captured context attached to replay so old
.jaspfiles and source-module tests decode the same way JASP would.Related PRs
Validation
test-jaspSyntax-lifecycle.R(133 pass, 3 gated real-Descriptives skips)test-result-object-decoding.R,test-runWrappedAnalysis.Rtest-desktop-jasp-contract.R,test-dataset-helpers.Rgit diff --check