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, 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.

An inspectable boundary

stan_code(model) can be read, diffed, archived, checked by stanc, compiled, and handed to the ordinary Stan ecosystem.

Generated code is part of the product

stan_code(model)

is not a debugging afterthought.

It is the review boundary between:

  • a compositional authoring language, and
  • a mature inference language.

The emitted program answers concrete questions:

  • Which variables are parameters?
  • Which computations run once vs. per gradient?
  • Were constraints transferred correctly?
  • What pointwise likelihood and predictive draws were added?
  • Did a post-hoc model rewrite change only what we intended?

“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
  • explicit scalar types
  • vector sizes
  • replicated generated quantities

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

What the compiler infers

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

activity analysis type + shape inference constraint propagation automatic generated-quantity twins BridgeStan log density

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(base_model, quote
    beta ~ normal(0, 5)
end)

The matching beta statement is replaced; the base model 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.

The submitted abstract is already historical

The abstract listed closures, keyword/default arguments, and macros as possible future work.

They now sit beside:

Julia-flavoured abstraction

closures · higher-order functions · variadics · required/optional kwargs · defaults · macro expansion · inline helpers

Statistical authoring

submodels · Base.merge variants · custom distribution triads · censoring/truncation/weights · fused GLMs · CV taint

Structured models

plate · ragged data · constrained ragged parameters · missing outcomes · ODEs · Torsten signatures

Companion reference: feature atlas

3. How?

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

The intellectual lineage: SlicStan

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:

  • no author-written Stan blocks
  • infer placement from information flow
  • make model fragments composable
  • preserve Stan as the inference target

StanBlocks is directly inspired by SlicStan; it is not a port of SlicStan’s formal language or proofs.

Same destination, different route

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.

Four compiler verbs

1 · capture
@slic stores Julia AST + bound data + defining module
2 · forward!
resolve calls; infer type, shape, constraints, captures
3 · backward!
mark everything that can affect a live likelihood
4 · distribute!
route declarations and statements into Stan blocks

Then show/stan_code emits Stan source; instantiate compiles it through BridgeStan.

@slic model → StanModel → stan_code → stanc / BridgeStan / CmdStan

One dependency graph, several execution times

mx = mean(x)                 # data only
alpha ~ normal(0, 2)         # affects y
beta  ~ normal(0, 1)         # affects y
mu = alpha + beta * (x - mx) # parameter dependent
y ~ normal(mu, 1)            # observed
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.

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 consumer is BayesianRegressionModels.jl: a formula layer that lowers real model families into @slic and @deffun components.

Near-term priorities:

  1. Stabilise the small public authoring contract and document every sharp edge.
  2. Make BRM.jl the ergonomic route for standard regression families.
  3. Keep StanBlocks available for bespoke PKPD and other compositional models.
  4. 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 · exact core emission

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);
}

Backup · deterministic control flow belongs in @deffun

@deffun centered_sum(x::vector[n])::real = 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 · 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.