From f8c4b94742e748d4a7b747953eddd1de9964bd01 Mon Sep 17 00:00:00 2001 From: Hong Ge Date: Wed, 6 May 2026 23:28:59 +0100 Subject: [PATCH 1/6] Add contributor onboarding notes --- CLAUDE.md | 246 ++++++++++++++++++++++++++++++------ docs/make.jl | 1 + docs/src/onboarding.md | 274 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 480 insertions(+), 41 deletions(-) create mode 100644 docs/src/onboarding.md diff --git a/CLAUDE.md b/CLAUDE.md index ad60cb5f3..a87ab94d5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,20 +1,38 @@ # CLAUDE.md -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +This file provides guidance to Claude Code (claude.ai/code) when working with +code in this repository. ## Project Overview -DynamicPPL.jl is the core probabilistic programming language and backend for the [Turing.jl](https://github.com/TuringLang/Turing.jl) ecosystem. It provides the `@model` macro for defining probabilistic models with tilde (`~`) statements, and infrastructure for evaluating, conditioning, and transforming those models. +DynamicPPL.jl is the core probabilistic programming language and backend for +the [Turing.jl](https://github.com/TuringLang/Turing.jl) ecosystem. It provides +the `@model` macro for defining probabilistic models with tilde (`~`) +statements, and infrastructure for evaluating, conditioning, fixing, +transforming, and inspecting those models. + +DynamicPPL builds on AbstractPPL.jl for shared PPL interfaces such as +`VarName`, contexts, conditioning/fixing, and evaluator protocols. For +contributor-facing context extracted from DynamicPPL and AbstractPPL project +history, see `docs/src/onboarding.md`. ## Test Structure -Tests are split into Group1 and Group2 for CI parallelism (controlled by the `GROUP` env var in `test/runtests.jl`). CI also runs Aqua.jl quality checks and doctests. +Tests are split into Group1 and Group2 for CI parallelism, controlled by the +`GROUP` environment variable in `test/runtests.jl`. CI also runs Aqua.jl +quality checks and doctests. -**Important**: Each test file should be self-contained. All dependencies must come from package imports, not relative imports or `include()` statements. This enables running individual test files via [TestPicker.jl](https://github.com/theogf/TestPicker.jl). +**Important**: Each test file should be self-contained. Dependencies should +come from package imports, not relative imports or `include()` statements. This +allows individual test files to be run with tools such as +[TestPicker.jl](https://github.com/theogf/TestPicker.jl). ## Formatting -Code formatting uses [JuliaFormatter.jl](https://github.com/domluna/JuliaFormatter.jl) v1 (not v2) with the **Blue style** (configured in `.JuliaFormatter.toml`). CI enforces formatting on all PRs. +Code formatting uses +[JuliaFormatter.jl](https://github.com/domluna/JuliaFormatter.jl) v1, not v2, +with the **Blue style** configured in `.JuliaFormatter.toml`. CI enforces +formatting on all PRs. ```bash julia --project -e 'using JuliaFormatter; format(".")' @@ -22,88 +40,234 @@ julia --project -e 'using JuliaFormatter; format(".")' ## Architecture -For how things work, see the [docs](https://turinglang.org/DynamicPPL.jl/stable/): [model evaluation](https://turinglang.org/DynamicPPL.jl/stable/evaluation/), [tilde pipeline](https://turinglang.org/DynamicPPL.jl/stable/tilde/), [init strategies](https://turinglang.org/DynamicPPL.jl/stable/init/), [transform strategies](https://turinglang.org/DynamicPPL.jl/stable/transforms/), [accumulators](https://turinglang.org/DynamicPPL.jl/stable/accs/overview/), [conditioning/fixing](https://turinglang.org/DynamicPPL.jl/stable/conditionfix/), [threading](https://turinglang.org/DynamicPPL.jl/stable/accs/threadsafe/). +For how things work, see the +[docs](https://turinglang.org/DynamicPPL.jl/stable/): model evaluation, the +tilde pipeline, init strategies, transform strategies, accumulators, +conditioning/fixing, threading, `VarNamedTuple`, and `LogDensityFunction`. ### Key Types - - **`Model`** (`src/model.jl`): Wraps a model function with its arguments and context. Created by the `@model` macro (`src/compiler.jl`). - - **`AbstractVarInfo`** (`src/abstract_varinfo.jl`): Interface for tracking random variables and accumulated quantities during model execution. - - **`VarNamedTuple`** (`src/varnamedtuple.jl`): A named-tuple-like structure keyed by `VarName`s (from AbstractPPL). Used as the primary representation for parameter values. - - **`LogDensityFunction`** (`src/logdensityfunction.jl`): Translation layer between named model parameters and flat `AbstractVector{<:Real}` for optimisers/samplers. Implements the `LogDensityProblems.jl` interface. + - **`Model`** (`src/model.jl`): wraps a model function with its arguments and + context. Created by the `@model` macro in `src/compiler.jl`. + - **`AbstractVarInfo`** (`src/abstract_varinfo.jl`): interface for tracking + random variables and accumulated quantities during model execution. + - **`VarName`** (from AbstractPPL): address for model variables, including + nested fields and indices. + - **`VarNamedTuple`** (`src/varnamedtuple.jl`): a named-tuple-like structure + keyed by `VarName`s. Used as the primary representation for named parameter + values where supported. + - **`LogDensityFunction`** (`src/logdensityfunction.jl`): translation layer + between named model parameters and flat `AbstractVector{<:Real}` inputs for + optimisers, samplers, and AD. Implements the `LogDensityProblems.jl` + interface. ### Extensions (`ext/`) -Optional AD backends and integrations, loaded via Julia's package extension system: +Optional AD backends and integrations, loaded via Julia's package extension +system: - - `DynamicPPLForwardDiffExt` — ForwardDiff AD - - `DynamicPPLMooncakeExt` — Mooncake AD (with precompilation workload) - - `DynamicPPLReverseDiffExt` — ReverseDiff AD - - `DynamicPPLEnzymeCoreExt` — Enzyme AD - - `DynamicPPLMCMCChainsExt` — MCMCChains integration - - `DynamicPPLMarginalLogDensitiesExt` — marginalization support + - `DynamicPPLForwardDiffExt`: ForwardDiff AD + - `DynamicPPLMooncakeExt`: Mooncake AD, with precompilation workload + - `DynamicPPLReverseDiffExt`: ReverseDiff AD + - `DynamicPPLEnzymeCoreExt`: EnzymeCore AD support + - `DynamicPPLMCMCChainsExt`: MCMCChains integration + - `DynamicPPLMarginalLogDensitiesExt`: marginalization support ### Testing Utilities (`src/test_utils/`) -`DynamicPPL.TestUtils` provides test models with known analytical solutions (`logprior_true`, `loglikelihood_true`, etc.) and an AD testing framework (`run_ad`, `ADResult`) used across the Turing ecosystem. +`DynamicPPL.TestUtils` provides test models with known analytical solutions +(`logprior_true`, `loglikelihood_true`, etc.) and an AD testing framework +(`run_ad`, `ADResult`) used across the Turing ecosystem. ## Review Guidelines -Common pitfalls and non-obvious constraints when writing or reviewing DynamicPPL code. +Common pitfalls and non-obvious constraints when writing or reviewing +DynamicPPL code. ### Prefer `OnlyAccsVarInfo` over `VarInfo` -New code should use `OnlyAccsVarInfo` (OAVI) + `init!!`, not `VarInfo` + `evaluate!!`. VarInfo is being phased out ([#1376](https://github.com/TuringLang/DynamicPPL.jl/issues/1376)) — it carries redundant state (`vi.values` duplicates `VectorValueAccumulator`) and is slower. Don't add new features to VarInfo. The migration path: `evaluate!!(model, vi)` becomes `init!!(model, oavi, InitFromParams(vi.values), vi.transform_strategy)`. +For new evaluation code, prefer `OnlyAccsVarInfo` plus `init!!` over adding +more behaviour to `VarInfo`. `VarInfo` is still important, but it combines +vector values, transform state, metadata, and accumulators, while many fast +paths need only a subset of that state. + +A common migration shape is: + +```julia +evaluate!!(model, vi) +``` + +to: + +```julia +init!!(model, oavi, InitFromParams(vi.values), vi.transform_strategy) +``` + +Choose the actual init strategy and accumulator set from the caller's needs. ### BangBang (`!!`) Return Values -Functions suffixed with `!!` (from BangBang.jl) attempt in-place mutation but may return a new object instead. **Always use the return value.** `VarInfo` and `AccumulatorTuple` are immutable structs, so `!!` functions unconditionally return new objects — discarding the return value is a silent bug with no warning. +Functions suffixed with `!!` follow BangBang.jl semantics: they may mutate in +place, but they may also return a replacement object. **Always use the return +value.** Discarding the return value can silently drop updates, especially for +immutable wrappers such as `VarInfo` and `AccumulatorTuple`. ```julia -# WRONG: mutation didn't happen, vi is unchanged +# WRONG: mutation may not happen; vi may be unchanged. accumulate_assume!!(vi, x, tval, logjac, vn, dist, template) # RIGHT vi = accumulate_assume!!(vi, x, tval, logjac, vn, dist, template) ``` -This applies transitively: if your function calls a `!!` function, it must also return the updated state. +This applies transitively: if your function calls a `!!` function, it usually +must also return the updated state. ### Accumulator Pitfalls -See [accumulator docs](https://turinglang.org/DynamicPPL.jl/stable/accs/overview/) for the full protocol. Common mistakes: - - - **`val` vs `tval` in `accumulate_assume!!`**: `val` is always in the original unlinked space (use it for `logpdf`). `tval` is the `TransformedValue` which may hold linked values. `logjac` is the log-Jacobian of the **forward** link transform (zero if unlinked). Confusing these is a common source of wrong log-densities. - - **`logpdf` vs `loglikelihood` for observations**: `LogLikelihoodAccumulator` uses `Distributions.loglikelihood`, not `logpdf`. For vector observations, `logpdf` returns a vector while `loglikelihood` returns a scalar sum. Using `logpdf` where `loglikelihood` is expected silently produces wrong types. See [JuliaStats/Distributions.jl#1972](https://github.com/JuliaStats/Distributions.jl/issues/1972). - - **Aliased `copy`**: `copy(acc)` must deep-copy all mutable internal state. Aliased containers (e.g. shared `Vector` fields) corrupt results when accumulators are copied for `ThreadSafeVarInfo`. +See the accumulator docs for the full protocol. Common mistakes: + + - **`val` vs `tval` in `accumulate_assume!!`**: `val` is the original + model-space value and is what `logpdf` should see. `tval` is the + `TransformedValue`, which may hold linked values. `logjac` is the + log-Jacobian of the forward link transform, or zero if unlinked. + - **`logpdf` vs `loglikelihood` for observations**: + `LogLikelihoodAccumulator` uses `Distributions.loglikelihood`, not `logpdf`. + For array-valued or product-like observations, the two can have different + shapes or aggregation semantics. Use the one the accumulator protocol + expects. + - **Aliased `copy`**: `copy(acc)` must not share mutable internal state unless + that sharing is intentional and documented. Aliased containers can corrupt + results when accumulators are copied for threaded evaluation. ### TransformedValue - - **`get_raw_value(tv)` errors for `DynamicLink` and `Unlink`.** These transforms are derived from the distribution, so you must use `get_raw_value(tv, dist)`. The one-argument form only works for `NoTransform` and `FixedTransform`. - - **`DynamicLink` re-derives the bijection from `dist` every evaluation.** This is necessary because the support of a variable can depend on other variables (e.g. `y ~ truncated(Normal(); lower=x)`), so the transform cannot be cached. When the support is known to be constant, [`FixedTransform` via `WithTransforms`](https://turinglang.org/DynamicPPL.jl/stable/fixed_transforms/) is an option. - - **`FixedTransform` must exactly match the target.** `apply_transform_strategy` errors if a `FixedTransform` doesn't match the expected `target_transform`. Fixed transforms don't compose with re-derived transforms. + - **`get_raw_value(tv)` needs a distribution for dynamic transforms.** + `DynamicLink` and `Unlink` derive their transform from the distribution, so + use `get_raw_value(tv, dist)`. The one-argument form is for cases where the + transform is already fully known. + - **`DynamicLink` re-derives the bijection from `dist` during evaluation.** + This is necessary because support can depend on earlier random variables, for + example `y ~ truncated(Normal(); lower=x)`. If support is known to be + constant, consider `FixedTransform` via `WithTransforms`. + - **`FixedTransform` must match the target transform.** Do not assume fixed + transforms compose with re-derived dynamic transforms. ### LogDensityFunction - - **`getlogjoint_internal` vs `getlogjoint`**: `getlogjoint_internal(vi) = getlogjoint(vi) - getlogjac(vi)`. Samplers operating in unconstrained space need `getlogjoint_internal` (the default). `getlogjoint` gives the density in constrained space without the Jacobian correction — using it for HMC/NUTS is wrong. - - **Compiled ReverseDiff tapes are input-dependent.** If your model has control flow that depends on parameter values (e.g. `if x > 0`), compiled ReverseDiff will only give correct gradients for inputs that trigger the same branch as the compilation input. Don't use `AutoReverseDiff(; compile=true)` with parameter-dependent branching. + - **`getlogjoint_internal` vs `getlogjoint`**: samplers operating in + unconstrained space usually need `getlogjoint_internal`. `getlogjoint` is the + constrained-space log joint. Using the wrong one changes Jacobian handling. + - **Compiled ReverseDiff tapes are input-dependent.** If model control flow + depends on parameter values, compiled ReverseDiff only gives correct + gradients for inputs that follow the same branch as the compilation input. + Do not use `AutoReverseDiff(; compile=true)` for parameter-dependent + branching. + - Keep evaluator APIs split into structural preparation and AD-specific + preparation. Put backend-specific gradient code in extensions where possible. + - Check aliasing in evaluator and AD APIs. `!!` methods may return buffers that + alias internal caches; copy before exposing or storing results long term. ### `VarNamedTuple` as Primary Data Structure -`VarNamedTuple` is the canonical representation for named parameter collections throughout DynamicPPL. New code should use it everywhere — for conditioning, fixing, parameter storage, and accumulator values. `NamedTuple` and `Dict{VarName}` are accepted as user-facing input but only insofar as they are converted to `VarNamedTuple` at the boundary. Don't propagate them through internal code. +`VarNamedTuple` is the canonical internal representation for named parameter +collections in new DynamicPPL code: conditioning/fixing values, parameter +storage, and accumulator values. `NamedTuple` and `Dict{VarName}` are accepted +as user-facing input, but should usually be converted to `VarNamedTuple` at API +boundaries rather than propagated internally. + +Preserve templates, shapes, and index structure when working with +`VarNamedTuple`. Array-valued variables, slices, and nested fields need enough +template information to round-trip between named values and flat vectors. Avoid +large mostly-empty shadow arrays for sparse indexed variables, and keep eltypes +concrete in hot paths. -See the [VarNamedTuple docs](https://turinglang.org/DynamicPPL.jl/stable/vnt/motivation/) for motivation — it is performant, general, and provides a single source of truth for named parameter collections. +See the VarNamedTuple docs for motivation: it is performant, general, and gives +one representation for named parameter collections. ### VarName - - **Use `@varname(x)`, not `:x` or `VarName(:x)`.** The macro constructs the correct optic for indexed access. `@varname(x[1])` creates a VarName with an index lens — constructing this manually is error-prone. - - **Subsumption, not equality, for containment checks.** `subsumes(@varname(x), @varname(x[1]))` is `true`, but they are not `==`. Conditioning on `@varname(x)` matches all sub-indices; conditioning on `@varname(x[1])` only matches that index. Use `subsumes` when checking if a VarName is "covered by" another. + - **Use `@varname(x)`, not `:x` or `VarName(:x)`.** The macro constructs the + correct optic for indexed access. `@varname(x[1])` creates a `VarName` with + an index lens; constructing this manually is error-prone. + - **Use subsumption for containment checks.** `subsumes(@varname(x), + @varname(x[1]))` is `true`, but the two names are not equal. Conditioning + on `@varname(x)` matches sub-indices; conditioning on `@varname(x[1])` + matches only that index. + - Treat `VarName` display, sorting, prefixing, unprefixing, and serialization + as downstream-facing interface behaviour. Chains, saved results, and + external packages can depend on stable names. + - Test nested fields, indices, ranges, `Colon`, and non-standard indices when + changing `VarName` optics. + +### `@model` Compiler Changes + +`@model` lowering must preserve ordinary Julia semantics, not only +probabilistic statements. For compiler changes, test positional and keyword +arguments, default values, splatting, closures, interpolation, return values, +no-observation models, and data- or parameter-dependent control flow. + +Keep macro hygiene explicit. User variables, generated temporaries, and globals +should not capture each other accidentally. Inspect expanded code when changing +compiler paths. Preserve model return values; returned quantities are +user-visible and distinct from accumulated random variables. ### Threading -See [threading docs](https://turinglang.org/DynamicPPL.jl/stable/accs/threadsafe/). Key edge case: `promote_for_threadsafe_eval(acc, T)` must be implemented if your accumulator stores typed containers that need to hold AD tracer types (e.g. ForwardDiff `Dual`s). The default is a no-op, which is wrong for accumulators with concrete float fields. +See the threading docs. Key edge case: +`promote_for_threadsafe_eval(acc, T)` must be implemented if an accumulator +stores typed containers that need to hold AD tracer types, such as ForwardDiff +`Dual`s. The default is a no-op, which is wrong for accumulators with concrete +float fields. + +Avoid designs that index storage by `Threads.threadid()`. Julia scheduling and +thread IDs are not a stable ownership model. + +## Julia Engineering Practices + + - Check type stability with `@inferred`, `@code_warntype`, and focused tests + when changing compiler output, VNTs, accumulators, transforms, or log-density + paths. + - Avoid unnecessary static parameters. Julia specializes on most ordinary + argument types, but is conservative for `Type`, `Function`, and `Vararg`. + Use `f(x, ::Type{T}) where {T}` when the type itself must specialize. + - Benchmark generated functions, macro output, and hot-path refactors before + assuming simpler code is equivalent. + - Prefer dispatch and small protocol functions over large conditional blocks. + - Avoid broad overloads of Base functions for arbitrary input types; they can + create method ambiguities and accidental API. + - Put backend-specific behaviour in package extensions or narrow integration + layers when possible. + - Make direct dependencies explicit enough to version-bound and test. Do not + rely on packages being loaded transitively. + - Use accessor functions for values downstream packages need. Direct field + access from Turing or other packages turns internal representation into + accidental API. + - Prefer `Base.maybeview` over eager slicing when indexed access should avoid + allocations but still support tuples and scalar indexing. + - Avoid fragile output-type prediction. When possible, compute an initial value + and allocate caches from the observed value. + - Keep doctests deterministic. Use `StableRNGs` when examples print random + values. ## Contributing - - Non-breaking changes target `main`; breaking changes target the `breaking` branch. - - CI runs tests on Ubuntu/Windows/macOS, Julia stable/min/1.11, with 1 and 2 threads. - - Julia ≥ 1.10.8 required (see `[compat]` in `Project.toml`). + - Non-breaking changes target `main`; breaking changes target the `breaking` + branch. + - CI runs tests on Ubuntu, Windows, and macOS, across stable, minimum, and + selected Julia versions, with both one and two threads. + - Identify whether the change is user-facing, internal, or downstream-facing + through Turing.jl. + - Add the smallest tests that exercise the behaviour. + - Add nested-submodel tests for context, prefix, conditioning, or fixing + changes. + - Add AD backend tests for log-density, transform, vector-parameter, or + `run_ad` changes. + - Add round-trip tests for flattening and unflattening changes, including + scalars, arrays, tuples, `NamedTuple`s, nested values, and mixed element + types. + - Check type stability and allocations for hot paths. + - Check dependency placement and compat bounds when touching Project files, + extensions, docs, or tests. + - Include benchmark numbers for performance-sensitive changes. + - Document and test new user-facing API. diff --git a/docs/make.jl b/docs/make.jl index 71fbfb2c0..83380bddc 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -38,6 +38,7 @@ makedocs(; ], pages=[ "Home" => "index.md", + "Contributor onboarding" => "onboarding.md", "Conditioning and fixing" => "conditionfix.md", "VarNamedTuple" => [ "vnt/motivation.md", diff --git a/docs/src/onboarding.md b/docs/src/onboarding.md new file mode 100644 index 000000000..ad9b95cf3 --- /dev/null +++ b/docs/src/onboarding.md @@ -0,0 +1,274 @@ +# Contributor onboarding + +This page summarizes recurring lessons from DynamicPPL and AbstractPPL history +for contributors who are new to Julia, Turing.jl, or DynamicPPL internals. + +The source pass covered GitHub history available on 2026-05-06. For +DynamicPPL, that included 422 issues, 957 pull requests, 6,958 issue/PR +comments, 3,726 PR reviews, and 5,176 inline review comments. For AbstractPPL, +that included 46 issues, 101 pull requests, 654 issue/PR comments, 332 PR +reviews, and 441 inline review comments. Linked issues and PRs are +representative starting points, not current API documentation. + +## What DynamicPPL Does + +DynamicPPL is the modelling and evaluation layer under Turing.jl. It provides +`@model`, tilde (`~`) statement handling, conditioning, fixing, parameter +transforms, accumulators, and log-density interfaces for samplers and automatic +differentiation. It uses AbstractPPL for shared interfaces such as `VarName`, +contexts, and evaluator protocols. + +A useful mental model: + + 1. `@model` lowers user code into a model function. + 2. Each ordinary `~` statement becomes an assume or observe statement. + 3. Contexts and initialisation strategies decide where values come from. + 4. Accumulators decide which quantities are collected. + 5. `LogDensityFunction` maps named model parameters to flat vectors. + +Start with these docs: + + - [Model evaluation](evaluation.md) + - [Tilde-statements](tilde.md) + - [Initialisation strategies](init.md) + - [Transform strategies](transforms.md) + - [Accumulators](accs/overview.md) + - [VarNamedTuple](vnt/motivation.md) + - [LogDensityFunction](ldf/overview.md) + +## Core Lessons + +### Prefer explicit evaluation state + +For new evaluation code, prefer explicit initialisation strategies and +accumulators over adding more responsibilities to `VarInfo`. `VarInfo` remains +important, but fast paths should carry only the state they need. + +A common migration shape is: + +```julia +evaluate!!(model, varinfo) +``` + +to: + +```julia +init!!( + model, + OnlyAccsVarInfo(accumulators...), + InitFromParams(varinfo.values), + varinfo.transform_strategy, +) +``` + +The exact strategy and accumulator set depend on the caller. + +### Use names and shapes carefully + +Use `@varname(x)` and `@varname(x[1])`; avoid manual construction of indexed +`VarName`s. Use subsumption for containment checks: `@varname(x)` can cover +`@varname(x[1])`, but they are not equal. + +`VarName` display, sorting, prefixing, unprefixing, and serialization are +downstream-facing interface behaviour. Test nested fields, indices, ranges, +`Colon`, and non-standard indices when changing them. Avoid broad `Base` +overloads such as generic `get(obj, vn)` unless the method is clearly owned. + +`VarNamedTuple` is the preferred internal container for named parameter values +where supported. Convert user-facing `NamedTuple` or `Dict{VarName}` inputs at +API boundaries. Preserve templates, shapes, and index structure so values can +round-trip between named form and flat vectors. Avoid large mostly-empty shadow +arrays and keep eltypes concrete in hot paths. + +### Keep `!!` return values + +DynamicPPL uses BangBang-style `!!` functions. They may mutate in place or +return a replacement object. Always use the returned value. + +```julia +vi = accumulate_assume!!(vi, value, tval, logjac, vn, dist, template) +``` + +If your function calls a `!!` function, it usually needs to return the updated +state as well. + +### Treat `@model` as Julia code + +`@model` lowering must preserve ordinary Julia behaviour as well as PPL +semantics. For compiler changes, test positional and keyword arguments, +defaults, splatting, closures, interpolation, return values, no-observation +models, and data- or parameter-dependent control flow. + +Macro hygiene matters. User variables, generated temporaries, and globals +should not capture each other accidentally. Returned quantities are +user-visible and are distinct from accumulated random variables. + +DynamicPPL tracks variables through tilde statements. A left-hand-side value can +be treated as a model variable even when it was derived earlier in the model. + +```julia +@model function f() + x ~ Normal() + y = x + 1 + return y ~ Normal() +end +``` + +If the intent is to add a likelihood term for a derived value, prefer +`@addlogprob!` or a clearer model structure. Do not copy old `.~` examples; the +dot-tilde pipeline was removed. + +Passing `missing` can affect whether a value is observed or latent. Add tests +for the exact data shape you support, especially arrays with missing values, +arrays of arrays, and mutable structs. + +### Test contexts with nested models + +Contexts change model evaluation without rewriting the model body. `condition`, +`fix`, `decondition`, `unfix`, `to_submodel`, and prefixes all interact. + +Prefer `condition`, `fix`, and `to_submodel` over hardcoded special cases. Use +the same `VarName` semantics as the tilde pipeline. Add nested-submodel tests +when changing contexts, prefixes, conditioning, or fixing. + +### Know which space values live in + +DynamicPPL moves between constrained model space and unconstrained sampler +space. Be explicit about which space each value lives in. + + - `val`: constrained model-space value used for distribution densities. + - `tval`: `TransformedValue`, which may contain a linked value. + - `logjac`: log absolute Jacobian contribution from the link transform. + - `getlogjoint`: constrained-space log joint. + - `getlogjoint_internal`: internal log density for sampler-facing paths. + - `vi[:]`: internal stored vector; do not assume it is in distribution support. + +`LogDensityFunction` is the usual boundary for HMC/NUTS, optimisers, and AD. +When changing log-density or transform code, test the relevant AD backends. +Avoid compiled ReverseDiff tapes for models whose control flow depends on +parameter values. + +Evaluator APIs should separate structural preparation from AD-specific +preparation. `!!` evaluator and gradient APIs may reuse internal buffers, so +copy results before storing them long term. + +## Julia Engineering Practices + + - Measure performance-sensitive changes. Small edits can affect inference, + allocations, invalidation, and downstream packages. + - Check type stability with `@inferred`, `@code_warntype`, and focused tests. + - Benchmark generated functions, macro output, and hot-path refactors. + - Keep field types and collection eltypes concrete in hot paths. + - Avoid unnecessary static parameters. Julia specializes on most ordinary + argument types, but is conservative for `Type`, `Function`, and `Vararg`. + - Use `f(x, ::Type{T}) where {T}` when the type itself must specialize. + - Prefer dispatch and small protocol functions over large conditional blocks. + - Put backend-specific behaviour in package extensions or narrow integration + layers when possible. + - Keep interface packages lightweight. Avoid heavy dependencies unless the + interface truly owns them. + - Do not rely on transitive dependencies. Direct API use should have an + explicit dependency that can be version-bound and tested. + - Use accessors for downstream needs instead of exposing internal fields. + - Prefer `Base.maybeview` over eager slicing when indexed access should avoid + allocations but still support tuples and scalar indexing. + - Avoid fragile output-type prediction. When possible, compute an initial value + and allocate caches from the observed value. + +## Copying, Accumulators, and Threading + +Be explicit about aliasing. Copy stored values when later mutation by model code +would otherwise change accumulated results. Use the cheapest correct copy: +`copy` or `collect` is often enough, while `deepcopy` can be much slower. + +Accumulators collect outputs from model execution, such as log probabilities, +raw values, vector values, pointwise log densities, and returned values. Add +only the accumulators you need. `copy(acc)` must not accidentally share mutable +internal state. + +Avoid designs that depend on `Threads.threadid()` indexing. Promote accumulator +storage when thread-safe evaluation must hold AD tracer types. Treat threaded +assume support as subtle unless current docs and tests cover the exact case. + +## Documentation, Tests, and CI + + - Keep test files self-contained. + - Use `DynamicPPL.TestUtils` models with known ground truth when possible. + - Add nested-submodel tests for contexts, prefixes, conditioning, or fixing. + - Add AD tests for log-density, transform, vector-parameter, or `run_ad` + changes. + - Add type-stability or allocation tests for hot paths. + - Add round-trip tests for flattening and unflattening changes. + - Run JuliaFormatter v1 with Blue style before submitting. + - Keep doctests deterministic. Use `StableRNGs` when examples print random + values. + - Use plain `julia` blocks for examples that are illustrative but should not be + checked. + - Put dependencies in the narrowest environment that owns them: runtime, + extension, test, or docs. + - Treat docs, Aqua, JET, formatting, and extension-loading failures as part of + the change. + +## API and Review Norms + + - Breaking changes need explicit justification and deprecation or versioning + plans. + - Internal names are not public API just because downstream packages use them, + but Turing.jl impact still matters. + - Prefer composable operators over special-case syntax. + - Document and test new user-facing API with examples. + - Generated or AI-assisted code still needs manual understanding, focused + tests, and a clear rationale. + - Include benchmark numbers for performance-sensitive changes. + +## Further Reading + + - Evaluation state and `VarInfo`: [#1132](https://github.com/TuringLang/DynamicPPL.jl/pull/1132), + [#1252](https://github.com/TuringLang/DynamicPPL.jl/issues/1252), + [#1311](https://github.com/TuringLang/DynamicPPL.jl/pull/1311), + [#1376](https://github.com/TuringLang/DynamicPPL.jl/issues/1376). + - Named parameter storage: [#1150](https://github.com/TuringLang/DynamicPPL.jl/pull/1150), + [#1183](https://github.com/TuringLang/DynamicPPL.jl/pull/1183), + [#1204](https://github.com/TuringLang/DynamicPPL.jl/pull/1204), + [#1238](https://github.com/TuringLang/DynamicPPL.jl/pull/1238), + AbstractPPL [#117](https://github.com/TuringLang/AbstractPPL.jl/issues/117), + [#122](https://github.com/TuringLang/AbstractPPL.jl/issues/122), + [#136](https://github.com/TuringLang/AbstractPPL.jl/issues/136), + [#150](https://github.com/TuringLang/AbstractPPL.jl/pull/150). + - Tilde syntax and contexts: [#519](https://github.com/TuringLang/DynamicPPL.jl/issues/519), + [#804](https://github.com/TuringLang/DynamicPPL.jl/pull/804), + [#892](https://github.com/TuringLang/DynamicPPL.jl/pull/892), + [#1221](https://github.com/TuringLang/DynamicPPL.jl/issues/1221). + - Transforms, log densities, and AD: + [#575](https://github.com/TuringLang/DynamicPPL.jl/pull/575), + [#1303](https://github.com/TuringLang/DynamicPPL.jl/pull/1303), + [#1348](https://github.com/TuringLang/DynamicPPL.jl/pull/1348), + [#1354](https://github.com/TuringLang/DynamicPPL.jl/pull/1354), + AbstractPPL [#155](https://github.com/TuringLang/AbstractPPL.jl/pull/155), + [#157](https://github.com/TuringLang/AbstractPPL.jl/pull/157). + - Julia engineering and CI: [#50](https://github.com/TuringLang/DynamicPPL.jl/pull/50), + [#147](https://github.com/TuringLang/DynamicPPL.jl/pull/147), + [#242](https://github.com/TuringLang/DynamicPPL.jl/pull/242), + [#733](https://github.com/TuringLang/DynamicPPL.jl/pull/733), + [#777](https://github.com/TuringLang/DynamicPPL.jl/issues/777), + AbstractPPL [#25](https://github.com/TuringLang/AbstractPPL.jl/pull/25), + [#44](https://github.com/TuringLang/AbstractPPL.jl/pull/44), + [#120](https://github.com/TuringLang/AbstractPPL.jl/issues/120). + - Accumulators and threading: [#429](https://github.com/TuringLang/DynamicPPL.jl/issues/429), + [#885](https://github.com/TuringLang/DynamicPPL.jl/pull/885), + [#925](https://github.com/TuringLang/DynamicPPL.jl/pull/925), + [#1137](https://github.com/TuringLang/DynamicPPL.jl/pull/1137), + [#1340](https://github.com/TuringLang/DynamicPPL.jl/pull/1340). + +## Before Opening a PR + + - Identify whether the change is user-facing, internal, or downstream-facing. + - Add the smallest tests that exercise the behaviour. + - Add nested-submodel tests for context, prefix, conditioning, or fixing + changes. + - Run relevant AD backend tests for log-density or transform changes. + - Check type stability and allocations for hot paths. + - Check dependency placement and compat bounds when touching Project files, + extensions, docs, or tests. + - Include performance numbers for performance-sensitive changes. + - Document and test new user-facing API. From 7eeae00f3fe86436e439bd90f342fcf0d93a8693 Mon Sep 17 00:00:00 2001 From: Hong Ge Date: Wed, 6 May 2026 23:38:13 +0100 Subject: [PATCH 2/6] format --- CLAUDE.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index a87ab94d5..6766fb068 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -190,8 +190,7 @@ one representation for named parameter collections. - **Use `@varname(x)`, not `:x` or `VarName(:x)`.** The macro constructs the correct optic for indexed access. `@varname(x[1])` creates a `VarName` with an index lens; constructing this manually is error-prone. - - **Use subsumption for containment checks.** `subsumes(@varname(x), - @varname(x[1]))` is `true`, but the two names are not equal. Conditioning + - **Use subsumption for containment checks.** `subsumes(@varname(x), @varname(x[1]))` is `true`, but the two names are not equal. Conditioning on `@varname(x)` matches sub-indices; conditioning on `@varname(x[1])` matches only that index. - Treat `VarName` display, sorting, prefixing, unprefixing, and serialization From cf8f630b43617f99d5127057ac503ca2d5685aa3 Mon Sep 17 00:00:00 2001 From: Hong Ge Date: Thu, 7 May 2026 15:28:21 +0100 Subject: [PATCH 3/6] julia, agents and claude files --- AGENTS.md | 98 +++++++++++++++++++ CLAUDE.md | 274 +----------------------------------------------------- JULIA.md | 158 +++++++++++++++++++++++++++++++ 3 files changed, 258 insertions(+), 272 deletions(-) create mode 100644 AGENTS.md create mode 100644 JULIA.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..21b3e7abd --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,98 @@ +# CLAUDE.md + +Guidance for Claude Code when working in this repository. + +See also: @JULIA.md for general Julia engineering and review guidance. + +## Project Overview + +DynamicPPL.jl is the core probabilistic programming language backend for the Turing.jl ecosystem. It provides the `@model` macro for tilde (`~`) statements and infrastructure for evaluating, conditioning, fixing, transforming, and inspecting probabilistic models. + +DynamicPPL builds on AbstractPPL.jl for shared PPL interfaces such as `VarName`, contexts, conditioning/fixing, and evaluator protocols. For project history and contributor context, see `docs/src/onboarding.md`. + +## Tests And Formatting + +- Tests are split into Group1 and Group2 for CI parallelism, controlled by `GROUP` in `test/runtests.jl`. +- CI also runs Aqua.jl quality checks, doctests, formatting, and multi-platform tests across selected Julia versions and thread counts. +- Each test file should be self-contained. Use package imports, not relative imports or `include()` statements, so files can run individually with tools such as TestPicker.jl. +- Formatting uses JuliaFormatter.jl v1, not v2, with Blue style from `.JuliaFormatter.toml`. + +```bash +julia --project -e 'using JuliaFormatter; format(".")' +``` + +## Architecture Pointers + +Use the docs for model evaluation, the tilde pipeline, init strategies, transform strategies, accumulators, conditioning/fixing, threading, `VarNamedTuple`, and `LogDensityFunction`. + +- `Model` (`src/model.jl`): wraps a model function, arguments, and context; created by `@model` in `src/compiler.jl`. +- `AbstractVarInfo` (`src/abstract_varinfo.jl`): interface for tracking random variables and accumulated quantities during model execution. +- `VarName` (AbstractPPL): address for model variables, including nested fields and indices. +- `VarNamedTuple` (`src/varnamedtuple.jl`): named-tuple-like parameter storage keyed by `VarName`. +- `LogDensityFunction` (`src/logdensityfunction.jl`): bridge between named model parameters and flat `AbstractVector{<:Real}` inputs for samplers, optimizers, and AD. +- Optional integrations live in `ext/`: ForwardDiff, Mooncake, ReverseDiff, EnzymeCore, MCMCChains, and MarginalLogDensities. +- `DynamicPPL.TestUtils` provides analytical test models and AD helpers (`run_ad`, `ADResult`) used across the Turing ecosystem. + +## DynamicPPL Review Notes + +- Prefer `OnlyAccsVarInfo` plus `init!!` for new evaluation code when fast paths only need accumulators or a subset of VarInfo state. +- `VarInfo` remains important, but it combines vector values, transform state, metadata, and accumulators. Many fast paths need only part of that state, so avoid adding behavior to `VarInfo` by default. +- Functions ending in `!!` follow BangBang.jl semantics: they may mutate or return a replacement object. Always use the return value, and return updated state from callers that invoke `!!`. + +```julia +# Wrong: updates may be lost. +accumulate_assume!!(vi, x, tval, logjac, vn, dist, template) + +# Right. +vi = accumulate_assume!!(vi, x, tval, logjac, vn, dist, template) +``` + +- In `accumulate_assume!!`, `val` is the model-space value and should be passed to `logpdf`; `tval` is the transformed value. +- `logjac` is the log-Jacobian of the forward link transform, or zero if unlinked. +- `LogLikelihoodAccumulator` uses `Distributions.loglikelihood`, not `logpdf`; array-valued or product-like observations can differ in shape or aggregation. +- `copy(acc)` must not share mutable internal state unless that sharing is intentional and documented. +- `get_raw_value(tv, dist)` is required for dynamic transforms because `DynamicLink` and `Unlink` derive their transform from the distribution; the raw value cannot be recovered correctly from `tv` alone when support is distribution-dependent. +- The one-argument `get_raw_value(tv)` is only for cases where the transform is already fully known. +- `DynamicLink` re-derives the bijection from `dist` during evaluation because support can depend on earlier random variables, such as `y ~ truncated(Normal(); lower=x)`. Do not cache or reuse a fixed bijection unless support is known to be constant. +- If support is known to be constant, consider `FixedTransform` via `WithTransforms`; fixed transforms must match the target transform. +- Samplers operating in unconstrained space usually need `getlogjoint_internal`; `getlogjoint` is the constrained-space log joint. +- Compiled ReverseDiff tapes are input-dependent; do not use `AutoReverseDiff(; compile=true)` when model control flow depends on parameter values. +- Keep evaluator APIs split into structural preparation and AD-specific preparation. Put backend-specific gradient code in extensions when possible. +- Check aliasing in evaluator and AD APIs. `!!` methods may return buffers that alias internal caches; copy before exposing results to callers, storing them long term, or reusing them after another model evaluation. + +## Names And Parameter Storage + +- Use `VarNamedTuple` as the canonical internal representation for named parameter collections in new code. +- Accept `NamedTuple` and `Dict{VarName}` at user-facing boundaries, but convert to `VarNamedTuple` rather than propagating them internally. +- Preserve templates, shapes, and index structure when round-tripping between named values and flat vectors. +- Avoid large mostly-empty shadow arrays for sparse indexed variables. +- Use `@varname(x)`, not `:x` or `VarName(:x)`. +- Use subsumption for containment checks: `subsumes(@varname(x), @varname(x[1]))` is true, but the names are not equal. +- Treat VarName display, sorting, prefixing, unprefixing, and serialization as downstream-facing interface behavior. +- Test nested fields, indices, ranges, `Colon`, and non-standard indices when changing VarName optics. + +## `@model` Compiler + +`@model` lowering must preserve ordinary Julia semantics, not only probabilistic statements. + +For compiler changes, test positional and keyword arguments, default values, splatting, closures, interpolation, return values, no-observation models, and data- or parameter-dependent control flow. + +Keep macro hygiene explicit. User variables, generated temporaries, and globals should not capture each other accidentally. Inspect expanded code when changing compiler paths. Preserve model return values; they are user-visible and distinct from accumulated random variables. + +## Threading + +- Implement `promote_for_threadsafe_eval(acc, T)` if an accumulator stores typed containers that need to hold AD tracer types such as ForwardDiff `Dual`s. The default no-op is wrong for accumulators with concrete float fields. +- Avoid designs that index storage by `Threads.threadid()`. Julia scheduling and thread IDs are not a stable ownership model. + +## Contributing Checklist + +- Non-breaking changes target `main`; breaking changes target `breaking`. +- Identify whether the change is user-facing, internal, or downstream-facing through Turing.jl. +- Add the smallest tests that exercise the behavior. +- Add nested-submodel tests for context, prefix, conditioning, or fixing changes. +- Add AD backend tests for log-density, transform, vector-parameter, or `run_ad` changes. +- Add round-trip tests for flattening and unflattening changes, including scalars, arrays, tuples, `NamedTuple`s, nested values, and mixed element types. +- Check type stability and allocations for hot paths. +- Check dependency placement and compat bounds when touching Project files, extensions, docs, or tests. +- Include benchmark numbers for performance-sensitive changes. +- Document and test new user-facing API. diff --git a/CLAUDE.md b/CLAUDE.md index 6766fb068..1d72d0548 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,272 +1,2 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with -code in this repository. - -## Project Overview - -DynamicPPL.jl is the core probabilistic programming language and backend for -the [Turing.jl](https://github.com/TuringLang/Turing.jl) ecosystem. It provides -the `@model` macro for defining probabilistic models with tilde (`~`) -statements, and infrastructure for evaluating, conditioning, fixing, -transforming, and inspecting those models. - -DynamicPPL builds on AbstractPPL.jl for shared PPL interfaces such as -`VarName`, contexts, conditioning/fixing, and evaluator protocols. For -contributor-facing context extracted from DynamicPPL and AbstractPPL project -history, see `docs/src/onboarding.md`. - -## Test Structure - -Tests are split into Group1 and Group2 for CI parallelism, controlled by the -`GROUP` environment variable in `test/runtests.jl`. CI also runs Aqua.jl -quality checks and doctests. - -**Important**: Each test file should be self-contained. Dependencies should -come from package imports, not relative imports or `include()` statements. This -allows individual test files to be run with tools such as -[TestPicker.jl](https://github.com/theogf/TestPicker.jl). - -## Formatting - -Code formatting uses -[JuliaFormatter.jl](https://github.com/domluna/JuliaFormatter.jl) v1, not v2, -with the **Blue style** configured in `.JuliaFormatter.toml`. CI enforces -formatting on all PRs. - -```bash -julia --project -e 'using JuliaFormatter; format(".")' -``` - -## Architecture - -For how things work, see the -[docs](https://turinglang.org/DynamicPPL.jl/stable/): model evaluation, the -tilde pipeline, init strategies, transform strategies, accumulators, -conditioning/fixing, threading, `VarNamedTuple`, and `LogDensityFunction`. - -### Key Types - - - **`Model`** (`src/model.jl`): wraps a model function with its arguments and - context. Created by the `@model` macro in `src/compiler.jl`. - - **`AbstractVarInfo`** (`src/abstract_varinfo.jl`): interface for tracking - random variables and accumulated quantities during model execution. - - **`VarName`** (from AbstractPPL): address for model variables, including - nested fields and indices. - - **`VarNamedTuple`** (`src/varnamedtuple.jl`): a named-tuple-like structure - keyed by `VarName`s. Used as the primary representation for named parameter - values where supported. - - **`LogDensityFunction`** (`src/logdensityfunction.jl`): translation layer - between named model parameters and flat `AbstractVector{<:Real}` inputs for - optimisers, samplers, and AD. Implements the `LogDensityProblems.jl` - interface. - -### Extensions (`ext/`) - -Optional AD backends and integrations, loaded via Julia's package extension -system: - - - `DynamicPPLForwardDiffExt`: ForwardDiff AD - - `DynamicPPLMooncakeExt`: Mooncake AD, with precompilation workload - - `DynamicPPLReverseDiffExt`: ReverseDiff AD - - `DynamicPPLEnzymeCoreExt`: EnzymeCore AD support - - `DynamicPPLMCMCChainsExt`: MCMCChains integration - - `DynamicPPLMarginalLogDensitiesExt`: marginalization support - -### Testing Utilities (`src/test_utils/`) - -`DynamicPPL.TestUtils` provides test models with known analytical solutions -(`logprior_true`, `loglikelihood_true`, etc.) and an AD testing framework -(`run_ad`, `ADResult`) used across the Turing ecosystem. - -## Review Guidelines - -Common pitfalls and non-obvious constraints when writing or reviewing -DynamicPPL code. - -### Prefer `OnlyAccsVarInfo` over `VarInfo` - -For new evaluation code, prefer `OnlyAccsVarInfo` plus `init!!` over adding -more behaviour to `VarInfo`. `VarInfo` is still important, but it combines -vector values, transform state, metadata, and accumulators, while many fast -paths need only a subset of that state. - -A common migration shape is: - -```julia -evaluate!!(model, vi) -``` - -to: - -```julia -init!!(model, oavi, InitFromParams(vi.values), vi.transform_strategy) -``` - -Choose the actual init strategy and accumulator set from the caller's needs. - -### BangBang (`!!`) Return Values - -Functions suffixed with `!!` follow BangBang.jl semantics: they may mutate in -place, but they may also return a replacement object. **Always use the return -value.** Discarding the return value can silently drop updates, especially for -immutable wrappers such as `VarInfo` and `AccumulatorTuple`. - -```julia -# WRONG: mutation may not happen; vi may be unchanged. -accumulate_assume!!(vi, x, tval, logjac, vn, dist, template) - -# RIGHT -vi = accumulate_assume!!(vi, x, tval, logjac, vn, dist, template) -``` - -This applies transitively: if your function calls a `!!` function, it usually -must also return the updated state. - -### Accumulator Pitfalls - -See the accumulator docs for the full protocol. Common mistakes: - - - **`val` vs `tval` in `accumulate_assume!!`**: `val` is the original - model-space value and is what `logpdf` should see. `tval` is the - `TransformedValue`, which may hold linked values. `logjac` is the - log-Jacobian of the forward link transform, or zero if unlinked. - - **`logpdf` vs `loglikelihood` for observations**: - `LogLikelihoodAccumulator` uses `Distributions.loglikelihood`, not `logpdf`. - For array-valued or product-like observations, the two can have different - shapes or aggregation semantics. Use the one the accumulator protocol - expects. - - **Aliased `copy`**: `copy(acc)` must not share mutable internal state unless - that sharing is intentional and documented. Aliased containers can corrupt - results when accumulators are copied for threaded evaluation. - -### TransformedValue - - - **`get_raw_value(tv)` needs a distribution for dynamic transforms.** - `DynamicLink` and `Unlink` derive their transform from the distribution, so - use `get_raw_value(tv, dist)`. The one-argument form is for cases where the - transform is already fully known. - - **`DynamicLink` re-derives the bijection from `dist` during evaluation.** - This is necessary because support can depend on earlier random variables, for - example `y ~ truncated(Normal(); lower=x)`. If support is known to be - constant, consider `FixedTransform` via `WithTransforms`. - - **`FixedTransform` must match the target transform.** Do not assume fixed - transforms compose with re-derived dynamic transforms. - -### LogDensityFunction - - - **`getlogjoint_internal` vs `getlogjoint`**: samplers operating in - unconstrained space usually need `getlogjoint_internal`. `getlogjoint` is the - constrained-space log joint. Using the wrong one changes Jacobian handling. - - **Compiled ReverseDiff tapes are input-dependent.** If model control flow - depends on parameter values, compiled ReverseDiff only gives correct - gradients for inputs that follow the same branch as the compilation input. - Do not use `AutoReverseDiff(; compile=true)` for parameter-dependent - branching. - - Keep evaluator APIs split into structural preparation and AD-specific - preparation. Put backend-specific gradient code in extensions where possible. - - Check aliasing in evaluator and AD APIs. `!!` methods may return buffers that - alias internal caches; copy before exposing or storing results long term. - -### `VarNamedTuple` as Primary Data Structure - -`VarNamedTuple` is the canonical internal representation for named parameter -collections in new DynamicPPL code: conditioning/fixing values, parameter -storage, and accumulator values. `NamedTuple` and `Dict{VarName}` are accepted -as user-facing input, but should usually be converted to `VarNamedTuple` at API -boundaries rather than propagated internally. - -Preserve templates, shapes, and index structure when working with -`VarNamedTuple`. Array-valued variables, slices, and nested fields need enough -template information to round-trip between named values and flat vectors. Avoid -large mostly-empty shadow arrays for sparse indexed variables, and keep eltypes -concrete in hot paths. - -See the VarNamedTuple docs for motivation: it is performant, general, and gives -one representation for named parameter collections. - -### VarName - - - **Use `@varname(x)`, not `:x` or `VarName(:x)`.** The macro constructs the - correct optic for indexed access. `@varname(x[1])` creates a `VarName` with - an index lens; constructing this manually is error-prone. - - **Use subsumption for containment checks.** `subsumes(@varname(x), @varname(x[1]))` is `true`, but the two names are not equal. Conditioning - on `@varname(x)` matches sub-indices; conditioning on `@varname(x[1])` - matches only that index. - - Treat `VarName` display, sorting, prefixing, unprefixing, and serialization - as downstream-facing interface behaviour. Chains, saved results, and - external packages can depend on stable names. - - Test nested fields, indices, ranges, `Colon`, and non-standard indices when - changing `VarName` optics. - -### `@model` Compiler Changes - -`@model` lowering must preserve ordinary Julia semantics, not only -probabilistic statements. For compiler changes, test positional and keyword -arguments, default values, splatting, closures, interpolation, return values, -no-observation models, and data- or parameter-dependent control flow. - -Keep macro hygiene explicit. User variables, generated temporaries, and globals -should not capture each other accidentally. Inspect expanded code when changing -compiler paths. Preserve model return values; returned quantities are -user-visible and distinct from accumulated random variables. - -### Threading - -See the threading docs. Key edge case: -`promote_for_threadsafe_eval(acc, T)` must be implemented if an accumulator -stores typed containers that need to hold AD tracer types, such as ForwardDiff -`Dual`s. The default is a no-op, which is wrong for accumulators with concrete -float fields. - -Avoid designs that index storage by `Threads.threadid()`. Julia scheduling and -thread IDs are not a stable ownership model. - -## Julia Engineering Practices - - - Check type stability with `@inferred`, `@code_warntype`, and focused tests - when changing compiler output, VNTs, accumulators, transforms, or log-density - paths. - - Avoid unnecessary static parameters. Julia specializes on most ordinary - argument types, but is conservative for `Type`, `Function`, and `Vararg`. - Use `f(x, ::Type{T}) where {T}` when the type itself must specialize. - - Benchmark generated functions, macro output, and hot-path refactors before - assuming simpler code is equivalent. - - Prefer dispatch and small protocol functions over large conditional blocks. - - Avoid broad overloads of Base functions for arbitrary input types; they can - create method ambiguities and accidental API. - - Put backend-specific behaviour in package extensions or narrow integration - layers when possible. - - Make direct dependencies explicit enough to version-bound and test. Do not - rely on packages being loaded transitively. - - Use accessor functions for values downstream packages need. Direct field - access from Turing or other packages turns internal representation into - accidental API. - - Prefer `Base.maybeview` over eager slicing when indexed access should avoid - allocations but still support tuples and scalar indexing. - - Avoid fragile output-type prediction. When possible, compute an initial value - and allocate caches from the observed value. - - Keep doctests deterministic. Use `StableRNGs` when examples print random - values. - -## Contributing - - - Non-breaking changes target `main`; breaking changes target the `breaking` - branch. - - CI runs tests on Ubuntu, Windows, and macOS, across stable, minimum, and - selected Julia versions, with both one and two threads. - - Identify whether the change is user-facing, internal, or downstream-facing - through Turing.jl. - - Add the smallest tests that exercise the behaviour. - - Add nested-submodel tests for context, prefix, conditioning, or fixing - changes. - - Add AD backend tests for log-density, transform, vector-parameter, or - `run_ad` changes. - - Add round-trip tests for flattening and unflattening changes, including - scalars, arrays, tuples, `NamedTuple`s, nested values, and mixed element - types. - - Check type stability and allocations for hot paths. - - Check dependency placement and compat bounds when touching Project files, - extensions, docs, or tests. - - Include benchmark numbers for performance-sensitive changes. - - Document and test new user-facing API. +@AGENTS.md +@JULIA.md \ No newline at end of file diff --git a/JULIA.md b/JULIA.md new file mode 100644 index 000000000..bcd26f423 --- /dev/null +++ b/JULIA.md @@ -0,0 +1,158 @@ +# JULIA.md + +Guidance for coding agents reviewing or changing Julia code in this repository. + +## Engineering Practices + +- Write generic numeric code unless the math or an external API requires a concrete type. Avoid unnecessary `Float64`, `Int`, `Real`, `Array`, `Vector`, and `Matrix` constraints. +- Preserve caller intent with `zero(x)`, `one(x)`, `oneunit(x)`, `oftype`, `promote`, and `promote_type`, especially for `Float32`, `BigFloat`, AD numbers, units, GPU scalars, and symbolic values. +- Keep struct fields concrete; prefer parametric fields over `field::Number` or `field::AbstractVector`. +- Avoid unnecessary static parameters. Julia specializes on most ordinary argument types, but is conservative for `Type`, `Function`, and `Vararg`. Use `f(x, ::Type{T}) where {T}` when the type itself must specialize. +- Check type stability with `@inferred`, `@code_warntype`, and focused tests when changing compiler output, VNTs, accumulators, transforms, or log-density paths. +- Benchmark generated functions, macro output, and hot-path refactors before assuming simpler code is equivalent. +- Prefer dispatch and small protocol functions over large conditional blocks. +- Avoid broad overloads of Base functions for arbitrary input types; they can create method ambiguities and accidental API. +- Put backend-specific behaviour in package extensions or narrow integration layers when possible. +- Make direct dependencies explicit enough to version-bound and test. Do not rely on packages being loaded transitively. +- Use accessor functions for values downstream packages need. Direct field access from another package turns internal representation into accidental API. +- Prefer `Base.maybeview` over eager slicing when indexed access should avoid allocations but still support tuples and scalar indexing. +- Avoid fragile output-type prediction. When possible, compute an initial value and allocate caches from the observed value. +- Keep doctests deterministic. Use `StableRNGs` when examples print random values. + +Pattern: + +```julia +# Avoid: too concrete and inference-hostile. +f(x::Float64) = x / 2 +buf = zeros(Float64, length(xs)) +struct Model + scale::Number +end + +# Prefer: generic arithmetic, input-derived allocation, concrete instances. +f(x) = x / oftype(x, 2) +buf = similar(xs, promote_type(eltype(xs), Float64), length(xs)) +struct Model{T} + scale::T +end +``` + +Public constructors are API too: if `NormalLike` is public, both `NormalLike(mu, sigma)` and `NormalLike(; mu, sigma)` are long-term commitments. + +## API And Design Review + +Review API convention separately from conceptual design. + +For public signatures, check: + +- dimension arguments use `dims=`, including tuple-valued `dims` when natural +- data comes first, except callable-first APIs such as `map(f, xs)` +- callable arguments come first when `do`-block syntax should work +- mutating and non-mutating pairs exist when both are natural +- configuration switches are keywords, not positional `Bool`, small `Int`, or `Symbol` flags +- reductions use `init=`, and sorting follows `lt=`, `by=`, and `rev=` +- output allocation respects input type via `similar` or an explicit destination buffer +- wrappers forward reasonable `kwargs...` +- related functions use consistent names, argument order, and keyword names +- type annotations are no narrower than the dispatch contract requires + +For package design, check: + +- exported names clearly belong to the package purpose +- abstractions have enough implementations to justify themselves +- concrete types that downstream users need to extend have protocol functions +- type parameters are used for dispatch, storage, or invariants +- overlapping functions are not historical duplicates +- expected inverse, mutating/non-mutating, parse/show, iteration, indexing, or conversion operations are present +- exported low-level helpers do not leak implementation details +- outputs of one public function can feed into related public functions +- custom `==` has matching `hash`, and `hash` is consistent with `isequal` +- duplicated Base functionality should instead extend a Base method +- failure behavior is consistent: throwing, `nothing`, sentinel values, or invalid results +- documented or tested internal names may need `public` or export annotations + +Treat public constructors, keyword arguments, exported names, aliases, abstract supertypes, and traits as long-term commitments. If a concrete type is public, its constructors are public too. + +## AD And Probability Code + +AD rules should preserve tangent-space meaning, not just match a finite-difference check at one ordinary `Float64` point. + +For AD rule code, check: + +- `rrule` and `frule` outputs have the right primal and tangent shapes +- structural arguments such as functions and types return the appropriate non-tangent marker +- true zero derivatives use the appropriate zero-tangent marker +- cotangents are projected back when they may leave the primal representation +- non-differentiable cases are represented explicitly +- structured inputs such as `Diagonal`, `Symmetric`, sparse arrays, and factorizations are tested +- higher-order AD is considered when mutation or captured buffers are introduced + +For complex AD, state whether the implementation assumes holomorphic differentiation over `Complex` or real differentiation after viewing `Complex` as two real variables. Test non-holomorphic functions such as `abs2`, `real`, `imag`, and `conj` for the real interpretation. + +For probability code, separate sample type, mathematical support, and reference measure. Do not infer too much from names like `Discrete` or `Continuous`; floating-point samples can still have atoms, and densities are always with respect to a reference measure. `pdf` may mean a density with units for continuous variables, a mass with respect to counting measure for discrete variables, or something more subtle for mixed or censored distributions. + +For probability changes, check: + +- domain boundaries and invalid parameters +- `pdf`, `logpdf`, `cdf`, `logcdf`, and `quantile` consistency +- extreme tails and near-degenerate parameters +- explicit RNG threading instead of hidden global RNG use +- parameter gradients, not just gradients with respect to observations +- type stability when parameters have different but compatible types +- strong numerical references such as `BigFloat`, known identities, or independent implementations +- deterministic examples and doctests when random values are printed + +Useful numerical checks: + +```julia +@test isfinite(logpdf(d, x)) +@test logcdf(d, x) <= 0 +@test isapprox(f(Float64(x)), Float64(f(big(x))); rtol=1e-12) +@test rand(StableRNG(1), d) == rand(StableRNG(1), d) +``` + +## Backend Compatibility + +Backend-compatible Julia code avoids scalar indexing, hidden CPU fallbacks, runtime string construction in kernels, and exception paths that compile into invalid device code. + +```julia +# Avoid scalar loops when broadcast expresses the operation. +for i in eachindex(y) + y[i] = f(x[i]) +end + +# Prefer backend-aware array operations. +y .= f.(x) +``` + +When moving structured objects to GPU storage, prefer explicit `Adapt.jl` support per type rather than generic field walking; generic reconstruction can violate constructor invariants or miss cached fields. + +## Review Checklist + +Search for: + +- over-specific types: `Float64`, `Float32`, `Int`, `Real`, `Array`, `Vector`, `Matrix` +- concrete allocation: `zeros(Float64, ...)`, `ones(Float64, ...)`, `similar(x, Float64, ...)` +- branch conditions such as `x == 0` or `x == 1` that may behave poorly for AD numbers or NaNs +- `ccall` or conversions that force `Cdouble` +- `collect`, scalar indexing, or CPU-only fallbacks in code that may receive GPU arrays +- broad signatures such as `f(x::Any)`, `f(x::AbstractArray)`, or very general Base overloads + +Request type-variety tests when code claims to be generic: + +```julia +@test f(Float32(1)) isa Float32 +@test f(big"1.0") isa BigFloat +@test ForwardDiff.derivative(x -> f(x), 1.0) isa Real +@test f(SVector(1.0, 2.0)) isa SVector +``` + +Split large changes by risk: mechanical cleanup, tests, non-breaking fixes, performance work, API additions, and breaking design changes. + +Report findings in tiers: + +- breaking API changes +- non-breaking additions or compatibility shims +- internal consistency or design questions + +For each finding, include the current pattern, proposed direction, why it matters, and whether it is breaking. Pause for user approval before turning review findings into implementation work, and frame uncertain design items as questions about intent. From b89be947cbfe805d8a25480574e63b0ec83db42e Mon Sep 17 00:00:00 2001 From: Hong Ge <3279477+yebai@users.noreply.github.com> Date: Thu, 7 May 2026 15:29:50 +0100 Subject: [PATCH 4/6] Update AGENTS.md --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 21b3e7abd..90895af91 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,4 +1,4 @@ -# CLAUDE.md +# AGENTS.md Guidance for Claude Code when working in this repository. From f59b2834b029b453b9a112d564442220d6dd5cf2 Mon Sep 17 00:00:00 2001 From: Hong Ge Date: Thu, 7 May 2026 16:21:30 +0100 Subject: [PATCH 5/6] julia, agents and claude files --- AGENTS.md | 109 +++++++++++++++++------------------ CLAUDE.md | 2 +- JULIA.md | 168 +++++++++++++++++------------------------------------- 3 files changed, 103 insertions(+), 176 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 90895af91..0c29595f9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,8 +1,6 @@ # AGENTS.md -Guidance for Claude Code when working in this repository. - -See also: @JULIA.md for general Julia engineering and review guidance. +Guidance for Claude Code in this repository. See @JULIA.md for general Julia practices. ## Project Overview @@ -12,64 +10,59 @@ DynamicPPL builds on AbstractPPL.jl for shared PPL interfaces such as `VarName`, ## Tests And Formatting -- Tests are split into Group1 and Group2 for CI parallelism, controlled by `GROUP` in `test/runtests.jl`. -- CI also runs Aqua.jl quality checks, doctests, formatting, and multi-platform tests across selected Julia versions and thread counts. -- Each test file should be self-contained. Use package imports, not relative imports or `include()` statements, so files can run individually with tools such as TestPicker.jl. -- Formatting uses JuliaFormatter.jl v1, not v2, with Blue style from `.JuliaFormatter.toml`. + - Tests are split into Group1/Group2 via `GROUP` in `test/runtests.jl`. -```bash -julia --project -e 'using JuliaFormatter; format(".")' -``` + - Test files are self-contained — use package imports, not relative imports or `include()`, so they run individually with TestPicker.jl. + - Formatting is JuliaFormatter v1 (Blue style): + + ```bash + julia --project -e 'using JuliaFormatter; format(".")' + ``` ## Architecture Pointers -Use the docs for model evaluation, the tilde pipeline, init strategies, transform strategies, accumulators, conditioning/fixing, threading, `VarNamedTuple`, and `LogDensityFunction`. + - `Model` (`src/model.jl`): wraps model function, args, context; created by `@model` in `src/compiler.jl`. + - `AbstractVarInfo` (`src/abstract_varinfo.jl`): tracks random variables and accumulated quantities during evaluation. + - `VarName` (AbstractPPL): address for model variables, including nested fields/indices. + - `VarNamedTuple` (`src/varnamedtuple.jl`): named-tuple-like parameter storage keyed by `VarName`. + - `LogDensityFunction` (`src/logdensityfunction.jl`): bridge from named parameters to flat `AbstractVector` for samplers/AD. + - `ext/`: ForwardDiff, Mooncake, ReverseDiff, EnzymeCore, MCMCChains, MarginalLogDensities integrations. + - `DynamicPPL.TestUtils`: analytical test models, `run_ad`, `ADResult`. -- `Model` (`src/model.jl`): wraps a model function, arguments, and context; created by `@model` in `src/compiler.jl`. -- `AbstractVarInfo` (`src/abstract_varinfo.jl`): interface for tracking random variables and accumulated quantities during model execution. -- `VarName` (AbstractPPL): address for model variables, including nested fields and indices. -- `VarNamedTuple` (`src/varnamedtuple.jl`): named-tuple-like parameter storage keyed by `VarName`. -- `LogDensityFunction` (`src/logdensityfunction.jl`): bridge between named model parameters and flat `AbstractVector{<:Real}` inputs for samplers, optimizers, and AD. -- Optional integrations live in `ext/`: ForwardDiff, Mooncake, ReverseDiff, EnzymeCore, MCMCChains, and MarginalLogDensities. -- `DynamicPPL.TestUtils` provides analytical test models and AD helpers (`run_ad`, `ADResult`) used across the Turing ecosystem. +## Key Invariants -## DynamicPPL Review Notes +Evaluator methods follow BangBang `!!` semantics (see JULIA.md). -- Prefer `OnlyAccsVarInfo` plus `init!!` for new evaluation code when fast paths only need accumulators or a subset of VarInfo state. -- `VarInfo` remains important, but it combines vector values, transform state, metadata, and accumulators. Many fast paths need only part of that state, so avoid adding behavior to `VarInfo` by default. -- Functions ending in `!!` follow BangBang.jl semantics: they may mutate or return a replacement object. Always use the return value, and return updated state from callers that invoke `!!`. +**`accumulate_assume!!`** — `val` is model-space (passed to `logpdf`); `tval` is transformed; `logjac` is the log-Jacobian of the forward link transform (zero if unlinked): ```julia -# Wrong: updates may be lost. -accumulate_assume!!(vi, x, tval, logjac, vn, dist, template) - -# Right. vi = accumulate_assume!!(vi, x, tval, logjac, vn, dist, template) ``` -- In `accumulate_assume!!`, `val` is the model-space value and should be passed to `logpdf`; `tval` is the transformed value. -- `logjac` is the log-Jacobian of the forward link transform, or zero if unlinked. -- `LogLikelihoodAccumulator` uses `Distributions.loglikelihood`, not `logpdf`; array-valued or product-like observations can differ in shape or aggregation. -- `copy(acc)` must not share mutable internal state unless that sharing is intentional and documented. -- `get_raw_value(tv, dist)` is required for dynamic transforms because `DynamicLink` and `Unlink` derive their transform from the distribution; the raw value cannot be recovered correctly from `tv` alone when support is distribution-dependent. -- The one-argument `get_raw_value(tv)` is only for cases where the transform is already fully known. -- `DynamicLink` re-derives the bijection from `dist` during evaluation because support can depend on earlier random variables, such as `y ~ truncated(Normal(); lower=x)`. Do not cache or reuse a fixed bijection unless support is known to be constant. -- If support is known to be constant, consider `FixedTransform` via `WithTransforms`; fixed transforms must match the target transform. -- Samplers operating in unconstrained space usually need `getlogjoint_internal`; `getlogjoint` is the constrained-space log joint. -- Compiled ReverseDiff tapes are input-dependent; do not use `AutoReverseDiff(; compile=true)` when model control flow depends on parameter values. -- Keep evaluator APIs split into structural preparation and AD-specific preparation. Put backend-specific gradient code in extensions when possible. -- Check aliasing in evaluator and AD APIs. `!!` methods may return buffers that alias internal caches; copy before exposing results to callers, storing them long term, or reusing them after another model evaluation. +**`LogLikelihoodAccumulator`** uses `Distributions.loglikelihood`, not `logpdf` — array/product observations differ in shape and aggregation. + +**Dynamic transforms** — `DynamicLink`/`Unlink` re-derive bijections from `dist` because support can depend on earlier RVs (e.g. `y ~ truncated(Normal(); lower=x)`). Use `get_raw_value(tv, dist)`; never cache a fixed bijection. Use `FixedTransform`/`WithTransforms` only when support is constant. + +**Log joint** — samplers in unconstrained space want `getlogjoint_internal`; constrained-space is `getlogjoint`. + +**ReverseDiff** — don't use `AutoReverseDiff(; compile=true)` when model control flow depends on parameter values (compiled tapes are input-dependent). + +## DynamicPPL Review Notes + + - Prefer `OnlyAccsVarInfo` + `init!!` for new evaluation code that needs only accumulators or a subset of `VarInfo` state. + - Avoid adding behaviour to `VarInfo` by default — it bundles values, transform state, metadata, and accumulators, but most fast paths need only part. + - Keep evaluator APIs split: structural prep vs AD-specific prep. Backend gradient code goes in extensions. ## Names And Parameter Storage -- Use `VarNamedTuple` as the canonical internal representation for named parameter collections in new code. -- Accept `NamedTuple` and `Dict{VarName}` at user-facing boundaries, but convert to `VarNamedTuple` rather than propagating them internally. -- Preserve templates, shapes, and index structure when round-tripping between named values and flat vectors. -- Avoid large mostly-empty shadow arrays for sparse indexed variables. -- Use `@varname(x)`, not `:x` or `VarName(:x)`. -- Use subsumption for containment checks: `subsumes(@varname(x), @varname(x[1]))` is true, but the names are not equal. -- Treat VarName display, sorting, prefixing, unprefixing, and serialization as downstream-facing interface behavior. -- Test nested fields, indices, ranges, `Colon`, and non-standard indices when changing VarName optics. + - Use `VarNamedTuple` as the canonical internal representation for named parameter collections in new code. + - Accept `NamedTuple` and `Dict{VarName}` at user-facing boundaries, but convert to `VarNamedTuple` rather than propagating them internally. + - Preserve templates, shapes, and index structure when round-tripping between named values and flat vectors. + - Avoid large mostly-empty shadow arrays for sparse indexed variables. + - Use `@varname(x)`, not `:x` or `VarName(:x)`. + - Use subsumption for containment checks: `subsumes(@varname(x), @varname(x[1]))` is true, but the names are not equal. + - Treat VarName display, sorting, prefixing, unprefixing, and serialization as downstream-facing interface behavior. + - Test nested fields, indices, ranges, `Colon`, and non-standard indices when changing VarName optics. ## `@model` Compiler @@ -81,18 +74,18 @@ Keep macro hygiene explicit. User variables, generated temporaries, and globals ## Threading -- Implement `promote_for_threadsafe_eval(acc, T)` if an accumulator stores typed containers that need to hold AD tracer types such as ForwardDiff `Dual`s. The default no-op is wrong for accumulators with concrete float fields. -- Avoid designs that index storage by `Threads.threadid()`. Julia scheduling and thread IDs are not a stable ownership model. + - Implement `promote_for_threadsafe_eval(acc, T)` for accumulators with concrete float fields — the default no-op leaves them unable to hold AD tracers like ForwardDiff `Dual`s. + - Avoid designs that index storage by `Threads.threadid()`. Julia scheduling and thread IDs are not a stable ownership model. ## Contributing Checklist -- Non-breaking changes target `main`; breaking changes target `breaking`. -- Identify whether the change is user-facing, internal, or downstream-facing through Turing.jl. -- Add the smallest tests that exercise the behavior. -- Add nested-submodel tests for context, prefix, conditioning, or fixing changes. -- Add AD backend tests for log-density, transform, vector-parameter, or `run_ad` changes. -- Add round-trip tests for flattening and unflattening changes, including scalars, arrays, tuples, `NamedTuple`s, nested values, and mixed element types. -- Check type stability and allocations for hot paths. -- Check dependency placement and compat bounds when touching Project files, extensions, docs, or tests. -- Include benchmark numbers for performance-sensitive changes. -- Document and test new user-facing API. + - Non-breaking changes target `main`; breaking changes target `breaking`. + - Identify whether the change is user-facing, internal, or downstream-facing through Turing.jl. + - Add the smallest tests that exercise the behavior. + - Add nested-submodel tests for context, prefix, conditioning, or fixing changes. + - Add AD backend tests for log-density, transform, vector-parameter, or `run_ad` changes. + - Add round-trip tests for flattening and unflattening changes, including scalars, arrays, tuples, `NamedTuple`s, nested values, and mixed element types. + - Check type stability and allocations for hot paths. + - Check dependency placement and compat bounds when touching Project files, extensions, docs, or tests. + - Include benchmark numbers for performance-sensitive changes. + - Document and test new user-facing API. diff --git a/CLAUDE.md b/CLAUDE.md index 1d72d0548..496e0e98e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,2 +1,2 @@ @AGENTS.md -@JULIA.md \ No newline at end of file +@JULIA.md diff --git a/JULIA.md b/JULIA.md index bcd26f423..92b210fe6 100644 --- a/JULIA.md +++ b/JULIA.md @@ -1,108 +1,77 @@ # JULIA.md -Guidance for coding agents reviewing or changing Julia code in this repository. - -## Engineering Practices - -- Write generic numeric code unless the math or an external API requires a concrete type. Avoid unnecessary `Float64`, `Int`, `Real`, `Array`, `Vector`, and `Matrix` constraints. -- Preserve caller intent with `zero(x)`, `one(x)`, `oneunit(x)`, `oftype`, `promote`, and `promote_type`, especially for `Float32`, `BigFloat`, AD numbers, units, GPU scalars, and symbolic values. -- Keep struct fields concrete; prefer parametric fields over `field::Number` or `field::AbstractVector`. -- Avoid unnecessary static parameters. Julia specializes on most ordinary argument types, but is conservative for `Type`, `Function`, and `Vararg`. Use `f(x, ::Type{T}) where {T}` when the type itself must specialize. -- Check type stability with `@inferred`, `@code_warntype`, and focused tests when changing compiler output, VNTs, accumulators, transforms, or log-density paths. -- Benchmark generated functions, macro output, and hot-path refactors before assuming simpler code is equivalent. -- Prefer dispatch and small protocol functions over large conditional blocks. -- Avoid broad overloads of Base functions for arbitrary input types; they can create method ambiguities and accidental API. -- Put backend-specific behaviour in package extensions or narrow integration layers when possible. -- Make direct dependencies explicit enough to version-bound and test. Do not rely on packages being loaded transitively. -- Use accessor functions for values downstream packages need. Direct field access from another package turns internal representation into accidental API. -- Prefer `Base.maybeview` over eager slicing when indexed access should avoid allocations but still support tuples and scalar indexing. -- Avoid fragile output-type prediction. When possible, compute an initial value and allocate caches from the observed value. -- Keep doctests deterministic. Use `StableRNGs` when examples print random values. - -Pattern: +Day-to-day Julia practices. + +## Engineering + + - Write generic numeric code unless the math or an external API forces a concrete type. Avoid `Float64`/`Int`/`Real`/`Array`/`Vector`/`Matrix` constraints that aren't load-bearing. + - Preserve caller types with `zero(x)`, `one(x)`, `oftype`, `promote`, `promote_type` — especially for `Float32`, `BigFloat`, AD numbers, units, and GPU scalars. + - Struct fields should be concrete via type parameters, not `field::Number` or `field::AbstractVector`. + - Julia doesn't specialize on `Type`, `Function`, or `Vararg` arguments. Use `f(x, ::Type{T}) where {T}` when the type itself must specialize. + - Check inference (`@inferred`, `@code_warntype`) when touching compiler output, VarNamedTuples, accumulators, transforms, or log-density paths. + - Benchmark generated functions, macro output, and hot-path refactors before assuming a simpler form is equivalent. + - Prefer dispatch and small protocol functions over large conditional blocks. + - Avoid broad Base overloads — they create method ambiguities and accidental API. + - Backend-specific behaviour goes in package extensions or narrow integration layers. + - Provide accessors for values downstream packages need — direct field access from another package becomes accidental API. + - Prefer `Base.maybeview` over eager slicing when allocation matters but tuple/scalar indexing must still work. + - Allocate output containers from observed values rather than predicting element types up front. + - Doctests must be deterministic — use `StableRNGs` when examples print random values. ```julia -# Avoid: too concrete and inference-hostile. +# Avoid: too concrete, inference-hostile. f(x::Float64) = x / 2 buf = zeros(Float64, length(xs)) struct Model scale::Number end -# Prefer: generic arithmetic, input-derived allocation, concrete instances. -f(x) = x / oftype(x, 2) +# Prefer: generic args, input-derived allocation, concrete fields. +f(x) = x / 2 buf = similar(xs, promote_type(eltype(xs), Float64), length(xs)) struct Model{T} scale::T end ``` -Public constructors are API too: if `NormalLike` is public, both `NormalLike(mu, sigma)` and `NormalLike(; mu, sigma)` are long-term commitments. +## Idioms -## API And Design Review + - `!!` semantics (BangBang.jl): methods ending in `!!` may mutate or return a replacement. Always reassign: `x = f!!(x, ...)`. + - Returns from `!!` methods may alias internal state — copy before holding long-term or reusing across calls. + - `copy(x)` must not share mutable internal state with `x` unless intentional and documented. + - Don't index thread-owned storage by `Threads.threadid()` — task scheduling makes IDs unstable. Pass per-task buffers explicitly or use a thread-safe collection. -Review API convention separately from conceptual design. +## Public APIs -For public signatures, check: +Signatures: -- dimension arguments use `dims=`, including tuple-valued `dims` when natural -- data comes first, except callable-first APIs such as `map(f, xs)` -- callable arguments come first when `do`-block syntax should work -- mutating and non-mutating pairs exist when both are natural -- configuration switches are keywords, not positional `Bool`, small `Int`, or `Symbol` flags -- reductions use `init=`, and sorting follows `lt=`, `by=`, and `rev=` -- output allocation respects input type via `similar` or an explicit destination buffer -- wrappers forward reasonable `kwargs...` -- related functions use consistent names, argument order, and keyword names -- type annotations are no narrower than the dispatch contract requires + - Dimension arguments use `dims=` (tuple-valued where natural). + - Data first; callable first when `do`-block syntax should work (`map(f, xs)`-style). + - Pair mutating and non-mutating versions when both make sense (`sort!`/`sort`). + - Configuration is keywords, not positional `Bool`/small `Int`/`Symbol` flags. + - Reductions take `init=`; sorting takes `lt=`/`by=`/`rev=`. + - Allocate output via `similar(x, ...)` or a destination buffer; don't hardcode `Vector{Float64}`. + - Wrappers forward `kwargs...`. + - Match argument order, keyword names, and return shape across related functions. -For package design, check: +Types: -- exported names clearly belong to the package purpose -- abstractions have enough implementations to justify themselves -- concrete types that downstream users need to extend have protocol functions -- type parameters are used for dispatch, storage, or invariants -- overlapping functions are not historical duplicates -- expected inverse, mutating/non-mutating, parse/show, iteration, indexing, or conversion operations are present -- exported low-level helpers do not leak implementation details -- outputs of one public function can feed into related public functions -- custom `==` has matching `hash`, and `hash` is consistent with `isequal` -- duplicated Base functionality should instead extend a Base method -- failure behavior is consistent: throwing, `nothing`, sentinel values, or invalid results -- documented or tested internal names may need `public` or export annotations + - Provide protocol functions (accessors, traits) so downstream packages can extend without reaching for internals. + - Type parameters serve dispatch, storage, or invariants — not decoration. + - Define `hash` whenever you define `==`, consistent with `isequal`. + - Extend an existing Base method rather than introducing a parallel name (`Base.length`, not `mylength`). + - Pick one failure mode (throw, `nothing`, sentinel) and document it. -Treat public constructors, keyword arguments, exported names, aliases, abstract supertypes, and traits as long-term commitments. If a concrete type is public, its constructors are public too. +Public constructors, keyword arguments, exported names, aliases, abstract supertypes, and traits are long-term commitments. A public concrete type commits to both `Foo(a, b)` and `Foo(; a, b)`. Mark internal names that downstream code already depends on as `public` rather than leaving them accidental. -## AD And Probability Code +## Probability -AD rules should preserve tangent-space meaning, not just match a finite-difference check at one ordinary `Float64` point. +When writing distribution-aware code (accumulators, transforms, log-density paths): -For AD rule code, check: - -- `rrule` and `frule` outputs have the right primal and tangent shapes -- structural arguments such as functions and types return the appropriate non-tangent marker -- true zero derivatives use the appropriate zero-tangent marker -- cotangents are projected back when they may leave the primal representation -- non-differentiable cases are represented explicitly -- structured inputs such as `Diagonal`, `Symmetric`, sparse arrays, and factorizations are tested -- higher-order AD is considered when mutation or captured buffers are introduced - -For complex AD, state whether the implementation assumes holomorphic differentiation over `Complex` or real differentiation after viewing `Complex` as two real variables. Test non-holomorphic functions such as `abs2`, `real`, `imag`, and `conj` for the real interpretation. - -For probability code, separate sample type, mathematical support, and reference measure. Do not infer too much from names like `Discrete` or `Continuous`; floating-point samples can still have atoms, and densities are always with respect to a reference measure. `pdf` may mean a density with units for continuous variables, a mass with respect to counting measure for discrete variables, or something more subtle for mixed or censored distributions. - -For probability changes, check: - -- domain boundaries and invalid parameters -- `pdf`, `logpdf`, `cdf`, `logcdf`, and `quantile` consistency -- extreme tails and near-degenerate parameters -- explicit RNG threading instead of hidden global RNG use -- parameter gradients, not just gradients with respect to observations -- type stability when parameters have different but compatible types -- strong numerical references such as `BigFloat`, known identities, or independent implementations -- deterministic examples and doctests when random values are printed - -Useful numerical checks: + - Separate sample type, mathematical support, and reference measure. Floating-point samples can still have atoms; `pdf` may be a density, a mass, or mixed for censored/truncated cases. + - Check domain boundaries and invalid parameters explicitly. + - Thread an explicit RNG; never reach for the global RNG implicitly. + - Consider parameter gradients, not just gradients with respect to observations. ```julia @test isfinite(logpdf(d, x)) @@ -111,48 +80,13 @@ Useful numerical checks: @test rand(StableRNG(1), d) == rand(StableRNG(1), d) ``` -## Backend Compatibility - -Backend-compatible Julia code avoids scalar indexing, hidden CPU fallbacks, runtime string construction in kernels, and exception paths that compile into invalid device code. - -```julia -# Avoid scalar loops when broadcast expresses the operation. -for i in eachindex(y) - y[i] = f(x[i]) -end - -# Prefer backend-aware array operations. -y .= f.(x) -``` - -When moving structured objects to GPU storage, prefer explicit `Adapt.jl` support per type rather than generic field walking; generic reconstruction can violate constructor invariants or miss cached fields. - -## Review Checklist - -Search for: +## Testing Generic Code -- over-specific types: `Float64`, `Float32`, `Int`, `Real`, `Array`, `Vector`, `Matrix` -- concrete allocation: `zeros(Float64, ...)`, `ones(Float64, ...)`, `similar(x, Float64, ...)` -- branch conditions such as `x == 0` or `x == 1` that may behave poorly for AD numbers or NaNs -- `ccall` or conversions that force `Cdouble` -- `collect`, scalar indexing, or CPU-only fallbacks in code that may receive GPU arrays -- broad signatures such as `f(x::Any)`, `f(x::AbstractArray)`, or very general Base overloads - -Request type-variety tests when code claims to be generic: +Exercise type variety when the contract is "works for any number type": ```julia @test f(Float32(1)) isa Float32 @test f(big"1.0") isa BigFloat -@test ForwardDiff.derivative(x -> f(x), 1.0) isa Real +@test ForwardDiff.derivative(f, 1.0) isa Real @test f(SVector(1.0, 2.0)) isa SVector ``` - -Split large changes by risk: mechanical cleanup, tests, non-breaking fixes, performance work, API additions, and breaking design changes. - -Report findings in tiers: - -- breaking API changes -- non-breaking additions or compatibility shims -- internal consistency or design questions - -For each finding, include the current pattern, proposed direction, why it matters, and whether it is breaking. Pause for user approval before turning review findings into implementation work, and frame uncertain design items as questions about intent. From e70b96bcdae99adf95d672f5256e2fe975e1b54a Mon Sep 17 00:00:00 2001 From: Hong Ge Date: Thu, 7 May 2026 17:45:30 +0100 Subject: [PATCH 6/6] final touches --- AGENTS.md | 46 ++++++++++++------------- JULIA.md | 4 +-- docs/src/onboarding.md | 76 ++++++------------------------------------ 3 files changed, 34 insertions(+), 92 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 0c29595f9..c051f5269 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,19 +1,20 @@ # AGENTS.md -Guidance for Claude Code in this repository. See @JULIA.md for general Julia practices. +Repository guidance for coding agents. See @JULIA.md for general Julia practices and `docs/src/onboarding.md` for newcomer-oriented background. ## Project Overview DynamicPPL.jl is the core probabilistic programming language backend for the Turing.jl ecosystem. It provides the `@model` macro for tilde (`~`) statements and infrastructure for evaluating, conditioning, fixing, transforming, and inspecting probabilistic models. -DynamicPPL builds on AbstractPPL.jl for shared PPL interfaces such as `VarName`, contexts, conditioning/fixing, and evaluator protocols. For project history and contributor context, see `docs/src/onboarding.md`. +DynamicPPL builds on AbstractPPL.jl for shared PPL interfaces such as `VarName`, contexts, conditioning/fixing, and evaluator protocols. ## Tests And Formatting - Tests are split into Group1/Group2 via `GROUP` in `test/runtests.jl`. - - Test files are self-contained — use package imports, not relative imports or `include()`, so they run individually with TestPicker.jl. - - Formatting is JuliaFormatter v1 (Blue style): + - CI also runs Aqua.jl quality checks and doctests. + - Test files are self-contained: use package imports, not relative imports or `include()`, so they run individually with TestPicker.jl. + - Formatting is JuliaFormatter v1 (Blue style), enforced by CI: ```bash julia --project -e 'using JuliaFormatter; format(".")' @@ -21,17 +22,18 @@ DynamicPPL builds on AbstractPPL.jl for shared PPL interfaces such as `VarName`, ## Architecture Pointers + - Docs: model evaluation, tilde pipeline, init strategies, transform strategies, accumulators, conditioning/fixing, and thread-safe accumulation. - `Model` (`src/model.jl`): wraps model function, args, context; created by `@model` in `src/compiler.jl`. - `AbstractVarInfo` (`src/abstract_varinfo.jl`): tracks random variables and accumulated quantities during evaluation. - `VarName` (AbstractPPL): address for model variables, including nested fields/indices. - `VarNamedTuple` (`src/varnamedtuple.jl`): named-tuple-like parameter storage keyed by `VarName`. - - `LogDensityFunction` (`src/logdensityfunction.jl`): bridge from named parameters to flat `AbstractVector` for samplers/AD. - - `ext/`: ForwardDiff, Mooncake, ReverseDiff, EnzymeCore, MCMCChains, MarginalLogDensities integrations. - - `DynamicPPL.TestUtils`: analytical test models, `run_ad`, `ADResult`. + - `LogDensityFunction` (`src/logdensityfunction.jl`): bridge from named parameters to flat `AbstractVector{<:Real}` for samplers, optimisers, and AD via LogDensityProblems.jl. + - `ext/`: `DynamicPPLForwardDiffExt`, `DynamicPPLMooncakeExt`, `DynamicPPLReverseDiffExt`, `DynamicPPLEnzymeCoreExt`, `DynamicPPLComponentArraysExt`, `DynamicPPLMCMCChainsExt`, and `DynamicPPLMarginalLogDensitiesExt`. + - `DynamicPPL.TestUtils`: analytical test models (`logprior_true`, `loglikelihood_true`, etc.), `run_ad`, `ADResult`. -## Key Invariants +## DynamicPPL Invariants -Evaluator methods follow BangBang `!!` semantics (see JULIA.md). +Evaluator methods follow BangBang `!!` semantics (see JULIA.md). `VarInfo` and `AccumulatorTuple` are immutable, so discarding a `!!` return value is a silent bug. **`accumulate_assume!!`** — `val` is model-space (passed to `logpdf`); `tval` is transformed; `logjac` is the log-Jacobian of the forward link transform (zero if unlinked): @@ -41,28 +43,21 @@ vi = accumulate_assume!!(vi, x, tval, logjac, vn, dist, template) **`LogLikelihoodAccumulator`** uses `Distributions.loglikelihood`, not `logpdf` — array/product observations differ in shape and aggregation. -**Dynamic transforms** — `DynamicLink`/`Unlink` re-derive bijections from `dist` because support can depend on earlier RVs (e.g. `y ~ truncated(Normal(); lower=x)`). Use `get_raw_value(tv, dist)`; never cache a fixed bijection. Use `FixedTransform`/`WithTransforms` only when support is constant. +**Dynamic transforms** — `DynamicLink`/`Unlink` re-derive bijections from `dist` because support can depend on earlier RVs (e.g. `y ~ truncated(Normal(); lower=x)`). Use `get_raw_value(tv, dist)`; the one-argument form only works for `NoTransform` and `FixedTransform`. Never cache a fixed bijection. Use `FixedTransform`/`WithTransforms` only when support is constant, and make sure the fixed transform exactly matches the target. -**Log joint** — samplers in unconstrained space want `getlogjoint_internal`; constrained-space is `getlogjoint`. +**Log joint** — `getlogjoint_internal(vi) = getlogjoint(vi) - getlogjac(vi)`. Samplers in unconstrained space want `getlogjoint_internal`; constrained-space is `getlogjoint`. **ReverseDiff** — don't use `AutoReverseDiff(; compile=true)` when model control flow depends on parameter values (compiled tapes are input-dependent). -## DynamicPPL Review Notes +## Review Focus - Prefer `OnlyAccsVarInfo` + `init!!` for new evaluation code that needs only accumulators or a subset of `VarInfo` state. - - Avoid adding behaviour to `VarInfo` by default — it bundles values, transform state, metadata, and accumulators, but most fast paths need only part. + - Avoid adding behaviour to `VarInfo` by default; it bundles values, transform state, metadata, and accumulators, but most fast paths need only part. - Keep evaluator APIs split: structural prep vs AD-specific prep. Backend gradient code goes in extensions. - -## Names And Parameter Storage - - - Use `VarNamedTuple` as the canonical internal representation for named parameter collections in new code. - - Accept `NamedTuple` and `Dict{VarName}` at user-facing boundaries, but convert to `VarNamedTuple` rather than propagating them internally. + - Use `VarNamedTuple` as the canonical internal representation for named parameter collections in new code. Convert user-facing `NamedTuple` and `Dict{VarName}` inputs at boundaries. - Preserve templates, shapes, and index structure when round-tripping between named values and flat vectors. - - Avoid large mostly-empty shadow arrays for sparse indexed variables. - - Use `@varname(x)`, not `:x` or `VarName(:x)`. - - Use subsumption for containment checks: `subsumes(@varname(x), @varname(x[1]))` is true, but the names are not equal. - - Treat VarName display, sorting, prefixing, unprefixing, and serialization as downstream-facing interface behavior. - - Test nested fields, indices, ranges, `Colon`, and non-standard indices when changing VarName optics. + - Ensure `copy(acc)` does not share mutable internal state; aliased accumulator containers corrupt results when copied for `ThreadSafeVarInfo`. + - Use `@varname(x)`, not `:x` or `VarName(:x)`. Use subsumption for containment checks, e.g. `subsumes(@varname(x), @varname(x[1]))`. Conditioning on `@varname(x)` covers subindices; conditioning on `@varname(x[1])` only matches that index. ## `@model` Compiler @@ -74,12 +69,13 @@ Keep macro hygiene explicit. User variables, generated temporaries, and globals ## Threading - - Implement `promote_for_threadsafe_eval(acc, T)` for accumulators with concrete float fields — the default no-op leaves them unable to hold AD tracers like ForwardDiff `Dual`s. - - Avoid designs that index storage by `Threads.threadid()`. Julia scheduling and thread IDs are not a stable ownership model. +Implement `promote_for_threadsafe_eval(acc, T)` for accumulators with concrete float fields; the default no-op leaves them unable to hold AD tracers like ForwardDiff `Dual`s. General threading guidance lives in JULIA.md. ## Contributing Checklist - Non-breaking changes target `main`; breaking changes target `breaking`. + - Julia `1.10.8` is the minimum supported version in `Project.toml`. + - CI runs Ubuntu/Windows/macOS, Julia stable/min/1.11, and both one- and two-thread configurations. - Identify whether the change is user-facing, internal, or downstream-facing through Turing.jl. - Add the smallest tests that exercise the behavior. - Add nested-submodel tests for context, prefix, conditioning, or fixing changes. diff --git a/JULIA.md b/JULIA.md index 92b210fe6..4067ee32c 100644 --- a/JULIA.md +++ b/JULIA.md @@ -1,6 +1,6 @@ # JULIA.md -Day-to-day Julia practices. +Shared day-to-day Julia practices. DynamicPPL-specific review notes live in `AGENTS.md`; newcomer context lives in `docs/src/onboarding.md`. ## Engineering @@ -8,7 +8,7 @@ Day-to-day Julia practices. - Preserve caller types with `zero(x)`, `one(x)`, `oftype`, `promote`, `promote_type` — especially for `Float32`, `BigFloat`, AD numbers, units, and GPU scalars. - Struct fields should be concrete via type parameters, not `field::Number` or `field::AbstractVector`. - Julia doesn't specialize on `Type`, `Function`, or `Vararg` arguments. Use `f(x, ::Type{T}) where {T}` when the type itself must specialize. - - Check inference (`@inferred`, `@code_warntype`) when touching compiler output, VarNamedTuples, accumulators, transforms, or log-density paths. + - Check inference (`@inferred`, `@code_warntype`) when touching generated code, custom containers, accumulators, transforms, or log-density paths. - Benchmark generated functions, macro output, and hot-path refactors before assuming a simpler form is equivalent. - Prefer dispatch and small protocol functions over large conditional blocks. - Avoid broad Base overloads — they create method ambiguities and accidental API. diff --git a/docs/src/onboarding.md b/docs/src/onboarding.md index ad9b95cf3..01f90f530 100644 --- a/docs/src/onboarding.md +++ b/docs/src/onboarding.md @@ -2,6 +2,7 @@ This page summarizes recurring lessons from DynamicPPL and AbstractPPL history for contributors who are new to Julia, Turing.jl, or DynamicPPL internals. +It is a starting point, not a checklist. For day-to-day Julia style, see `JULIA.md`; for coding-agent instructions, see `AGENTS.md`. The source pass covered GitHub history available on 2026-05-06. For DynamicPPL, that included 422 issues, 957 pull requests, 6,958 issue/PR @@ -152,28 +153,11 @@ Evaluator APIs should separate structural preparation from AD-specific preparation. `!!` evaluator and gradient APIs may reuse internal buffers, so copy results before storing them long term. -## Julia Engineering Practices - - - Measure performance-sensitive changes. Small edits can affect inference, - allocations, invalidation, and downstream packages. - - Check type stability with `@inferred`, `@code_warntype`, and focused tests. - - Benchmark generated functions, macro output, and hot-path refactors. - - Keep field types and collection eltypes concrete in hot paths. - - Avoid unnecessary static parameters. Julia specializes on most ordinary - argument types, but is conservative for `Type`, `Function`, and `Vararg`. - - Use `f(x, ::Type{T}) where {T}` when the type itself must specialize. - - Prefer dispatch and small protocol functions over large conditional blocks. - - Put backend-specific behaviour in package extensions or narrow integration - layers when possible. - - Keep interface packages lightweight. Avoid heavy dependencies unless the - interface truly owns them. - - Do not rely on transitive dependencies. Direct API use should have an - explicit dependency that can be version-bound and tested. - - Use accessors for downstream needs instead of exposing internal fields. - - Prefer `Base.maybeview` over eager slicing when indexed access should avoid - allocations but still support tuples and scalar indexing. - - Avoid fragile output-type prediction. When possible, compute an initial value - and allocate caches from the observed value. +## Working in Julia + +DynamicPPL code often sits on hot paths for inference and AD. Small edits can change inference, allocations, invalidation, or downstream package behaviour, so performance-sensitive changes need measurement rather than intuition. + +The general rules live in `JULIA.md`. The ones most likely to matter here are generic numeric code, concrete storage types, deterministic doctests, extension-based backend integrations, and type-stability checks for compiler output, `VarNamedTuple`s, accumulators, transforms, and log-density paths. ## Copying, Accumulators, and Threading @@ -190,36 +174,11 @@ Avoid designs that depend on `Threads.threadid()` indexing. Promote accumulator storage when thread-safe evaluation must hold AD tracer types. Treat threaded assume support as subtle unless current docs and tests cover the exact case. -## Documentation, Tests, and CI - - - Keep test files self-contained. - - Use `DynamicPPL.TestUtils` models with known ground truth when possible. - - Add nested-submodel tests for contexts, prefixes, conditioning, or fixing. - - Add AD tests for log-density, transform, vector-parameter, or `run_ad` - changes. - - Add type-stability or allocation tests for hot paths. - - Add round-trip tests for flattening and unflattening changes. - - Run JuliaFormatter v1 with Blue style before submitting. - - Keep doctests deterministic. Use `StableRNGs` when examples print random - values. - - Use plain `julia` blocks for examples that are illustrative but should not be - checked. - - Put dependencies in the narrowest environment that owns them: runtime, - extension, test, or docs. - - Treat docs, Aqua, JET, formatting, and extension-loading failures as part of - the change. - -## API and Review Norms - - - Breaking changes need explicit justification and deprecation or versioning - plans. - - Internal names are not public API just because downstream packages use them, - but Turing.jl impact still matters. - - Prefer composable operators over special-case syntax. - - Document and test new user-facing API with examples. - - Generated or AI-assisted code still needs manual understanding, focused - tests, and a clear rationale. - - Include benchmark numbers for performance-sensitive changes. +## Getting a PR Ready + +For a first contribution, scope the change by deciding whether it is user-facing, internal, or downstream-facing through Turing.jl. Add the smallest tests that exercise the behaviour, then widen coverage only where the change touches shared machinery: nested submodels for contexts and prefixes, AD backends for log-density or transform paths, round trips for flattening and unflattening, and type-stability or allocation checks for hot paths. + +Run JuliaFormatter before submitting and treat docs, Aqua, JET, formatting, and extension-loading failures as part of the change. Put dependencies in the narrowest environment that owns them: runtime, extension, test, or docs. ## Further Reading @@ -259,16 +218,3 @@ assume support as subtle unless current docs and tests cover the exact case. [#925](https://github.com/TuringLang/DynamicPPL.jl/pull/925), [#1137](https://github.com/TuringLang/DynamicPPL.jl/pull/1137), [#1340](https://github.com/TuringLang/DynamicPPL.jl/pull/1340). - -## Before Opening a PR - - - Identify whether the change is user-facing, internal, or downstream-facing. - - Add the smallest tests that exercise the behaviour. - - Add nested-submodel tests for context, prefix, conditioning, or fixing - changes. - - Run relevant AD backend tests for log-density or transform changes. - - Check type stability and allocations for hot paths. - - Check dependency placement and compat bounds when touching Project files, - extensions, docs, or tests. - - Include performance numbers for performance-sensitive changes. - - Document and test new user-facing API.