StanBlocks.jl

A Julia frontend for writing and composing Stan models

Nikolas Siccha · Generable Inc.

StanCon 2026 · Uppsala

18 August 2026

A deliberately modest promise

Write one model in a restricted, Julia-flavoured language:

model = @slic (; x, y) begin
    alpha ~ normal(0, 2)
    beta  ~ normal(0, 1)
    sigma ~ exponential(1)
    y ~ normal(alpha + beta * x, sigma)
end

Get ordinary, inspectable Stan:

parameters {
  real alpha;
  real beta;
  real<lower=0> sigma;
}
model {
  alpha ~ normal(0, 2);
  beta ~ normal(0, 1);
  sigma ~ exponential(1);
  y ~ normal(alpha + beta * x,
             sigma);
}

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, closures, keyword & default arguments, automatic but optional type & shape inference, (ragged) plates, metaprogramming, Julia-style multiple dispatch, and more.

1. Why?

Why put a language in front of an already good language?

Julia + performant AD versus Stan

Julia + performant AD

  • expressive, general-purpose language
  • mathematical code may need expert rewriting
  • AD must cope with that full expressiveness
  • historically incomplete, wrong, or slow
  • Enzyme makes far more possible today

Stan

  • restricted language for statistical models
  • obvious mathematical code is a strong baseline
  • AD is central to the inference stack
  • narrower target is easier to optimize reliably
  • generated code remains inspectable

Julia maximizes expressive freedom; Stan narrows the problem to make differentiable programs predictable.

The Stan bargain

“Not ideal, but good” performance

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.

Reliability

Static checks, explicit constraints and Jacobians, a mature sampler, and years of use on difficult models.

Stable semantics

The generated target is a small, explicit language designed around Bayesian computation rather than general-purpose execution.

A review boundary

stan_code(model) is the boundary between a compositional authoring language and a mature inference language — read it, diff it, archive it, check it with stanc, hand it to the Stan ecosystem.

“Generated” does not have to mean “opaque.”

2. What?

A small declarative model surface with a surprisingly high ceiling.

One model declaration

StanBlocks source

m = @slic (; x, y) begin
    alpha ~ normal(0, 2)
    beta  ~ normal(0, 1)
    sigma ~ exponential(1)

    mu = alpha + beta * x
    y ~ normal(mu, sigma)
end

What is missing?

  • block declarations
  • types and shapes

Main caveat — model-level control flow: keep @slic flat; use @deffun or plate.

Essential Stan emission

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 = ...;
}
Placement

data · transformed data · parameters · transformed parameters · model · generated quantities

Representation

types · shapes · constraints · function signatures · captured dimensions

Workflow outputs

pointwise log likelihood · posterior predictive draws · executable model descriptor

Composition is the reason for the frontend

Reusable submodel

@slic random_slope(x::vector[n]) = begin
    beta ~ normal(0, 1)
    return beta * x
end

m = @slic (; x, y) begin
    eta ~ random_slope(x)
    y ~ normal(eta, 1)
end

Emits a prefixed parameter eta_beta.

Post-hoc variant

wide_prior = Base.merge(random_slope, quote
    beta ~ normal(0, 5)
end)

The matching beta statement is replaced; random_slope stays unchanged.

The same mechanism supports:

  • nested submodels
  • swapping in new data
  • typed positional arguments
  • generated model variants

The payoff grows with a family of bespoke models, not with the first ten-line model.

3. How?

Julia syntax on the front, SlicStan’s information-flow idea in the middle, Stan on the back.

The intellectual lineage: SlicStan

StanBlocks is directly inspired by Gorinova, Gordon & Sutton (POPL 2019). Shared ideas: no author-written Stan blocks, placement inferred from information flow, composable model fragments, and Stan preserved as the inference target.

SlicStan StanBlocks.jl (MlicStan?)
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

One model, three activity analyses

m = @slic (; y, subject) begin
    J     = maximum(subject)
    mu    ~ normal(0, 1)
    tau   ~ exponential(1)
    alpha :: vector[J] ~ normal(mu, tau)   # per-subject effect
    sigma ~ exponential(1)
    y ~ normal(alpha[subject], sigma)
end

Same source. Only the data you bind changes — activity analysis and metadata decide, per statement, where each sampling statement ends up.

Prior prediction

Bind no outcome — m(; subject).

Nothing to condition on, so the whole model forward-simulates in generated quantities — mu, tau, alpha, sigma, and y itself.

Posterior fit

Bind y observed.

y ~ normal(alpha[subject], sigma) becomes the likelihood; mu, tau, alpha, sigma are fitted, and y_gen is the posterior-predictive replica.

Population prediction · CV

Bind maybecv(:subject, subject).

Taint flows from the grouping into alpha: the per-subject effects leave parameters and re-draw from normal(mu, tau) in generated quantities, while mu, tau, sigma stay fitted.

4. Started pre-agents — still relevant?

Yes, but the value proposition changes.

From keystroke savings to a model contract

Oct 2024Initial repository: block inference, Julia syntax, Stan emission.
2025Production-shaped models, generated quantities, broader Stan surface.
2026Agent-assisted expansion: plates, closures, higher-order functions, hardening.

