Skip to content

Support model methods & standalone functions for models with external C++ (user_header)#1197

Open
ssp3nc3r wants to merge 1 commit into
stan-dev:masterfrom
ssp3nc3r:fix-model-methods-external-cpp
Open

Support model methods & standalone functions for models with external C++ (user_header)#1197
ssp3nc3r wants to merge 1 commit into
stan-dev:masterfrom
ssp3nc3r:fix-model-methods-external-cpp

Conversation

@ssp3nc3r

@ssp3nc3r ssp3nc3r commented Jun 15, 2026

Copy link
Copy Markdown

Submission Checklist

  • Run unit tests
  • Declare copyright holder and agree to license (see below)

Summary

compile_model_methods = TRUE (the $log_prob() / $grad_log_prob() / $hessian() methods) and compile_standalone = TRUE (expose_functions()) currently fail to compile for any model that uses an external C++ user_header — even though the very same model samples without issue. This blocks a common workflow: validating a hand-written C++ log density / analytic gradient against the pure-Stan autodiff version via grad_log_prob().

Reproducible example

bernoulli_external.stan (declares make_odds, defined in the header):

functions { real make_odds(real theta); }
data { int<lower=0> N; array[N] int<lower=0,upper=1> y; }
parameters { real<lower=0,upper=1> theta; }
model { theta ~ beta(1, 1); y ~ bernoulli(theta); }
generated quantities { real odds = make_odds(theta); }

make_odds.hpp (in the model's namespace, per the CmdStan convention):

#include <stan/math.hpp>
#include <ostream>
namespace bernoulli_external_model_namespace {
  template <typename T0__, stan::require_all_t<stan::is_stan_scalar<T0__>>* = nullptr>
  stan::promote_args_t<T0__> make_odds(const T0__& theta, std::ostream* pstream__) {
    return theta / (1 - theta);
  }
}
mod <- cmdstan_model("bernoulli_external.stan",
                     user_header = "make_odds.hpp",
                     compile_model_methods = TRUE)

mod$sample() works, but compiling the model methods aborts with:

error: use of undeclared identifier 'make_odds'
Root cause

Two independent problems in the standalone / model-methods build path:

  1. Mangled namespace. The standalone & model-methods C++ is generated by calling stanc directly through an argument vector (no shell) in get_standalone_hpp(), reusing the same --name='foo_model' flag that is built for the make-based executable build. The make path is processed by a shell that strips the quotes; the direct call is not, so the literal quotes become part of the model name, producing the namespace 'foo_model'_namespace (x39model...x39_namespace after C++ name-mangling). External C++ functions are defined by the user in foo_model_namespace, so they are not visible to the generated code.

  2. Header not included. rcpp_source_stan() does not -include the user header when compiling the methods via Rcpp::sourceCpp(), unlike CmdStan's makefile, which adds -include $(USER_HEADER).

Fix
  • Build an unquoted variant of the stanc options for the two direct get_standalone_hpp() calls; the make path keeps the quoted form (so names/paths with spaces are still safe there).
  • Stash the user-header path on the model-methods env (and on self$functions) and force-include it in rcpp_source_stan(), mirroring CmdStan's -include $(USER_HEADER).

This also enables compile_standalone = TRUE / expose_functions() for models with external C++, which share rcpp_source_stan().

Verification

For a normal model and an identical model whose single likelihood term is routed through an external C++ function, with the fix log_prob() and grad_log_prob() match the pure-Stan autodiff version exactly:

PLAIN     log_prob=-51.45881541  grad=(22.628038, 70.420847)
EXTERNAL  log_prob=-51.45881541  grad=(22.628038, 70.420847)
max|lp diff| = 0   max|grad diff| = 0

Adds a regression test in tests/testthat/test-model-methods.R using the existing bernoulli_external fixture (full test-model-methods.R passes locally on macOS / CmdStan 2.39.0).

Copyright and Licensing

Please list the copyright holder for the work you are submitting
(this will be you or your assignee, such as a university or company):

Scott Spencer

By submitting this pull request, the copyright holder is agreeing to
license the submitted work under the following licenses:

…ders

`compile_model_methods = TRUE` and `compile_standalone = TRUE` previously
failed for any model using an external C++ `user_header`, for two reasons:

1. The standalone / model-methods C++ was generated by calling stanc directly
   (via an argument vector, no shell) using the same quoted `--name='foo_model'`
   flag that is built for the make-based executable build. With no shell to
   strip them, the quotes became part of the model name, producing the
   namespace `'foo_model'_namespace`. External C++ functions, which are defined
   in `foo_model_namespace`, were therefore not visible and compilation failed
   with "use of undeclared identifier".

2. `rcpp_source_stan()` did not `-include` the user header when compiling the
   model methods via `Rcpp::sourceCpp()`, unlike CmdStan's makefile which adds
   `-include $(USER_HEADER)`.

Pass an unquoted form of the stanc options to `get_standalone_hpp()`, and
force-include the user header in `rcpp_source_stan()`. With both fixes,
`grad_log_prob()` on a model with a hand-written C++ likelihood matches the
pure-Stan autodiff version exactly. Adds a regression test.
@ssp3nc3r ssp3nc3r force-pushed the fix-model-methods-external-cpp branch from 62ffb92 to c4534ec Compare June 15, 2026 21:25
@codecov-commenter

codecov-commenter commented Jun 15, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.16%. Comparing base (bdbf824) to head (c4534ec).

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1197      +/-   ##
==========================================
+ Coverage   91.10%   91.16%   +0.06%     
==========================================
  Files          15       15              
  Lines        6125     6138      +13     
==========================================
+ Hits         5580     5596      +16     
+ Misses        545      542       -3     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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.

2 participants