Skip to main content

semantic/semantic_index/
extraction_budget.rs

1//! Structural limits checked before allocating Heddle-owned semantic facts.
2//!
3//! This bounds extraction expansion; it is not a tree-sitter allocator quota.
4use crate::parser::ParseBudget;
5
6#[derive(Debug, thiserror::Error)]
7pub enum ExtractionBudgetError {
8    #[error("semantic analysis interrupted")]
9    Interrupted,
10    #[error("semantic analysis extraction budget exceeded: {0}")]
11    Exceeded(&'static str),
12}
13
14pub(super) const MAX_SOURCE_BYTES: usize = 1 << 20;
15const MAX_NODES: usize = 32_768;
16const MAX_DEPTH: usize = 128;
17const MAX_SPAN_BYTES: usize = 16 << 20;
18const MAX_ANCESTOR_BYTES: usize = 16 << 20;
19
20/// Walk through the existing tree with a cursor, before any definition,
21/// token-stream, syntax-index or semantic-fact vectors are built. Node counts
22/// bound vector cardinality; summed spans bound repeated subtree processing;
23/// ancestor names separately bound container-name duplication.
24pub(super) fn admit(
25    root: tree_sitter::Node<'_>,
26    budget: &ParseBudget,
27) -> Result<(), ExtractionBudgetError> {
28    let mut cursor = root.walk();
29    let mut depth = 0usize;
30    let mut nodes = 0usize;
31    let mut span_bytes = 0usize;
32    let mut ancestor_bytes = 0usize;
33    let mut parent_name_bytes = 0usize;
34    loop {
35        if budget.interrupted() {
36            return Err(ExtractionBudgetError::Interrupted);
37        }
38        let node = cursor.node();
39        nodes = nodes.saturating_add(1);
40        span_bytes = span_bytes.saturating_add(node.byte_range().len());
41        ancestor_bytes = ancestor_bytes.saturating_add(parent_name_bytes);
42        if nodes > MAX_NODES {
43            return Err(ExtractionBudgetError::Exceeded("AST node count"));
44        }
45        if depth > MAX_DEPTH {
46            return Err(ExtractionBudgetError::Exceeded("AST depth"));
47        }
48        if span_bytes > MAX_SPAN_BYTES {
49            return Err(ExtractionBudgetError::Exceeded("repeated subtree spans"));
50        }
51        if ancestor_bytes > MAX_ANCESTOR_BYTES {
52            return Err(ExtractionBudgetError::Exceeded("ancestor names"));
53        }
54        if cursor.goto_first_child() {
55            parent_name_bytes = parent_name_bytes.saturating_add(name_bytes(node));
56            depth += 1;
57            continue;
58        }
59        loop {
60            if cursor.goto_next_sibling() {
61                break;
62            }
63            if !cursor.goto_parent() {
64                return Ok(());
65            }
66            parent_name_bytes = parent_name_bytes.saturating_sub(name_bytes(cursor.node()));
67            depth -= 1;
68        }
69    }
70}
71
72fn name_bytes(node: tree_sitter::Node<'_>) -> usize {
73    // A type field covers Rust impl receivers and Go receiver/type spellings;
74    // charging both fields is conservative when their syntax overlaps.
75    ["name", "type", "receiver"]
76        .into_iter()
77        .filter_map(|field| node.child_by_field_name(field))
78        .fold(0usize, |total, child| {
79            total.saturating_add(child.byte_range().len())
80        })
81}