Skip to content

Language Guide

Metodi Latinov edited this page Sep 18, 2026 · 2 revisions

Language Guide

It is Stan

A .laplace file is a Stan file. Every rule in the [Stan reference manual](https://mc-stan.org/docs/reference-manual/) applies unchanged: the same blocks in the same order, the same types, the same statements, the same constraints. Laplace does not add types, operators, control flow, or syntax sugar to any of it.

What Laplace adds is exactly two things:

  1. A library { } block, for declaring which packages the file uses.
  2. A pkg::func(...) call syntax, for calling functions from those packages.

Both disappear during the build. The library { } block is deleted, and pkg::func becomes pkg__func. What comes out is ordinary Stan that stanc compiles with no knowledge that Laplace exists.

What Laplace reads, and what it doesn't

This is worth understanding early, because it explains most of Laplace's behaviour.

Laplace is not a Stan compiler and never parses Stan's grammar. It parses the library { } block in depth, scans shallowly for top-level block keywords and function signatures, and treats everything else as opaque text. Every statement in every block is copied to the output byte for byte, with one exception: pkg::func( call sites are rewritten.

Two consequences follow. First, Laplace cannot quietly change the meaning of your model, because it never understood it in the first place. That is the basis of the promise that the generated .stan is trustworthy. Second, Laplace will not catch Stan errors. A missing semicolon or a type mismatch builds fine and fails later in stanc. Run laplace build --validate to have Laplace type-check the output with stanc immediately after writing it.

Occurrences of pkg::func( inside // comments and inside string literals are left alone, so a comment mentioning a namespaced call stays readable in the output.

The library { } block

library {
  import gaussian_process
  import transformations@0.1.0
}
  • One import per line, or several separated by semicolons.
  • // comments are allowed inside the block.
  • import pkg uses whatever version is pinned in laplace.lock. A @0.1.0 suffix is accepted, but it currently documents the expected version rather than enforcing it: the lockfile decides what gets compiled in. See Packages and Dependencies.
  • Package names may contain ASCII letters, digits, _ and -. The name is the one in the package's own manifest, which is not necessarily its repository name.
  • By convention the block goes at the top of the file, before functions { } or data { }.

An empty library { } block is valid and simply gets deleted. A file with no block at all is a plain Stan file that Laplace passes straight through.

Calling library functions

Call an exported function of an imported package as pkg::func(...), anywhere in the file:

transformed data {
  vector[N] x_std = transformations::standardize(x);
}

This includes your own functions { } block, which matters in practice. A common pattern is to wrap a library call in a function of your own so that the model block and generated quantities can't drift apart:

library {
  import gaussian_process
}

functions {
  // one definition used everywhere below
  matrix kernel(array[] real x1, array[] real x2, real alpha, real rho) {
    return gaussian_process::matern32_cross_cov(x1, x2, alpha, rho);
  }
}

Functions you write yourself are never touched. They keep their names and stay in your functions { } block; imported functions are spliced in above them, and a functions { } block is synthesized for you if your model doesn't have one.

What the build checks

Laplace enforces a small number of rules, each as a build error rather than a silent success:

  • Calling pkg::func when pkg isn't in your library { } block.
  • Calling a function the package doesn't list in its exports, even if that function exists in the package's source.
  • Importing a package that isn't in laplace.lock, which tells you to run laplace add first.
  • Two packages in the same build defining a private helper with the same name. Private helpers keep their original names, and Stan has one flat function namespace, so this fails loudly naming both packages instead of emitting two definitions.

Imports are private

Importing a package gives you its exports and nothing else. If regression depends on stats internally, your model still cannot call stats::mean_ until you add and import stats yourself. Libraries are under the same rule: a package may only call packages listed in its own dependencies. There is no re-export mechanism.

A build also contains exactly one version of any package. If your project and a library disagree about which version they need, the build stops and names everyone involved, rather than compiling two copies that would mangle to the same name.

Doc comments

Library functions are documented with plain // comments above the definition, starting with a line reading @laplace:

// @laplace
// @brief Squared exponential (RBF) covariance matrix.
// @param x Vector of input locations.
// @param alpha Marginal standard deviation of the GP.
// @param rho Length-scale of the GP.
// @return An N x N positive semi-definite covariance matrix.
// @example gaussian_process::rbf_cov(x, 1.0, 0.5)
// @math
// k(x, x') = \alpha^2 \exp(-\|x - x'\|^2 / (2\rho^2))
matrix rbf_cov(vector x, real alpha, real rho) {
  return gp_exp_quad_cov(x, alpha, rho);
}

Recognised tags are @brief, @param (one per parameter), @return, @example and @math. @example and @math are preserved verbatim, since they hold code and LaTeX rather than prose.

These are ordinary Stan comments. stanc ignores them, the file stays valid Stan on its own, and they survive into the compiled output, so the documentation travels with the code.

As a reader, you rarely need to open a library's source to see them:

laplace doc transformations::standardize

This reads a sidecar generated at install time, so there is no network access and no re-parsing at lookup time.

Writing libraries

Libraries are written in a relaxed dialect with the .laplacelib extension, which allows a library { } block and namespaced calls but rejects the model-shaped blocks, since a library provides functions to a model rather than being one. That, along with the manifest, exports and publishing, is covered in Writing a Library.

See also

Clone this wiki locally