Skip to content

Writing a Library

Metodi Latinov edited this page Sep 18, 2026 · 1 revision

Writing a Library

A Laplace library is a directory of Stan functions plus a small manifest. If you already keep a folder of functions you reuse, you are most of the way there: nothing has to be rewritten, moved, or duplicated.

This page covers the whole process, from an existing folder of .stan files to a library someone else can install with one command.

1. A library is just a directory

Take a directory of Stan function definitions that already work as ordinary Stan, and add one file called laplace.toml. It is now a Laplace package.

my-library/
  kernels.stan
  laplace.toml

All .stan and .laplacelib files in the directory are read, sorted by filename and concatenated, so you can split a large library across several files however you like. .stan files are passed through verbatim. .laplacelib files are the library dialect, covered in section 6.

2. The manifest

name = "gaussian_process"
version = "0.1.0"
exports = ["rbf_cov", "matern32_cov", "latent"]

name is what users type in pkg::func() calls and in their library { } block. It is independent of the directory name and of the repository name, and you can change it freely.

Choose it carefully, because it becomes part of every compiled symbol: gaussian_process::rbf_cov compiles to gaussian_process__rbf_cov. That means the name has to be a valid Stan identifier. Do not use hyphens. A repository can be called laplace-gaussian_processes, but the package inside it should be gaussian_process.

version should follow semantic versioning, since users depend on your library through ranges such as ^0.1.

exports lists the functions callable from outside the package.

3. exports is the privacy boundary

Only functions named in exports can be called as pkg::func from another package or from a model. This is separate from documentation: a function with a full @laplace comment block that is missing from exports is still private, and a function in exports that has no doc comment is still callable.

Only exported functions get the pkg__ prefix in compiled output. Private helpers keep their original names, and Stan has a single flat function namespace, so if two packages in the same build both define a private softplus, the build fails with an error naming both packages. It does not silently emit two definitions.

Give private helpers distinctive names. A short prefix of your own is the simplest approach, which is why laplace-survival names its internals srv_expm1_div rather than expm1_div.

Every name in exports must actually exist in the package's source. If it doesn't, builds that use your library fail.

4. Document your exports

Documentation is a comment block directly above the function, opening with a line that reads @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. These are plain Stan comments, so stanc ignores them and your file stays valid Stan on its own.

Document every export. This is what users see from laplace doc pkg::func, and it is what travels into their compiled .stan file, since the comments are spliced in along with the code. Two things worth getting right:

  • Write @example with the package name your manifest actually declares. It is easy to write transformation::standardize in a library whose name is transformations, leaving users with an example that doesn't resolve.
  • One @param per parameter, with names matching the signature.

5. Scaffolding with laplace init

Rather than writing the manifest by hand, run laplace init inside the package directory. It guesses name from the directory name, sets version = "0.1.0", and fills exports with every @laplace-documented function, printing which functions were included and which were left out:

$ laplace init
wrote laplace.toml for `mypkg`
included 1 exported function: add_one
note: 1 undocumented function left out of exports (add manually if this guess is wrong): helper

It refuses to overwrite an existing laplace.toml.

Current limitations

laplace init has three rough edges today. None of them blocks publishing a library, but knowing about them saves confusion. Fixes are tracked on Implementation Plans.

  • It only scans .stan files. build and install read .laplacelib files too, but init does not, so a library written in the library dialect produces an empty exports list. The message in that case says no @laplace-documented functions were found, which reads as "your doc comments are wrong" when the real problem is that no files were read at all. The expected fix is for init to use the same source-reading path as build and install, and to distinguish "no files found" from "no documented functions found". In the meantime, write the exports list by hand.
  • Overloaded functions are repeated. init writes one entry per signature, so four overloads of the same name produce four identical entries and an inflated count in its output. This is harmless — membership tests ignore duplicates, and renaming is idempotent — so an existing manifest does not need cleaning up. The expected fix is to deduplicate before writing.
  • Hyphens in the directory name are not sanitized. Running init in my-library/ guesses name = "my-library", which is not a valid Stan identifier. Edit the name by hand. The expected fix is to sanitize the guess.

One other thing to know as an author: laplace doc currently prints only the first overload of a name, so users cannot discover the other signatures from the CLI. Until that is fixed, list overloads explicitly in your README.

6. When your library needs another library

A library can build on another library. Two things change.

The manifest gains a [dependencies] table, with the same syntax a project uses:

name = "regression"
version = "1.0.0"
exports = ["centre"]

[dependencies]
stats = "^1.0"

And the source file gets the .laplacelib extension, so it can carry a library { } block and namespaced calls:

// regression.laplacelib
library {
  import stats
}

// @laplace
// @brief Centre a vector on its mean.
// @param x The vector to centre.
// @return `x` minus its mean.
vector centre(vector x) {
  return x - stats::mean_(x);
}

.laplacelib is a relaxed dialect: bare function definitions, an optional functions { } wrapper, and an optional library { } block. The model-shaped blocks (data, transformed data, parameters, transformed parameters, model, generated quantities) are rejected with a clear error, because a library provides functions to a model rather than being one.

A package can mix .laplacelib and plain .stan files freely. The .stan ones are ordinary Stan, passed through verbatim, and cannot import anything.

Your imports are private. A model that depends on regression does not thereby get access to stats, and your package may only call packages listed in its own [dependencies]. There is no re-export mechanism.

7. Publishing on git

Users install libraries straight from a git repository, so publishing means putting the package somewhere they can point at and tagging it.

If the package sits at the repository root, next to .git, users need nothing extra. If it sits below the top level — beside a README, or as one of several packages — they pass --subdir. The layout used across the existing Laplace libraries puts the package in a laplace/ directory:

laplace-survival/
  README.md
  laplace/
    laplace.toml
    survival.laplacelib

Then a user adds it with:

laplace add survival \
  --git https://github.com/mlatinov/laplace-survival --tag 0.1.0 --subdir laplace

Tag only after the manifest is committed

A tag is a pointer to a commit, and Laplace reads the repository exactly as that commit left it. If you tag 0.1.0 and then move or add laplace.toml, every user of that tag gets an error saying the package has no laplace.toml, even though the file is plainly visible on the repository's main page.

So cut the tag last, after the manifest is committed at the path consumers will pass to --subdir. If you need to correct a published tag:

git tag -d 0.1.0
git push origin :refs/tags/0.1.0
git tag 0.1.0
git push origin 0.1.0

Moving a published tag changes what existing users resolve to, so prefer cutting a new version once anyone else is depending on yours.

8. Before you publish: a checklist

  • name is a valid Stan identifier, with no hyphens, and reads well as a prefix in pkg::func.
  • Every function in exports exists in the source.
  • Every export has an @laplace doc block, with @param names matching the signature and @example using the real package name.
  • Private helpers have distinctive names unlikely to collide with another library's.
  • version follows semantic versioning.
  • A README lists what the library covers, including all overloads, since laplace doc shows only the first.
  • A test model builds against the library and passes stanc, for example with laplace build model.laplace --validate.
  • The tag is cut after the manifest is committed, and laplace add --git ... --tag ... --subdir ... works from a clean directory.

Once that passes, open an issue on the [laplace repository](https://github.com/mlatinov/laplace) to have your library listed on Ecosystem.

See also