Skip to main content

logicaffeine_language/
ast_depth.rs

1//! The AST depth gate — "parsed ⇒ bounded".
2//!
3//! Every consumer downstream of the parser (optimizer, codegen, tree-walker,
4//! VM compiler, transpiler) walks the AST **recursively**; a tree nested
5//! thousands of levels deep (a 10,000-term `1+1+…` chain, a parenthesis
6//! tower, a generated block pyramid) overflows their stacks and aborts the
7//! process — on every surface at once (CLI, REPL, LSP, web Studio).
8//!
9//! The gate enforces one contract at the single choke point
10//! ([`Parser::parse_program`](crate::Parser)): any program that parses has
11//! nesting depth ≤ the enforced limit ([`max_ast_depth`]), so downstream
12//! recursion is bounded by construction. Programs past the limit get a graceful
13//! [`AstTooDeep`](crate::error::ParseErrorKind::AstTooDeep) diagnostic with
14//! a Socratic fix (split into `Let` bindings).
15//!
16//! The walker itself is **iterative** (an explicit work stack) — measuring
17//! the depth must never be the thing that overflows. Its matches are
18//! **exhaustive with no wildcard**: adding an AST variant with children
19//! fails compilation here, forcing the author to classify the new
20//! children — the ratchet that keeps the gate honest as the language grows.
21//!
22//! Boxed noun-phrase payloads (`Categorical`, `Relation`, `NeoEvent`,
23//! comparatives) carry [`Term`](crate::ast::logic::Term)s whose nesting is
24//! bounded by English sentence structure, not by program size — they count
25//! as one level.
26
27use crate::ast::logic::LogicExpr;
28use crate::ast::stmt::{ClosureBody, Expr, ReadSource, SelectBranch, Stmt, StringPart, TypeExpr};
29use crate::error::{ParseError, ParseErrorKind};
30use crate::token::Span;
31
32/// The DEFAULT maximum AST nesting depth.
33///
34/// The budget, measured: the parser's own descent costs ~7 KiB of stack per
35/// nesting level in debug builds (the fattest case — release and the
36/// downstream walkers are far leaner), and the tightest standard
37/// environments give ~2 MiB (worker threads) or ~1 MiB (browser wasm).
38/// 128 levels keeps the worst case inside the smallest stack with margin,
39/// while being ~3× deeper than any hand-written program observed in the
40/// corpus. Machines with deep stacks raise it via `LOGOS_MAX_AST_DEPTH`
41/// (see [`max_ast_depth`]) — the limit protects the environment, so the
42/// environment gets to size it. Enforced at parse time (recursion guard)
43/// AND on the built tree, covering parenthesis towers, block pyramids, and
44/// iteratively-built operator chains alike.
45pub const DEFAULT_MAX_AST_DEPTH: usize = 128;
46
47/// The smallest limit the override accepts — below this, ordinary programs
48/// stop parsing.
49pub const MIN_AST_DEPTH: usize = 16;
50
51/// The EFFECTIVE depth limit: [`DEFAULT_MAX_AST_DEPTH`] unless the
52/// `LOGOS_MAX_AST_DEPTH` environment variable overrides it.
53///
54/// The default is sized for the tightest standard environment; machines
55/// with deep stacks (a big `ulimit -s` / `RUST_MIN_STACK`) can raise it,
56/// and constrained embedders can lower it — no rebuild required. Read once
57/// per process.
58pub fn max_ast_depth() -> usize {
59    static LIMIT: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
60    *LIMIT.get_or_init(|| {
61        limit_from(std::env::var("LOGOS_MAX_AST_DEPTH").ok().as_deref())
62    })
63}
64
65/// Pure resolution of the override value (unit-testable without touching
66/// process environment): a parseable value is clamped to at least
67/// [`MIN_AST_DEPTH`]; anything else falls back to the default.
68pub fn limit_from(env_value: Option<&str>) -> usize {
69    match env_value.and_then(|v| v.trim().parse::<usize>().ok()) {
70        Some(n) => n.max(MIN_AST_DEPTH),
71        None => DEFAULT_MAX_AST_DEPTH,
72    }
73}
74
75/// The parse-time recursion budget: a quarter of the AST limit (min the
76/// floor).
77///
78/// The parser's own descent is BY FAR the fattest stack consumer — each
79/// parenthesis level re-enters the whole precedence/condition chain,
80/// measured at ~32 KiB per level in debug builds — so it gets a much
81/// tighter budget than the built-tree limit (32 levels of parens at the
82/// default, deeper than any sane expression). Scales with
83/// `LOGOS_MAX_AST_DEPTH` like everything else, so deep-stack machines can
84/// raise both together.
85pub fn max_parse_recursion() -> usize {
86    (max_ast_depth() / 4).max(MIN_AST_DEPTH)
87}
88
89/// A work-stack node: any AST reference that can contain further nesting.
90enum Node<'w, 'a> {
91    S(&'w Stmt<'a>),
92    E(&'w Expr<'a>),
93    T(&'w TypeExpr<'a>),
94    L(&'w LogicExpr<'a>),
95}
96
97/// Measure the maximum nesting depth of a parsed program, iteratively.
98pub fn program_depth(stmts: &[Stmt<'_>]) -> usize {
99    let mut work: Vec<(Node<'_, '_>, usize)> = stmts.iter().map(|s| (Node::S(s), 1)).collect();
100    let mut max_depth = 0usize;
101
102    while let Some((node, depth)) = work.pop() {
103        max_depth = max_depth.max(depth);
104        let d = depth + 1;
105        match node {
106            Node::S(stmt) => push_stmt_children(stmt, d, &mut work),
107            Node::E(expr) => push_expr_children(expr, d, &mut work),
108            Node::T(ty) => push_type_children(ty, d, &mut work),
109            Node::L(logic) => push_logic_children(logic, d, &mut work),
110        }
111    }
112    max_depth
113}
114
115/// Validate a parsed program against the effective limit
116/// ([`max_ast_depth`]).
117pub fn validate_program_depth(stmts: &[Stmt<'_>], span: Span) -> Result<(), ParseError> {
118    let limit = max_ast_depth();
119    let depth = program_depth(stmts);
120    if depth > limit {
121        return Err(ParseError {
122            kind: ParseErrorKind::AstTooDeep { depth, max_depth: limit },
123            span,
124        });
125    }
126    Ok(())
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn override_resolution() {
135        assert_eq!(limit_from(None), DEFAULT_MAX_AST_DEPTH);
136        assert_eq!(limit_from(Some("1000")), 1000);
137        assert_eq!(limit_from(Some(" 2048 ")), 2048);
138        assert_eq!(limit_from(Some("3")), MIN_AST_DEPTH, "floor applies");
139        assert_eq!(limit_from(Some("potato")), DEFAULT_MAX_AST_DEPTH);
140        assert_eq!(limit_from(Some("")), DEFAULT_MAX_AST_DEPTH);
141    }
142}
143
144fn push_stmt_children<'w, 'a>(stmt: &'w Stmt<'a>, d: usize, work: &mut Vec<(Node<'w, 'a>, usize)>) {
145    macro_rules! e {
146        ($x:expr) => {
147            work.push((Node::E($x), d))
148        };
149    }
150    macro_rules! block {
151        ($b:expr) => {
152            for s in $b.iter() {
153                work.push((Node::S(s), d));
154            }
155        };
156    }
157    match stmt {
158        Stmt::Let { ty, value, var: _, mutable: _ } => {
159            if let Some(t) = ty {
160                work.push((Node::T(t), d));
161            }
162            e!(value);
163        }
164        Stmt::Set { value, target: _ } => e!(value),
165        Stmt::Call { args, function: _ } => for a in args { e!(a); },
166        Stmt::If { cond, then_block, else_block } => {
167            e!(cond);
168            block!(then_block);
169            if let Some(b) = else_block {
170                block!(b);
171            }
172        }
173        Stmt::While { cond, body, decreasing } => {
174            e!(cond);
175            if let Some(x) = decreasing {
176                e!(x);
177            }
178            block!(body);
179        }
180        Stmt::Repeat { iterable, body, pattern: _ } => {
181            e!(iterable);
182            block!(body);
183        }
184        Stmt::Return { value } => {
185            if let Some(x) = value {
186                e!(x);
187            }
188        }
189        Stmt::Break => {}
190        Stmt::Assert { proposition } => work.push((Node::L(proposition), d)),
191        Stmt::Trust { proposition, justification: _ } => work.push((Node::L(proposition), d)),
192        Stmt::RuntimeAssert { condition, hard: _ } => e!(condition),
193        Stmt::Give { object, recipient } => {
194            e!(object);
195            e!(recipient);
196        }
197        Stmt::Show { object, recipient } => {
198            e!(object);
199            e!(recipient);
200        }
201        Stmt::SetField { object, value, field: _ } => {
202            e!(object);
203            e!(value);
204        }
205        Stmt::StructDef { .. } => {}
206        Stmt::FunctionDef { params, body, return_type, .. } => {
207            for (_, t) in params {
208                work.push((Node::T(t), d));
209            }
210            if let Some(t) = return_type {
211                work.push((Node::T(t), d));
212            }
213            block!(body);
214        }
215        Stmt::Inspect { target, arms, has_otherwise: _ } => {
216            e!(target);
217            for arm in arms {
218                block!(arm.body);
219            }
220        }
221        Stmt::Push { value, collection } => {
222            e!(value);
223            e!(collection);
224        }
225        Stmt::Pop { collection, into: _ } => e!(collection),
226        Stmt::Add { value, collection } => {
227            e!(value);
228            e!(collection);
229        }
230        Stmt::Remove { value, collection } => {
231            e!(value);
232            e!(collection);
233        }
234        Stmt::SetIndex { collection, index, value } => {
235            e!(collection);
236            e!(index);
237            e!(value);
238        }
239        Stmt::Splice { body } => block!(body),
240        Stmt::Zone { body, name: _, capacity: _, source_file: _ } => block!(body),
241        Stmt::Concurrent { tasks } => block!(tasks),
242        Stmt::Parallel { tasks } => block!(tasks),
243        Stmt::ReadFrom { source, var: _ } => match source {
244            ReadSource::Console => {}
245            ReadSource::File(path) => e!(path),
246        },
247        Stmt::WriteFile { content, path } => {
248            e!(content);
249            e!(path);
250        }
251        Stmt::Spawn { .. } => {}
252        Stmt::SendMessage { message, destination, .. } => {
253            e!(message);
254            e!(destination);
255        }
256        Stmt::AwaitMessage { source, .. } => e!(source),
257        Stmt::StreamMessage { values, destination } => {
258            e!(values);
259            e!(destination);
260        }
261        Stmt::MergeCrdt { source, target } => {
262            e!(source);
263            e!(target);
264        }
265        Stmt::IncreaseCrdt { object, amount, field: _ } => {
266            e!(object);
267            e!(amount);
268        }
269        Stmt::DecreaseCrdt { object, amount, field: _ } => {
270            e!(object);
271            e!(amount);
272        }
273        Stmt::AppendToSequence { sequence, value } => {
274            e!(sequence);
275            e!(value);
276        }
277        Stmt::ResolveConflict { object, value, field: _ } => {
278            e!(object);
279            e!(value);
280        }
281        Stmt::Check { .. } => {}
282        Stmt::Listen { address, secure } => {
283            e!(address);
284            if let Some(pad) = secure {
285                e!(pad.pad);
286            }
287        }
288        Stmt::ConnectTo { address, secure } => {
289            e!(address);
290            if let Some(pad) = secure {
291                e!(pad.pad);
292            }
293        }
294        Stmt::LetPeerAgent { address, var: _ } => e!(address),
295        Stmt::Sleep { milliseconds } => e!(milliseconds),
296        Stmt::Sync { topic, var: _ } => e!(topic),
297        Stmt::Mount { path, var: _ } => e!(path),
298        Stmt::LaunchTask { args, function: _ } => for a in args { e!(a); },
299        Stmt::LaunchTaskWithHandle { args, handle: _, function: _ } => {
300            for a in args { e!(a); }
301        }
302        Stmt::CreatePipe { .. } => {}
303        Stmt::SendPipe { value, pipe } => {
304            e!(value);
305            e!(pipe);
306        }
307        Stmt::ReceivePipe { pipe, var: _ } => e!(pipe),
308        Stmt::TrySendPipe { value, pipe, result: _ } => {
309            e!(value);
310            e!(pipe);
311        }
312        Stmt::TryReceivePipe { pipe, var: _ } => e!(pipe),
313        Stmt::StopTask { handle } => e!(handle),
314        Stmt::Select { branches } => {
315            for branch in branches {
316                match branch {
317                    SelectBranch::Receive { pipe, body, var: _ } => {
318                        e!(pipe);
319                        block!(body);
320                    }
321                    SelectBranch::Timeout { milliseconds, body } => {
322                        e!(milliseconds);
323                        block!(body);
324                    }
325                }
326            }
327        }
328        // Formal blocks carry owned proof ASTs consumed by the proof layer,
329        // whose own conversion is bounded separately; they count one level.
330        Stmt::Theorem(_) | Stmt::Definition(_) | Stmt::Axiom(_) | Stmt::Theory(_) => {}
331        Stmt::Escape { .. } => {}
332        Stmt::Require { .. } => {}
333    }
334}
335
336fn push_expr_children<'w, 'a>(expr: &'w Expr<'a>, d: usize, work: &mut Vec<(Node<'w, 'a>, usize)>) {
337    macro_rules! e {
338        ($x:expr) => {
339            work.push((Node::E($x), d))
340        };
341    }
342    match expr {
343        Expr::Literal(_) | Expr::Identifier(_) | Expr::OptionNone | Expr::Escape { .. } => {}
344        Expr::BinaryOp { left, right, op: _ } => {
345            e!(left);
346            e!(right);
347        }
348        Expr::Not { operand } => e!(operand),
349        Expr::Call { args, function: _ } => for a in args { e!(a); },
350        Expr::Index { collection, index } => {
351            e!(collection);
352            e!(index);
353        }
354        Expr::Slice { collection, start, end } => {
355            e!(collection);
356            e!(start);
357            e!(end);
358        }
359        Expr::Copy { expr } => e!(expr),
360        Expr::Give { value } => e!(value),
361        Expr::Length { collection } => e!(collection),
362        Expr::Contains { collection, value } => {
363            e!(collection);
364            e!(value);
365        }
366        Expr::Union { left, right } => {
367            e!(left);
368            e!(right);
369        }
370        Expr::Intersection { left, right } => {
371            e!(left);
372            e!(right);
373        }
374        Expr::ManifestOf { zone } => e!(zone),
375        Expr::ChunkAt { index, zone } => {
376            e!(index);
377            e!(zone);
378        }
379        Expr::List(items) => for x in items { e!(x); },
380        Expr::Tuple(items) => for x in items { e!(x); },
381        Expr::Range { start, end } => {
382            e!(start);
383            e!(end);
384        }
385        Expr::FieldAccess { object, field: _ } => e!(object),
386        Expr::New { type_args, init_fields, type_name: _ } => {
387            for t in type_args {
388                work.push((Node::T(t), d));
389            }
390            for (_, x) in init_fields {
391                e!(x);
392            }
393        }
394        Expr::NewVariant { fields, enum_name: _, variant: _ } => {
395            for (_, x) in fields {
396                e!(x);
397            }
398        }
399        Expr::OptionSome { value } => e!(value),
400        Expr::WithCapacity { value, capacity } => {
401            e!(value);
402            e!(capacity);
403        }
404        Expr::Closure { params, body, return_type } => {
405            for (_, t) in params {
406                work.push((Node::T(t), d));
407            }
408            if let Some(t) = return_type {
409                work.push((Node::T(t), d));
410            }
411            match body {
412                ClosureBody::Expression(x) => e!(x),
413                ClosureBody::Block(b) => {
414                    for s in *b {
415                        work.push((Node::S(s), d));
416                    }
417                }
418            }
419        }
420        Expr::CallExpr { callee, args } => {
421            e!(callee);
422            for a in args { e!(a); };
423        }
424        Expr::InterpolatedString(parts) => {
425            for part in parts {
426                match part {
427                    StringPart::Literal(_) => {}
428                    StringPart::Expr { value, format_spec: _, debug: _ } => e!(value),
429                }
430            }
431        }
432    }
433}
434
435fn push_type_children<'w, 'a>(ty: &'w TypeExpr<'a>, d: usize, work: &mut Vec<(Node<'w, 'a>, usize)>) {
436    match ty {
437        TypeExpr::Primitive(_) | TypeExpr::Named(_) => {}
438        TypeExpr::Generic { params, base: _ } => {
439            for t in *params {
440                work.push((Node::T(t), d));
441            }
442        }
443        TypeExpr::Function { inputs, output } => {
444            for t in *inputs {
445                work.push((Node::T(t), d));
446            }
447            work.push((Node::T(output), d));
448        }
449        TypeExpr::Refinement { base, predicate, var: _ } => {
450            work.push((Node::T(base), d));
451            work.push((Node::L(predicate), d));
452        }
453        TypeExpr::Persistent { inner } | TypeExpr::Mutable { inner } => {
454            work.push((Node::T(inner), d));
455        }
456    }
457}
458
459fn push_logic_children<'w, 'a>(
460    logic: &'w LogicExpr<'a>,
461    d: usize,
462    work: &mut Vec<(Node<'w, 'a>, usize)>,
463) {
464    macro_rules! l {
465        ($x:expr) => {
466            work.push((Node::L($x), d))
467        };
468    }
469    match logic {
470        // Terms and noun-phrase payloads nest by English sentence structure,
471        // not program size — leaves for depth purposes.
472        LogicExpr::Predicate { .. }
473        | LogicExpr::Identity { .. }
474        | LogicExpr::Metaphor { .. }
475        | LogicExpr::Atom(_)
476        | LogicExpr::Categorical(_)
477        | LogicExpr::Relation(_)
478        | LogicExpr::NeoEvent(_)
479        | LogicExpr::Comparative { .. }
480        | LogicExpr::Superlative { .. } => {}
481        LogicExpr::Quantifier { body, .. } => l!(body),
482        LogicExpr::Modal { operand, .. } => l!(operand),
483        LogicExpr::Temporal { body, .. } => l!(body),
484        LogicExpr::TemporalBinary { left, right, .. } => {
485            l!(left);
486            l!(right);
487        }
488        LogicExpr::Aspectual { body, .. } => l!(body),
489        LogicExpr::Voice { body, .. } => l!(body),
490        LogicExpr::BinaryOp { left, right, .. } => {
491            l!(left);
492            l!(right);
493        }
494        LogicExpr::UnaryOp { operand, .. } => l!(operand),
495        LogicExpr::Question { body, .. } => l!(body),
496        LogicExpr::YesNoQuestion { body } => l!(body),
497        LogicExpr::Lambda { body, .. } => l!(body),
498        LogicExpr::App { function, argument } => {
499            l!(function);
500            l!(argument);
501        }
502        LogicExpr::Intensional { content, .. } => l!(content),
503        LogicExpr::Event { predicate, .. } => l!(predicate),
504        LogicExpr::Imperative { action } => l!(action),
505        LogicExpr::Exclamative { body, .. } => l!(body),
506        LogicExpr::Optative { wish } => l!(wish),
507        LogicExpr::Implicature { assertion, implicature } => {
508            l!(assertion);
509            l!(implicature);
510        }
511        LogicExpr::SpeechAct { content, .. } => l!(content),
512        LogicExpr::Counterfactual { antecedent, consequent } => {
513            l!(antecedent);
514            l!(consequent);
515        }
516        LogicExpr::Causal { effect, cause } => {
517            l!(effect);
518            l!(cause);
519        }
520        LogicExpr::Concessive { main, concession } => {
521            l!(main);
522            l!(concession);
523        }
524        LogicExpr::Scopal { body, .. } => l!(body),
525        LogicExpr::Control { infinitive, .. } => l!(infinitive),
526        LogicExpr::Presupposition { assertion, presupposition } => {
527            l!(assertion);
528            l!(presupposition);
529        }
530        LogicExpr::Focus { scope, .. } => l!(scope),
531        LogicExpr::TemporalAnchor { body, .. } => l!(body),
532        LogicExpr::Distributive { predicate } => l!(predicate),
533        LogicExpr::GroupQuantifier { restriction, body, .. } => {
534            l!(restriction);
535            l!(body);
536        }
537    }
538}