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
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
//! Segmented-stack trampoline shared by both simplification traversals.
//!
//! Both bottom-up traversals in this module — [`crate::simplify::engine`]'s
//! sequential `simplify_node` and [`crate::simplify::parallel`]'s
//! `simplify_node_par` — recurse once per level of the expression tree, with
//! no bound on how deep that goes. A stack overflow is not a catchable error
//! on any platform this crate targets: it aborts the process, so no caller can
//! turn it into a refusal, and no `Result` can carry it.
//!
//! [`with_stack_segment`] removes the bound entirely rather than replacing it
//! with a smaller one. Before the current *segment* (the run of levels
//! executing on one thread) spends its budget, the next level continues on a
//! freshly spawned thread with a stack this module sized. Depth is therefore
//! limited by how many threads the OS will give us, not by any one stack, and
//! nothing is truncated or refused along the way.
//!
//! # This is not the same guard as `MAX_EXPR_DEPTH`
//!
//! [`crate::kernel::MAX_EXPR_DEPTH`] refuses an over-deep expression at the
//! **PyO3 boundary**, which covers the expressions a Python caller *hands in*.
//! It does not cover the ones this crate *builds*: `diff`, expansion and the
//! integrator all deepen an expression after it crossed that boundary, and
//! nothing re-checks. Nor does it cover Rust callers of
//! [`crate::simplify::simplify`], who never pass a boundary at all. And its
//! own docs note that it is calibrated for a release build on an 8 MiB main
//! thread — a `cargo test` worker or a rayon worker has 2 MiB and can overflow
//! well below it. The boundary check is a courtesy to the caller; this is what
//! makes the traversal itself safe.
//!
//! # Two budgets, either of which refills
//!
//! * **Levels** — an exact count of recursion levels entered since this
//! segment began, converted from the byte budget by
//! [`WORST_CASE_LEVEL_BYTES`].
//! * **Bytes** — how much stack the recursion has actually consumed, measured
//! by [`probe_against_base`].
//!
//! Bytes alone used to be the whole mechanism, on the reasoning that depth is
//! a poor proxy for stack use because debug frames are several times larger
//! than release ones. That reasoning is right about *tuning* and wrong about
//! *safety*: the byte probe can under-read to zero (see
//! [`probe_against_base`] — under AddressSanitizer it does so on every call),
//! a governor that under-reads does not refill, and a governor that does not
//! refill aborts the process. The level count cannot under-read, so it is what actually bounds
//! the recursion; the byte budget is kept as the backstop for the case the
//! level count was mis-calibrated for, namely frames fatter than
//! [`WORST_CASE_LEVEL_BYTES`].
use Cell;
/// Stack size handed to each refill thread by [`with_stack_segment`].
const SEGMENT_STACK_BYTES: usize = 16 * 1024 * 1024;
/// Stack the traversal may consume on a thread it did not create. Rayon
/// workers get 2 MiB by default, so this leaves a wide margin for the frames
/// already below us and for whatever the rule engine needs.
const FOREIGN_STACK_BUDGET: usize = 512 * 1024;
/// Stack the traversal may consume on a segment thread it created itself.
/// Kept well under [`SEGMENT_STACK_BYTES`] so a single node visit can never
/// straddle the end of the segment.
const OWNED_STACK_BUDGET: usize = SEGMENT_STACK_BYTES - 4 * 1024 * 1024;
/// Upper bound on the stack one recursion level of a traversal occupies,
/// used to convert the two byte budgets above into level counts.
///
/// One level is `simplify_node` → `with_stack_segment` → `ExprPool::with`
/// → `simplify_children` → `simplify_args` and the `Vec` collection
/// machinery inlined into it — around nineteen frames in an unoptimised
/// build. Measured at **10 832 bytes** per level in the fattest configuration
/// this crate is built in (debug + AddressSanitizer, whose redzones inflate
/// every local); `16 * 1024` leaves ~50% headroom over that.
///
/// Over-estimating costs one extra thread spawn on a deep expression and
/// nothing at all on a shallow one, because the count only ever triggers the
/// same refill the byte budget already implements. Under-estimating would
/// cost a process abort, so the number is chosen high.
const WORST_CASE_LEVEL_BYTES: usize = 16 * 1024;
/// Recursion levels the traversal may enter on a thread it did not create.
///
/// The count-based twin of [`FOREIGN_STACK_BUDGET`]. It binds first in every
/// build (frames are smaller than [`WORST_CASE_LEVEL_BYTES`] everywhere the
/// crate is built), which is the intent: it is the condition that does not
/// depend on the byte probe being honest.
///
/// A tight foreign bound costs at most one extra thread spawn per top-level
/// call, not one per level — once the first segment exists, every deeper
/// refill uses the much larger [`OWNED_DEPTH_BUDGET`].
pub const FOREIGN_DEPTH_BUDGET: usize = FOREIGN_STACK_BUDGET / WORST_CASE_LEVEL_BYTES;
/// Recursion levels the traversal may enter on a segment thread it created
/// itself: the count-based twin of [`OWNED_STACK_BUDGET`].
pub const OWNED_DEPTH_BUDGET: usize = OWNED_STACK_BUDGET / WORST_CASE_LEVEL_BYTES;
// The budgets above are derived by division, so a future edit to any of the
// byte constants can silently produce a nonsensical one. Checked here rather
// than in a `#[test]` so a mis-calibration is a build failure, not a run.
const _: = ;
/// The current thread's segment: where it started, how much it may spend, and
/// how much it has spent.
///
/// One `Cell` of four `usize`s rather than four separate `thread_local!`s
/// because this is read and written on **every node visit** of the sequential
/// simplifier, which is the hottest traversal in the crate. Bundling them
/// makes an entry/exit pair two thread-local lookups instead of eight.
thread_local!
/// Holds one recursion level of the current [`Segment`] for as long as it lives.
///
/// A guard rather than a matched increment/decrement pair because the rule
/// engine and the pool can panic, and a leaked increment would make every
/// later traversal on that thread believe it was already at its depth budget
/// and refill immediately, forever.
;
/// Run `f`, moving it to a thread with a fresh [`SEGMENT_STACK_BYTES`] stack
/// once the current segment has spent either of its budgets.
///
/// The argument passed to `f` is `true` exactly when this call spawned the
/// segment `f` runs on, i.e. `f` is the *root* of that thread's segment and
/// will be the last thing to return on it. Callers that keep thread-local
/// side records — [`crate::simplify::rules`]'s declined-expansion log is the
/// one that matters — use it to drain their record before the thread dies,
/// since the caller's own thread will never see it. Callers with no such
/// record ignore it.
///
/// Segments are spawned scoped, so borrowed pools, rules and memos stay valid.
///
/// Inside a segment the ambient rayon pool is no longer installed, so nested
/// `par_iter` calls fall back to the global pool. That only affects subtrees
/// deep enough to exhaust a whole segment, where returning an answer at all
/// matters more than which pool schedules the work.
///
/// # Panics
///
/// Panics if the OS refuses to create the segment thread — depth is bounded by
/// the thread limit, and past it there is no stack left to continue on. This
/// is an unwind, which a caller can catch and the PyO3 layer turns into an
/// exception; the stack overflow it replaces was an abort, which nothing can.
pub
/// Given the segment's current stack baseline and a fresh probe address,
/// return the baseline to keep and the bytes consumed since the segment began.
///
/// [`with_stack_segment`] takes the address of a local as a stack-depth probe
/// and feeds it here. Stacks grow downwards on every platform this crate
/// targets, so a *smaller* address means deeper.
///
/// The baseline is re-established whenever the probe lands at or above it.
/// That matters because Rayon reuses its workers: the baseline used to be
/// latched on a thread's first probe and never revisited, so a worker that
/// happened to take its first `simplify_par` task from deep inside a call
/// chain kept that deep address as its baseline forever. Every later task on
/// that worker started *above* the stale baseline, the subtraction floored
/// the difference at 0, and the traversal read its own stack usage as zero no
/// matter how deep it went — so it never refilled, and ran off the end of the
/// worker's 2 MiB stack. A stack overflow aborts the process, which is
/// precisely what this machinery exists to prevent.
///
/// Re-baselining upwards is always safe: an address above the current
/// baseline means the frames that baseline was measured against have already
/// returned, so it describes a stack that no longer exists.
///
/// # The probe can under-read, and does under AddressSanitizer
///
/// The probe assumes the local whose address it takes lives on the real
/// stack. Under ASan's stack-use-after-return detection it does not: locals
/// whose address escapes are moved into a per-thread "fake stack" ring, whose
/// addresses *ascend* with recursion depth and then wrap. Ascending
/// addresses hit the re-baseline branch on every call, so this reported `0`
/// however deep the traversal went; after a wrap it returned a bounded value
/// that never approached [`OWNED_STACK_BUDGET`]. The traversal therefore
/// never refilled and ran off the end of a real stack that was filling up all
/// along — the nightly `asan` shard died in
/// `par_survives_deep_chain_on_worker_thread` this way, and raising
/// `RUST_MIN_STACK` did not help because the probe reports the same `0` at
/// any stack size.
///
/// Reading the real stack pointer instead would need inline assembly, which
/// this crate does not use, and no `cfg` distinguishes "locals are being
/// relocated" from "they are not". So the byte budget is advisory: it can
/// only make [`with_stack_segment`] refill *earlier* than the level count
/// does, never later. Nothing may depend on it alone.
///
/// This half is split out from the probe because it is pure arithmetic that
/// can be checked against synthetic addresses on any target, while the probe
/// half is not observable under instrumentation that relocates locals.