Skip to content

Commit ee423bf

Browse files
authored
Merge pull request #1231 from stan-dev/bugfix-issue-1227
maintain separate direct and Make-quoted stanc options
2 parents 9efd39a + ae9e624 commit ee423bf

9 files changed

Lines changed: 322 additions & 71 deletions

File tree

NEWS.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,17 @@ variables are, instead of erroring. (#1225)
1515
which previously errored. (#1225)
1616
* The `CMDSTANR_NO_VER_CHECK` R option and environment variable are deprecated
1717
as of CmdStanR 1.0.0; use the lowercase `cmdstanr_no_ver_check` forms instead.
18+
* `$compile()` now works with named `stanc_options` values such as
19+
`canonicalize`. The values were shell-quoted for Make and the same quoted
20+
strings were also passed to `stanc` directly, which rejected them. (#1227)
21+
* `$compile()` now enables `allow-undefined` for user headers supplied through
22+
`cpp_options`, not just through the `user_header` argument. (#1227)
23+
* `stanc` failures during `$compile()` are now reported immediately, with the
24+
`stanc` error message. Previously they surfaced several steps later. (#1227)
25+
* Errors for include paths that do not exist now report the resolved absolute
26+
path. (#1227)
27+
* Numeric `stanc_options` values such as `list("max-line-length" = 78)` are no
28+
longer dropped. (#1233)
1829
* CmdStanModel methods now correctly handle `#include` directories with spaces
1930
in their paths. (#820)
2031
* `$include_paths()` now returns absolute paths, and relative include paths are

R/model.R

Lines changed: 49 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,7 @@ compile <- function(quiet = TRUE,
598598
include_paths <- private$precompile_include_paths_
599599
}
600600
private$include_paths_ <- resolve_path(include_paths)
601+
include_paths <- private$include_paths_
601602
if (is.null(dir) && !is.null(private$dir_)) {
602603
dir <- absolute_path(private$dir_)
603604
} else if (!is.null(dir)) {
@@ -630,7 +631,6 @@ compile <- function(quiet = TRUE,
630631
}
631632

632633
cpp_options[["USER_HEADER"]] <- wsl_safe_path(absolute_path(user_header))
633-
stanc_options[["allow-undefined"]] <- TRUE
634634
private$using_user_header_ <- TRUE
635635
} else if (!is.null(cpp_options[["USER_HEADER"]])) {
636636
if (!is.null(cpp_options[["user_header"]])) {
@@ -648,6 +648,7 @@ compile <- function(quiet = TRUE,
648648

649649

650650
if (!is.null(user_header)) {
651+
stanc_options[["allow-undefined"]] <- TRUE
651652
user_header <- absolute_path(user_header) # As mentioned above, just absolute, not wsl_safe_path()
652653
if (!file.exists(user_header)) {
653654
stop(paste0("User header file '", user_header, "' does not exist."), call. = FALSE)
@@ -709,29 +710,18 @@ compile <- function(quiet = TRUE,
709710
if (is.null(stanc_options[["name"]])) {
710711
stanc_options[["name"]] <- paste0(self$model_name(), "_model")
711712
}
712-
stanc_built_options <- c()
713-
for (i in seq_len(length(stanc_options))) {
714-
option_name <- names(stanc_options)[i]
715-
if (isTRUE(as.logical(stanc_options[[i]]))) {
716-
stanc_built_options <- c(stanc_built_options, paste0("--", option_name))
717-
} else if (is.null(option_name) || !nzchar(option_name)) {
718-
stanc_built_options <- c(stanc_built_options, paste0("--", stanc_options[[i]]))
719-
} else if (option_name == "name") { # Quoting model name mangles generated namespace
720-
stanc_built_options <- c(stanc_built_options, paste0("--", option_name, "=", stanc_options[[i]]))
721-
} else {
722-
stanc_built_options <- c(stanc_built_options, paste0("--", option_name, "=", "'", stanc_options[[i]], "'"))
723-
}
724-
}
725-
stancflags_combined <- stanc_built_options
713+
stancflags_combined <- stanc_options_to_args(stanc_options, quote_values = TRUE)
714+
stancflags_direct <- stanc_options_to_args(stanc_options)
726715
stancflags_local <- get_cmdstan_flags("STANCFLAGS")
727716
if (length(stancflags_local) > 0) {
728717
stancflags_combined <- c(stancflags_combined, stancflags_local)
718+
stancflags_direct <- c(stancflags_direct, stancflags_local)
729719
}
730720
stanc_inc_paths <- include_paths_stanc3_args(include_paths, direct_call = TRUE)
731-
stancflags_standalone <- c("--standalone-functions", stanc_inc_paths, stancflags_combined)
721+
stancflags_standalone <- c("--standalone-functions", stanc_inc_paths, stancflags_direct)
732722
self$functions$hpp_code <- get_standalone_hpp(temp_stan_file, stancflags_standalone)
733723
private$model_methods_env_ <- new.env()
734-
private$model_methods_env_$hpp_code_ <- get_standalone_hpp(temp_stan_file, c(stanc_inc_paths, stancflags_combined))
724+
private$model_methods_env_$hpp_code_ <- get_standalone_hpp(temp_stan_file, c(stanc_inc_paths, stancflags_direct))
735725
self$functions$external <- !is.null(user_header)
736726
self$functions$existing_exe <- FALSE
737727

@@ -796,12 +786,8 @@ compile <- function(quiet = TRUE,
796786
)
797787
)
798788
if (is.na(run_log$status) || run_log$status != 0) {
799-
err_msg <- "An error occured during compilation! See the message above for more information."
800-
if (grepl("auto-format flag to stanc", run_log$stderr)) {
801-
format_msg <- "\nTo fix deprecated or removed syntax please see ?cmdstanr::format for an example."
802-
err_msg <- paste(err_msg, format_msg)
803-
}
804-
stop(err_msg, call. = FALSE)
789+
stop("An error occurred during compilation! See the message above for more information.",
790+
call. = FALSE)
805791
}
806792
if (file.exists(exe)) {
807793
file.remove(exe)
@@ -991,17 +977,7 @@ check_syntax <- function(pedantic = FALSE,
991977
if (is.null(stanc_options[["name"]])) {
992978
stanc_options[["name"]] <- paste0(self$model_name(), "_model")
993979
}
994-
stanc_built_options <- c()
995-
for (i in seq_len(length(stanc_options))) {
996-
option_name <- names(stanc_options)[i]
997-
if (isTRUE(as.logical(stanc_options[[i]]))) {
998-
stanc_built_options <- c(stanc_built_options, paste0("--", option_name))
999-
} else if (is.null(option_name) || !nzchar(option_name)) {
1000-
stanc_built_options <- c(stanc_built_options, paste0("--", stanc_options[[i]]))
1001-
} else {
1002-
stanc_built_options <- c(stanc_built_options, paste0("--", option_name, "=", stanc_options[[i]]))
1003-
}
1004-
}
980+
stanc_built_options <- stanc_options_to_args(stanc_options)
1005981

1006982
withr::with_path(
1007983
c(
@@ -1111,37 +1087,21 @@ format <- function(overwrite_file = FALSE,
11111087
max_line_length,
11121088
lower = 1, len = 1, null.ok = TRUE
11131089
)
1114-
stanc_options <- private$precompile_stanc_options_
1090+
stanc_options <- as.list(private$precompile_stanc_options_)
11151091
stancflags_val <- include_paths_stanc3_args(
11161092
self$include_paths(),
11171093
direct_call = TRUE
11181094
)
1119-
stanc_options["auto-format"] <- TRUE
1095+
stanc_options[["auto-format"]] <- TRUE
11201096
if (!is.null(max_line_length)) {
1121-
stanc_options["max-line-length"] <- max_line_length
1097+
stanc_options[["max-line-length"]] <- max_line_length
11221098
}
11231099
if (isTRUE(canonicalize)) {
1124-
stanc_options["print-canonical"] <- TRUE
1100+
stanc_options[["print-canonical"]] <- TRUE
11251101
} else if (is.list(canonicalize) && length(canonicalize) > 0){
1126-
stanc_options["canonicalize"] <- paste0(canonicalize, collapse = ",")
1127-
}
1128-
stanc_built_options <- c()
1129-
for (i in seq_len(length(stanc_options))) {
1130-
option_name <- names(stanc_options)[i]
1131-
if (isTRUE(as.logical(stanc_options[[i]])) && !is.numeric(stanc_options[[i]])) {
1132-
stanc_built_options <- c(stanc_built_options, paste0("--", option_name))
1133-
} else if (is.null(option_name) || !nzchar(option_name)) {
1134-
stanc_built_options <- c(
1135-
stanc_built_options,
1136-
paste0("--", stanc_options[[i]])
1137-
)
1138-
} else {
1139-
stanc_built_options <- c(
1140-
stanc_built_options,
1141-
paste0("--", option_name, "=", stanc_options[[i]])
1142-
)
1143-
}
1102+
stanc_options[["canonicalize"]] <- paste0(canonicalize, collapse = ",")
11441103
}
1104+
stanc_built_options <- stanc_options_to_args(stanc_options)
11451105
withr::with_path(
11461106
c(
11471107
toolchain_PATH_env_var(),
@@ -2471,6 +2431,39 @@ assert_stan_file_exists <- function(stan_file) {
24712431
}
24722432
}
24732433

2434+
#' Turn a `stanc_options` list into `stanc` command line arguments
2435+
#'
2436+
#' @param stanc_options (list) Named or unnamed stanc options. Logical values
2437+
#' mark boolean flags and any other value is passed as `--name=value`.
2438+
#' @param quote_values (logical) Single-quote option values? Only the
2439+
#' `STANCFLAGS` string handed to Make needs quoting, because Make expands it
2440+
#' through a shell. Arguments for direct `stanc` calls are passed to processx
2441+
#' as separate elements and must be left unquoted (#1227).
2442+
#' @return A character vector of arguments, one per element.
2443+
#' @noRd
2444+
stanc_options_to_args <- function(stanc_options, quote_values = FALSE) {
2445+
args <- c()
2446+
for (i in seq_len(length(stanc_options))) {
2447+
option_name <- names(stanc_options)[i]
2448+
option_value <- stanc_options[[i]]
2449+
if (is.null(option_name) || !nzchar(option_name)) {
2450+
# Unnamed options are already flag names, e.g. list("allow-undefined")
2451+
args <- c(args, paste0("--", option_value))
2452+
} else if (is.logical(option_value)) {
2453+
# TRUE emits a bare flag, FALSE leaves the flag out entirely
2454+
if (isTRUE(option_value)) {
2455+
args <- c(args, paste0("--", option_name))
2456+
}
2457+
} else if (isTRUE(quote_values) && option_name != "name") {
2458+
# Quoting the model name mangles the generated namespace
2459+
args <- c(args, paste0("--", option_name, "=", "'", option_value, "'"))
2460+
} else {
2461+
args <- c(args, paste0("--", option_name, "=", option_value))
2462+
}
2463+
}
2464+
args
2465+
}
2466+
24742467
#' Build stanc include-path arguments
24752468
#'
24762469
#' Make receives include paths through `STANCFLAGS` and needs paths containing

R/utils.R

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -931,6 +931,7 @@ get_standalone_hpp <- function(stan_file, stancflags) {
931931
name <- strip_ext(basename(stan_file))
932932
path <- dirname(stan_file)
933933
hpp_path <- file.path(path, paste0(name, ".hpp"))
934+
on.exit(unlink(hpp_path), add = TRUE)
934935

935936
status <- withr::with_path(
936937
c(
@@ -944,13 +945,25 @@ get_standalone_hpp <- function(stan_file, stancflags) {
944945
error_on_status = FALSE
945946
)
946947
)
947-
if (status$status == 0) {
948-
hpp <- suppressWarnings(readLines(hpp_path, warn = FALSE))
949-
unlink(hpp_path)
950-
hpp
951-
} else {
952-
invisible(NULL)
948+
if (is.na(status$status) || status$status != 0) {
949+
if (length(status$stderr) > 0 && nzchar(status$stderr)) {
950+
message(status$stderr)
951+
}
952+
err_msg <- paste0(
953+
"An error occurred during compilation! See the message above for more ",
954+
"information. (stanc exited with status ", status$status, ")"
955+
)
956+
if (length(status$stderr) > 0 &&
957+
grepl("auto-format flag to stanc", status$stderr)) {
958+
err_msg <- paste0(
959+
err_msg,
960+
"\nTo fix deprecated or removed syntax please see ",
961+
"?cmdstanr::format for an example."
962+
)
963+
}
964+
stop(err_msg, call. = FALSE)
953965
}
966+
suppressWarnings(readLines(hpp_path, warn = FALSE))
954967
}
955968

956969
get_function_name <- function(fun_start, fun_end, model_lines) {

tests/testthat/_snaps/utils.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,24 @@
1+
# get_standalone_hpp() reports stanc failures
2+
3+
Code
4+
get_standalone_hpp(stan_file, "--canonicalize='deprecations'")
5+
Message
6+
stanc: invalid canonicalize value
7+
Condition
8+
Error:
9+
! An error occurred during compilation! See the message above for more information. (stanc exited with status 124)
10+
11+
# get_standalone_hpp() suggests formatting deprecated syntax
12+
13+
Code
14+
get_standalone_hpp(stan_file, character())
15+
Message
16+
Syntax error: Use the auto-format flag to stanc
17+
Condition
18+
Error:
19+
! An error occurred during compilation! See the message above for more information. (stanc exited with status 1)
20+
To fix deprecated or removed syntax please see ?cmdstanr::format for an example.
21+
122
# copy_temp_files retains sources if any copy fails
223

324
Code

tests/testthat/test-fit-mcmc.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ test_that("inc_warmup in draws() works", {
270270
expect_equal(dim(y4), NULL)
271271
})
272272

273-
test_that("inc_warmup in draws() works", {
273+
test_that("inc_warmup in draws() works with a single chain", {
274274
x3 <- fit_mcmc_2$draws(inc_warmup = FALSE)
275275
expect_equal(dim(x3), c(10000, 1, 5))
276276
expect_error(fit_mcmc_2$draws(inc_warmup = TRUE),

tests/testthat/test-model-compile-user_header.R

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,38 @@
1+
# This test is deliberately placed above the file-level skip_if(os_is_macos())
2+
# below: it mocks the stanc call and never compiles, so it needs no toolchain
3+
# and should run on every platform.
4+
test_that("cpp_options user headers allow undefined functions", {
5+
stan_file <- testing_stan_file("bernoulli_external")
6+
user_header <- withr::local_tempfile(lines = "", fileext = ".hpp")
7+
received_stancflags <- list()
8+
local_mocked_bindings(
9+
get_cmdstan_flags = function(flag_name) character(),
10+
get_standalone_hpp = function(stan_file, stancflags) {
11+
received_stancflags <<- append(received_stancflags, list(stancflags))
12+
""
13+
}
14+
)
15+
16+
for (option_name in c("USER_HEADER", "user_header")) {
17+
model <- cmdstan_model(stan_file, compile = FALSE)
18+
model$compile(
19+
cpp_options = setNames(list(user_header), option_name),
20+
force_recompile = TRUE,
21+
dry_run = TRUE
22+
)
23+
}
24+
25+
expect_length(received_stancflags, 4)
26+
expect_equal(
27+
vapply(
28+
received_stancflags,
29+
function(x) "--allow-undefined" %in% x,
30+
logical(1)
31+
),
32+
rep(TRUE, 4)
33+
)
34+
})
35+
136
skip_if(os_is_macos())
237

338
w_path <- function(f) {

0 commit comments

Comments
 (0)