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};
7use shape_value::ValueWord;
8use std::collections::HashMap;
9use std::sync::Arc;
10
11/// A variable in the execution context
12#[derive(Debug, Clone)]
13pub struct Variable {
14    /// The variable's current value (NaN-boxed for compact 8-byte storage)
15    pub value: ValueWord,
16    /// The variable kind (let, var, const)
17    pub kind: VarKind,
18    /// Whether the variable has been initialized
19    pub is_initialized: bool,
20    /// Whether this is a function-scoped variable (var) vs block-scoped (let/const)
21    pub is_function_scoped: bool,
22    /// Optional format hint for display (e.g., "Percent" for meta lookup)
23    pub format_hint: Option<String>,
24    /// Optional format parameter overrides from type alias (e.g., { decimals: 4 } from type Percent4 = Percent { decimals: 4 })
25    pub format_overrides: Option<HashMap<String, ValueWord>>,
26}
27
28impl Variable {
29    /// Create a new variable
30    pub fn new(kind: VarKind, value: Option<ValueWord>) -> Self {
31        Self::with_format(kind, value, None, None)
32    }
33
34    /// Create a new variable with format hint and parameter overrides
35    pub fn with_format(
36        kind: VarKind,
37        value: Option<ValueWord>,
38        format_hint: Option<String>,
39        format_overrides: Option<HashMap<String, ValueWord>>,
40    ) -> Self {
41        let is_function_scoped = matches!(kind, VarKind::Var);
42        let (value, is_initialized) = match value {
43            Some(v) => (v, true),
44            None => (ValueWord::none(), false),
45        };
46
47        Self {
48            value,
49            kind,
50            is_initialized,
51            is_function_scoped,
52            format_hint,
53            format_overrides,
54        }
55    }
56
57    /// Check if this variable can be assigned to
58    pub fn can_assign(&self) -> bool {
59        match self.kind {
60            VarKind::Const => !self.is_initialized, // const can only be assigned during initialization
61            VarKind::Let | VarKind::Var => true,
62        }
63    }
64
65    /// Assign a value to this variable
66    pub fn assign(&mut self, value: ValueWord) -> Result<()> {
67        if !self.can_assign() {
68            return Err(ShapeError::RuntimeError {
69                message: "Cannot assign to const variable after initialization".to_string(),
70                location: None,
71            });
72        }
73
74        self.value = value;
75        self.is_initialized = true;
76        Ok(())
77    }
78
79    /// Get the value as ValueWord reference, checking initialization
80    pub fn get_value(&self) -> Result<&ValueWord> {
81        if !self.is_initialized {
82            return Err(ShapeError::RuntimeError {
83                message: "Variable used before initialization".to_string(),
84                location: None,
85            });
86        }
87        Ok(&self.value)
88    }
89}
90
91impl super::ExecutionContext {
92    /// Set a variable value (for simple assignment without declaration)
93    pub fn set_variable(&mut self, name: &str, value: ValueWord) -> Result<()> {
94        self.set_variable_nb(name, value)
95    }
96
97    /// Set a variable value from ValueWord (avoids ValueWord conversion)
98    pub fn set_variable_nb(&mut self, name: &str, value: ValueWord) -> Result<()> {
99        // Search from innermost to outermost scope for existing variable
100        for scope in self.variable_scopes.iter_mut().rev() {
101            if let Some(variable) = scope.get_mut(name) {
102                return variable.assign(value);
103            }
104        }
105
106        // If variable doesn't exist, create a new 'var' variable in current scope
107        if let Some(scope) = self.variable_scopes.last_mut() {
108            let variable = Variable::new(VarKind::Var, Some(value));
109            scope.insert(name.to_string(), variable);
110            Ok(())
111        } else {
112            Err(ShapeError::RuntimeError {
113                message: "No scope available for variable assignment".to_string(),
114                location: None,
115            })
116        }
117    }
118
119    /// Get a variable value as ValueWord
120    pub fn get_variable(&self, name: &str) -> Result<Option<ValueWord>> {
121        // Search from innermost to outermost scope
122        for scope in self.variable_scopes.iter().rev() {
123            if let Some(variable) = scope.get(name) {
124                return Ok(Some(variable.get_value()?.clone()));
125            }
126        }
127        Ok(None)
128    }
129
130    /// Get a variable value as ValueWord (avoids ValueWord materialization)
131    pub fn get_variable_nb(&self, name: &str) -> Result<Option<ValueWord>> {
132        for scope in self.variable_scopes.iter().rev() {
133            if let Some(variable) = scope.get(name) {
134                return Ok(Some(variable.get_value()?.clone()));
135            }
136        }
137        Ok(None)
138    }
139
140    /// Declare a new variable (with let, var, const)
141    pub fn declare_variable(
142        &mut self,
143        name: &str,
144        kind: VarKind,
145        value: Option<ValueWord>,
146    ) -> Result<()> {
147        self.declare_variable_with_format(name, kind, value, None, None)
148    }
149
150    /// Declare a new variable with format hint and parameter overrides
151    ///
152    /// This is the full version that supports type aliases with meta parameter overrides,
153    /// e.g., `type Percent4 = Percent { decimals: 4 }` would store:
154    /// - format_hint: Some("Percent")
155    /// - format_overrides: Some({ "decimals": 4 })
156    pub fn declare_variable_with_format(
157        &mut self,
158        name: &str,
159        kind: VarKind,
160        value: Option<ValueWord>,
161        format_hint: Option<String>,
162        format_overrides: Option<HashMap<String, ValueWord>>,
163    ) -> Result<()> {
164        // Check if variable already exists in current scope
165        if let Some(current_scope) = self.variable_scopes.last() {
166            if current_scope.contains_key(name) {
167                return Err(ShapeError::RuntimeError {
168                    message: format!("Variable '{}' already declared in current scope", name),
169                    location: None,
170                });
171            }
172        }
173
174        // const variables must be initialized
175        if matches!(kind, VarKind::Const) && value.is_none() {
176            return Err(ShapeError::RuntimeError {
177                message: format!("const variable '{}' must be initialized", name),
178                location: None,
179            });
180        }
181
182        // Add to current scope
183        if let Some(scope) = self.variable_scopes.last_mut() {
184            let variable = Variable::with_format(kind, value, format_hint, format_overrides);
185            scope.insert(name.to_string(), variable);
186            Ok(())
187        } else {
188            Err(ShapeError::RuntimeError {
189                message: "No scope available for variable declaration".to_string(),
190                location: None,
191            })
192        }
193    }
194
195    /// Get the format hint for a variable (if any)
196    pub fn get_variable_format_hint(&self, name: &str) -> Option<String> {
197        // Search from innermost to outermost scope
198        for scope in self.variable_scopes.iter().rev() {
199            if let Some(variable) = scope.get(name) {
200                return variable.format_hint.clone();
201            }
202        }
203        None
204    }
205
206    /// Get the format overrides for a variable (if any)
207    ///
208    /// Returns parameter overrides from type alias, e.g., { "decimals": 4 }
209    /// for a variable declared as `let x: Percent4` where `type Percent4 = Percent { decimals: 4 }`
210    pub fn get_variable_format_overrides(&self, name: &str) -> Option<HashMap<String, ValueWord>> {
211        // Search from innermost to outermost scope
212        for scope in self.variable_scopes.iter().rev() {
213            if let Some(variable) = scope.get(name) {
214                return variable.format_overrides.clone();
215            }
216        }
217        None
218    }
219
220    /// Get both format hint and overrides for a variable
221    pub fn get_variable_format_info(
222        &self,
223        name: &str,
224    ) -> (Option<String>, Option<HashMap<String, ValueWord>>) {
225        for scope in self.variable_scopes.iter().rev() {
226            if let Some(variable) = scope.get(name) {
227                return (
228                    variable.format_hint.clone(),
229                    variable.format_overrides.clone(),
230                );
231            }
232        }
233        (None, None)
234    }
235
236    /// Declare variables matching a pattern
237    pub fn declare_pattern(
238        &mut self,
239        pattern: &shape_ast::ast::DestructurePattern,
240        kind: shape_ast::ast::VarKind,
241        value: ValueWord,
242    ) -> Result<()> {
243        use shape_ast::ast::DestructurePattern;
244
245        match pattern {
246            DestructurePattern::Identifier(name, _) => {
247                self.declare_variable(name, kind, Some(value))
248            }
249            DestructurePattern::Array(patterns) => {
250                // Destructure array
251                if let Some(view) = value.as_any_array() {
252                    let arr = view.to_generic();
253                    let mut rest_index = None;
254                    for (i, pattern) in patterns.iter().enumerate() {
255                        if let DestructurePattern::Rest(inner) = pattern {
256                            rest_index = Some(i);
257                            let rest_values = if i <= arr.len() {
258                                arr[i..].to_vec()
259                            } else {
260                                Vec::new()
261                            };
262                            self.declare_pattern(
263                                inner,
264                                kind,
265                                ValueWord::from_array(Arc::new(rest_values)),
266                            )?;
267                            break;
268                        } else {
269                            let val = arr.get(i).map(|nb| nb.clone()).unwrap_or(ValueWord::none());
270                            self.declare_pattern(pattern, kind, val)?;
271                        }
272                    }
273
274                    if rest_index.is_none() && patterns.len() > arr.len() {
275                        for pattern in &patterns[arr.len()..] {
276                            self.declare_pattern(pattern, kind, ValueWord::none())?;
277                        }
278                    }
279                    Ok(())
280                } else {
281                    Err(ShapeError::RuntimeError {
282                        message: "Cannot destructure non-array value as array".to_string(),
283                        location: None,
284                    })
285                }
286            }
287            DestructurePattern::Object(fields) => {
288                // Destructure object
289                if let Some(obj) = crate::type_schema::typed_object_to_hashmap(&value) {
290                    for field in fields {
291                        if field.key == "..." {
292                            // Handle rest pattern in object
293                            if let DestructurePattern::Rest(rest_pattern) = &field.pattern {
294                                if let DestructurePattern::Identifier(rest_name, _) =
295                                    rest_pattern.as_ref()
296                                {
297                                    // Collect remaining fields
298                                    let rest_pairs: Vec<(&str, ValueWord)> = obj
299                                        .iter()
300                                        .filter(|(k, _)| {
301                                            !fields.iter().any(|f| f.key == **k && f.key != "...")
302                                        })
303                                        .map(|(k, v)| (k.as_str(), v.clone()))
304                                        .collect();
305                                    let rest_val =
306                                        crate::type_schema::typed_object_from_pairs(&rest_pairs);
307                                    self.declare_variable(rest_name, kind, Some(rest_val))?;
308                                }
309                            }
310                        } else {
311                            let val = obj.get(&field.key).cloned().unwrap_or(ValueWord::none());
312                            self.declare_pattern(&field.pattern, kind, val)?;
313                        }
314                    }
315                    Ok(())
316                } else {
317                    Err(ShapeError::RuntimeError {
318                        message: "Cannot destructure non-object value as object".to_string(),
319                        location: None,
320                    })
321                }
322            }
323            DestructurePattern::Rest(_) => {
324                // Rest patterns should be handled in array/object context
325                Err(ShapeError::RuntimeError {
326                    message: "Rest pattern cannot be used at top level".to_string(),
327                    location: None,
328                })
329            }
330            DestructurePattern::Decomposition(bindings) => {
331                // Decomposition extracts component types from an intersection object
332                if crate::type_schema::typed_object_to_hashmap(&value).is_some() {
333                    for binding in bindings {
334                        self.declare_variable(&binding.name, kind, Some(value.clone()))?;
335                    }
336                    Ok(())
337                } else {
338                    Err(ShapeError::RuntimeError {
339                        message: "Cannot decompose non-object value".to_string(),
340                        location: None,
341                    })
342                }
343            }
344        }
345    }
346
347    /// Set variables matching a pattern (for assignments)
348    pub fn set_pattern(
349        &mut self,
350        pattern: &shape_ast::ast::DestructurePattern,
351        value: ValueWord,
352    ) -> Result<()> {
353        use shape_ast::ast::DestructurePattern;
354
355        match pattern {
356            DestructurePattern::Identifier(name, _) => self.set_variable(name, value),
357            DestructurePattern::Array(patterns) => {
358                // Destructure array
359                if let Some(view) = value.as_any_array() {
360                    let arr = view.to_generic();
361                    let mut rest_index = None;
362                    for (i, pattern) in patterns.iter().enumerate() {
363                        if let DestructurePattern::Rest(inner) = pattern {
364                            rest_index = Some(i);
365                            let rest_values = if i <= arr.len() {
366                                arr[i..].to_vec()
367                            } else {
368                                Vec::new()
369                            };
370                            self.set_pattern(inner, ValueWord::from_array(Arc::new(rest_values)))?;
371                            break;
372                        } else {
373                            let val = arr.get(i).map(|nb| nb.clone()).unwrap_or(ValueWord::none());
374                            self.set_pattern(pattern, val)?;
375                        }
376                    }
377
378                    if rest_index.is_none() && patterns.len() > arr.len() {
379                        for pattern in &patterns[arr.len()..] {
380                            self.set_pattern(pattern, ValueWord::none())?;
381                        }
382                    }
383                    Ok(())
384                } else {
385                    Err(ShapeError::RuntimeError {
386                        message: "Cannot destructure non-array value as array".to_string(),
387                        location: None,
388                    })
389                }
390            }
391            DestructurePattern::Object(fields) => {
392                // Destructure object
393                if let Some(obj) = crate::type_schema::typed_object_to_hashmap(&value) {
394                    for field in fields {
395                        if field.key == "..." {
396                            // Handle rest pattern in object
397                            if let DestructurePattern::Rest(rest_pattern) = &field.pattern {
398                                if let DestructurePattern::Identifier(rest_name, _) =
399                                    rest_pattern.as_ref()
400                                {
401                                    // Collect remaining fields
402                                    let rest_pairs: Vec<(&str, ValueWord)> = obj
403                                        .iter()
404                                        .filter(|(k, _)| {
405                                            !fields.iter().any(|f| f.key == **k && f.key != "...")
406                                        })
407                                        .map(|(k, v)| (k.as_str(), v.clone()))
408                                        .collect();
409                                    let rest_val =
410                                        crate::type_schema::typed_object_from_pairs(&rest_pairs);
411                                    self.set_variable(rest_name, rest_val)?;
412                                }
413                            }
414                        } else {
415                            let val = obj.get(&field.key).cloned().unwrap_or(ValueWord::none());
416                            self.set_pattern(&field.pattern, val)?;
417                        }
418                    }
419                    Ok(())
420                } else {
421                    Err(ShapeError::RuntimeError {
422                        message: "Cannot destructure non-object value as object".to_string(),
423                        location: None,
424                    })
425                }
426            }
427            DestructurePattern::Rest(_) => {
428                // Rest patterns should be handled in array/object context
429                Err(ShapeError::RuntimeError {
430                    message: "Rest pattern cannot be used at top level".to_string(),
431                    location: None,
432                })
433            }
434            DestructurePattern::Decomposition(bindings) => {
435                // Decomposition sets variables by extracting component types
436                if crate::type_schema::typed_object_to_hashmap(&value).is_some() {
437                    for binding in bindings {
438                        // For now, set each binding to the full object
439                        // Full implementation requires TypeSchema lookup
440                        self.set_variable(&binding.name, value.clone())?;
441                    }
442                    Ok(())
443                } else {
444                    Err(ShapeError::RuntimeError {
445                        message: "Cannot decompose non-object value".to_string(),
446                        location: None,
447                    })
448                }
449            }
450        }
451    }
452
453    /// Get all variable names currently in scope
454    pub fn get_all_variable_names(&self) -> Vec<String> {
455        let mut names = Vec::new();
456        // Collect names from all scopes (outer to inner)
457        for scope in &self.variable_scopes {
458            for name in scope.keys() {
459                if !names.contains(name) {
460                    names.push(name.clone());
461                }
462            }
463        }
464        names
465    }
466
467    /// Get the kind of a variable (let, var, const)
468    pub fn get_variable_kind(&self, name: &str) -> Option<VarKind> {
469        // Search from innermost to outermost scope
470        for scope in self.variable_scopes.iter().rev() {
471            if let Some(variable) = scope.get(name) {
472                return Some(variable.kind);
473            }
474        }
475        None
476    }
477
478    /// Get all root-scope binding names (from the outermost scope).
479    ///
480    /// This is useful for REPL persistence where we need to inform the
481    /// bytecode compiler about bindings from previous sessions.
482    pub fn root_scope_binding_names(&self) -> Vec<String> {
483        if let Some(root_scope) = self.variable_scopes.first() {
484            root_scope.keys().cloned().collect()
485        } else {
486            Vec::new()
487        }
488    }
489}
490
491#[cfg(test)]
492mod tests {
493    use super::*;
494
495    #[test]
496    fn test_variable_let_creation() {
497        let var = Variable::new(VarKind::Let, Some(ValueWord::from_f64(42.0)));
498        assert!(var.is_initialized);
499        assert!(!var.is_function_scoped);
500        assert!(var.can_assign());
501    }
502
503    #[test]
504    fn test_variable_const_creation() {
505        let var = Variable::new(VarKind::Const, Some(ValueWord::from_f64(42.0)));
506        assert!(var.is_initialized);
507        assert!(!var.can_assign()); // Const cannot be reassigned
508    }
509
510    #[test]
511    fn test_variable_var_creation() {
512        let var = Variable::new(
513            VarKind::Var,
514            Some(ValueWord::from_string(std::sync::Arc::new(
515                "hello".to_string(),
516            ))),
517        );
518        assert!(var.is_initialized);
519        assert!(var.is_function_scoped);
520        assert!(var.can_assign());
521    }
522
523    #[test]
524    fn test_variable_uninitialized() {
525        let var = Variable::new(VarKind::Let, None);
526        assert!(!var.is_initialized);
527        assert!(var.get_value().is_err());
528    }
529
530    #[test]
531    fn test_variable_assignment() {
532        let mut var = Variable::new(VarKind::Let, Some(ValueWord::from_f64(1.0)));
533        assert!(var.assign(ValueWord::from_f64(2.0)).is_ok());
534        assert_eq!(var.get_value().unwrap().as_f64(), Some(2.0));
535    }
536
537    #[test]
538    fn test_const_reassignment_fails() {
539        let mut var = Variable::new(VarKind::Const, Some(ValueWord::from_f64(1.0)));
540        assert!(var.assign(ValueWord::from_f64(2.0)).is_err());
541    }
542
543    #[test]
544    fn test_const_initial_assignment() {
545        let mut var = Variable::new(VarKind::Const, None);
546        assert!(var.can_assign()); // Can assign during initialization
547        assert!(var.assign(ValueWord::from_f64(42.0)).is_ok());
548        assert!(!var.can_assign()); // Cannot assign after initialization
549    }
550
551    // =========================================================================
552    // Format Overrides Tests
553    // =========================================================================
554
555    #[test]
556    fn test_variable_with_format_overrides() {
557        let mut overrides = HashMap::new();
558        overrides.insert("decimals".to_string(), ValueWord::from_f64(4.0));
559
560        let var = Variable::with_format(
561            VarKind::Let,
562            Some(ValueWord::from_f64(0.1234)),
563            Some("Percent".to_string()),
564            Some(overrides.clone()),
565        );
566
567        assert!(var.is_initialized);
568        assert_eq!(var.format_hint, Some("Percent".to_string()));
569        assert!(var.format_overrides.is_some());
570        let stored_overrides = var.format_overrides.unwrap();
571        assert_eq!(
572            stored_overrides.get("decimals").and_then(|v| v.as_f64()),
573            Some(4.0)
574        );
575    }
576
577    #[test]
578    fn test_context_declare_variable_with_format() {
579        use super::super::ExecutionContext;
580
581        let mut ctx = ExecutionContext::new_empty();
582        let mut overrides = HashMap::new();
583        overrides.insert("decimals".to_string(), ValueWord::from_f64(4.0));
584
585        ctx.declare_variable_with_format(
586            "rate",
587            VarKind::Let,
588            Some(ValueWord::from_f64(0.15)),
589            Some("Percent".to_string()),
590            Some(overrides),
591        )
592        .unwrap();
593
594        // Verify format hint
595        let hint = ctx.get_variable_format_hint("rate");
596        assert_eq!(hint, Some("Percent".to_string()));
597
598        // Verify format overrides
599        let stored_overrides = ctx.get_variable_format_overrides("rate");
600        assert!(stored_overrides.is_some());
601        assert_eq!(
602            stored_overrides
603                .unwrap()
604                .get("decimals")
605                .and_then(|v| v.as_f64()),
606            Some(4.0)
607        );
608
609        // Verify combined info
610        let (hint, overrides) = ctx.get_variable_format_info("rate");
611        assert_eq!(hint, Some("Percent".to_string()));
612        assert!(overrides.is_some());
613    }
614
615    #[test]
616    fn test_context_get_format_info_not_found() {
617        use super::super::ExecutionContext;
618
619        let ctx = ExecutionContext::new_empty();
620        let (hint, overrides) = ctx.get_variable_format_info("nonexistent");
621        assert!(hint.is_none());
622        assert!(overrides.is_none());
623    }
624}