Skip to content

Support source-module wrapped analysis replay#204

Open
FBartos wants to merge 22 commits into
masterfrom
bridge/jasp-syntax-runtime-contract
Open

Support source-module wrapped analysis replay#204
FBartos wants to merge 22 commits into
masterfrom
bridge/jasp-syntax-runtime-contract

Conversation

@FBartos

@FBartos FBartos commented May 14, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR provides the jaspBase runtime side of the jaspTools -> jaspSyntax bridge:

  • lets runWrappedAnalysis() resolve analyses from explicit source module paths and QML files, not only installed module packages
  • persists and retrieves standalone analysis state through the native callback-file contract instead of reaching into jaspTools internals
  • carries the opaque jaspSyntax/SyntaxInterface column encoder context through result materialization
  • decodes R-facing result copies returned by $toRObject(), including JASP-owned table names, footnotes, stored result-state figures, and ggplot labels/data/mappings/metadata
  • keeps encoded names in the backend/runtime path while avoiding deep mutation of opaque analysis-owned model objects
  • treats missing or failing native decoding as a bug for encoded tokens, rather than falling back to R-side maps
  • avoids double-rendering function/base plots when writing images

Why

Desktop/SyntaxInterface owns token replacement, jaspSyntax exposes that bridge, and jaspBase owns the boundary between backend analysis objects and R-facing result copies. This PR keeps that boundary explicit: JASP-owned display/result surfaces are decoded eagerly from the captured encoder context, while arbitrary downstream module internals stay opaque.

That fixes source-module replay and toRObject() decoding without adding downstream module dependencies to core packages or duplicating Desktop encoding rules in R.

Related PRs

Verification

  • focused jaspBase tests passed: test-result-object-decoding.R, test-runWrappedAnalysis.R
  • focused jaspSyntax tests passed against the rebuilt local Desktop bridge: test-desktop-jasp-contract.R, test-dataset-helpers.R
  • focused jaspTools lifecycle tests passed: test-jaspSyntax-lifecycle.R
  • R parse check passed
  • git diff --check

Comment thread R/common.R
Comment thread R/common.R Outdated
Comment thread R/common.R Outdated
@FBartos FBartos requested a review from vandenman May 27, 2026 07:53
Comment thread R/writeImage.R Outdated
Comment on lines +326 to +331
.decodeJaspPlotObject <- function(plot) {
tryCatch(
decodeplot(plot, returnGrob = FALSE),
error = function(e) plot
)
}

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.

Two issues.

  1. If a user loads a dataset, then does the analysis, it works. If they load another dataset afterward, and show then print/ save the plot from the previous analysis, this no longer works, no? We need some hook to know which encoding/ decoding object should be used for which plot object.
  2. If we do save or saveRDS on the output from jasp, then restart the r session and then try to replay the plot, all the information about encoding/ decoding is no longer available because this lives on the C++ side of jaspSyntax. We need a way around this. This could be done by immediately storing the decodedplot and not doing it on demand.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed; the later architecture now handles both cases without relying on the live dataset. runJaspResults() captures a per-analysis decode context after dataset preload, finishJaspResults() / .saveState() materialize stored state with that context, and state$figures stores decoded plot objects for replay/export. For saved or reloaded results, saveImage(), editImage(), and rewriteImages() use the stored-result-state path instead of the currently active dataset. The actual token replacement now comes from the opaque jaspSyntax/SyntaxInterface encoder context rather than an R-side mapping.

@FBartos

FBartos commented May 29, 2026

Copy link
Copy Markdown
Contributor Author

Implemented the eager-decoding follow-up from the review/spec discussion.

What changed:

  • Added an internal per-analysis decode context in jaspBase that captures the column mapping and factor-level mapping while the native/jaspSyntax state is still live.
  • Materialize decoded state before saving: finishJaspResults() now decodes figures and other state, and .saveState() defensively enforces the same contract.
  • Kept plot objects editable: ggplot/jaspGraphs/qgraph objects are stored as decoded editable objects, not rendered gTrees; base/function plots still materialize through recorded/grob behavior as before.
  • Updated the C++ plot handoff so the decoded object returned by writeImageJaspResults() is written back into plot state for all plot types, not only function plots.
  • Routed edit options and plotly conversion through the decoded object, so PNG/edit metadata/interactive JSON have the same labels.
  • decodeJaspResultState() is no longer exported; the state repair/materialization helper is internal now.
  • Extended R-facing object decoding to names, attributes, data frames, character vectors, factors, and numeric factor tokens using the captured context.

