Skip to content

model: unify conditioned, fixed, and argument bindings - #1501

Open
yebai wants to merge 303 commits into
mainfrom
bindings
Open

yebai wants to merge 303 commits into
mainfrom
bindings

Conversation

@yebai

@yebai yebai commented Sep 16, 2026 •

Copy link
Copy Markdown
Member

Conditioning had three sources of truth: model arguments, condition's context and fix's. They stacked unpredictably (#1012), ignored each other at arguments (#958), and used missing to mark latent variables (#1464). This PR stores one table of bindings on the Model. Observations and fixed bindings form two layers; a later binding replaces an earlier one at the same address, and fixed bindings shadow observations.

@model f(x) = x ~ Normal()
m = condition(fix(f(0.0); x=1.0); x=2.0)
m()         # 1.0: the fixed binding shadows the observation
unfix(m)()  # 2.0: removing it uncovers the observation

Binding an argument replaces its value before the body runs, so f(data) and condition(f(...); x=data) observe the same value, and decondition removes either. Explicit bindings of enclosing models win, outermost first; a parent's argument-supplied observations never reach a child. A submodel's return slot cannot be bound; bind its LHS variables, such as a.y (#1246). Only whole top-level missing or nothing arguments and missing elements of supported top-level argument arrays remain latent (removing them, #1464, remains open); an indexed or field tilde into a whole one, such as x[1] ~ Normal(), throws and asks for concrete storage. Other placeholders throw where a tilde reads them, and placeholders in condition or fix values throw when bound. An argument with latent parts is copied, so latent draws do not reach the caller's value. A positional template such as @of(z = of(Array, 3)) (from AbstractPPL) supplies storage for partial bindings of local variables; AbstractDict binding inputs throw.

The full rules are under "Binding rules" in docs/src/conditionfix.md, and HISTORY.md gives each break's replacement.

With 1 of 1,000 entries deconditioned (missing on main), a Mooncake gradient drops from 302 to 21 µs; a partial binding costs 22 against 17 µs (Julia 1.12).

Fixes #1012; fixes #958; fixes #1246.

@yebai
yebai added this pull request to stack #1505 September 16, 2026 21:16
@yebai
yebai requested a balanced review from Copilot September 16, 2026 21:18
@codecov

codecov Bot commented Sep 16, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.68293% with 66 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.63%. Comparing base (94e4e4a) to head (7a01c51).

Files with missing lines Patch % Lines
ext/DynamicPPLReverseDiffExt.jl 68.53% 28 Missing ⚠️
src/compiler.jl 86.78% 23 Missing ⚠️
src/model.jl 97.24% 6 Missing ⚠️
src/submodel.jl 96.42% 3 Missing ⚠️
src/subsample.jl 75.00% 2 Missing ⚠️
src/contexts/prefix.jl 50.00% 1 Missing ⚠️
src/utils.jl 83.33% 1 Missing ⚠️
src/varnamedtuple/getset.jl 96.87% 1 Missing ⚠️
src/varnamedtuple/macro.jl 87.50% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1501      +/-   ##
==========================================
+ Coverage   84.70%   89.63%   +4.92%     
==========================================
  Files          52       52              
  Lines        4590     7165    +2575     
==========================================
+ Hits         3888     6422    +2534     
- Misses        702      743      +41     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The no-prefix submodel path can silently ignore an argument binding, and the documented context access emits an internal-variable warning.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Unifies model arguments, conditioned values, and fixed values as role-tagged model bindings, replacing CondFixContext and missing-based latent selection.

Changes:

  • Implements binding precedence, partial overrides, deconditioning, and unfixing.
  • Updates compiler, prefix, submodel, and VarNamedTuple handling.
  • Migrates extensions, documentation, and regression tests.
File summaries
File Description
HISTORY.md Documents binding semantics.
docs/src/api.md Updates prediction and context APIs.
docs/src/conditionfix.md Documents explicit bindings.
docs/src/evaluation.md Explains evaluation data flow.
docs/src/onboarding.md Updates contributor guidance.
docs/src/tilde.md Revises tilde lowering documentation.
ext/DynamicPPLComponentArraysExt.jl Supports component-backed bindings.
ext/DynamicPPLInputProvenanceExt.jl Traces model-bound inputs.
ext/DynamicPPLMCMCChainsExt.jl Handles absent chain values.
src/DynamicPPL.jl Removes obsolete includes.
src/compiler.jl Generates role-aware model evaluation.
src/contexts.jl Updates context examples.
src/contexts/conditionfix.jl Removes CondFixContext.
src/contexts/init.jl Rejects missing parameters.
src/contexts/prefix.jl Adds prefix templates.
src/debug_utils.jl Adapts model diagnostics.
src/model.jl Implements unified model bindings.
src/submodel.jl Merges parent and child bindings.
src/subsample.jl Accepts argument observations.
src/varname.jl Removes obsolete argument helpers.
src/varnamedtuple.jl Adds nested templates.
src/varnamedtuple/getset.jl Handles nested template indexing.
src/varnamedtuple/partial_array.jl Adds indexed-value preparation.
src/varnamedtuple/vnt.jl Refines lookup and documentation.
test/compiler.jl Tests new lowering behavior.
test/conditionfix.jl Expands binding regression coverage.
test/context_implementations.jl Tests evaluation separation.
test/contexts.jl Removes obsolete context tests.
test/contexts/init.jl Tests missing-value rejection.
test/debug_utils.jl Updates diagnostic tests.
test/ext/DynamicPPLInputProvenanceExt.jl Tests bound-input provenance.
test/model.jl Migrates model behavior tests.
test/runtests.jl Removes deleted test suite.
test/submodels.jl Tests nested binding behavior.
test/subsample.jl Updates observation tests.
test/utils.jl Revises transformation tests.
test/varnamedtuple.jl Tests nested templates and equality.
Review details
  • Files reviewed: 37/37 changed files
  • Comments generated: 2
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/submodel.jl Outdated
Comment thread docs/src/evaluation.md
@github-actions

github-actions Bot commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Benchmarks

Performance Ratio: gradient time divided by log-density time.

Primal times are for main (94e4e4a) and this PR (7a01c51). For very small models the gradient ratios vary across runs and machines, so compare the raw timings instead. The benchmarks are meant for DynamicPPL developers and mainly catch obvious allocation and type-stability regressions; see the benchmark notes for details.

================================================================================================================================
                                                                     primal                            gradient                 
                                                             ----------------------  -------------------------------------------
Model                                         dim    linked        main          PR     FwdDiff    RvsDiff    Mooncake    Enzyme
--------------------------------------------------------------------------------------------------------------------------------
Simple assume observe*                          1     false     5.56 ns     5.56 ns        9.56    1356.77       10.57      6.13
Simple assume observe*                          1      true     5.56 ns     5.56 ns        9.53    1346.80        9.94      5.80
Smorgasbord                                   201     false     5.37 μs     5.33 μs       73.21     150.05        5.76      1.38
Smorgasbord                                   201      true     6.44 μs      6.4 μs       73.33     224.01        5.87      1.52
Smorgasbord fixed                             100     false           —     2.55 μs       49.74     185.39        6.81      1.27
Smorgasbord fixed                             100      true           —     3.01 μs       57.20     207.40        6.85      1.48
Smorgasbord partial condition                 201     false           —     5.73 μs       67.62     138.98        7.98       err
Smorgasbord partial condition                 201      true           —     6.79 μs       76.60     158.23        7.85       err
Smorgasbord partial decondition               202     false           —     7.79 μs       76.41     105.16        4.94       err
Smorgasbord partial decondition               202      true           —     8.88 μs       84.03     123.71        5.00       err
NamedTuple-field LHS variable*                  1     false           —     5.57 ns        9.58    1337.72       10.50      5.84
NamedTuple-field LHS variable*                  1      true           —     5.56 ns        9.04    1411.66       10.50      6.15
MvNormal argument-supplied observation*         1     false           —     81.2 ns        2.05     979.67        3.26      2.17
MvNormal argument-supplied observation*         1      true           —     79.3 ns        2.04     939.56        3.46      2.20
Loop univariate 1k                           1000     false     16.8 μs     16.9 μs      869.18     331.52        4.95      0.81
Loop univariate 1k                           1000      true     18.3 μs     18.1 μs     1356.39     313.14        4.74      0.78
Multivariate 1k                              1000     false    964.0 ns     1.04 μs     1324.77     988.48        1.75      3.34
Multivariate 1k                              1000      true    951.0 ns     1.08 μs     1323.00     947.92        1.88      3.25
Loop univariate 10k                         10000     false    163.0 μs    163.0 μs    25002.98     364.32        5.24      0.76
Loop univariate 10k                         10000      true    177.0 μs    176.0 μs    23888.17     337.94        4.87      0.69
Multivariate 10k                            10000     false      8.8 μs     8.91 μs    30261.79    1151.89        1.78      3.08
Multivariate 10k                            10000      true     8.77 μs     8.99 μs    29529.96    1115.91        1.85      2.92
Dynamic                                        15     false           —     1.31 μs         err      44.83       15.16      6.07
Dynamic                                        10      true     1.82 μs     1.81 μs        1.59      63.18       14.06      7.19
Submodel*                                       1     false     5.56 ns     5.56 ns        9.69    1615.18       10.51      6.16
Submodel*                                       1      true     5.57 ns     5.57 ns        9.62    1552.18       10.53      6.75
LDA                                             6      true     3.73 μs     3.61 μs        1.42      12.06       28.41     34.85
================================================================================================================================
Environment
Julia Version 1.13.1
Commit 96ca370cf0e (2026-09-25 19:34 UTC)
Build Info:
  Official https://julialang.org release
Platform Info:
  OS: Linux (x86_64-linux-gnu)
  CPU: 4 × AMD EPYC 7763 64-Core Processor
  WORD_SIZE: 64
  LLVM: libLLVM-20.1.8 (ORCJIT, znver3)
  GC: Built with stock GC
Threads: 1 default, 1 interactive, 1 GC (on 4 virtual cores)

@github-actions

Copy link
Copy Markdown
Contributor

DynamicPPL.jl documentation for PR #1501 is available at:
https://TuringLang.github.io/DynamicPPL.jl/previews/PR1501/

Base automatically changed from traces to main September 30, 2026 13:29
The left fold preserves variadic `typejoin` order and its empty result.
1000-element component construction, condition / fix medians:
Julia 1.13: 349 / 349 us -> 167 / 168 us.
Julia 1.10: 31.6 / 31.6 ms -> 174 / 177 us.
Unbound and ordinary argument evaluation stay inferred with zero
allocations; their evaluation paths do not use this construction kernel.

Assisted-by: Codex <codex@openai.com>
Prune sentinels without subset searches, specialize flat fields, and
find removed fixed components without removing every retained sibling.

After P3, component decondition / unfix medians at length 1000:
Julia 1.13: 4.34 / 18.56 ms -> 43.2 / 226 us.
Julia 1.10: 4.30 / 16.87 ms -> 48.4 / 230 us.
Whole-field decondition is 1.4 ns / 0 allocations. Argument-default
restoration keeps whole-field unfix at 202 ns (1.13), 168 ns (1.10),
down from 420 / 432 ns before this change.
Unbound and 1000-observation `init!!` retain inference and zero
allocations (1.13: 39.5 / 497 ns; 1.10: 46.3 / 3488 ns).

Assisted-by: Codex <codex@openai.com>
Use bulk extraction for concrete value types. Keep the existing path
for heterogeneous values and growable storage to preserve promotion,
container types, and masks exactly.

1000-element conditioned / fixed component query medians:
Julia 1.13: 18.1 / 12.0 us -> 0.71 / 0.67 us,
2012 / 1013 allocations -> 7 / 7 allocations.
Julia 1.10: 15.6 / 10.4 us -> 0.78 / 0.64 us,
2006 / 1007 allocations -> 4 / 4 allocations.
Whole-value queries remain allocation-free at about 1.5 ns.
Unbound and 1000-observation `init!!` retain inference and zero
allocations (1.13: 39.6 / 510 ns; 1.10: 46.0 / 3458 ns).

Assisted-by: Codex <codex@openai.com>
Assisted-by: Codex <codex@openai.com>
Assisted-by: Codex <codex@openai.com>
Assisted-by: Codex <codex@openai.com>
Assisted-by: Codex <codex@openai.com>
Keep keyword splats grouped in defaults until evaluator invocation, so
restoration can recover their source argument name and values.

Assisted-by: Codex <codex@openai.com>
Convert component replacements exactly before merging, including parent
bindings of child arguments. AD observation components need an argument
template that can hold their tracer type.

Assisted-by: Codex <codex@openai.com>
For a partly latent NamedTuple with a 1000-by-1000 covariate matrix
and 1000 responses, warmed logdensity calls on Julia 1.13.1 improve
from 8,008,688 bytes / 584.07 us to 8,424 bytes / 7.74 us.
The latent response buffer remains private to each evaluation.

Assisted-by: Codex <codex@openai.com>
Assisted-by: Codex <codex@openai.com>
Assisted-by: Codex <codex@openai.com>
Assisted-by: Codex <codex@openai.com>
Assisted-by: Codex <codex@openai.com>
Assisted-by: Codex <codex@openai.com>
Keep removal separate: `unfix` restores argument observations.

Assisted-by: Codex <codex@openai.com>
Assisted-by: Codex <codex@openai.com>
Let internal read and shape-check exceptions propagate unchanged.

Assisted-by: Codex <codex@openai.com>
Assisted-by: Codex <codex@openai.com>
`TransformedValue`, `FixedTransform`, `WithTransforms`, `LinkSome`, and
`UnlinkSome` defined `isequal` but not `hash`, so equal values (and
`VarInfo`s containing them) hashed differently.

Assisted-by: Claude Code <noreply@anthropic.com>
yebai added 26 commits October 7, 2026 14:52
Binding every element of `x ~ MvNormal(...)` without an argument or a
schema, as in `condition(m, @varname(x[1]) => 1.0, @varname(x[2]) =>
2.0)`, threw "both bound and unbound subvariables". Storage without a
template records only the bound indices, so that claim is kept for
bindings that leave a hole. Otherwise the error says that parts cannot
form the value without a template, and gives the remedy for binding all
of it and for leaving some elements unbound.

Assisted-by: Claude Code <noreply@anthropic.com>
Growable storage has no template that gives linear indexing a shape.
Treating `x[2]` as `x[2, 1]`, or the reverse, silently applies a binding
at a different address. Lookup and removal must keep these addresses
distinct, including below prefixes and nested slices.

A tilde with a different index count from its symbol's growable bindings
must fail, even when the model itself mixes index counts. Such partial
bindings need storage from a binding schema or argument. Empty bindings
remain harmless, and templated storage keeps Julia's indexing semantics.

Errors name the binding and tilde with their user-visible prefixes and
explain the index counts. Schema guidance names the storage address and
uses the binding call's local root and namespace path: merged submodel
bindings do not retain the scope of the call that supplied them.

Assisted-by: Claude Code <noreply@anthropic.com>
Assisted-by: Codex <codex@openai.com>
`_binding_storage` drops growable storage whole, including the whole
bindings stored in it, so indices below them could not be resolved.
Under an index prefix, after
`condition(prefix(m, @varname(q[2])), @varname(q[2].x) => ones(3))`,
binding or removing `q[2].x[end]` threw "no storage at `q[2].x`"
although the whole binding there owns the shape; likewise for a whole
binding at `q[2]` or `q`, both forms in one call, and `p.q[2]`. The
same held without a prefix, e.g. `x[1][end]` after `x[1] => ones(3)`.
The template for an address now descends growable storage, and its
slices, at integer indices or ranges to the binding stored there; the
growable level still resolves no `end` or `:`.

A slice now bounds the index below it with or without storage, so
`x[1:2][3] => v` throws instead of being dropped silently, as it was
on fresh storage. This replaces a pre-check that bounded such indices
only against existing growable storage.

Slices that reach beyond growable storage still contain the owners
stored within them. Keep those owners available while resolving the
slice so bindings and removals use the same bounds and resolve `end`
from the selected value. Valid removals through these slices must
remove the selected element and restore the original storage extent.
Resizing preserves growable storage so removal cannot introduce a shape
owner that prevents later bindings. Linear slices that require growing
multidimensional storage are rejected for removal as for binding.

Assisted-by: Claude Code <noreply@anthropic.com>
Assisted-by: Codex <codex@openai.com>
Value arguments do not use the parameter type hint. Computing it for
all arguments repeatedly scans the complete parameter collection in
prefixed submodels and triggers costly boxing on Julia 1.13.

Query the hint only for `Type` and `TypeWrap` conversion. Delegate all
arguments to the two-argument `convert_model_argument` method, using
`Any` for value arguments so custom conversions still run. Check that
values skip the query and custom type arguments still query the hint.

For 100 latent Cartesian-indexed children without type arguments,
evaluation drops from milliseconds to under a microsecond on Julia
1.13, with allocations falling from 20402 to 2. Julia 1.12 drops from
tens of microseconds to under a microsecond, retaining 2 allocations.
The log joint is unchanged.

Assisted-by: Codex <codex@openai.com>
Type arguments still need a parameter type hint in every child model.
The pair-based fold constructs variable names it never uses and boxes
its type accumulator on Julia 1.13, making indexed submodels costly.

Fold stored values directly, ignoring unfilled slots and combining
leaf types with `promote_type`. This applies to `InitFromParams` and
the `DefaultContext` hint. Scan each array-like block once: repeated
payload scans make abstract-element blocks quadratic. Allocate a
separate visited buffer only when encountering a block, preserving
the allocation-free fold for ordinary values. Masks can be immutable,
so deduplication must not depend on mutating a mask copy.

For 100 children with type arguments on Julia 1.13, evaluation drops
from about a millisecond to a few microseconds, and from 20002 to 2
allocations. The log joint is unchanged. Cover nested storage,
mutable and immutable masks, empty collections, array-like blocks,
and generic numeric types. The allocation, counting and immutable-mask
regressions fail before their fixes.

Assisted-by: Codex <codex@openai.com>
Boolean masks describe positions, so their storage extent is their
length and their slice length is the number of true entries. Recognise
these masks as multi-indices when no template supplies the shape.

Child slices use their own coordinates. Growing them with parent indices
confuses offsets with lengths and retains dimensions dropped by scalar
indices, preventing bindings that the equivalent scalar address permits.
Use the selected shape in both construction and update paths. Bound
mask children by the selected count too, so bindings and removals beyond
the slice throw instead of silently dropping the edit.

Cover masks and dimension-dropping slices with integer-index controls,
including binding and removal diagnostics for the original address.

Assisted-by: Codex <codex@openai.com>
Empty-index writes reach a BangBang dimension reduction with no
identity, crashing both model LHS assignment and partial value storage.
Use a nonempty Cartesian index at the shared array write boundary while
keeping `x[]` intact in variable addresses and binding tables.

Growable binding storage cannot infer which index arity the model uses.
Reject mismatched reads so `x[]` and `x[1]` never silently alias without
a template. Deferred indices must resolve before counting so Cartesian
indices and masks preserve their dimensionality. Cover sampling, binding
removal, submodels, named storage, and gradients through local and
deconditioned argument arrays.

Assisted-by: Codex <codex@openai.com>
Preserve dense numeric array storage and exact scalar types when a
sample is reused as a template. Reject storage that `OfNamedTuple`
cannot describe faithfully, including growable arrays with no
established shape.

Assisted-by: Codex <codex@openai.com>
Masked slots have no value contract and may hold unassigned references.
Make compact listings respect the mask, as the plain-text tree already
does, while preserving their data and indexing behavior.

Assisted-by: Codex <codex@openai.com>
Allow sampled layouts to supply binding storage without pruning unused
variables or model arguments. Resolve templates under the model prefix
and reject the old local spelling with the required absolute name.

Keep existing owners in the edited layer and check conflicts only
where the input uses template storage. Use binding template
terminology in user-facing documentation, errors, and test
descriptions.

Defer templates across submodel boundaries until arguments are known.
Child arguments must retain their own storage and value conversion.
Require concrete parent indices until a whole binding owns the shape.

Retain only template leaves overlapping the call's bindings so unused
entries cannot replace deferred child storage. Merge deferred entries
by role and name; repeated edits must not grow the binding table or
change its type. Keep conflicts deferred until child arguments are
known.

Reuse merged values when attaching templates. Splitting merged child
bindings into their layers still allocates; avoid adding another
storage representation to bypass it. On Julia 1.12, one intermediate
child costs 26,736/24,976 bytes for condition/fix versus 26,336/24,672
before. Twenty identical edits allocate 12,928 bytes, the same as one,
versus 58,960 before. Runtime return inference remains `Any`.

Assisted-by: Codex <codex@openai.com>
Retain template names as model metadata so ignored entries can be
checked without keeping unused storage alive. Check names against the
model and reached child namespaces, including prefixed and unprefixed
children. Untaken child branches may still use an entry, so warnings do
not fail model validation.

Match full LHS paths so unused fields under valid roots still warn.
Merge template-name tuples without transient vector/set allocation.
Runtime binding allocates 8,512 bytes versus 8,832 before this change
and 8,368 at the parent revision.

Assisted-by: Codex <codex@openai.com>
PartialArray views can contain undefined references behind false mask
entries. Copying their backing data eagerly makes valid nested removals
throw before reaching the stored binding.

Consult the mask when copying reference storage and widening partial
storage, and densify only stored values. Keep bulk copying for isbits
storage so numeric copies retain their fast path.

Assign reference values directly after mask-aware promotion so fallback
setters cannot copy unassigned slots when the element type is unchanged.

Assisted-by: Codex <codex@openai.com>
An in-bounds array slot remains a valid binding address when its value
is unassigned. Separate stored-value lookup from bounds validation so
adding such a binding and removing it agree.

Traverse slices through views so unrelated unassigned elements do not
prevent reaching assigned children. Reject child addresses whose storage
is unassigned with the same named error for bindings and removals.

Scalar selections retain `getindex` semantics because some array types
return zero-dimensional containers from scalar views.

Subsampling guards validate membership against their backing storage and
keep their wrappers when indexed. Generic array bounds checks and views
would inspect the hidden full-data shape or discard batch metadata.

Keep storage validation and array membership queries out of Mooncake
reverse differentiation because `isassigned` can use unsupported
exception handling. Payload lookup and copying remain differentiable.

Assisted-by: Codex <codex@openai.com>
Empty selections have no indices that require storage. Taking their
maximum raises a reduction error before binding bounds can report the
invalid child, while an empty unit range's endpoint can imply storage
that the selection never uses. Give both zero extent so bounds checks,
growth and template sizing agree without changing nonempty indices.

Cover binding and removal errors, empty whole-slice bindings, and
one- and two-dimensional storage beside the Boolean-mask controls.

Assisted-by: Codex <codex@openai.com>
Covariates cannot receive latent draws, so inspecting their writable
storage is unnecessary and can recurse forever through cyclic arrays.
Restrict that inspection to arguments that become latent on removal.

Assisted-by: Codex <codex@openai.com>
A latent tilde can replace argument storage when the draw does not fit
its element type, and always replaces a whole field. Another argument
reference then reads stale values: a whole-field tilde on `(a=v, b=v)`
can score -3.84 instead of -5.84 without an error on any backend.

Reject memory reachable twice within latent arguments, across arguments,
or through an argument-bound branch. Name the top-level arguments at
binding or removal calls, at placeholder construction, or when recursive
evaluation reaches the child. Recognize reshaped and wrapped array
memory as shared. The check also covers storage the body only reads.

Separate condition/fix values remain valid because latent tildes do not
write them. Exclude argument branches replaced by explicit bindings.
Only latent arguments need repeat tracking; other arguments need a
lookup against latent storage, keeping large covariates inexpensive.
Recursive checks use inherited replacements.

Walk reachable fields and owning array or memory elements so unfamiliar
containers cannot hide latent storage. Wrappers reach their backing
storage once, avoiding false sharing on partial removals. Distinct empty
arrays own no shared memory; repeated array identities still alias.
Compare numeric wrappers by identity, without requiring promotion.

Arguments with latent parts reject dictionary keys other than isbits
values, `Symbol` or `String` when copied. Arbitrary retained keys can
expose latent storage, so use supported keys or a separate covariate.
Latent branches also reject custom `Number` leaves whose fields reach
mutable storage, including immutable wrappers: retaining them can let a
draw mutate caller storage. Keep storage outside the number. Bound and
covariate numeric leaves remain accepted.

Isbits numbers, `BigFloat`, `BigInt`, and extension-declared opaque AD
values remain supported, including ForwardDiff duals with non-isbits
values. Base numeric wrappers are retained only when their fields
recursively satisfy this rule, so wrapping a storage-bearing custom
number cannot let a latent draw mutate caller storage.
Reject storage reachable from retained values when an ordinary latent
path also reaches it: copying that path would preserve caller storage
and let a draw mutate it. Use the opaque-value hook for retention and
latent traversal so extensions declare opaque values once. Check at the
named copy boundary to catch caller mutation after removal.

Keep the shared-storage walk specialized and reuse its graph for
argument diagnostics. This avoids reconstructing deep paths, recursive
inference in diagnostics, and per-element covariate allocations.

Document independent storage and reading a draw through its own address.
For ReverseDiff tracked arrays, use an allocating operation such as
`m .+ 0`, since `copy(m)` retains storage. Body-created aliases remain
outside this check.

Assisted-by: Claude Code <noreply@anthropic.com>
Assisted-by: Codex <codex@openai.com>
Give binding rules one authoritative statement with their check phases.
Remove duplicated and contradictory prose about argument storage.

Assisted-by: Codex <codex@openai.com>
Preserve placeholder diagnostics and guards during consolidation.

Assisted-by: Codex <codex@openai.com>
Preserve `Missing` precedence and the missing-only initialization guard
while avoiding separate placeholder walks.

Use `Base.any` to retain its specialization for heterogeneous tuples.
At n=1000, `Any` array checks run 5.0-7.9x faster and halve allocations
across Julia 1.10, 1.12 and 1.13. Julia 1.10 `Union` array checks fall
from 494 to 422 ns. Large heterogeneous numeric tuples allocate nothing
on Julia 1.12 and 1.13, with about 3 ms of compilation.

Assisted-by: Codex <codex@openai.com>
The old hint failed when followed because prefixed models require
absolute names in binding templates.

Assisted-by: Codex <codex@openai.com>
Give users an actionable migration for each binding restriction.
Keep release notes complete without repeating released changes.

Assisted-by: Codex <codex@openai.com>
Avoid rewriting an unchanged property while a latent assignment
unwinds a nested optic, and report replacement of a `const` field
through the argument binding error.

Assisted-by: Codex <codex@openai.com>
Assisted-by: Claude Code <noreply@anthropic.com>
ReverseDiff must adapt tracked arrays before latent writes. Rebuild
mutable owners when their concrete or const fields cannot accept the
adapted child, preserving private tape-connected arguments and their
caller values.

Assisted-by: Codex <codex@openai.com>
ForwardDiff Dual values may contain custom numeric payloads with mutable
storage. Inspect the primal and partials before retaining a Dual so
latent copies preserve caller arguments while ordinary Duals stay
allocation-free.

Assisted-by: Codex <codex@openai.com>
Assisted-by: Codex <codex@openai.com>
Symbolic indices in a child's names resolve only against storage in
the layer being edited; templates still apply when the child runs.
Give integer-index advice for them, document the rule and cover both
binding roles.

Assisted-by: Codex <codex@openai.com>
Assisted-by: Claude Code <noreply@anthropic.com>
yebai added 3 commits October 10, 2026 14:59
Indexed tilde writes into `g(missing)` raised
`MethodError: no method matching possible(::typeof(BangBang._setindex!),
::Missing, ...)`. Check the top-level argument once before using the
existing optic setter chain.

Assisted-by: Codex <codex@openai.com>
Assisted-by: Claude Code <noreply@anthropic.com>
Consolidate repeated models, input tables, and binding/removal checks.
Reduce test/conditionfix.jl from 6,882 to 6,712 lines.
Covered src/ and ext/ lines are unchanged: 3,477 before and after
(3,447 src/ and 30 ext/); no covered lines lost.

Assisted-by: Codex <codex@openai.com>
Assisted-by: Claude Code <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

VNTs and submodels Handle fixing and conditioning together (speculative) turn model.f parameters into ConditionContext

3 participants