Perform single substitution: `body[arg/Var(depth)]`. Replaces `Var(depth)` with `arg` (lifted by `depth`), shifts free variables above `depth` down by 1. Uses `lbr()` for fast-path skipping. The internal traversal is memoized by content hash so shared sub-expressions within `body` are walked once per depth. Memoization scratch is borrowed from `env.subst_scratch` to avoid allocating a fresh `FxH
( env: &mut InternTable<M>, body: &KExpr<M>, arg: &KExpr<M>, depth: u64, )
| 43 | /// allocating a fresh `FxHashMap` per call. We `mem::take` it out |
| 44 | /// (replacing with an empty placeholder) so the borrow checker lets us |
| 45 | /// thread `&mut env` and `&mut scratch` separately into `subst_cached`, |
| 46 | /// then put it back on the way out. `subst_cached` does not call back |
| 47 | /// into `subst`, so there is no risk of recursive scratch use. |
| 48 | pub fn subst<M: KernelMode>( |
| 49 | env: &mut InternTable<M>, |
| 50 | body: &KExpr<M>, |
| 51 | arg: &KExpr<M>, |
| 52 | depth: u64, |
| 53 | ) -> KExpr<M> { |
| 54 | if *IX_SUBST_COUNT_LOG && depth == 0 { |
| 55 | let n = SUBST_COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed); |
| 56 | if n.is_multiple_of(100_000) && n > 0 { |
| 57 | log::info!("[subst] count={n}"); |
| 58 | } |
| 59 | } |
| 60 | // Fast path: no loose bound vars at or below `depth` means nothing to |
| 61 | // substitute; returning the original Arc is cheap and cache-free. |
| 62 | if body.lbr() <= depth { |
| 63 | return body.clone(); |
| 64 | } |
| 65 | let mut cache = std::mem::take(&mut env.subst_scratch); |
| 66 | cache.clear(); |
| 67 | let result = subst_cached(env, body, arg, depth, &mut cache); |
| 68 | env.subst_scratch = cache; |
| 69 | result |
| 70 | } |