Companion jaspTools follow-up:

  • Pushed Fix plot fallback snapshots jaspTools#82 so the fast test plot writer also asks jaspBase for the decoded plot object instead of storing the raw encoded object.
  • Fixed the viewer helper to create the html output directory before copying assets; this came up when replaying the MixedModels example with view = TRUE.

Verification:

  • R CMD INSTALL . for jaspBase passes.
  • R CMD check --no-manual --no-vignettes . completes and runs testthat. It still reports the existing source-tree/package-metadata warnings/notes, including the already-known undeclared optional jaspSyntax usage, but no test failures.
  • Focused jaspBase tests pass: test-result-object-decoding.R, test-runWrappedAnalysis.R.
  • Focused jaspTools tests pass: test-runAnalysis-fast-test-plots.R, test-view.R.
  • Replayed MixedModelsGLMM from Larks and Owls.jasp; it returns results successfully and the returned state has no JaspColumn_.../jaspColumn... tokens.

@FBartos

FBartos commented May 29, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up from the final desiderata audit: I found one remaining provenance gap in the live R6 wrapper path. oRObject() called the C++ oRObject() materializer first, and that C++ code can consult the currently active global decoder before the R-side eager decoder sees the object. That meant a live wrapper could still be decoded against the wrong dataset after an analysis/dataset switch, even though saved state was already eager-decoded.

Fixed in 17d2533:

unJaspResults() stores the per-analysis decode context on the result wrapper and propagates it to child wrappers.

  • R-facing oRObject() materialization now temporarily installs a context-backed internal decoder while C++ materializes the object, then applies the recursive R decoder with the same context. This keeps C++ behavior intact but prevents late active-dataset provenance leaks.
  • saveImage(), editImage(), and
    ewriteImages() now treat loaded plot state as stored result state rather than consulting a live analysis decoder. If encoded legacy state is encountered without context, it warns and leaves it unchanged instead of silently decoding against the wrong dataset.
  • Added a regression test where the active decoder intentionally maps the same token to a wrong dataset name; the R6 wrapper still returns the original analysis names and factor labels.

Verification:

  • est-result-object-decoding.R passes against the installed temp-library jaspBase.
  • est-runWrappedAnalysis.R passes.
  • jaspTools focused fast-plot/view tests pass with the updated jaspBase source.
  • MixedModels MixedModelsGLMM replay from Larks and Owls.jasp passes through jaspTools::runAnalysis() with no encoded state tokens.
  • R CMD check --no-manual --no-vignettes exits 0; it still reports the existing source-tree warnings/notes unrelated to this change.

@FBartos

FBartos commented May 29, 2026

Copy link
Copy Markdown
Contributor Author

@vandenman this is ready for your review. The final desiderata pass is pushed, including the decode-context provenance follow-up for live result materialization and stored plot replay/export.

weblate and others added 3 commits May 29, 2026 16:01
Currently translated at 100.0% (26 of 26 strings)

Translation: JASP/jaspBase
Translate-URL: https://hosted.weblate.org/projects/jasp/jaspbase/vi/

Co-authored-by: Thành Khôi Lê <lethanhkhoi@gmail.com>
@FBartos

FBartos commented May 29, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up after the direct jaspMixedModels::MixedModelsLMM() cake repro: this did point to a structural boundary issue in the decoder, not just an lme4 edge case.

I pushed 0a64ba4 with three related changes:

  • Decode only JASP-owned / display-facing result surfaces. .decodeJaspRObject() now preserves opaque S3/S4/call/name objects instead of recursively rewriting arbitrary package-owned internals. This avoids mutating lme4 internals such as lmerResp.
  • Treat JASP mixed table cells by structure, not by the bare class name. The repro also exposed that afex::mixed() returns model objects with class mixed, so using inherits(x, mixed) as provenance was wrong. The decoder now recognizes only JASP mixed vectors/cells and leaves foreign mixed model objects intact.
  • Keep state$other opaque. That field contains jaspState payloads restored into later runs, so it is analysis-owned state. Plot replay/export/edit state under state$figures is still eagerly decoded.

I also moved the per-analysis decode-context capture until after dataset preload, because that is when jaspSyntax exposes the requested encoded column names. Without that, toRObject() had a context object but not the JaspColumn_* -> original name mapping.

