Refactor: Use Value::Closure enum variant

Previously, compiled closures were stored as `Value::Object` and then
downcasted. This commit introduces a dedicated `Value::Closure` enum
variant to represent compiled closures. This improves type safety and
simplifies handling of closures throughout the compiler and VM.

This change involves:
- Updating type definitions to use `Value::Closure`.
- Adjusting downcasting logic to directly access the `Closure` struct.
- Modifying `run_with_args` and `run_with_args_observed` to expect
  `Value::Closure` for tail-call targets.
- Refining the handling of closures in the `Optimizer` and `Inliner` for
  better type clarity.
This commit is contained in:
2026-03-22 19:41:23 +01:00
parent 6c37b0c64f
commit 843ee7dfed
7 changed files with 134 additions and 141 deletions
+3 -3
View File
@@ -3,7 +3,7 @@ use crate::ast::nodes::{
IdentifierBinding, LambdaBinding, Node, NodeKind, UpvalueIdx, VirtualId, IdentifierBinding, LambdaBinding, Node, NodeKind, UpvalueIdx, VirtualId,
}; };
use crate::ast::types::{Purity, Value}; use crate::ast::types::{Purity, Value};
use crate::ast::closure::Closure;
use std::cell::RefCell; use std::cell::RefCell;
use std::rc::Rc; use std::rc::Rc;
@@ -343,9 +343,9 @@ impl Optimizer {
} }
} }
if let NodeKind::Constant(Value::Object(ref obj)) = callee_opt.kind if let NodeKind::Constant(Value::Closure(ref closure_rc)) = callee_opt.kind
&& path.inlining_depth < 5 && path.inlining_depth < 5
&& let Some(closure) = obj.as_any().downcast_ref::<Closure>() && let closure = closure_rc.as_ref()
&& (closure.upvalues.is_empty() && (closure.upvalues.is_empty()
|| closure.function_node.ty.purity >= Purity::SideEffectFree) || closure.function_node.ty.purity >= Purity::SideEffectFree)
&& !closure.function_node.ty.is_recursive && !closure.function_node.ty.is_recursive
+3 -7
View File
@@ -2,7 +2,7 @@ use crate::ast::nodes::{
Address, AnalyzedNode, IdentifierBinding, NodeKind, VirtualId, Address, AnalyzedNode, IdentifierBinding, NodeKind, VirtualId,
}; };
use crate::ast::types::{Purity, Value}; use crate::ast::types::{Purity, Value};
use crate::ast::closure::Closure;
use std::cell::RefCell; use std::cell::RefCell;
use std::collections::HashSet; use std::collections::HashSet;
use std::rc::Rc; use std::rc::Rc;
@@ -35,12 +35,8 @@ impl<'a> Inliner<'a> {
| Value::Keyword(_) | Value::Keyword(_)
| Value::Record(_, _) | Value::Record(_, _)
| Value::DateTime(_) => true, | Value::DateTime(_) => true,
Value::Object(obj) => { Value::Closure(closure_rc) => {
if let Some(closure) = obj.as_any().downcast_ref::<Closure>() { closure_rc.upvalues.is_empty() && !closure_rc.function_node.ty.is_recursive
closure.upvalues.is_empty() && !closure.function_node.ty.is_recursive
} else {
false
}
} }
_ => false, _ => false,
}; };
+4 -6
View File
@@ -3,7 +3,7 @@ use crate::ast::nodes::{
NodeKind, VirtualId, NodeKind, VirtualId,
}; };
use crate::ast::types::{Identity, Value}; use crate::ast::types::{Identity, Value};
use crate::ast::closure::Closure;
use std::collections::HashSet; use std::collections::HashSet;
// --- PathTracker --- // --- PathTracker ---
@@ -56,12 +56,10 @@ impl UsageInfo {
self.collect(value); self.collect(value);
} }
NodeKind::Constant(v) => { NodeKind::Constant(v) => {
if let Value::Object(obj) = v if let Value::Closure(closure_rc) = v {
&& let Some(closure) = obj.as_any().downcast_ref::<Closure>()
{
self.used_identities self.used_identities
.insert(closure.function_node.identity.clone()); .insert(closure_rc.function_node.identity.clone());
self.collect(&closure.function_node); self.collect(&closure_rc.function_node);
} }
} }
NodeKind::Identifier { binding, .. } => { NodeKind::Identifier { binding, .. } => {
+3 -12
View File
@@ -668,13 +668,11 @@ impl Environment {
info.positional_count, info.positional_count,
node.ty.stack_size, node.ty.stack_size,
)); ));
let closure_obj: Rc<dyn Object> = closure;
return Rc::new(NativeFunction { return Rc::new(NativeFunction {
purity: Purity::Impure, purity: Purity::Impure,
func: Rc::new(move |args| { func: Rc::new(move |args| {
let mut vm = VM::new(root_values.clone()); let mut vm = VM::new(root_values.clone());
match vm.run_with_args(closure_obj.clone(), args) { match vm.run_with_args(closure.clone(), args) {
Ok(v) => v, Ok(v) => v,
Err(e) => panic!("Myc Runtime Error: {}", e), Err(e) => panic!("Myc Runtime Error: {}", e),
} }
@@ -692,12 +690,7 @@ impl Environment {
Err(e) => panic!("Myc Runtime Error: {}", e), Err(e) => panic!("Myc Runtime Error: {}", e),
}; };
if let Value::Object(obj) = &res if let Value::Closure(obj) = &res {
&& obj
.as_any()
.downcast_ref::<Closure>()
.is_some()
{
match vm.run_with_args(obj.clone(), args) { match vm.run_with_args(obj.clone(), args) {
Ok(v) => v, Ok(v) => v,
Err(e) => panic!("Myc Runtime Error (Closure): {}", e), Err(e) => panic!("Myc Runtime Error (Closure): {}", e),
@@ -837,9 +830,7 @@ impl Environment {
// 2. Execute the root closure immediately to get the actual script result. // 2. Execute the root closure immediately to get the actual script result.
// All Myc scripts are wrapped in a parameterless lambda for consistency. // All Myc scripts are wrapped in a parameterless lambda for consistency.
let mut final_result = result; let mut final_result = result;
if let Ok(Value::Object(obj)) = &final_result if let Ok(Value::Closure(obj)) = &final_result {
&& let Some(_closure) = obj.as_any().downcast_ref::<Closure>()
{
final_result = vm.run_with_args_observed(&mut observer, obj.clone(), &[]); final_result = vm.run_with_args_observed(&mut observer, obj.clone(), &[]);
} }
+2 -2
View File
@@ -432,8 +432,8 @@ pub fn register(env: &Environment) {
panic!("create-ticker expects exactly 1 argument (the condition closure)"); panic!("create-ticker expects exactly 1 argument (the condition closure)");
} }
let closure_obj = if let Value::Object(obj) = &args[0] { let closure_obj = if let Value::Closure(rc) = &args[0] {
obj.clone() rc.clone()
} else { } else {
panic!("create-ticker expects a closure as its argument"); panic!("create-ticker expects a closure as its argument");
}; };
+6 -1
View File
@@ -1,3 +1,4 @@
use crate::ast::closure::Closure;
use chrono::{TimeZone, Utc}; use chrono::{TimeZone, Utc};
use std::any::Any; use std::any::Any;
use std::cell::RefCell; use std::cell::RefCell;
@@ -257,7 +258,8 @@ pub enum Value {
Record(std::sync::Arc<RecordLayout>, ValueList), Record(std::sync::Arc<RecordLayout>, ValueList),
FieldAccessor(Keyword), FieldAccessor(Keyword),
Function(Rc<NativeFunction>), Function(Rc<NativeFunction>),
Object(Rc<dyn Object>), // For compiled Closures and other opaque types Closure(Rc<Closure>), // Compiled function with captured upvalues
Object(Rc<dyn Object>), // For Series, Streams, SyntaxNodes, and custom extensions
Cell(Rc<RefCell<Value>>), // Boxed value for captures Cell(Rc<RefCell<Value>>), // Boxed value for captures
} }
@@ -277,6 +279,7 @@ impl PartialEq for Value {
} }
(Value::FieldAccessor(a), Value::FieldAccessor(b)) => a == b, (Value::FieldAccessor(a), Value::FieldAccessor(b)) => a == b,
(Value::Function(a), Value::Function(b)) => Rc::ptr_eq(a, b), (Value::Function(a), Value::Function(b)) => Rc::ptr_eq(a, b),
(Value::Closure(a), Value::Closure(b)) => Rc::ptr_eq(a, b),
(Value::Object(a), Value::Object(b)) => Rc::ptr_eq(a, b), (Value::Object(a), Value::Object(b)) => Rc::ptr_eq(a, b),
(Value::Cell(a), Value::Cell(b)) => Rc::ptr_eq(a, b), (Value::Cell(a), Value::Cell(b)) => Rc::ptr_eq(a, b),
_ => false, _ => false,
@@ -617,6 +620,7 @@ impl Value {
Value::Record(layout, _) => StaticType::Record(layout.clone()), Value::Record(layout, _) => StaticType::Record(layout.clone()),
Value::FieldAccessor(k) => StaticType::FieldAccessor(*k), Value::FieldAccessor(k) => StaticType::FieldAccessor(*k),
Value::Function(_) => StaticType::Any, // Dynamic function Value::Function(_) => StaticType::Any, // Dynamic function
Value::Closure(_) => StaticType::Object("closure"),
Value::Object(o) => StaticType::Object(o.type_name()), Value::Object(o) => StaticType::Object(o.type_name()),
Value::Cell(c) => c.borrow().static_type(), Value::Cell(c) => c.borrow().static_type(),
} }
@@ -660,6 +664,7 @@ impl fmt::Display for Value {
} }
Value::FieldAccessor(k) => write!(f, ".{}", k.name()), Value::FieldAccessor(k) => write!(f, ".{}", k.name()),
Value::Function(f_meta) => write!(f, "<native fn, {:?}>", f_meta.purity), Value::Function(f_meta) => write!(f, "<native fn, {:?}>", f_meta.purity),
Value::Closure(_) => write!(f, "<closure>"),
Value::Object(o) => write!(f, "<{:?}>", o), Value::Object(o) => write!(f, "<{:?}>", o),
Value::Cell(c) => write!(f, "{}", c.borrow()), Value::Cell(c) => write!(f, "{}", c.borrow()),
} }
+113 -110
View File
@@ -2,7 +2,7 @@ use crate::ast::closure::Closure;
use crate::ast::nodes::{Address, ExecNode, IdentifierBinding, NodeKind, StackOffset}; use crate::ast::nodes::{Address, ExecNode, IdentifierBinding, NodeKind, StackOffset};
use crate::ast::rtl::series::{RecordSeries, SeriesView}; use crate::ast::rtl::series::{RecordSeries, SeriesView};
use crate::ast::rtl::streams::{build_map_stream, build_pipeline_node, StreamNode}; use crate::ast::rtl::streams::{build_map_stream, build_pipeline_node, StreamNode};
use crate::ast::types::{Object, PipeFn, Value}; use crate::ast::types::{PipeFn, Value};
use std::cell::RefCell; use std::cell::RefCell;
use std::rc::Rc; use std::rc::Rc;
@@ -88,7 +88,7 @@ pub struct VM {
frames: Vec<CallFrame>, frames: Vec<CallFrame>,
/// Side-channel for tail-call signaling. Set by `eval_internal` when a tail-call /// Side-channel for tail-call signaling. Set by `eval_internal` when a tail-call
/// is requested; consumed by `resolve_tail_calls` and the inline TCO loop. /// is requested; consumed by `resolve_tail_calls` and the inline TCO loop.
tail_call: Option<(Rc<dyn Object>, Vec<Value>)>, tail_call: Option<(Value, Vec<Value>)>,
} }
impl VM { impl VM {
@@ -111,60 +111,68 @@ impl VM {
mut result: Result<Value, String>, mut result: Result<Value, String>,
) -> Result<Value, String> { ) -> Result<Value, String> {
loop { loop {
if let Some((next_obj, next_args)) = self.tail_call.take() { if let Some((next_val, next_args)) = self.tail_call.take() {
if let Ok(closure_rc) = next_obj.clone().into_rc_any().downcast::<Closure>() { match next_val {
self.stack.clear(); Value::Closure(closure_rc) => {
// frames should be empty here since we popped before entering the loop self.stack.clear();
self.frames.push(CallFrame { // frames should be empty here since we popped before entering the loop
stack_base: 0, self.frames.push(CallFrame {
closure: Some(closure_rc.clone()), stack_base: 0,
}); closure: Some(closure_rc.clone()),
let closure = closure_rc.as_ref(); });
if let Some(count) = closure.positional_count let closure = closure_rc.as_ref();
&& next_args.len() == count as usize if let Some(count) = closure.positional_count
{ && next_args.len() == count as usize
self.stack.extend(next_args); {
} else { self.stack.extend(next_args);
self.unpack(closure.parameter_node.as_ref(), &next_args, &mut 0)?; } else {
} self.unpack(closure.parameter_node.as_ref(), &next_args, &mut 0)?;
}
// PRE-ALLOCATION // PRE-ALLOCATION
let current_stack = self.stack.len() as u32; let current_stack = self.stack.len() as u32;
if closure.stack_size > current_stack { if closure.stack_size > current_stack {
self.stack.resize(closure.stack_size as usize, Value::Void); self.stack.resize(closure.stack_size as usize, Value::Void);
} }
result = self.eval_observed(observer, &closure.exec_node); result = self.eval_observed(observer, &closure.exec_node);
self.frames.pop(); self.frames.pop();
} else if let Some(series) = next_obj.as_series() {
if next_args.len() != 1 {
return Err(format!(
"{} indexer expects exactly 1 argument (the lookback index), got {}",
next_obj.type_name(),
next_args.len()
));
} }
if let Value::Int(idx) = &next_args[0] { Value::Object(obj) => {
if *idx < 0 { if let Some(series) = obj.as_series() {
if next_args.len() != 1 {
return Err(format!(
"{} indexer expects exactly 1 argument (the lookback index), got {}",
obj.type_name(),
next_args.len()
));
}
if let Value::Int(idx) = &next_args[0] {
if *idx < 0 {
return Err(format!(
"{} lookback index cannot be negative: {}",
obj.type_name(),
idx
));
}
result = Ok(series.get_item(*idx as usize).unwrap_or(Value::Void));
} else {
return Err(format!(
"{} index must be an integer, got {}",
obj.type_name(),
next_args[0]
));
}
} else {
return Err(format!( return Err(format!(
"{} lookback index cannot be negative: {}", "Tail call target is not callable: {}",
next_obj.type_name(), obj.type_name()
idx
)); ));
} }
result = Ok(series.get_item(*idx as usize).unwrap_or(Value::Void));
} else {
return Err(format!(
"{} index must be an integer, got {}",
next_obj.type_name(),
next_args[0]
));
} }
} else { other => {
return Err(format!( return Err(format!("Tail call target is not callable: {}", other));
"Tail call target is not callable: {}", }
next_obj.type_name()
));
} }
} else { } else {
return result; return result;
@@ -174,23 +182,18 @@ impl VM {
pub fn run_with_args( pub fn run_with_args(
&mut self, &mut self,
closure_obj: Rc<dyn Object>, closure_rc: Rc<Closure>,
args: &[Value], args: &[Value],
) -> Result<Value, String> { ) -> Result<Value, String> {
self.run_with_args_observed(&mut NoOpObserver, closure_obj, args) self.run_with_args_observed(&mut NoOpObserver, closure_rc, args)
} }
pub fn run_with_args_observed<O: VMObserver>( pub fn run_with_args_observed<O: VMObserver>(
&mut self, &mut self,
observer: &mut O, observer: &mut O,
closure_obj: Rc<dyn Object>, closure_rc: Rc<Closure>,
args: &[Value], args: &[Value],
) -> Result<Value, String> { ) -> Result<Value, String> {
let closure_rc = closure_obj
.into_rc_any()
.downcast::<Closure>()
.map_err(|_| "Object is not a closure".to_string())?;
self.stack.clear(); self.stack.clear();
self.frames.clear(); self.frames.clear();
@@ -416,8 +419,8 @@ impl VM {
let mut pipe_vm = VM::new(self.globals.clone()); let mut pipe_vm = VM::new(self.globals.clone());
let executor: Box<PipeFn> = match lambda_val { let executor: Box<PipeFn> = match lambda_val {
Value::Object(obj) => { Value::Closure(rc) => {
let my_closure = obj.clone(); let my_closure = rc.clone();
Box::new(move |args: &[Value]| -> Value { Box::new(move |args: &[Value]| -> Value {
match pipe_vm.run_with_args(my_closure.clone(), args) { match pipe_vm.run_with_args(my_closure.clone(), args) {
Ok(res) => res, Ok(res) => res,
@@ -465,7 +468,7 @@ impl VM {
info.positional_count, info.positional_count,
stack_size, stack_size,
); );
Ok(Value::Object(Rc::new(closure))) Ok(Value::Closure(Rc::new(closure)))
} }
NodeKind::Call { callee, args } => { NodeKind::Call { callee, args } => {
let func_val = self.eval_internal(obs, callee)?; let func_val = self.eval_internal(obs, callee)?;
@@ -481,8 +484,8 @@ impl VM {
self.stack.truncate(base); self.stack.truncate(base);
match func_val { match func_val {
Value::Object(obj) => { Value::Closure(_) | Value::Object(_) => {
self.tail_call = Some((obj, arg_vals)); self.tail_call = Some((func_val, arg_vals));
return Ok(Value::Void); return Ok(Value::Void);
} }
Value::Function(f) => return Ok((f.func)(&arg_vals)), Value::Function(f) => return Ok((f.func)(&arg_vals)),
@@ -612,57 +615,56 @@ impl VM {
self.stack.truncate(base); self.stack.truncate(base);
return res; return res;
} }
Value::Object(obj) => { Value::Closure(closure_rc) => {
if let Ok(closure_rc) = self.frames.push(CallFrame {
obj.clone().into_rc_any().downcast::<Closure>() stack_base: base,
closure: Some(closure_rc.clone()),
});
let closure = closure_rc.as_ref();
let unpack_res = if let Some(count) = closure.positional_count
&& (self.stack.len() - base) == count as usize
{ {
self.frames.push(CallFrame { Ok(())
stack_base: base, } else {
closure: Some(closure_rc.clone()), let args_for_unpack = self.stack[base..].to_vec();
}); self.stack.truncate(base);
let closure = closure_rc.as_ref(); if let NodeKind::Tuple { elements } =
&closure.parameter_node.kind
let unpack_res = if let Some(count) = closure.positional_count
&& (self.stack.len() - base) == count as usize
{ {
Ok(()) let mut offset = 0;
let mut res = Ok(());
for el in elements {
if let Err(e) =
self.unpack(el.as_ref(), &args_for_unpack, &mut offset)
{
res = Err(e);
break;
}
}
res
} else { } else {
let args_for_unpack = self.stack[base..].to_vec(); self.unpack(closure.parameter_node.as_ref(), &args_for_unpack, &mut 0)
self.stack.truncate(base); }
if let NodeKind::Tuple { elements } = };
&closure.parameter_node.kind
{
let mut offset = 0;
let mut res = Ok(());
for el in elements {
if let Err(e) =
self.unpack(el.as_ref(), &args_for_unpack, &mut offset)
{
res = Err(e);
break;
}
}
res
} else {
self.unpack(closure.parameter_node.as_ref(), &args_for_unpack, &mut 0)
}
};
let res = match unpack_res { let res = match unpack_res {
Ok(_) => { Ok(_) => {
// PRE-ALLOCATION // PRE-ALLOCATION
let current_stack = (self.stack.len() - base) as u32; let current_stack = (self.stack.len() - base) as u32;
if closure.stack_size > current_stack { if closure.stack_size > current_stack {
self.stack.resize(base + closure.stack_size as usize, Value::Void); self.stack.resize(base + closure.stack_size as usize, Value::Void);
}
self.eval_internal(obs, &closure.exec_node)
} }
Err(e) => Err(e), self.eval_internal(obs, &closure.exec_node)
}; }
Err(e) => Err(e),
};
self.frames.pop(); self.frames.pop();
res res
} else if let Some(series) = obj.as_series() { }
Value::Object(obj) => {
if let Some(series) = obj.as_series() {
let arg_len = self.stack.len() - base; let arg_len = self.stack.len() - base;
let res = if arg_len != 1 { let res = if arg_len != 1 {
Err(format!( Err(format!(
@@ -707,6 +709,7 @@ impl VM {
Value::Record(_, _) => "Record", Value::Record(_, _) => "Record",
Value::FieldAccessor(_) => "FieldAccessor", Value::FieldAccessor(_) => "FieldAccessor",
Value::Function(_) => "Function", Value::Function(_) => "Function",
Value::Closure(_) => "Closure",
Value::Object(_) => "Object", Value::Object(_) => "Object",
Value::Cell(_) => "Cell", Value::Cell(_) => "Cell",
}; };
@@ -714,8 +717,8 @@ impl VM {
} }
}; };
if let Some((next_obj, next_args)) = self.tail_call.take() { if let Some((next_val, next_args)) = self.tail_call.take() {
current_func = Value::Object(next_obj); current_func = next_val;
self.stack.truncate(base); self.stack.truncate(base);
self.stack.extend(next_args); self.stack.extend(next_args);
continue; continue;
@@ -736,7 +739,7 @@ impl VM {
let frame = self.frames.last().ok_or("No call frame for 'again'")?; let frame = self.frames.last().ok_or("No call frame for 'again'")?;
if let Some(closure_obj) = &frame.closure { if let Some(closure_obj) = &frame.closure {
self.tail_call = Some((closure_obj.clone() as Rc<dyn Object>, arg_vals)); self.tail_call = Some((Value::Closure(closure_obj.clone()), arg_vals));
Ok(Value::Void) Ok(Value::Void)
} else { } else {
Err("'again' called outside of a closure".to_string()) Err("'again' called outside of a closure".to_string())