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,
};
use crate::ast::types::{Purity, Value};
use crate::ast::closure::Closure;
use std::cell::RefCell;
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
&& let Some(closure) = obj.as_any().downcast_ref::<Closure>()
&& let closure = closure_rc.as_ref()
&& (closure.upvalues.is_empty()
|| closure.function_node.ty.purity >= Purity::SideEffectFree)
&& !closure.function_node.ty.is_recursive
+3 -7
View File
@@ -2,7 +2,7 @@ use crate::ast::nodes::{
Address, AnalyzedNode, IdentifierBinding, NodeKind, VirtualId,
};
use crate::ast::types::{Purity, Value};
use crate::ast::closure::Closure;
use std::cell::RefCell;
use std::collections::HashSet;
use std::rc::Rc;
@@ -35,12 +35,8 @@ impl<'a> Inliner<'a> {
| Value::Keyword(_)
| Value::Record(_, _)
| Value::DateTime(_) => true,
Value::Object(obj) => {
if let Some(closure) = obj.as_any().downcast_ref::<Closure>() {
closure.upvalues.is_empty() && !closure.function_node.ty.is_recursive
} else {
false
}
Value::Closure(closure_rc) => {
closure_rc.upvalues.is_empty() && !closure_rc.function_node.ty.is_recursive
}
_ => false,
};
+4 -6
View File
@@ -3,7 +3,7 @@ use crate::ast::nodes::{
NodeKind, VirtualId,
};
use crate::ast::types::{Identity, Value};
use crate::ast::closure::Closure;
use std::collections::HashSet;
// --- PathTracker ---
@@ -56,12 +56,10 @@ impl UsageInfo {
self.collect(value);
}
NodeKind::Constant(v) => {
if let Value::Object(obj) = v
&& let Some(closure) = obj.as_any().downcast_ref::<Closure>()
{
if let Value::Closure(closure_rc) = v {
self.used_identities
.insert(closure.function_node.identity.clone());
self.collect(&closure.function_node);
.insert(closure_rc.function_node.identity.clone());
self.collect(&closure_rc.function_node);
}
}
NodeKind::Identifier { binding, .. } => {
+3 -12
View File
@@ -668,13 +668,11 @@ impl Environment {
info.positional_count,
node.ty.stack_size,
));
let closure_obj: Rc<dyn Object> = closure;
return Rc::new(NativeFunction {
purity: Purity::Impure,
func: Rc::new(move |args| {
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,
Err(e) => panic!("Myc Runtime Error: {}", e),
}
@@ -692,12 +690,7 @@ impl Environment {
Err(e) => panic!("Myc Runtime Error: {}", e),
};
if let Value::Object(obj) = &res
&& obj
.as_any()
.downcast_ref::<Closure>()
.is_some()
{
if let Value::Closure(obj) = &res {
match vm.run_with_args(obj.clone(), args) {
Ok(v) => v,
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.
// All Myc scripts are wrapped in a parameterless lambda for consistency.
let mut final_result = result;
if let Ok(Value::Object(obj)) = &final_result
&& let Some(_closure) = obj.as_any().downcast_ref::<Closure>()
{
if let Ok(Value::Closure(obj)) = &final_result {
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)");
}
let closure_obj = if let Value::Object(obj) = &args[0] {
obj.clone()
let closure_obj = if let Value::Closure(rc) = &args[0] {
rc.clone()
} else {
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 std::any::Any;
use std::cell::RefCell;
@@ -257,7 +258,8 @@ pub enum Value {
Record(std::sync::Arc<RecordLayout>, ValueList),
FieldAccessor(Keyword),
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
}
@@ -277,6 +279,7 @@ impl PartialEq for Value {
}
(Value::FieldAccessor(a), Value::FieldAccessor(b)) => 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::Cell(a), Value::Cell(b)) => Rc::ptr_eq(a, b),
_ => false,
@@ -617,6 +620,7 @@ impl Value {
Value::Record(layout, _) => StaticType::Record(layout.clone()),
Value::FieldAccessor(k) => StaticType::FieldAccessor(*k),
Value::Function(_) => StaticType::Any, // Dynamic function
Value::Closure(_) => StaticType::Object("closure"),
Value::Object(o) => StaticType::Object(o.type_name()),
Value::Cell(c) => c.borrow().static_type(),
}
@@ -660,6 +664,7 @@ impl fmt::Display for Value {
}
Value::FieldAccessor(k) => write!(f, ".{}", k.name()),
Value::Function(f_meta) => write!(f, "<native fn, {:?}>", f_meta.purity),
Value::Closure(_) => write!(f, "<closure>"),
Value::Object(o) => write!(f, "<{:?}>", o),
Value::Cell(c) => write!(f, "{}", c.borrow()),
}
+33 -30
View File
@@ -2,7 +2,7 @@ use crate::ast::closure::Closure;
use crate::ast::nodes::{Address, ExecNode, IdentifierBinding, NodeKind, StackOffset};
use crate::ast::rtl::series::{RecordSeries, SeriesView};
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::rc::Rc;
@@ -88,7 +88,7 @@ pub struct VM {
frames: Vec<CallFrame>,
/// 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.
tail_call: Option<(Rc<dyn Object>, Vec<Value>)>,
tail_call: Option<(Value, Vec<Value>)>,
}
impl VM {
@@ -111,8 +111,9 @@ impl VM {
mut result: Result<Value, String>,
) -> Result<Value, String> {
loop {
if let Some((next_obj, next_args)) = self.tail_call.take() {
if let Ok(closure_rc) = next_obj.clone().into_rc_any().downcast::<Closure>() {
if let Some((next_val, next_args)) = self.tail_call.take() {
match next_val {
Value::Closure(closure_rc) => {
self.stack.clear();
// frames should be empty here since we popped before entering the loop
self.frames.push(CallFrame {
@@ -136,11 +137,13 @@ impl VM {
result = self.eval_observed(observer, &closure.exec_node);
self.frames.pop();
} else if let Some(series) = next_obj.as_series() {
}
Value::Object(obj) => {
if let Some(series) = obj.as_series() {
if next_args.len() != 1 {
return Err(format!(
"{} indexer expects exactly 1 argument (the lookback index), got {}",
next_obj.type_name(),
obj.type_name(),
next_args.len()
));
}
@@ -148,7 +151,7 @@ impl VM {
if *idx < 0 {
return Err(format!(
"{} lookback index cannot be negative: {}",
next_obj.type_name(),
obj.type_name(),
idx
));
}
@@ -156,16 +159,21 @@ impl VM {
} else {
return Err(format!(
"{} index must be an integer, got {}",
next_obj.type_name(),
obj.type_name(),
next_args[0]
));
}
} else {
return Err(format!(
"Tail call target is not callable: {}",
next_obj.type_name()
obj.type_name()
));
}
}
other => {
return Err(format!("Tail call target is not callable: {}", other));
}
}
} else {
return result;
}
@@ -174,23 +182,18 @@ impl VM {
pub fn run_with_args(
&mut self,
closure_obj: Rc<dyn Object>,
closure_rc: Rc<Closure>,
args: &[Value],
) -> 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>(
&mut self,
observer: &mut O,
closure_obj: Rc<dyn Object>,
closure_rc: Rc<Closure>,
args: &[Value],
) -> 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.frames.clear();
@@ -416,8 +419,8 @@ impl VM {
let mut pipe_vm = VM::new(self.globals.clone());
let executor: Box<PipeFn> = match lambda_val {
Value::Object(obj) => {
let my_closure = obj.clone();
Value::Closure(rc) => {
let my_closure = rc.clone();
Box::new(move |args: &[Value]| -> Value {
match pipe_vm.run_with_args(my_closure.clone(), args) {
Ok(res) => res,
@@ -465,7 +468,7 @@ impl VM {
info.positional_count,
stack_size,
);
Ok(Value::Object(Rc::new(closure)))
Ok(Value::Closure(Rc::new(closure)))
}
NodeKind::Call { callee, args } => {
let func_val = self.eval_internal(obs, callee)?;
@@ -481,8 +484,8 @@ impl VM {
self.stack.truncate(base);
match func_val {
Value::Object(obj) => {
self.tail_call = Some((obj, arg_vals));
Value::Closure(_) | Value::Object(_) => {
self.tail_call = Some((func_val, arg_vals));
return Ok(Value::Void);
}
Value::Function(f) => return Ok((f.func)(&arg_vals)),
@@ -612,10 +615,7 @@ impl VM {
self.stack.truncate(base);
return res;
}
Value::Object(obj) => {
if let Ok(closure_rc) =
obj.clone().into_rc_any().downcast::<Closure>()
{
Value::Closure(closure_rc) => {
self.frames.push(CallFrame {
stack_base: base,
closure: Some(closure_rc.clone()),
@@ -662,7 +662,9 @@ impl VM {
self.frames.pop();
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 res = if arg_len != 1 {
Err(format!(
@@ -707,6 +709,7 @@ impl VM {
Value::Record(_, _) => "Record",
Value::FieldAccessor(_) => "FieldAccessor",
Value::Function(_) => "Function",
Value::Closure(_) => "Closure",
Value::Object(_) => "Object",
Value::Cell(_) => "Cell",
};
@@ -714,8 +717,8 @@ impl VM {
}
};
if let Some((next_obj, next_args)) = self.tail_call.take() {
current_func = Value::Object(next_obj);
if let Some((next_val, next_args)) = self.tail_call.take() {
current_func = next_val;
self.stack.truncate(base);
self.stack.extend(next_args);
continue;
@@ -736,7 +739,7 @@ impl VM {
let frame = self.frames.last().ok_or("No call frame for 'again'")?;
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)
} else {
Err("'again' called outside of a closure".to_string())