Verification after the change:

  • pkgload::load_all('.', quiet = TRUE); testthat::test_file('tests/testthat/test-result-object-decoding.R') passes.
  • Installed jaspBase into C:/JASP-Packages/_verify-lib-4.6 and reran the cake MixedModelsLMM repro. It returns ANOVA Summary, Plot, and scanning the R-facing toRObject() output finds no visible JaspColumn_* tokens.
  • R.exe CMD check --no-manual --no-vignettes . completes with the existing source-tree warnings/notes; tests pass.

Comment thread R/resultDecoding.R Outdated
Comment on lines +90 to +93
defaultMapping <- tryCatch(
getExportedValue("jaspSyntax", "columnMapping")(strict = FALSE),
error = function(e) character()
)

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 should at least throw a warning, no? Same for the requestedMapping below?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes. This changed in the later fallback-removal pass. The R-side mapping/requestedMapping fallback is gone, and encoded-token decoding now goes through jaspSyntax::decodeColumnText() backed by native SyntaxInterface. If encoded legacy state is encountered without a usable context, jaspBase warns that it cannot safely decode it instead of silently applying a stale or approximate R mapping; native decoder failures for encoded tokens propagate as errors.

Comment thread R/common.R
Comment on lines +847 to +851
plt <- .decodeJaspPlotObject(
plt,
returnGrob = FALSE,
decodeContext = .jaspDecodeContext(source = "stored-result-state")
)

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.

In saveImage we should always use the returnGrob path because otherwise we might show encoded column names in plots within jasp.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed. saveImage() now decodes through .decodeJaspPlotObject(..., returnGrob = TRUE, decodeContext = .jaspDecodeContext(source = stored-result-state)), so the saved/printed object uses the stored result context and avoids showing encoded labels from a live-dataset mismatch.

Comment thread R/common.R
Comment on lines +1057 to +1061
plot <- .decodeJaspPlotObject(
jaspPlotCPP$plotObject,
returnGrob = FALSE,
decodeContext = .jaspDecodeContext(source = "stored-result-state")
)

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.

revert this? when reassigned to $plotobject this should do the de/ encoding then within writeImage?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This path changed with the stored-state architecture. We no longer rely on a live decoder or raw encoded object here; rewriteImages() decodes the stored plot object with .jaspDecodeContext(source = stored-result-state), edits the decoded editable object, and then assigns that decoded object back to plotObject. The actual rendering path still runs through writeImageJaspResults(), which keeps state/rendered output synchronized.

Comment thread R/common.R
Comment on lines +1112 to +1116
plot <- .decodeJaspPlotObject(
jaspPlotCPP$plotObject,
returnGrob = FALSE,
decodeContext = .jaspDecodeContext(source = "stored-result-state")
)

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.

revert this as well, see prior comments.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Same resolution as the prior plot-state comments: this is now using the stored-result-state context rather than live analysis state. editImage() decodes the stored editable plot object before resize/edit and assigns the decoded object back, while writeImageJaspResults() remains responsible for rendering and returning the object that should be persisted.

Comment thread R/common.R
Comment on lines +1323 to +1346
.runWrappedAnalysisWithVerbosity <- function(expr, verbose = "analysis") {
verbose <- .normalizeRunWrappedAnalysisVerbose(verbose)
showAnalysis <- .runWrappedAnalysisShowsAnalysis(verbose)
showJasp <- .runWrappedAnalysisShowsJasp(verbose)

if (!showJasp) {
outputFile <- tempfile("jaspBase-runWrappedAnalysis-")
outputConnection <- file(outputFile, open = "wt")
outputSink <- sink.number(type = "output")
on.exit({
while (sink.number(type = "output") > outputSink)
sink(type = "output")
close(outputConnection)
unlink(outputFile)
}, add = TRUE)

sink(outputConnection, type = "output")
}

if (showAnalysis)
return(.runWrappedAnalysisWithDecodedConditions(expr))

suppressWarnings(suppressMessages(expr))
}

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 works, but why not adjust https://github.com/jasp-stats/jasp-desktop/blob/22c167b122776652968722eaf7c09debb24e3203/Common/log.cpp#L18 so that we can disable or enable this in a static jasp-build from jaspSyntax?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed, this belongs in Desktop rather than jaspBase. The corresponding Desktop PR now changes Common/log.cpp so logType::null is a real sink instead of std::cout, and the SyntaxInterface bridge can be silenced from jaspSyntax via the verbose toggle. See jasp-desktop#6249, specifically the Common/log.cpp reply explaining why syntaxBridgeSetVerbose(false) needed that change.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Small correction to the cross-PR note: the final Desktop implementation no longer changes Common/log.cpp or the global logType::null default. That broader approach was reverted after Joris' review.

