Skip to content
Merged
2 changes: 2 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export(cleanUp)
export(columnMapping)
export(decodeAnalysisResults)
export(decodeColumnNames)
export(columnDecoderSnapshot)
export(decodeColumnText)
export(generateAnalysisWrapper)
export(generateModuleWrappers)
export(getVariableNames)
Expand Down
12 changes: 10 additions & 2 deletions R/RcppExports.R
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ clearNativeStateNative <- function() {
invisible(.Call(`_jaspSyntax_clearNativeStateNative`))
}

setParameter <- function(name, value) {
.Call(`_jaspSyntax_setParameter`, name, value)
setParameterNative <- function(name, value) {
.Call(`_jaspSyntax_setParameterNative`, name, value)
}

loadDataSet <- function(data) {
Expand Down Expand Up @@ -57,3 +57,11 @@ getVariableNames <- function() {
.Call(`_jaspSyntax_getVariableNames`)
}

columnDecoderSnapshotNative <- function() {
.Call(`_jaspSyntax_columnDecoderSnapshotNative`)
}

decodeColumnTextNative <- function(values, decoderSnapshotJson) {
.Call(`_jaspSyntax_decodeColumnTextNative`, values, decoderSnapshotJson)
}

28 changes: 22 additions & 6 deletions R/bridgeSubprocess.R
Original file line number Diff line number Diff line change
Expand Up @@ -181,13 +181,15 @@
.runBridgeSubprocess <- function(task, target, input, failureLabel) {
stdoutPath <- tempfile(paste0("jaspSyntax_", task, "_"), fileext = ".out")
stderrPath <- tempfile(paste0("jaspSyntax_", task, "_"), fileext = ".err")
on.exit(unlink(c(stdoutPath, stderrPath)), add = TRUE)
resultPath <- tempfile(paste0("jaspSyntax_", task, "_"), fileext = ".rds")
on.exit(unlink(c(stdoutPath, stderrPath, resultPath)), add = TRUE)
packageSpec <- .bridgeSubprocessPackageSpec()
launchError <- NULL

result <- tryCatch(
tryCatch(
callr::r(
func = function(target, input, packageSpec, loadPackage) {
tryCatch(
func = function(target, input, packageSpec, loadPackage, resultPath) {
result <- tryCatch(
{
loadPackage(packageSpec)
do.call(getNamespace("jaspSyntax")[[target]], input)
Expand All @@ -196,12 +198,15 @@
structure(list(message = conditionMessage(e)), class = "jaspSyntax_subprocess_error")
}
)
saveRDS(result, resultPath)
invisible(NULL)
},
args = list(
target = target,
input = input,
packageSpec = packageSpec,
loadPackage = .bridgeSubprocessPackageLoader()
loadPackage = .bridgeSubprocessPackageLoader(),
resultPath = resultPath
),
libpath = .libPaths(),
stdout = stdoutPath,
Expand All @@ -211,12 +216,23 @@
error = "error"
),
error = function(e) {
structure(list(message = conditionMessage(e)), class = "jaspSyntax_subprocess_error")
launchError <<- e
NULL
}
)

output <- .readBridgeSubprocessOutput(stdoutPath, stderrPath)
outputSuffix <- .bridgeSubprocessOutputSuffix(output)
result <- if (file.exists(resultPath)) {
readRDS(resultPath)
} else {
message <- if (!is.null(launchError)) {
conditionMessage(launchError)
} else {
"subprocess did not return a result"
}
structure(list(message = message), class = "jaspSyntax_subprocess_error")
}

if (inherits(result, "jaspSyntax_subprocess_error")) {
stop(
Expand Down
165 changes: 165 additions & 0 deletions R/columnDecoder.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
#' Capture a Native Column Decoder
#'
#' Captures the current SyntaxInterface `ColumnEncoder` decode mapping in a
#' serializable object. The captured decoder can be reused after the active
#' native dataset changes.
#'
#' @param columnMapping Optional named character vector mapping encoded column
#' tokens to decoded user-facing names. This is mainly for tests and for
#' callers that already captured the mapping before native state changed.
#'
#' @return A serializable column decoder object.
#'
#' @export
columnDecoderSnapshot <- function(columnMapping = NULL) {
if (!is.null(columnMapping)) {
return(.columnDecoderSnapshotFromMapping(columnMapping))
}

rawSnapshot <- columnDecoderSnapshotNative()
.columnDecoderSnapshotFromJson(rawSnapshot)
}

#' Decode Text With a Native Column Decoder
#'
#' Decodes embedded JASP column tokens using SyntaxInterface's native
#' `ColumnEncoder` replacement rules.
#'
#' @param text Character vector to decode.
#' @param decoderSnapshot Optional decoder returned by `columnDecoderSnapshot()`.
#' When omitted, the current native bridge decoder is used.
#'
#' @return A character vector with native column tokens decoded.
#'
#' @export
decodeColumnText <- function(text, decoderSnapshot = NULL) {
if (!is.character(text)) {
stop("`text` must be a character vector", call. = FALSE)
}

snapshotJson <- .columnDecoderSnapshotJson(decoderSnapshot)
decoded <- tryCatch(
decodeColumnTextNative(text, snapshotJson),
error = function(e) {
if (.hasColumnDecoderFallback(decoderSnapshot)) {
return(.decodeColumnTextFallback(text, decoderSnapshot))
}
stop(e)

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.

Good: decodeColumnTextNative genuinely call the C++ decoding functionality!
Bad: .decodeColumnTextFallback. This reimplements the functionality in R. Do we even want this function to exist?

@FBartos & @boutinb what do you think? If we always create the snapshotJson on the fly, then I think we should be able to guarantee this? IF we can guarantee this then I think the whole fallback idea is somewhat overengineered and also as a liability. If the fallback is triggered, it means we have "broken" the guarantee which should just error. If the fallback is triggered, it does the decoding differently from how JASP does it. That can result in silently incorrect decoding of results, which I find far worse than showing e.g., encoded results as a fallback with a warning or so.

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.

Yes I agree, better an error than a wrong result.

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 with this direction. The fallback is gone now: decodeColumnText() always dispatches encoded-token decoding to native SyntaxInterface, and native failures are treated as bugs instead of triggering an R replacement path. The old snapshot/mapping contract was replaced with an opaque Desktop ColumnEncoder context, so jaspSyntax carries provenance but does not implement token replacement itself.

}
)
if (!is.character(decoded) || length(decoded) != length(text)) {
stop("Native column decoder returned an invalid result.", call. = FALSE)
}

names(decoded) <- names(text)
decoded
}

.columnDecoderSnapshotFromMapping <- function(columnMapping) {
columnMapping <- .validateAnalysisResultColumnMapping(columnMapping)
if (is.null(columnMapping)) {
columnMapping <- stats::setNames(character(), character())
}

columns <- unname(Map(
function(encoded, decoded) list(encoded = encoded, decoded = decoded),
names(columnMapping),
unname(columnMapping)
))

rawSnapshot <- as.character(jsonlite::toJSON(
list(version = 1L, columns = columns),
auto_unbox = TRUE,
null = "null"
))

.newColumnDecoderSnapshot(rawSnapshot, columnMapping)
}

.columnDecoderSnapshotFromJson <- function(rawSnapshot) {
if (!is.character(rawSnapshot) || length(rawSnapshot) != 1L || is.na(rawSnapshot)) {
stop("Native column decoder snapshot must be a single JSON string.", call. = FALSE)
}

parsed <- jsonlite::fromJSON(rawSnapshot, simplifyVector = FALSE)
columns <- parsed[["columns"]]
if (is.null(columns) || length(columns) == 0L) {
return(.newColumnDecoderSnapshot(
rawSnapshot,
stats::setNames(character(), character())
))
}

encoded <- vapply(columns, `[[`, character(1L), "encoded", USE.NAMES = FALSE)
decoded <- vapply(columns, `[[`, character(1L), "decoded", USE.NAMES = FALSE)
mapping <- stats::setNames(decoded, encoded)

.newColumnDecoderSnapshot(rawSnapshot, mapping)
}

.newColumnDecoderSnapshot <- function(rawSnapshot, columnMapping) {
structure(
list(
version = 1L,
columns = .validateAnalysisResultColumnMapping(columnMapping),
native = rawSnapshot
),
class = "jaspSyntaxColumnDecoder"
)
}

.columnDecoderSnapshotJson <- function(decoderSnapshot = NULL) {
if (is.null(decoderSnapshot)) {
return("")
}

if (inherits(decoderSnapshot, "jaspSyntaxColumnDecoder")) {
return(decoderSnapshot[["native"]])
}

if (is.character(decoderSnapshot) && length(decoderSnapshot) == 1L && !is.na(decoderSnapshot)) {
return(decoderSnapshot)
}

if (is.character(decoderSnapshot) && !is.null(names(decoderSnapshot))) {
return(.columnDecoderSnapshotFromMapping(decoderSnapshot)[["native"]])
}

stop("`decoderSnapshot` must be a native decoder snapshot or named column mapping.", call. = FALSE)
}

.decodeColumnTextWithMapping <- function(text, columnMapping) {
decodeColumnText(text, .columnDecoderSnapshotFromMapping(columnMapping))
}

.decodeColumnTextFallback <- function(text, decoderSnapshot = NULL) {
mapping <- .columnDecoderSnapshotMapping(decoderSnapshot)
if (length(mapping) == 0L) {
return(text)
}

tokens <- names(mapping)
tokens <- tokens[order(nchar(tokens), decreasing = TRUE)]
for (token in tokens) {
text <- gsub(token, unname(mapping[[token]]), text, fixed = TRUE)
}

text
}

.hasColumnDecoderFallback <- function(decoderSnapshot = NULL) {
inherits(decoderSnapshot, "jaspSyntaxColumnDecoder") ||
(is.character(decoderSnapshot) && !is.null(names(decoderSnapshot)))
}

.columnDecoderSnapshotMapping <- function(decoderSnapshot = NULL) {
if (inherits(decoderSnapshot, "jaspSyntaxColumnDecoder")) {
return(.validateAnalysisResultColumnMapping(decoderSnapshot[["columns"]]))
}

if (is.character(decoderSnapshot) && !is.null(names(decoderSnapshot))) {
return(.validateAnalysisResultColumnMapping(decoderSnapshot))
}

stats::setNames(character(), character())
}
9 changes: 0 additions & 9 deletions R/options.R
Original file line number Diff line number Diff line change
Expand Up @@ -667,15 +667,6 @@ parseQmlOptions <- function(qmlFile, options = NULL, moduleName = "jaspModule",
preloadData = preloadData
)

if (!is.character(rawOptions) || length(rawOptions) != 1L || !nzchar(rawOptions)) {
stop(
"jaspSyntax::loadQmlAndParseOptions() failed for QML file `",
qmlFile,
"`",
call. = FALSE
)
}

if (identical(output, "json")) {
return(rawOptions)
}
Expand Down
36 changes: 36 additions & 0 deletions R/parameters.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
.normalizeVerboseParameter <- function(value) {
if (is.null(value) || length(value) == 0L || is.na(value[[1L]]))
stop("`verbose` must be one of 'all', 'analysis', 'jasp', 'none', TRUE, or FALSE.", call. = FALSE)

value <- value[[1L]]
if (is.logical(value))
return(if (isTRUE(value)) "all" else "analysis")

if (is.character(value)) {
value <- tolower(trimws(value))
if (value %in% c("true", "yes", "on", "1"))
return("all")
if (value %in% c("false", "no", "off", "0"))
return("analysis")
if (value %in% c("all", "analysis", "jasp", "none"))
return(value)
}

stop("`verbose` must be one of 'all', 'analysis', 'jasp', 'none', TRUE, or FALSE.", call. = FALSE)
}

.verboseParameterShowsNativeOutput <- function(verbose) {
verbose %in% c("all", "jasp")
}

#' @export
setParameter <- function(name, value) {
if (identical(as.character(name), "verbose")) {
verbose <- .normalizeVerboseParameter(value)
result <- setParameterNative(name, .verboseParameterShowsNativeOutput(verbose))
options(jaspSyntax.verbose = verbose)
return(result)
}

setParameterNative(name, value)
}
Loading