Skip to content
Merged
4 changes: 2 additions & 2 deletions DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
Package: jaspSyntax
Type: Package
Title: Makes JASP analyses available in R
Version: 1.3.2
Date: 2026-05-11
Version: 1.3.3
Date: 2026-06-04
Author: JASP Team
Website: jasp-stats.org
Maintainer: JASP Team <info@jasp-stats.org>
Expand Down
3 changes: 2 additions & 1 deletion NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ export(clearDatasetState)
export(clearNativeState)
export(clearQmlForms)
export(cleanUp)
export(columnMapping)
export(columnEncoderContext)
export(decodeAnalysisResults)
export(decodeColumnNames)
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`)
}

columnEncoderContextNative <- function() {
.Call(`_jaspSyntax_columnEncoderContextNative`)
}

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

63 changes: 45 additions & 18 deletions R/bridgeSubprocess.R
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@
}

.bridgeSubprocessPackageLoader <- function() {
function(packageSpec) {
loader <- function(packageSpec) {
pathEntries <- function(path = Sys.getenv("PATH", unset = "")) {
entries <- strsplit(path, .Platform$path.sep, fixed = TRUE)[[1L]]
normalizePath(entries[nzchar(entries)], winslash = "/", mustWork = FALSE)
Expand Down Expand Up @@ -146,10 +146,13 @@
character(0)
}
buildDirs <- buildDirs[dir.exists(buildDirs)]
Sys.setenv(PATH = paste(unique(c(dllDirs, buildDirs, currentPathEntries)), collapse = .Platform$path.sep))
Sys.setenv(PATH = paste(unique(c(buildDirs, dllDirs, currentPathEntries)), collapse = .Platform$path.sep))
message("jaspSyntax subprocess source package: ", packagePath)
message("jaspSyntax subprocess DLL dirs: ", paste(dllDirs, collapse = ";"))
message("jaspSyntax subprocess PATH head: ", paste(head(strsplit(Sys.getenv("PATH"), .Platform$path.sep, fixed = TRUE)[[1L]], 8L), collapse = ";"))
if (length(buildDirs) > 0L) {
message("jaspSyntax subprocess build DLL dirs: ", paste(buildDirs, collapse = ";"))
}
message("jaspSyntax subprocess PATH head: ", paste(utils::head(strsplit(Sys.getenv("PATH"), .Platform$path.sep, fixed = TRUE)[[1L]], 8L), collapse = ";"))
}

if (!requireNamespace("pkgload", quietly = TRUE)) {
Expand All @@ -161,6 +164,26 @@
suppressPackageStartupMessages(library(jaspSyntax))
}
}
environment(loader) <- baseenv()
loader
}

.bridgeSubprocessRunner <- function() {
runner <- function(target, input, packageSpec, loadPackage, resultPath) {
result <- tryCatch(
{
loadPackage(packageSpec)
do.call(getNamespace("jaspSyntax")[[target]], input)
},
error = function(e) {
structure(list(message = conditionMessage(e)), class = "jaspSyntax_subprocess_error")
}
)
saveRDS(result, resultPath)
invisible(NULL)
}
environment(runner) <- baseenv()
runner
}

.readBridgeSubprocessOutput <- function(stdoutPath, stderrPath) {
Expand All @@ -181,27 +204,20 @@
.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(
{
loadPackage(packageSpec)
do.call(getNamespace("jaspSyntax")[[target]], input)
},
error = function(e) {
structure(list(message = conditionMessage(e)), class = "jaspSyntax_subprocess_error")
}
)
},
func = .bridgeSubprocessRunner(),
args = list(
target = target,
input = input,
packageSpec = packageSpec,
loadPackage = .bridgeSubprocessPackageLoader()
loadPackage = .bridgeSubprocessPackageLoader(),
resultPath = resultPath
),
libpath = .libPaths(),
stdout = stdoutPath,
Expand All @@ -211,12 +227,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
153 changes: 153 additions & 0 deletions R/columnDecoder.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
#' Capture a Native Column Encoder Context
#'
#' Captures the source state needed by SyntaxInterface to reconstruct the native
#' `ColumnEncoder`: dataset column names/types and extra QML option encodings.
#' The context can be reused after the active native dataset changes.
#'
#' @return A serializable column encoder context.
#'
#' @export
columnEncoderContext <- function() {
rawContext <- columnEncoderContextNative()
.columnEncoderContextFromJson(rawContext)
}

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

contextJson <- .columnEncoderContextJson(encoderContext)
decoded <- tryCatch(
decodeColumnTextNative(text, contextJson),
error = function(e) {
stop(
"Native column decoder failed: ",
conditionMessage(e),
call. = FALSE
)
}
)
if (!is.character(decoded) || length(decoded) != length(text)) {
stop("Native column decoder returned an invalid result.", call. = FALSE)
}

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

.columnEncoderContextFromJson <- function(rawContext) {
if (!is.character(rawContext) || length(rawContext) != 1L || is.na(rawContext)) {
stop("Native column encoder context must be a single JSON string.", call. = FALSE)
}

parsed <- jsonlite::fromJSON(rawContext, simplifyVector = FALSE)
.newColumnEncoderContext(
rawContext = rawContext,
columns = .normalizeColumnEncoderContextColumns(parsed[["columns"]]),
extra = .normalizeColumnEncoderContextColumns(parsed[["extra"]])
)
}

.newColumnEncoderContext <- function(rawContext = NULL, columns = list(), extra = list()) {
columns <- .normalizeColumnEncoderContextColumns(columns)
extra <- .normalizeColumnEncoderContextColumns(extra)

if (is.null(rawContext)) {
rawContext <- as.character(jsonlite::toJSON(
list(version = 1L, columns = columns, extra = extra),
auto_unbox = TRUE,
null = "null"
))
}

structure(
list(
version = 1L,
columns = columns,
extra = extra,
native = rawContext
),
class = "jaspSyntaxColumnEncoderContext"
)
}

.normalizeColumnEncoderContextColumns <- function(columns = NULL) {
if (is.null(columns) || length(columns) == 0L) {
return(list())
}

if (is.data.frame(columns)) {
columns <- split(columns, seq_len(nrow(columns)))
}

if (!is.list(columns)) {
stop("Column encoder context columns must be a list.", call. = FALSE)
}

lapply(columns, function(column) {
if (!is.list(column) || is.null(column[["name"]]) || is.null(column[["type"]])) {
stop("Column encoder context entries must contain `name` and `type`.", call. = FALSE)
}

name <- column[["name"]]
type <- column[["type"]]
if (!is.character(name) || length(name) != 1L || is.na(name) || !nzchar(name) ||
!is.character(type) || length(type) != 1L || is.na(type) || !nzchar(type)) {
stop("Column encoder context `name` and `type` entries must be non-empty strings.", call. = FALSE)
}

list(name = name, type = type)
})
}

.columnEncoderContextJson <- function(encoderContext = NULL) {
if (is.null(encoderContext)) {
return("")
}

if (inherits(encoderContext, "jaspSyntaxColumnEncoderContext")) {
return(encoderContext[["native"]])
}

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

if (is.list(encoderContext) &&
(!is.null(encoderContext[["columns"]]) || !is.null(encoderContext[["extra"]]))) {
return(.newColumnEncoderContext(
columns = encoderContext[["columns"]],
extra = encoderContext[["extra"]]
)[["native"]])
}

stop("`encoderContext` must be a native column encoder context.", call. = FALSE)
}

.containsEncodedBridgeColumnTokens <- function(text) {
if (!is.character(text) || length(text) == 0L) {
return(FALSE)
}

any(grepl(
"(JaspColumn_[[:alnum:]_]+_Encoded|JaspExtraOptions_[[:alnum:]_]+_Encoded|jaspColumn[0-9]+)",
text,
perl = TRUE
), na.rm = TRUE)
}
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