The current Desktop shape is narrower: SyntaxInterface initializes logging with the same Boost null stream pattern used by Engine/MainWindow, then toggles Log::setWhere()/Log::setDefaultDestination() for the bridge. So this remains owned by the native SyntaxInterface/desktop layer, but does not change global logging behavior in Common.

Comment thread R/writeImage.R Outdated
Comment on lines +82 to +83
plotObject <- .decodeJaspPlotObject(plot, returnGrob = FALSE, decodeContext = decodeContext)
plot2draw <- plotObject

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 should be reverted, see prior comments about encoding.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed by moving the decode ownership rather than keeping the earlier R-side approach. writeImageJaspResults() now receives/passes a decode context, decodes the plot through jaspBase's JASP-owned result boundary, and delegates token replacement to native jaspSyntax/SyntaxInterface. The R-side mapping fallback that motivated this concern has been removed.

Comment thread src/jaspPlot.cpp
// Keep the state object in sync with the rendered object. R returns an
// editable decoded object for ggplot-like plots and a materialized object
// for function/base plots.
if(writeResult.containsElementNamed("obj"))

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 did this need to change? This was not broken?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This became necessary once we eagerly store decoded plot state. Without writing the returned object back into plotInfo[obj], the PNG could be rendered from a decoded/materialized object while the stored plot object remained encoded. The C++ change keeps the persisted state object in sync with the object returned by writeImageJaspResults() for all plot types, not only function/base plots.

Comment thread R/resultDecoding.R
x
}