Agents make this cheaper

  • write Stan boilerplate
  • translate a sketch into a full model
  • generate repetitive signatures and tests
  • explore compiler edge cases quickly
  • producing, finding, and fixing bugs

Agents do not make this disappear

  • one source for a family of model variants
  • a reusable statistical vocabulary
  • generated Stan that humans and tools can inspect

Agents reduce the cost of writing code. StanBlocks reduces the amount of independent model meaning we have to maintain.

5. What next?

Move up a layer without hiding the layer below.

StanBlocks as one compiler layer

BRM.jl
formulae, terms, priors, model-family vocabulary
StanBlocks.jl
composition, tracing, block/type/shape inference
BridgeStan.jl
compiled Stan model — log density & gradients (AD)
JuliaBayes.jl
shared Julia-facing workflow and ecosystem integration

The largest current public (but unregistered) consumer is BayesianRegressionModels.jl: a brms-like formula layer that currently lowers into Stan via StanBlocks.jl, but will target Julia via Turing.jl as well in the future.

Near-term priorities:

  1. Make BRM.jl the ergonomic route for standard regression families.
  2. Keep StanBlocks available for bespoke PKPD and other compositional models.
  3. Connect through JuliaBayes interfaces rather than inventing another closed workflow.

JuliaBayes contributors

Nikolas Siccha

Nikolas Siccha

Generable Inc.

Penelope Yong

Penelope Yong

Alan Turing Institute

Peter Thestrup Waade

Peter Thestrup Waade

TNU, ETH Zürich

Ryan Senne

Ryan Senne

Boston University

Sam Abbott

Sam Abbott

LSHTM (epinowcast · epiforecasts)

Simon Steiger

Simon Steiger

Karolinska Institutet

Summary

The pitch

  1. Julia + AD maximizes expressiveness; Stan narrows the problem to make differentiable statistical programs predictable.
  2. Stan is a strong inference target: reliable, good out of the box, and explicit.
  3. StanBlocks makes model families composable while keeping generated Stan inspectable.
  4. Agents strengthen the case for a compiler contract, even as they weaken the case for mere syntax reduction.

Links

Headshot of Nikolas Siccha

Nikolas Siccha
Generable Inc.

QR code linking to the StanCon 2026 slides

Slides

Backup

Source-to-emission examples and the honest edges of the current language.

Backup · deterministic control flow belongs in @deffun

@deffun centered_sum(x) = begin
    xbar = mean(x)
    acc = 0.0
    for xi in x
        acc += xi - xbar
    end
    acc
end
real centered_sum(vector x) {
  real xbar = mean(x);
  real acc = 0.0;
  for (i in 1:num_elements(x)) {
    real xi = x[i];
    acc += xi - xbar;
  }
  return acc;
}

@slic stays flat; @deffun owns loops, branches, mutation, iteration, and bounded comprehensions.

Backup · higher-order functions specialize at compile time

@deffun apply_twice(f, x) = f(f(x))

# f is passed like data — pick any callee
z = apply_twice(exp, mu)   # ⇒ exp(exp(mu))
real apply_twice_exp(real x) {
  return exp(exp(x));
}

// at the call site:
real z = apply_twice_exp(mu);

The function argument is resolved at compile time: StanBlocks emits one specialized Stan function per callee (apply_twice_exp), so no runtime function object ever reaches Stan.

Backup · plate is the loop that samples

theta ~ plate(y; outer = 4) do yi
    t ~ normal(mu0, 1)
    yi ~ normal(t, sigma)
    t
end
parameters {
  vector[4] theta_t;
}
transformed parameters {
  for (i in 1:4)
    theta[i] = theta_t[i];
}
model {
  for (i in 1:4) {
    theta_t[i] ~ normal(mu0, 1);
    y[i] ~ normal(theta_t[i], sigma);
  }
}

plate promotes fresh cell variables to outer storage and clones the compiler-owned loop into the Stan blocks that need it.

Backup · missing continuous outcomes

m = @slic (; y = Union{Missing,Float64}[1.0, missing, 3.0]) begin
    mu ~ normal(0, 2)
    sigma ~ exponential(1)
    y ~ normal(mu, sigma)
end

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.

Backup · observation semantics compose with a base family

y_weighted  ~ weighted(normal, w, mu, sigma)
y_truncated ~ truncated(normal, mu, sigma; lower=lo, upper=hi)
y_censored  ~ censored(normal, mu, sigma; lower=lo, upper=hi)
y_interval  ~ interval_censored(normal, lo, hi, mu, sigma)

The compiler selects the matching:

  • aggregate density/mass function
  • pointwise companion
  • CDF/CCDF terms
  • predictive RNG

For censoring, the emitted density uses tail mass at the threshold atoms and the predictive RNG clamps a base-family draw.

Backup · executable descriptor

For the regression example:

inputs:
  y      observed=true
  x      observed=false

outputs:
  alpha, beta, sigma   parameter / posterior
  y_likelihood        generated_quantity / pointwise_loglik / source=y
  y_gen               generated_quantity / draw / source=y

operations:
  transpile · instantiate · fit · predict · pointwise_loglik

Consumers do not need a parallel registry of what a model can do.

Backup · current hard edges

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.

Backup · sources and further reading

Talk slot: 13:40–14:00, Tuesday 18 August 2026, Ångström Laboratory, Uppsala.