A Julia frontend for writing and composing Stan models
StanCon 2026 · Uppsala
18 August 2026
Write one model in a restricted, Julia-flavoured language:
StanBlocks is a frontend, not a replacement for Stan.
… with quality-of-life improvements such as activity analysis, submodels, user-defined types (currently enabling e.g. NamedTuples and ragged data structures), higher-order user-defined functions, automatic but optional type & shape inference, (ragged) plates, metaprogramming, Julia-style multiple dispatch, and more.
Why put a language in front of an already good language?
Julia maximizes expressive freedom; Stan narrows the problem to make differentiable programs predictable.
Out of the box. Not always the theoretical optimum—but mature vectorisation, fused kernels, reverse-mode AD, and NUTS adaptation make “write the obvious model” a strong baseline.
Static checks, explicit constraints and Jacobians, a mature sampler, and years of use on difficult models.
The generated target is a small, explicit language designed around Bayesian computation rather than general-purpose execution.
stan_code(model) can be read, diffed, archived, checked by stanc, compiled, and handed to the ordinary Stan ecosystem.
is not a debugging afterthought.
It is the review boundary between:
The emitted program answers concrete questions:
“Generated” does not have to mean “opaque.”
A small declarative model surface with a surprisingly high ceiling.
What is missing?
data {
int x_n; vector[x_n] x;
int y_n; vector[y_n] y;
}
parameters {
real alpha; real beta;
real<lower=0> sigma;
}
transformed parameters {
vector[x_n] mu = alpha + beta * x;
}
model {
alpha ~ normal(0, 2);
beta ~ normal(0, 1);
sigma ~ exponential(1);
y ~ normal(mu, sigma);
}
generated quantities {
vector[y_n] y_likelihood = ...;
vector[y_n] y_gen = ...;
}data · transformed data · parameters · transformed parameters · model · generated quantities
types · shapes · constraints · function signatures · captured dimensions
pointwise log likelihood · posterior predictive draws · executable model descriptor
activity analysis type + shape inference constraint propagation automatic generated-quantity twins BridgeStan log density
Emits a prefixed parameter eta_beta.
The payoff grows with a family of bespoke models, not with the first ten-line model.
The abstract listed closures, keyword/default arguments, and macros as possible future work.
They now sit beside:
closures · higher-order functions · variadics · required/optional kwargs · defaults · macro expansion · inline helpers
submodels · Base.merge variants · custom distribution triads · censoring/truncation/weights · fused GLMs · CV taint
plate · ragged data · constrained ragged parameters · missing outcomes · ODEs · Torsten signatures
Companion reference: feature atlas
Julia syntax on the front, SlicStan’s information-flow idea in the middle, Stan on the back.
Gorinova, Gordon & Sutton (POPL 2019) asked (not verbatim):
What if a Stan-like language were compositional and blockless, then translated back to Stan?
Shared ideas:
StanBlocks is directly inspired by SlicStan; it is not a port of SlicStan’s formal language or proofs.
| SlicStan | StanBlocks.jl | |
|---|---|---|
| Frontend | A new Stan-like language | A restricted Julia macro DSL |
| Core analysis | Information-flow type system | Forward trace + reverse likelihood tracking |
| Abstraction | Flexible model functions | Julia dispatch, closures, kwargs, macros, @slic submodels |
| Distinctive reach | Formal semantics; marginalizing out discrete parameters | Auto generated quantities, model variants, descriptors, BridgeStan/Julia integration |
| Implementation posture | Research implementation in F# | Julia package used by larger Julia model DSLs |
| Output | Stan | Stan |
SlicStan comparison based on the POPL paper and its public repository.
@slic stores Julia AST + bound data + defining module
Then show/stan_code emits Stan source; instantiate compiles it through BridgeStan.
| Binding | Compiler conclusion | Stan destination |
|---|---|---|
x, y |
supplied values | data |
mx |
data-only computation | transformed data |
alpha, beta |
sampled and likelihood-relevant | parameters + model |
mu |
deterministic and parameter-dependent | transformed parameters |
y_likelihood, y_gen |
post-fit workflow outputs | generated quantities |
The source reads in model order; the compiler schedules it in inference order.
Yes, but the value proposition changes.
plates, closures, higher-order functions, hardening.
Agents reduce the cost of writing code. StanBlocks reduces the amount of independent model meaning we have to maintain.
Move up a layer without hiding the layer below.
The largest current public consumer is BayesianRegressionModels.jl: a formula layer that lowers real model families into @slic and @deffun components.
Near-term priorities:
JuliaBayes contributors
Nikolas Siccha
Generable Inc.
Penelope Yong
Alan Turing Institute
Peter Thestrup Waade
TNU, ETH Zürich
Ryan Senne
Boston University
Sam Abbott
LSHTM (epinowcast · epiforecasts)
Simon Steiger
Karolinska Institutet
Source-to-emission examples and the honest edges of the current language.
The main example currently emits (helper functions omitted):
data {
int x_n;
vector[x_n] x;
int y_n;
vector[y_n] y;
}
transformed data {
}
parameters {
real alpha;
real beta;
real<lower=0.0> sigma;
}
transformed parameters {
vector[x_n] mu = (alpha + (beta * x));
}
model {
alpha ~ normal(0, 2);
beta ~ normal(0, 1);
sigma ~ exponential(1);
y ~ normal(mu, sigma);
}
generated quantities {
vector[y_n] y_likelihood = normal_lpdfs(y, mu, sigma);
vector[y_n] y_gen = normal_vector_rng(y_n, mu, sigma);
}@deffun@slic stays flat; @deffun owns loops, branches, mutation, iteration, and bounded comprehensions.
plate is the loop that samplesplate promotes fresh cell variables to outer storage and clones the compiler-owned loop into the Stan blocks that need it.
Essential rewrite:
data { vector[y_obs_n] y_obs; array[...] int y_ii_obs; array[...] int y_ii_mis; }
parameters { real mu; real<lower=0> sigma; }
model { y_obs ~ normal(mu, sigma); }
generated quantities {
vector[y_ii_mis_n] y_mis = normal_vector_rng(y_ii_mis_n, mu, sigma);
vector[...] y = merge_missing(y_obs, y_mis, y_ii_obs, y_ii_mis);
}The usual case adds no missing-value sampler dimensions: imputed values are posterior-predictive generated quantities.
The compiler selects the matching:
For censoring, the emitted density uses tail mass at the threshold atoms and the predictive RNG clamps a base-family draw.
For the regression example:
Consumers do not need a parallel registry of what a model can do.
| Boundary | Current rule |
|---|---|
| Model-level control flow | Keep @slic flat; use @deffun or plate |
plate recursion / scan |
Cells must be independent; use a deterministic recurrence |
| Plate cell shape | Scalars and fixed vectors; no matrix-valued cell result |
| General containers | No general 3-D+ Julia container surface |
| Missing data | Continuous outcomes only; no missing predictors or discrete parameters |
| Ragged observations | Continuous; groupwise log likelihood; no ragged integer draws |
| Arbitrary Julia calls | Register through @deffun/builtins; no opaque auto-transpilation |
| Validation | transpiles is not enough: run stanc and semantic/gradient checks |
A small language is useful only when its “no” is explicit.
Talk slot: 13:40–14:00, Tuesday 18 August 2026, Ångström Laboratory, Uppsala.