Skip to main content

shape_runtime/context/
variables.rs

1//! Variable management for ExecutionContext
2//!
3//! This module handles variable storage, scoping, and pattern destructuring.
4
5use shape_ast::ast::VarKind;
6use shape_ast::error::{Result, ShapeError};
7// ADR-006 §2.7: GENERIC_CARRIER — `Variable.value: KindedSlot`. Adjacent
8// `kind: VarKind` (Let/Var/Const) is the storage class, NOT the
9// NativeKind, so the slot carries its own kind via `KindedSlot`.
10// `format_overrides` becomes `HashMap<String, KindedSlot>`; `KindedSlot`'s
11// per-element `Drop` retires heap refcounts cleanly without a wrapper
12// type (the deleted `ValueMap` wrapped `HashMap<String, ValueWord>` for
13// the same purpose).
14use shape_value::KindedSlot;
15use std::collections::HashMap;
16
17/// A variable in the execution context
18#[derive(Debug, Clone)]
19pub struct Variable {
20    /// The variable's current value. ADR-006 §2.7 GENERIC_CARRIER —
21    /// `KindedSlot` pairs the 8-byte slot with its `NativeKind` so heap
22    /// refcounts are managed by `KindedSlot::Drop`/`Clone`.
23    pub value: KindedSlot,
24    /// The variable kind (let, var, const)
25    pub kind: VarKind,
26    /// Whether the variable has been initialized
27    pub is_initialized: bool,
28    /// Whether this is a function-scoped variable (var, Flexible ownership)
29    /// vs block-scoped (let/const, Owned{Immutable,Mutable} ownership)
30    pub is_function_scoped: bool,
31    /// Optional format hint for display (e.g., "Percent" for meta lookup)
32    pub format_hint: Option<String>,
33    /// Optional format parameter overrides from type alias (e.g.,
34    /// `{ decimals: 4 }` from `type Percent4 = Percent { decimals: 4 }`).
35    /// `KindedSlot::Drop` releases each value's refcount when the
36    /// `HashMap` is dropped — no wrapper type required.
37    pub format_overrides: Option<HashMap<String, KindedSlot>>,
38}
39
40impl Variable {
41    /// Create a new variable
42    pub fn new(kind: VarKind, value: Option<KindedSlot>) -> Self {
43        Self::with_format(kind, value, None, None)
44    }
45
46    /// Create a new variable with format hint and parameter overrides.
47    pub fn with_format(
48        kind: VarKind,
49        value: Option<KindedSlot>,
50        format_hint: Option<String>,
51        format_overrides: Option<HashMap<String, KindedSlot>>,
52    ) -> Self {
53        let is_function_scoped = matches!(kind, VarKind::Var);
54        let (value, is_initialized) = match value {
55            Some(v) => (v, true),
56            None => (KindedSlot::none(), false),
57        };
58
59        Self {
60            value,
61            kind,
62            is_initialized,
63            is_function_scoped,
64            format_hint,
65            format_overrides,
66        }
67    }
68
69    /// Check if this variable can be assigned to
70    pub fn can_assign(&self) -> bool {
71        match self.kind {
72            VarKind::Const => !self.is_initialized, // const can only be assigned during initialization
73            VarKind::Let | VarKind::Var => true,
74        }
75    }
76
77    /// Assign a value to this variable
78    pub fn assign(&mut self, value: KindedSlot) -> Result<()> {
79        if !self.can_assign() {
80            return Err(ShapeError::RuntimeError {
81                message: "Cannot assign to const variable after initialization".to_string(),
82                location: None,
83            });
84        }
85
86        self.value = value;
87        self.is_initialized = true;
88        Ok(())
89    }
90
91    /// Get the value as `KindedSlot` reference, checking initialization
92    pub fn get_value(&self) -> Result<&KindedSlot> {
93        if !self.is_initialized {
94            return Err(ShapeError::RuntimeError {
95                message: "Variable used before initialization".to_string(),
96                location: None,
97            });
98        }
99        Ok(&self.value)
100    }
101}
102
103impl super::ExecutionContext {
104    /// Set a variable value (for simple assignment without declaration)
105    pub fn set_variable(&mut self, name: &str, value: KindedSlot) -> Result<()> {
106        // Search from innermost to outermost scope for existing variable
107        for scope in self.variable_scopes.iter_mut().rev() {
108            if let Some(variable) = scope.get_mut(name) {
109                return variable.assign(value);
110            }
111        }
112
113        // If variable doesn't exist, create a new 'var' variable in current scope
114        if let Some(scope) = self.variable_scopes.last_mut() {
115            let variable = Variable::new(VarKind::Var, Some(value));
116            scope.insert(name.to_string(), variable);
117            Ok(())
118        } else {
119            Err(ShapeError::RuntimeError {
120                message: "No scope available for variable assignment".to_string(),
121                location: None,
122            })
123        }
124    }
125
126    /// Get a variable value as `KindedSlot` (clones the slot, retaining refcount)
127    pub fn get_variable(&self, name: &str) -> Result<Option<KindedSlot>> {
128        // Search from innermost to outermost scope
129        for scope in self.variable_scopes.iter().rev() {
130            if let Some(variable) = scope.get(name) {
131                return Ok(Some(variable.get_value()?.clone()));
132            }
133        }
134        Ok(None)
135    }
136
137    /// Declare a new variable (with let, var, const)
138    pub fn declare_variable(
139        &mut self,
140        name: &str,
141        kind: VarKind,
142        value: Option<KindedSlot>,
143    ) -> Result<()> {
144        self.declare_variable_with_format(name, kind, value, None, None)
145    }
146
147    /// Declare a new variable with format hint and parameter overrides
148    ///
149    /// This is the full version that supports type aliases with meta parameter overrides,
150    /// e.g., `type Percent4 = Percent { decimals: 4 }` would store:
151    /// - format_hint: Some("Percent")
152    /// - format_overrides: Some({ "decimals": 4 })
153    pub fn declare_variable_with_format(
154        &mut self,
155        name: &str,
156        kind: VarKind,
157        value: Option<KindedSlot>,
158        format_hint: Option<String>,
159        format_overrides: Option<HashMap<String, KindedSlot>>,
160    ) -> Result<()> {
161        // Check if variable already exists in current scope
162        if let Some(current_scope) = self.variable_scopes.last() {
163            if current_scope.contains_key(name) {
164                return Err(ShapeError::RuntimeError {
165                    message: format!("Variable '{}' already declared in current scope", name),
166                    location: None,
167                });
168            }
169        }
170
171        // const variables must be initialized
172        if matches!(kind, VarKind::Const) && value.is_none() {
173            return Err(ShapeError::RuntimeError {
174                message: format!("const variable '{}' must be initialized", name),
175                location: None,
176            });
177        }
178
179        // Add to current scope
180        if let Some(scope) = self.variable_scopes.last_mut() {
181            let variable = Variable::with_format(kind, value, format_hint, format_overrides);
182            scope.insert(name.to_string(), variable);
183            Ok(())
184        } else {
185            Err(ShapeError::RuntimeError {
186                message: "No scope available for variable declaration".to_string(),
187                location: None,
188            })
189        }
190    }
191
192    /// Get the format hint for a variable (if any)
193    pub fn get_variable_format_hint(&self, name: &str) -> Option<String> {
194        // Search from innermost to outermost scope
195        for scope in self.variable_scopes.iter().rev() {
196            if let Some(variable) = scope.get(name) {
197                return variable.format_hint.clone();
198            }
199        }
200        None
201    }
202
203    /// Get the format overrides for a variable (if any)
204    ///
205    /// Returns parameter overrides from type alias, e.g., { "decimals": 4 }
206    /// for a variable declared as `let x: Percent4` where
207    /// `type Percent4 = Percent { decimals: 4 }`. The returned
208    /// `HashMap<String, KindedSlot>` bumps each override value's refcount
209    /// (via `KindedSlot::Clone`) so the original variable keeps its own
210    /// ownership.
211    pub fn get_variable_format_overrides(
212        &self,
213        name: &str,
214    ) -> Option<HashMap<String, KindedSlot>> {
215        // Search from innermost to outermost scope
216        for scope in self.variable_scopes.iter().rev() {
217            if let Some(variable) = scope.get(name) {
218                return variable.format_overrides.clone();
219            }
220        }
221        None
222    }
223
224    /// Get both format hint and overrides for a variable
225    pub fn get_variable_format_info(
226        &self,
227        name: &str,
228    ) -> (Option<String>, Option<HashMap<String, KindedSlot>>) {
229        for scope in self.variable_scopes.iter().rev() {
230            if let Some(variable) = scope.get(name) {
231                return (
232                    variable.format_hint.clone(),
233                    variable.format_overrides.clone(),
234                );
235            }
236        }
237        (None, None)
238    }
239
240    // declare_pattern / set_pattern removed by the strict-typing
241    // bulldozer (see docs/defections.md 2026-05-06: AST-evaluation
242    // runtime executors deletion). Both methods recursively decoded
243    // ValueWord via as_any_array / typed_object_to_hashmap and were
244    // only reachable from the four deleted executors and the deleted
245    // lib.rs query-exec stub. They will be rebuilt against typed slot
246    // storage by the ast-walking-interpreter-strict-rebuild workstream.
247
248    /// Get all variable names currently in scope
249    pub fn get_all_variable_names(&self) -> Vec<String> {
250        let mut names = Vec::new();
251        // Collect names from all scopes (outer to inner)
252        for scope in &self.variable_scopes {
253            for name in scope.keys() {
254                if !names.contains(name) {
255                    names.push(name.clone());
256                }
257            }
258        }
259        names
260    }
261
262    /// Get the kind of a variable (let, var, const)
263    pub fn get_variable_kind(&self, name: &str) -> Option<VarKind> {
264        // Search from innermost to outermost scope
265        for scope in self.variable_scopes.iter().rev() {
266            if let Some(variable) = scope.get(name) {
267                return Some(variable.kind);
268            }
269        }
270        None
271    }
272
273    /// Get all root-scope binding names (from the outermost scope).
274    ///
275    /// This is useful for REPL persistence where we need to inform the
276    /// bytecode compiler about bindings from previous sessions.
277    pub fn root_scope_binding_names(&self) -> Vec<String> {
278        if let Some(root_scope) = self.variable_scopes.first() {
279            root_scope.keys().cloned().collect()
280        } else {
281            Vec::new()
282        }
283    }
284}
285
286#[cfg(test)]
287mod tests {
288    use super::*;
289
290    #[test]
291    fn test_variable_let_creation() {
292        let var = Variable::new(VarKind::Let, Some(KindedSlot::from_number(42.0)));
293        assert!(var.is_initialized);
294        assert!(!var.is_function_scoped);
295        assert!(var.can_assign());
296    }
297
298    #[test]
299    fn test_variable_const_creation() {
300        let var = Variable::new(VarKind::Const, Some(KindedSlot::from_number(42.0)));
301        assert!(var.is_initialized);
302        assert!(!var.can_assign()); // Const cannot be reassigned
303    }
304
305    #[test]
306    fn test_variable_var_creation() {
307        let var = Variable::new(
308            VarKind::Var,
309            Some(KindedSlot::from_string_arc(std::sync::Arc::new(
310                "hello".to_string(),
311            ))),
312        );
313        assert!(var.is_initialized);
314        assert!(var.is_function_scoped);
315        assert!(var.can_assign());
316    }
317
318    #[test]
319    fn test_variable_uninitialized() {
320        let var = Variable::new(VarKind::Let, None);
321        assert!(!var.is_initialized);
322        assert!(var.get_value().is_err());
323    }
324
325    #[test]
326    fn test_variable_assignment() {
327        let mut var = Variable::new(VarKind::Let, Some(KindedSlot::from_number(1.0)));
328        assert!(var.assign(KindedSlot::from_number(2.0)).is_ok());
329        assert_eq!(var.get_value().unwrap().slot().as_f64(), 2.0);
330    }
331
332    #[test]
333    fn test_const_reassignment_fails() {
334        let mut var = Variable::new(VarKind::Const, Some(KindedSlot::from_number(1.0)));
335        assert!(var.assign(KindedSlot::from_number(2.0)).is_err());
336    }
337
338    #[test]
339    fn test_const_initial_assignment() {
340        let mut var = Variable::new(VarKind::Const, None);
341        assert!(var.can_assign()); // Can assign during initialization
342        assert!(var.assign(KindedSlot::from_number(42.0)).is_ok());
343        assert!(!var.can_assign()); // Cannot assign after initialization
344    }
345
346    // =========================================================================
347    // Format Overrides Tests
348    // =========================================================================
349
350    #[test]
351    fn test_variable_with_format_overrides() {
352        let mut overrides = HashMap::new();
353        overrides.insert("decimals".to_string(), KindedSlot::from_number(4.0));
354
355        let var = Variable::with_format(
356            VarKind::Let,
357            Some(KindedSlot::from_number(0.1234)),
358            Some("Percent".to_string()),
359            Some(overrides.clone()),
360        );
361
362        assert!(var.is_initialized);
363        assert_eq!(var.format_hint, Some("Percent".to_string()));
364        assert!(var.format_overrides.is_some());
365        let stored_overrides = var.format_overrides.unwrap();
366        assert_eq!(
367            stored_overrides.get("decimals").map(|ks| ks.slot().as_f64()),
368            Some(4.0)
369        );
370    }
371
372    #[test]
373    fn test_context_declare_variable_with_format() {
374        use super::super::ExecutionContext;
375
376        let mut ctx = ExecutionContext::new_empty();
377        let mut overrides = HashMap::new();
378        overrides.insert("decimals".to_string(), KindedSlot::from_number(4.0));
379
380        ctx.declare_variable_with_format(
381            "rate",
382            VarKind::Let,
383            Some(KindedSlot::from_number(0.15)),
384            Some("Percent".to_string()),
385            Some(overrides),
386        )
387        .unwrap();
388
389        // Verify format hint
390        let hint = ctx.get_variable_format_hint("rate");
391        assert_eq!(hint, Some("Percent".to_string()));
392
393        // Verify format overrides
394        let stored_overrides = ctx.get_variable_format_overrides("rate");
395        assert!(stored_overrides.is_some());
396        assert_eq!(
397            stored_overrides
398                .unwrap()
399                .get("decimals")
400                .map(|ks| ks.slot().as_f64()),
401            Some(4.0)
402        );
403
404        // Verify combined info
405        let (hint, overrides) = ctx.get_variable_format_info("rate");
406        assert_eq!(hint, Some("Percent".to_string()));
407        assert!(overrides.is_some());
408    }
409
410    #[test]
411    fn test_context_get_format_info_not_found() {
412        use super::super::ExecutionContext;
413
414        let ctx = ExecutionContext::new_empty();
415        let (hint, overrides) = ctx.get_variable_format_info("nonexistent");
416        assert!(hint.is_none());
417        assert!(overrides.is_none());
418    }
419}