diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..35c0dc0e --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +* text=auto eol=lf +*.rda binary diff --git a/R/utils.R b/R/utils.R index 94a53137..3bb90770 100644 --- a/R/utils.R +++ b/R/utils.R @@ -950,6 +950,32 @@ compile_functions <- function(env, verbose = FALSE, global = FALSE) { prep_fun_cpp(funs[ind], fun_end, env$hpp_code) }) + reserved_names <- unique( + unlist( + lapply(stan_funs, function(stan_fun) { + regmatches( + stan_fun, + gregexpr("(?<=_stan_)[[:alnum:]_]+", stan_fun, perl = TRUE) + )[[1]] + }), + use.names = FALSE + ) + ) + + if (length(reserved_names) > 0) { + stop( + paste0( + "expose_functions() can't expose this Stan function because the function ", + "name and/or one or more argument names use a reserved keyword ", + "(typically in the C++ toolchain used to compile Stan). Please rename ", + "the function/arguments in your Stan functions block and try again. ", + "Conflicting names: ", + paste(reserved_names, collapse = ", ") + ), + call. = FALSE + ) + } + env$fun_names <- sapply(seq_len(length(funs) - 1), function(ind) { get_function_name(funs[ind], funs[ind + 1], env$hpp_code) }) diff --git a/tests/testthat/test-model-expose-functions.R b/tests/testthat/test-model-expose-functions.R index 4adc74a9..7373cfad 100644 --- a/tests/testthat/test-model-expose-functions.R +++ b/tests/testthat/test-model-expose-functions.R @@ -380,6 +380,86 @@ test_that("Overloaded functions give meaningful errors", { "Overloaded functions are currently not able to be exposed to R! The following overloaded functions were found: fun1, fun3") }) +reserved_names_msg <- function(names) { + paste( + "expose_functions() can't expose this Stan function because the function", + "name and/or one or more argument names use a reserved keyword", + "(typically in the C++ toolchain used to compile Stan). Please rename", + "the function/arguments in your Stan functions block and try again.", + paste("Conflicting names:", paste(names, collapse = ", ")) + ) +} + +test_that("Reserved names in Stan code give the same error", { + stan_file <- write_stan_file( + " + functions { + real min(real max, real class) { + return max - class; + } + } + " + ) + + funmod <- cmdstan_model(stan_file, force_recompile = TRUE) + expect_error( + funmod$expose_functions(), + reserved_names_msg(c("min", "max", "class")), + fixed = TRUE + ) +}) + +test_that("Multiple reserved C++ keywords in Stan code give the same error", { + stan_file <- write_stan_file( + " + functions { + real template(real class, real namespace, real private) { + return class + namespace + private; + } + } + " + ) + + funmod <- cmdstan_model(stan_file, force_recompile = TRUE) + expect_error( + funmod$expose_functions(), + reserved_names_msg(c("template", "class", "namespace", "private")), + fixed = TRUE + ) +}) + +test_that("Reserved keywords in Stan function bodies are allowed", { + stan_file <- write_stan_file( + " + functions { + real add_one(real x) { + real class = 1; + return x + class; + } + } + " + ) + + funmod <- cmdstan_model(stan_file, force_recompile = TRUE) + expect_no_error(funmod$expose_functions()) + expect_equal(funmod$functions$add_one(2), 3) +}) + +test_that("Stan code with no reserved names exposes functions", { + stan_file <- write_stan_file( + " + functions { + real add_pair(real left, real right) { + return left + right; + } + } + " + ) + + funmod <- cmdstan_model(stan_file, force_recompile = TRUE) + expect_no_error(funmod$expose_functions()) +}) + test_that("Exposing external functions errors before v2.32", { fake_cmdstan_version("2.26.0")