1use shape_ast::ast::VarKind;
6use shape_ast::error::{Result, ShapeError};
7use shape_value::ValueWord;
8use std::collections::HashMap;
9use std::sync::Arc;
10
11#[derive(Debug, Clone)]
13pub struct Variable {
14 pub value: ValueWord,
16 pub kind: VarKind,
18 pub is_initialized: bool,
20 pub is_function_scoped: bool,
22 pub format_hint: Option<String>,
24 pub format_overrides: Option<HashMap<String, ValueWord>>,
26}
27
28impl Variable {
29 pub fn new(kind: VarKind, value: Option<ValueWord>) -> Self {
31 Self::with_format(kind, value, None, None)
32 }
33
34 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 pub fn can_assign(&self) -> bool {
59 match self.kind {
60 VarKind::Const => !self.is_initialized, VarKind::Let | VarKind::Var => true,
62 }
63 }
64
65 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 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 pub fn set_variable(&mut self, name: &str, value: ValueWord) -> Result<()> {
94 self.set_variable_nb(name, value)
95 }
96
97 pub fn set_variable_nb(&mut self, name: &str, value: ValueWord) -> Result<()> {
99 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 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 pub fn get_variable(&self, name: &str) -> Result<Option<ValueWord>> {
121 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 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 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 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 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 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 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 pub fn get_variable_format_hint(&self, name: &str) -> Option<String> {
197 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 pub fn get_variable_format_overrides(&self, name: &str) -> Option<HashMap<String, ValueWord>> {
211 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 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 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 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 if let Some(obj) = crate::type_schema::typed_object_to_hashmap(&value) {
290 for field in fields {
291 if field.key == "..." {
292 if let DestructurePattern::Rest(rest_pattern) = &field.pattern {
294 if let DestructurePattern::Identifier(rest_name, _) =
295 rest_pattern.as_ref()
296 {
297 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 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 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 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 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 if let Some(obj) = crate::type_schema::typed_object_to_hashmap(&value) {
394 for field in fields {
395 if field.key == "..." {
396 if let DestructurePattern::Rest(rest_pattern) = &field.pattern {
398 if let DestructurePattern::Identifier(rest_name, _) =
399 rest_pattern.as_ref()
400 {
401 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 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 if crate::type_schema::typed_object_to_hashmap(&value).is_some() {
437 for binding in bindings {
438 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 pub fn get_all_variable_names(&self) -> Vec<String> {
455 let mut names = Vec::new();
456 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 pub fn get_variable_kind(&self, name: &str) -> Option<VarKind> {
469 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 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()); }
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()); assert!(var.assign(ValueWord::from_f64(42.0)).is_ok());
548 assert!(!var.can_assign()); }
550
551 #[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 let hint = ctx.get_variable_format_hint("rate");
596 assert_eq!(hint, Some("Percent".to_string()));
597
598 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 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}