exp_rs/eval/
context_stack.rs

1//! Context stack management for iterative evaluation
2//!
3//! This module provides a non-recursive way to manage evaluation contexts
4//! during iterative AST evaluation.
5
6use crate::Real;
7use crate::context::EvalContext;
8use crate::error::ExprError;
9use crate::types::HString;
10use alloc::rc::Rc;
11use alloc::vec::Vec;
12use heapless::FnvIndexMap;
13
14/// Maximum number of contexts we can track
15const MAX_CONTEXTS: usize = 128;
16
17/// Manages evaluation contexts without recursion
18pub struct ContextStack {
19    /// Stack of contexts, indexed by ID
20    contexts: Vec<Option<ContextWrapper>>,
21    /// Next available context ID
22    next_id: usize,
23    /// Maps context IDs to their parent IDs for variable lookup
24    parent_map: FnvIndexMap<usize, Option<usize>, MAX_CONTEXTS>,
25}
26
27/// Wrapper for context with additional metadata
28struct ContextWrapper {
29    /// The actual context
30    context: Rc<EvalContext>,
31    /// Whether this context owns its data (vs being a reference)
32    #[allow(dead_code)]
33    is_owned: bool,
34}
35
36impl Default for ContextStack {
37    fn default() -> Self {
38        Self::new()
39    }
40}
41
42impl ContextStack {
43    /// Create a new context stack
44    pub fn new() -> Self {
45        Self {
46            contexts: Vec::with_capacity(8),
47            next_id: 0,
48            parent_map: FnvIndexMap::new(),
49        }
50    }
51
52    /// Clear the stack while preserving capacity
53    pub fn clear(&mut self) {
54        self.contexts.clear();
55        self.next_id = 0;
56        self.parent_map.clear();
57    }
58
59    /// Push a context onto the stack, returning its ID
60    pub fn push_context(&mut self, ctx: Option<Rc<EvalContext>>) -> Result<usize, ExprError> {
61        let id = self.next_id;
62
63        // Check capacity
64        if id >= MAX_CONTEXTS {
65            return Err(ExprError::CapacityExceeded("context stack"));
66        }
67
68        self.next_id += 1;
69
70        // Ensure vector has space
71        if self.contexts.len() <= id {
72            self.contexts.resize_with(id + 1, || None);
73        }
74
75        // Store context
76        if let Some(ctx) = ctx {
77            self.contexts[id] = Some(ContextWrapper {
78                context: ctx,
79                is_owned: false,
80            });
81        } else {
82            // Create default context
83            self.contexts[id] = Some(ContextWrapper {
84                context: Rc::new(EvalContext::default()),
85                is_owned: true,
86            });
87        }
88
89        // No parent by default
90        self.parent_map
91            .insert(id, None)
92            .map_err(|_| ExprError::CapacityExceeded("parent map"))?;
93
94        Ok(id)
95    }
96
97    /// Push a new context with a specified parent
98    pub fn push_context_with_parent(
99        &mut self,
100        ctx: EvalContext,
101        parent_id: usize,
102    ) -> Result<usize, ExprError> {
103        let id = self.next_id;
104
105        // Check capacity
106        if id >= MAX_CONTEXTS {
107            return Err(ExprError::CapacityExceeded("context stack"));
108        }
109
110        self.next_id += 1;
111
112        // Ensure vector has space
113        if self.contexts.len() <= id {
114            self.contexts.resize_with(id + 1, || None);
115        }
116
117        // Store context
118        self.contexts[id] = Some(ContextWrapper {
119            context: Rc::new(ctx),
120            is_owned: true,
121        });
122
123        // Set parent relationship
124        self.parent_map
125            .insert(id, Some(parent_id))
126            .map_err(|_| ExprError::CapacityExceeded("parent map"))?;
127
128        Ok(id)
129    }
130
131    /// Get a context by ID
132    pub fn get_context(&self, id: usize) -> Option<&Rc<EvalContext>> {
133        self.contexts
134            .get(id)
135            .and_then(|opt| opt.as_ref())
136            .map(|wrapper| &wrapper.context)
137    }
138
139    /// Look up a variable, checking parent contexts if needed
140    pub fn lookup_variable(&self, ctx_id: usize, name: &HString) -> Option<Real> {
141        let mut current_id = Some(ctx_id);
142        let mut visited_contexts = Vec::new();
143
144        while let Some(id) = current_id {
145            if let Some(ctx) = self.get_context(id) {
146                // Parameters are stored in the context, we need to handle this differently
147                // For now, skip parameter checking as it's not exposed in EvalContext
148
149                // Then check regular variables
150                if let Some(&value) = ctx.variables.get(name) {
151                    return Some(value);
152                }
153
154                // Check constants
155                if let Some(&value) = ctx.constants.get(name) {
156                    return Some(value);
157                }
158
159                // Check the context's own parent chain
160                visited_contexts.push(id);
161                if let Some(ref parent_ctx) = ctx.parent {
162                    // Follow the context's parent chain
163                    return self.lookup_in_context_chain(
164                        parent_ctx.as_ref(),
165                        name,
166                        &visited_contexts,
167                    );
168                }
169            }
170
171            // Move to parent context in the stack
172            current_id = self.parent_map.get(&id).and_then(|&parent| parent);
173        }
174
175        None
176    }
177
178    /// Helper to look up a variable in a context chain
179    fn lookup_in_context_chain(
180        &self,
181        ctx: &EvalContext,
182        name: &HString,
183        visited: &[usize],
184    ) -> Option<Real> {
185        // Check variables
186        if let Some(&value) = ctx.variables.get(name) {
187            return Some(value);
188        }
189
190        // Check constants
191        if let Some(&value) = ctx.constants.get(name) {
192            return Some(value);
193        }
194
195        // Follow parent chain
196        if let Some(ref parent) = ctx.parent {
197            return self.lookup_in_context_chain(parent.as_ref(), name, visited);
198        }
199
200        None
201    }
202
203    /// Get the parent ID of a context
204    pub fn get_parent_id(&self, ctx_id: usize) -> Option<usize> {
205        self.parent_map.get(&ctx_id).and_then(|&parent| parent)
206    }
207}