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
//! The depth ceiling as the simplification entry points actually need it.
//!
//! [`crate::kernel::MAX_EXPR_DEPTH`] is one number for every walker in the
//! crate, calibrated against the shallowest one that recurses without a net.
//! The simplification traversals are not that. `simplify::engine`'s
//! `simplify_node` and `simplify_node_indexed` 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, so applying a 2 048-level
//! ceiling to them costs capability and buys nothing.
//!
//! Neither of the two `parallel`-gated strategies changes that, and both had
//! to be checked rather than assumed: `simplify::parallel`'s
//! `simplify_node_par` goes through the same trampoline, and
//! `simplify::redex` buckets the DAG by height and rewrites a level at a time,
//! so it does not recurse at all. With the feature off, both entry points
//! fall back to `simplify::engine`. Every branch of every `cfg` on this path
//! is therefore stack-safe, which is what makes it safe to lift the ceiling
//! from an entry point whose implementation depends on a feature flag.
//!
//! One thing on that path is still a plain recursion, though, and this module
//! is what keeps it from being reached.
//!
//! # The one route that is still bounded by the stack
//!
//! Every default simplification pass ends the same way. `simplify_with`,
//! `simplify_par_with_config` and `simplify_redex_with_config` each collect
//! the facts implied by static symbol domains — a [`Domain::Positive`] or
//! [`Domain::NonZero`] symbol anywhere in the result — and, if there are any,
//! hand the expression to [`crate::simplify::colored_egraph`] for an
//! assumption-gated pass. That pass is *not* trampolined:
//! `ColoredEgraph::from_expr`'s node collection and `ColoredEgraph::rebuild`
//! both descend one native frame per level, and on an 8 MiB stack a release
//! build segfaults somewhere between 60 000 and 100 000 levels. It is also
//! quadratic in node count — a 5 000-level chain took 4.8 s, a 20 000-level
//! one 100 s — so "make it trampolined" would trade a crash for a hang.
//!
//! So the ceiling still has to apply, but only to the expressions that reach
//! that pass. [`check_simplify_depth`] is that test: past
//! [`MAX_EXPR_DEPTH`](crate::kernel::MAX_EXPR_DEPTH) it asks whether this
//! expression would take the colored route, and refuses only if it would.
//!
//! # Why the predicate is the real collector rather than a cheaper copy
//!
//! [`check_simplify_depth`] decides by running
//! `simplify::assumptions`'s own `collect_static_domain_facts` and looking at
//! whether it produced anything. A hand-written "does this contain a
//! positive symbol" scan would be faster and would drift: the moment a new
//! `Domain` or node kind starts contributing a fact, the guard and the thing
//! it guards would disagree, and the disagreement that matters — guard says
//! no, simplifier says yes — is a segfault. Sharing the collector makes that
//! impossible by construction.
//!
//! The cost is one extra iterative walk of an expression already past 2 048
//! levels deep. Expressions at or under the ceiling never reach it: the depth
//! comparison is checked first and is a single array read, so the hot path
//! pays exactly what it paid before.
//!
//! # What this does not cover
//!
//! * **The e-graph simplifiers.** `simplify_egraph` and `simplify_colored`
//! are separate engines with their own unbounded recursion, reached through
//! their own entry points. They keep [`check_expr_depth`] and are not this
//! function's business.
//! * **Rendering the derivation log.** The log a simplification returns holds
//! `ExprId`s, and `alkahest-py` renders them with the same recursive printer
//! `str()` uses, which overflows around 24 500 levels. That is handled
//! where it happens, in `alkahest-py`'s `make_derived_result`, by recording
//! an over-deep step's depth instead of its text.
//! * **Explicit assumption contexts.** A caller who passes an
//! [`AssumptionContext`](crate::simplify::AssumptionContext) has asked for
//! the colored pass outright, so its entry point keeps the unconditional
//! [`check_expr_depth`].
//!
//! [`Domain::Positive`]: crate::kernel::Domain::Positive
//! [`Domain::NonZero`]: crate::kernel::Domain::NonZero
use crateSideCondition;
use crate;
use crate;
/// Refuse `id` only if simplifying it would recurse without a net.
///
/// The bottom-up simplification traversals are stack-safe at any depth (see
/// the [module documentation](self)), so this accepts expressions far past
/// [`MAX_EXPR_DEPTH`](crate::kernel::MAX_EXPR_DEPTH) — a 100 000-level `sin`
/// chain simplifies in about 0.1 s. What it still refuses is a too-deep
/// expression that would be handed to the assumption-gated colored e-graph
/// pass, which is a plain recursion: that is an expression past the ceiling
/// containing a `Domain::Positive` or `Domain::NonZero` symbol.
///
/// The error, when there is one, is the same [`DepthLimitError`] with the same
/// `E-DEPTH-001` code and the same `limit` as [`check_expr_depth`]; a caller
/// cannot tell the two guards apart, and does not need to.
///
/// O(1) at or below the ceiling. Past it, one iterative walk of the
/// expression — no recursion, so this cannot overflow the stack in the course
/// of deciding whether something else would.
///
/// ```
/// use alkahest_cas::kernel::{Domain, ExprPool, MAX_EXPR_DEPTH};
/// use alkahest_cas::simplify::check_simplify_depth;
///
/// let pool = ExprPool::new();
///
/// // Deep, but nothing in it routes to the colored pass: accepted.
/// let x = pool.symbol("x", Domain::Real);
/// let mut deep = x;
/// for _ in 0..MAX_EXPR_DEPTH + 100 {
/// deep = pool.func("sin", vec![deep]);
/// }
/// assert!(check_simplify_depth(&pool, deep).is_ok());
///
/// // The same shape over a positive symbol takes the recursive route.
/// let p = pool.symbol("p", Domain::Positive);
/// let mut deep_positive = p;
/// for _ in 0..MAX_EXPR_DEPTH + 100 {
/// deep_positive = pool.func("sin", vec![deep_positive]);
/// }
/// let err = check_simplify_depth(&pool, deep_positive).unwrap_err();
/// assert_eq!(err.limit, MAX_EXPR_DEPTH);
/// ```
/// Whether a default simplification of `expr` would end in the colored
/// e-graph pass.
///
/// Deliberately the same collector the simplifier itself uses, so the two
/// cannot disagree about which expressions take that route.