.decodeJaspFactorValues <- function(x, fieldName = NULL, decodeContext) {

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 reimplements the decoding from jasp. But is this really the best appraoch? Shouldn't we just reinstantiate that object? See https://github.com/jasp-stats/jasp-desktop/blob/development/Common/columnencoder.h, we could make an R6 object in jaspSyntax that provides an API to this and then use that instead? This is reimplementing the exact logic in that file.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed. The current implementation no longer reimplements ColumnEncoder token replacement in jaspBase. That logic moved to the native bridge in jasp-desktop#6249, and jaspSyntax#8 carries the opaque Desktop encoder context plus calls native decodeColumnText(). jaspBase now only decides which JASP-owned result surfaces to traverse and passes encoded text to jaspSyntax/native decoding; the R mapping/gsub fallback has been removed.

Carry the native column decoder snapshot through jaspBase result materialization so tables, plots, wrappers, and stored state decode against the analysis dataset rather than live global state. Keep fallback decoding shallow when jaspSyntax is unavailable and avoid double-rendering function/base plots when writing images.
@FBartos

FBartos commented Jun 1, 2026

Copy link
Copy Markdown
Contributor Author

@vandenman I pushed the follow-up implementation based on the decoder-ownership discussion.

The current architecture is now split as follows:

  • desktop/SyntaxInterface owns native ColumnEncoder snapshots and text-token replacement: jasp-desktop#6249
  • jaspSyntax exposes the native decoder snapshot/text API to R: jaspSyntax#8
  • jaspBase owns the R-facing decode boundary and decides which JASP-owned surfaces are decoded here

In this PR, jaspBase now carries the captured decoder context through result materialization, decodes tables/footnotes/stored figures/ggplot labels, and adds plot-specific decoding for ggplot data, mappings, layer data/mappings, facet mappings, and plot metadata. It still keeps arbitrary analysis-owned model state opaque, so this should not reintroduce the earlier too-deep object rewriting problem.

This also fixes the MixedModelsLMM smoke case that exposed encoded names inside the returned plot object: with patched jaspBase installed into a temporary R library,
es() no longer has encoded JaspColumn_ / jaspColumn tokens in the broad result scan.

Validation on Windows/R 4.6: focused jaspBase tests, focused jaspSyntax tests after rebuilding/installing against local SyntaxInterface, SyntaxInterface export check 16/16, and the MixedModelsLMM smoke above.

@FBartos

FBartos commented Jun 2, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the silent fallback concern in 3189fcc.\n\nWhat changed:\n- Removed .decodeJaspColumnTextWithMapping() and the R-side mapping/gsub fallback from result decoding.\n- Removed the �llowLiveFallback path through decodeColNames().\n- Result/state/plot/condition decoding now propagates native jaspSyntax::decodeColumnText() failures instead of returning encoded text.\n- decodeColNames() still passes plain text through outside JASP, but now errors if an encoded bridge token is encountered without an installed decoder.\n- decodeName() no longer swallows decoder errors before passing names into C++ wrappers.\n\nThe intended ownership is now strict: jaspBase owns R result traversal only; token replacement must come from jaspSyntax -> native SyntaxInterface / ColumnEncoder. jaspSyntax PR #8 contains the matching bridge cleanup; jasp-desktop #6249 owns the native decoder API.\n\nFocused checks passed locally against source-loaded jaspSyntax and jaspBase:\n- est-result-object-decoding.R\n- est-runWrappedAnalysis.R\n\nConsumer checks in jaspTools #79 also passed with local source-loaded jaspSyntax/jaspBase:\n- est-rbridge-shim.R\n- est-jaspSyntax-lifecycle.R\n- est-generated-example-tests.R\n\nThe native runtime still emits the known stack-imbalance warnings at process shutdown in the runWrappedAnalysis-focused test, but assertions pass.

@FBartos

FBartos commented Jun 3, 2026

Copy link
Copy Markdown
Contributor Author

@vandenman I pushed the architecture-level follow-up from the decoding review.

jaspBase no longer receives or normalizes R-side column mappings. It carries the opaque jaspSyntax/SyntaxInterface encoder context and treats native decoding failures for encoded tokens as bugs, while still limiting eager decoding to JASP-owned result copies rather than downstream module internals.

Related PRs: jasp-desktop jasp-stats/jasp-desktop#6249, jaspSyntax jasp-stats/jaspSyntax#8, jaspTools jasp-stats/jaspTools#79.

@boutinb boutinb left a comment

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.

I have tested jaspSyntax with this jaspBase, and it works.
If @vandenman agrees, we could merge this.

Comment thread R/resultDecoding.R Outdated
Comment on lines +167 to +201
.jaspEncodedColumnTokenPattern <- function() "(JaspColumn_[[:alnum:]_]+_Encoded|JaspExtraOptions_[[:alnum:]_]+_Encoded|jaspColumn[0-9]+)"

.containsJaspEncodedTokens <- function(x) {
if (!is.character(x) || length(x) == 0L)
return(FALSE)
any(grepl(.jaspEncodedColumnTokenPattern(), x, perl = TRUE), na.rm = TRUE)
}

.decodeJaspColumnText <- function(x, columnEncoderContext = NULL) {
if (!is.character(x) || length(x) == 0L)
return(x)
if (!.containsJaspEncodedTokens(x))
return(x)

if (is.list(columnEncoderContext) && "columnEncoderContext" %in% names(columnEncoderContext))
columnEncoderContext <- columnEncoderContext[["columnEncoderContext"]]
if (is.null(columnEncoderContext))
return(x)

decoded <- tryCatch(
getExportedValue("jaspSyntax", "decodeColumnText")(x, columnEncoderContext),
error = function(e) {
stop(
"jaspBase result decoding requires a working native jaspSyntax column decoder: ",
conditionMessage(e),
call. = FALSE
)
}
)
if (is.character(decoded) && length(decoded) == length(x)) {
names(decoded) <- names(x)
decoded
} else {
stop("Native jaspSyntax column decoder returned an invalid result.", call. = FALSE)
}

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.

Following the "parse don't validate" idea, just call the decoder immediately. Do not check for the presence of an encoded token, the columnencoder can do this much more efficiently. Do not validate the results afterward. All this should and is handled by the columnEncoder itself.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Implemented in 1b414bf.

.decodeJaspColumnText() now only unwraps the stored decode context and delegates directly to jaspSyntax::decodeColumnText() whenever a context is available. I removed the jaspBase-side encoded-token pre-scan and the post-call result-shape validation, so token/no-token handling and decoder result contracts stay below this layer.

I also added a focused regression test that verifies plain text is still delegated when a decode context is present; that would have failed with the old pre-scan.

Focused checks passed locally with the matching local jaspSyntax/SyntaxInterface installed first on .libPaths():

  • test-result-object-decoding.R
  • test-runWrappedAnalysis.R

@vandenman

Copy link
Copy Markdown
Contributor

Changes look good but this needs jaspSyntax to be available/ installable first.

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.

5 participants