1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
//! The expression-depth ceiling that keeps deep trees from killing the process.
//!
//! # Why this exists
//!
//! Almost every operation on an expression is a structural recursion over the
//! DAG: printing, differentiation, substitution, translation to Lean or
//! SMT-LIB, evaluation. Each level of the expression costs one or more native
//! stack frames, and a native stack overflow is **not** an exception — the
//! kernel delivers `SIGSEGV` and the process dies with no traceback, no error
//! code, and nothing for a caller's `except Exception` to catch. For an
//! unattended run that is strictly worse than a wrong answer: a wrong answer
//! can be logged.
//!
//! # What is bounded
//!
//! One number, [`MAX_EXPR_DEPTH`], against one quantity: the **cached node
//! depth** of an already-interned expression — the longest root-to-leaf path,
//! `1 + max(child depths)`, as returned by [`ExprPool::depth`]. Not stack
//! bytes, not node count, not width. A 50 000-term `Add` is depth 2 and is
//! accepted; a `Pow` tower of depth 2 049 is refused however few nodes it has.
//!
//! Nothing in this module measures the stack. The one mechanism in this crate
//! that does is a different one with a different failure mode — see *What this
//! does not cover*.
//!
//! The number came from bisection on the shipped release build with the usual
//! 8 MiB main-thread stack (`ulimit -s 8192`), on a chain of `sin`
//! applications:
//!
//! | operation | deepest that returned | first that segfaulted |
//! |---|---|---|
//! | `symbolic_grad` (reverse-mode DFS) | 4 625 | 4 687 |
//! | `simplify`, `to_lean` | 9 216 | 9 472 |
//! | `latex` | 13 312 | 13 824 |
//! | `unicode_str` | 15 360 | 15 872 |
//! | `str` / `repr` | 23 552 | 24 576 |
//!
//! [`MAX_EXPR_DEPTH`] sits below the worst of those with room to spare, so one
//! number covers all of them instead of each walker carrying its own. The
//! `simplify` row is history rather than a current measurement: that traversal
//! no longer overflows at any depth (below). `symbolic_grad` is the shallowest
//! walker that still depends on this ceiling, and is what the number is
//! calibrated against.
//!
//! Since the simplification traversals stopped needing it, the simplification
//! entry points no longer apply this ceiling unconditionally — they call
//! [`check_simplify_depth`](crate::simplify::check_simplify_depth), which
//! applies it only to the inputs that still reach a recursion. See *What this
//! does not cover*.
//!
//! That calibration assumes the **shipped release build on an 8 MiB stack**,
//! which is what a Python caller gets on the main thread. A debug build has
//! frames several times larger — one level of the simplifier was measured at
//! 10 832 bytes under debug + AddressSanitizer — and a `cargo test` worker or a
//! Rayon worker has a 2 MiB stack, so those configurations can overflow *below*
//! this limit. A test that means to reach the cap should run on a thread it
//! sized itself.
//!
//! # How it is enforced, and by whom
//!
//! [`ExprPool`] caches each node's depth at intern time, so
//! [`check_expr_depth`] is a single array read and an integer compare. That
//! matters: the guard sits on hot paths such as `__str__`, and anything that
//! had to walk the tree to find its depth would cost more than it saves.
//!
//! Every call site is an entry point rather than a traversal: no walker checks
//! its own input. `alkahest-py` calls [`check_expr_depth`] — through its
//! `guard_depth` / `guard_expr_depth` wrappers — or [`check_expr_depths`] at
//! upwards of fifty of them: every renderer, `diff`, `symbolic_grad`, `subs`,
//! the evaluators and the JIT entry, the polynomial converters, the
//! integrator, the SMT-LIB and Lean emitters, the plotters. The
//! simplification entry points instead call `guard_simplify_depth`, which
//! wraps this crate's own
//! [`check_simplify_depth`](crate::simplify::check_simplify_depth) — the only
//! caller of [`check_expr_depth`] inside `alkahest-cas`, and itself reached
//! only from `alkahest-py`. Both lists are pinned side by side in
//! `tests/test_expression_depth_limit.py`.
//!
//! # What this does not cover
//!
//! * **Rust callers.** A downstream user of `alkahest_cas` never crosses the
//! PyO3 boundary, so nothing checks their input on their behalf. Calling
//! [`check_expr_depth`] at their own entry points is up to them.
//! * **Expressions this crate builds.** The check happens on the way in.
//! `diff`, expansion, `series` and the integrator all deepen an expression
//! *after* it was checked, and nothing re-checks the result.
//! * **New bindings.** A `#[pyfunction]` added without a `guard_depth` call
//! inherits nothing; the guard is a convention held up by that test file, not
//! by a type.
//! * **Rendering a derivation log.** A [`DerivationLog`](crate::DerivationLog)
//! holds `ExprId`s, and `alkahest-py` renders them into the `derivation` and
//! `steps` a `DerivedResult` carries using the same recursive printer
//! `str()` uses — which overflows around 24 500 levels. A simplification
//! that is itself stack-safe at any depth can therefore still hand back a
//! log too deep to print, so `alkahest-py`'s `make_derived_result` checks
//! each step against this ceiling and records an over-deep expression's
//! depth in place of its text.
//! * **The bottom-up simplification traversals**, which have something
//! stronger. `simplify::engine`'s `simplify_node` and
//! `simplify_node_indexed`, and — under the `parallel` feature —
//! `simplify::parallel`'s `simplify_node_par`, all run under the
//! segmented-stack trampoline in `simplify::stack`, which continues the
//! recursion on a freshly spawned, larger-stacked thread before the current
//! one is spent. For those the depth bound is removed rather than lowered:
//! they are limited by how many threads the OS will hand out, not by any one
//! stack, and they truncate nothing. The trampoline itself is not
//! feature-gated and is not confined to the parallel simplifier;
//! `simplify::redex` reaches the same place differently, by scheduling the
//! DAG level by level and never recursing, while the e-graph passes do
//! neither.
//!
//! Because that is real capability, the simplification entry points no
//! longer spend it: they call
//! [`check_simplify_depth`](crate::simplify::check_simplify_depth), which
//! applies this ceiling only to an input that would reach the one recursion
//! still on that path — the assumption-gated colored e-graph pass every
//! default simplification ends with when the expression contains a
//! `Domain::Positive` or `Domain::NonZero` symbol. A 100 000-level `sin`
//! chain now simplifies (measured: 0.10 s, 162 MiB) where it used to be
//! refused at 2 049. The e-graph entry points (`simplify_egraph`,
//! `simplify_log_exp`, `AssumptionContext.simplify`) are separate engines
//! that do recurse, and keep [`check_expr_depth`].
//!
//! The trampoline is also where this crate's one stack *measurement* lives, and
//! it is deliberately not what bounds that recursion. The probe takes the
//! address of a local; under AddressSanitizer's stack-use-after-return
//! detection such locals are relocated into a per-thread fake-stack ring whose
//! addresses *ascend* with depth, so the probe reads 0 however deep the
//! traversal goes. A governor that under-reads never refills, which is how the
//! nightly `asan` shard died; raising `RUST_MIN_STACK` could not have fixed it,
//! because a probe that reports 0 reports 0 at any stack size. That trampoline
//! is therefore bounded by an exact count of recursion levels, with the byte
//! probe kept only as an advisory backstop. None of it applies to
//! [`check_expr_depth`], which reads a cached integer and measures no stack at
//! all — under a sanitizer it behaves exactly as it does anywhere else.
//!
//! # Configuration
//!
//! There is none. [`MAX_EXPR_DEPTH`] is a compile-time constant with no
//! environment variable, per-pool override or Python setting behind it. The
//! recursive-descent parser's own ceiling is *defined* as equal to it — it has
//! to count separately, because the overflow it prevents happens before any
//! node is interned and so before there is a cached depth to read.
//!
//! # What a caller should do about a refusal
//!
//! [`DepthLimitError`] is a normal, catchable, coded error (`E-DEPTH-001`,
//! cause `Resource`). `alkahest-py` maps it to `alkahest.DepthLimitError`,
//! which derives from `AlkahestError` and so from `ValueError`. It is a
//! refusal, not a panic and not a silent decline: the call raises rather than
//! returning a partial answer, and the batch entry points (`simplify_many`,
//! `diff_many`) record it on the failing item instead of raising. Rebuild the
//! expression with less nesting, or split the work into subexpressions.
//!
//! `Add` and `Mul` no longer contribute to this problem at all. Both splice
//! nested same-operator children at construction (see [`ExprPool::add`] /
//! [`ExprPool::mul`]), so a balanced `Add` of 100 000 terms and the same terms
//! accumulated one at a time with `+` are now *the same* depth-2 node — the
//! left-associated chain that a parser or a `fold` produces can no longer trip
//! this ceiling. What still nests, and so still has to be refused, is
//! everything else: `Pow` towers, `Func` chains such as `sin(sin(sin(…)))`, and
//! `Piecewise`/`RootSum` nesting.
use crateAlkahestError;
use crate;
use fmt;
/// Deepest expression a PyO3 entry point that recurses will accept.
///
/// Compared against the cached node depth ([`ExprPool::depth`]); see the module
/// documentation for the measurements behind the number and for the callers
/// that actually apply it. The shallowest walker to fall over did so at depth
/// 4 687 on an 8 MiB stack, so this leaves a factor of ~2.3 for stacks that
/// already have frames on them, for debug builds (whose frames are several
/// times larger than release ones), and for future walkers that use more stack
/// per level than today's.
///
/// It is deliberately *one* number rather than a per-walker table: a caller
/// that gets `str(expr)` to work should not then be surprised by a segfault
/// from `symbolic_grad(expr)`. What it is not is one number for every
/// *operation*. An operation that does not recurse does not need a recursion
/// limit, and charging it one is pure lost capability, so the simplification
/// entry points ask
/// [`check_simplify_depth`](crate::simplify::check_simplify_depth) instead —
/// which applies this same constant, but only to the inputs that would reach
/// the one recursive pass left on that path. So the contract is per
/// operation, while the number is still one number: an expression either
/// reaches a recursion, and 2 048 is the ceiling, or it does not, and there is
/// none.
///
/// It is not automatic either way — a walker added later is covered only once
/// its entry point calls [`check_expr_depth`].
///
/// Compile-time only. Nothing reads it from the environment or lets a caller
/// raise it, so a caller that needs deeper trees has to reshape them.
pub const MAX_EXPR_DEPTH: u32 = 2048;
/// An expression was too deeply nested to be processed by recursion.
///
/// Returned rather than risking a stack overflow; see the [module
/// documentation](self).
/// Refuse `id` if recursing over it would risk a stack overflow.
///
/// O(1) — compares the node depth cached when `id` was interned against
/// [`MAX_EXPR_DEPTH`]. It does not walk the expression and does not measure
/// the stack.
///
/// Call this at the entry point of anything that walks an expression
/// recursively: nothing calls it for you. Every caller is in `alkahest-py`
/// except [`check_simplify_depth`](crate::simplify::check_simplify_depth),
/// which wraps it for the entry points that recurse only on some inputs. See
/// the [module documentation](self) for why, and for what the check does not
/// reach.
///
/// ```
/// use alkahest_cas::kernel::depth::{check_expr_depth, MAX_EXPR_DEPTH};
/// use alkahest_cas::kernel::{Domain, ExprPool};
///
/// let pool = ExprPool::new();
/// let x = pool.symbol("x", Domain::Real);
/// assert!(check_expr_depth(&pool, x).is_ok());
///
/// let mut deep = x;
/// for _ in 0..MAX_EXPR_DEPTH {
/// deep = pool.func("sin", vec![deep]);
/// }
/// let err = check_expr_depth(&pool, deep).unwrap_err();
/// assert_eq!(err.limit, MAX_EXPR_DEPTH);
/// ```
/// Like [`check_expr_depth`] but for a batch of expressions.
///
/// Reports the first offender, so a caller handed a hundred expressions does
/// not have to find the bad one itself.