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